fix(frontend): improve error handling

This commit is contained in:
MiniDigger | Martin 2022-12-28 12:43:25 +01:00
parent 8f690a66d1
commit 332b21e8e2
5 changed files with 109 additions and 86 deletions

View File

@ -5,7 +5,10 @@ import { computed } from "vue";
import { useSettingsStore } from "~/store/useSettingsStore";
import { settingsLog } from "~/lib/composables/useLog";
import { useAuthStore } from "~/store/auth";
import "regenerator-runtime/runtime"; // popper needs this?
import { onErrorCaptured, transformAxiosError } from "#imports";
// popper needs this?
import "regenerator-runtime/runtime";
// keep in sync with error.vue, cause reasons
const authStore = useAuthStore();
@ -22,6 +25,10 @@ useHead({
class: "background-body text-[#262626] dark:text-[#E0E6f0]",
},
});
onErrorCaptured((err) => {
console.log("captured", transformAxiosError(err)); // TODO error handling
});
</script>
<template>

View File

@ -15,160 +15,162 @@ import {
ReviewQueueEntry,
RoleTable,
} from "hangar-internal";
import { Ref } from "vue";
import { AsyncData } from "nuxt/app";
import { useApi, useInternalApi } from "~/composables/useApi";
import { useAsyncData } from "#imports";
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }) {
return (await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", "get", params))).data;
return extract(await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", "get", params)));
}
export async function useUser(user: string) {
return (await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user))).data;
return extract(await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user)));
}
export async function useOrganization(user: string) {
return (await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`))).data;
return extract(await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`)));
}
export async function useProject(user: string, project: string) {
return (await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project))).data;
return extract(await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project)));
}
export async function useStargazers(user: string, project: string) {
return (await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`))).data;
return extract(await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`)));
}
export async function useWatchers(user: string, project: string) {
return (await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`))).data;
return extract(await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`)));
}
export async function useStaff(params?: { offset?: number; limit?: number; sort?: string[] }) {
return (await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", "GET", params))).data;
return extract(await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", "GET", params)));
}
export async function useAuthors(params?: { offset?: number; limit?: number; sort?: string[] }) {
return (await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", "GET", params))).data;
return extract(await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", "GET", params)));
}
export async function useUsers() {
return (await useAsyncData("useUsers", () => useApi<PaginatedResult<User>>("users"))).data;
return extract(await useAsyncData("useUsers", () => useApi<PaginatedResult<User>>("users")));
}
export async function useInvites() {
return (await useAsyncData("useInvites", () => useInternalApi<Invites>("invites"))).data;
return extract(await useAsyncData("useInvites", () => useInternalApi<Invites>("invites")));
}
export async function useNotifications() {
return (await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications"))).data;
return extract(await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications")));
}
export async function useUnreadNotifications() {
return (await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications"))).data;
return extract(await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications")));
}
export async function useReadNotifications() {
return (await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications"))).data;
return extract(await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications")));
}
export async function useRecentNotifications(amount: number) {
return (await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount))).data;
return extract(await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount)));
}
export async function useUnreadNotificationsCount() {
return (await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount"))).data;
return extract(await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount")));
}
export async function useResolvedFlags() {
return (await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved"))).data;
return extract(await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved")));
}
export async function useUnresolvedFlags() {
return (await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved"))).data;
return extract(await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved")));
}
export async function useProjectFlags(projectId: number) {
return (await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId))).data;
return extract(await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId)));
}
export async function useProjectNotes(projectId: number) {
return (await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId))).data;
return extract(await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId)));
}
export async function useProjectChannels(user: string, project: string) {
return (await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`))).data;
return extract(await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`)));
}
export async function useProjectVersions(user: string, project: string) {
return (await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`)))
.data;
return extract(
await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`))
);
}
export async function useProjectVersionsInternal(user: string, project: string, version: string) {
return (
await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
)
).data;
return await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
);
}
export async function usePage(user: string, project: string, path?: string) {
return (
await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
)
).data;
return await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
);
}
export async function useHealthReport() {
return (await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health"))).data;
return extract(await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health")));
}
export async function useActionLogs() {
return (await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/"))).data;
return extract(await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/")));
}
export async function useVersionApprovals() {
return (
await useAsyncData("useVersionApprovals", () =>
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
)
).data;
return await useAsyncData("useVersionApprovals", () =>
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
);
}
export async function usePossibleOwners() {
return (await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners"))).data;
return extract(await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners")));
}
export async function useOrgVisibility(user: string) {
return (await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`)))
.data;
return extract(
await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`))
);
}
export async function useVersionInfo(): Promise<Ref<VersionInfo | undefined>> {
return (await useAsyncData("useVersionInfo", () => useInternalApi<VersionInfo>(`data/version-info`))).data;
export async function useVersionInfo() {
return extract(await useAsyncData("useVersionInfo", () => useInternalApi<VersionInfo>(`data/version-info`)));
}
export async function useUserData(user: string) {
return (
await useAsyncData("useUserData:" + user, async () => {
// noinspection ES6MissingAwait
const data = await Promise.all([
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
useApi<PaginatedResult<Project>>(`projects`, "get", {
owner: user,
}),
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
useApi<ProjectCompact[]>(`users/${user}/pinned`),
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
watching: data[1] as PaginatedResult<ProjectCompact>,
projects: data[2] as PaginatedResult<Project>,
organizations: data[3] as { [key: string]: RoleTable },
pinned: data[4] as ProjectCompact[],
};
})
).data;
return await useAsyncData("useUserData:" + user, async () => {
// noinspection ES6MissingAwait
const data = await Promise.all([
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
useApi<PaginatedResult<Project>>(`projects`, "get", {
owner: user,
}),
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
useApi<ProjectCompact[]>(`users/${user}/pinned`),
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
watching: data[1] as PaginatedResult<ProjectCompact>,
projects: data[2] as PaginatedResult<Project>,
organizations: data[3] as { [key: string]: RoleTable },
pinned: data[4] as ProjectCompact[],
};
});
}
function extract<T, E>(asyncData: AsyncData<T, E>) {
if (asyncData.error?.value) {
throw asyncData.error.value;
} else {
return asyncData.data;
}
}

View File

@ -9,6 +9,7 @@ import { useConfig } from "~/lib/composables/useConfig";
import { handleRequestError, useRequestEvent } from "#imports";
import { useAxios } from "~/composables/useAxios";
import { useNotificationStore } from "~/lib/store/notification";
import { transformAxiosError } from "~/composables/useErrorHandling";
class Auth {
loginUrl(redirectUrl: string): string {
@ -25,7 +26,7 @@ class Auth {
if ("status" in result && result?.status === 200 && result?.data) {
location.replace(result?.data);
} else {
useNotificationStore().error("Error while logging out?!");
await useNotificationStore().error("Error while logging out?!");
}
}
@ -92,8 +93,7 @@ class Auth {
} catch (e) {
this.refreshPromise = null;
if ((e as AxiosError).response?.data) {
const { trace, ...err } = (e as AxiosError).response?.data as { trace: any };
authLog("Refresh failed", err);
authLog("Refresh failed", transformAxiosError(e));
} else {
authLog("Refresh failed");
}
@ -128,7 +128,7 @@ class Auth {
return;
}
const user = await useInternalApi<HangarUser>("users/@me").catch((err) => {
authLog("no user, with err", Object.assign({}, err));
authLog("no user, with err", transformAxiosError(err));
return this.invalidate(axios);
});
if (user) {

View File

@ -1,9 +1,10 @@
import { AxiosError } from "axios";
import axios, { AxiosError } from "axios";
import { HangarApiException, HangarValidationException, MultiHangarApiException } from "hangar-api";
import { Composer } from "vue-i18n";
import { ref } from "vue";
import { useNotificationStore } from "~/lib/store/notification";
import { I18n } from "~/lib/i18n";
import { createError } from "#imports";
export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value, msg: string | undefined = undefined) {
if (import.meta.env.SSR) {
@ -11,9 +12,11 @@ export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value,
return ref();
}
const notfication = useNotificationStore();
const transformed = transformAxiosError(err);
if (!err.isAxiosError) {
// everything should be an AxiosError
console.log(err);
console.log("no axios request error", transformed);
notfication.error(transformed.message?.toString() || "Unknown error");
} else if (err.response && typeof err.response.data === "object" && err.response.data) {
if ("isHangarApiException" in err.response.data) {
for (const errorMsg of collectErrors(err.response.data as HangarApiException, i18n)) {
@ -30,9 +33,10 @@ export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value,
} else {
notfication.error(msg ? `${i18n.t(msg)}: ${err.response.statusText}` : err.response.statusText);
}
console.log("request error", err.response);
console.log("request error", transformed);
} else {
console.log(err);
console.log("unknown error", transformed);
notfication.error(transformed.message?.toString() || "Unknown error");
}
return ref();
}
@ -41,13 +45,16 @@ function _handleRequestError(err: AxiosError, i18n: Composer) {
function writeResponse(object: unknown) {
console.log("writeResponse", object);
// throw new Error("TODO: Implement me"); // TODO
createError({ statusCode: object.status, statusMessage: object.statusText });
}
const transformed = transformAxiosError(err);
if (!err.isAxiosError) {
// everything should be an AxiosError
writeResponse({
status: 500,
});
console.log(err);
console.log("handle not axios error", transformed);
} else if (err.response && typeof err.response.data === "object" && err.response.data) {
if ("isHangarApiException" in err.response.data) {
const data =
@ -70,9 +77,9 @@ function _handleRequestError(err: AxiosError, i18n: Composer) {
}
} else {
writeResponse({
statusText: "This shouldn't happen...",
status: 500,
statusText: "Internal Error: " + transformed.code,
});
console.log(err);
}
}
@ -87,3 +94,15 @@ function collectErrors(exception: HangarApiException | MultiHangarApiException,
return res;
}
}
export function transformAxiosError(err: AxiosError | unknown): Record<string, unknown> {
return axios.isAxiosError(err)
? {
code: err?.code,
requestUrl: err?.request?.path || err?.config?.url,
status: err?.response?.status,
data: err?.response?.data,
message: err?.message,
}
: (err as Record<string, unknown>);
}

View File

@ -5,6 +5,7 @@ import { defineNuxtPlugin, useAuth, useRequestEvent } from "#imports";
import { useAuthStore } from "~/store/auth";
import { authLog, axiosLog } from "~/lib/composables/useLog";
import { useConfig } from "~/lib/composables/useConfig";
import { transformAxiosError } from "~/composables/useErrorHandling";
export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
const config = useConfig();
@ -58,13 +59,7 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
}
}
} else {
const transformedError = {
code: err?.code,
requestUrl: err?.request?.path,
status: err?.response?.status,
data: err?.response?.data,
};
axiosLog("got error", transformedError);
axiosLog("got error", transformAxiosError(err));
}
// Progress bar
@ -81,7 +76,7 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
};
});
function addAuthHeader(config: AxiosRequestConfig, token: string | undefined) {
function addAuthHeader(config: AxiosRequestConfig, token: string | undefined | null) {
if (!config.headers) {
config.headers = {};
}