From a691b78ff70df6380317faf74cd8539a5235bf8b Mon Sep 17 00:00:00 2001 From: MiniDigger | Martin Date: Sun, 28 Jul 2024 12:50:53 +0200 Subject: [PATCH] fix: greatly improve error handling, maybe fix bun? --- .../controller/api/v1/UsersController.java | 1 - .../api/v1/interfaces/IUsersController.java | 1 - frontend/src/composables/useApi.ts | 2 +- frontend/src/composables/useErrorHandling.ts | 63 +++++++++++-------- frontend/src/composables/useErrorRedirect.ts | 8 ++- frontend/src/error.vue | 2 +- frontend/src/pages/[user].vue | 4 +- frontend/src/pages/[user]/index.vue | 18 +++--- 8 files changed, 56 insertions(+), 43 deletions(-) diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/UsersController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/UsersController.java index 2c94491cc..b493d1f6d 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/UsersController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/UsersController.java @@ -7,7 +7,6 @@ import io.papermc.hangar.model.api.PaginatedResult; import io.papermc.hangar.model.api.User; import io.papermc.hangar.model.api.project.ProjectCompact; -import io.papermc.hangar.model.api.project.ProjectSortingStrategy; import io.papermc.hangar.model.api.requests.RequestPagination; import io.papermc.hangar.security.annotations.Anyone; import io.papermc.hangar.security.annotations.ratelimit.RateLimit; diff --git a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java index e63a1bbf3..efa122137 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/api/v1/interfaces/IUsersController.java @@ -3,7 +3,6 @@ import io.papermc.hangar.model.api.PaginatedResult; import io.papermc.hangar.model.api.User; import io.papermc.hangar.model.api.project.ProjectCompact; -import io.papermc.hangar.model.api.project.ProjectSortingStrategy; import io.papermc.hangar.model.api.requests.RequestPagination; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts index b23f2d6c3..86dd6638e 100644 --- a/frontend/src/composables/useApi.ts +++ b/frontend/src/composables/useApi.ts @@ -39,7 +39,7 @@ function request(url: string, method: AxiosRequestConfig["method"], data: obj }) .catch((error: AxiosError) => { const { trace, ...err } = (error.response?.data as { trace: any }) || {}; - authLog("failed", err); + fetchLog("failed", err); reject(error); }); }); diff --git a/frontend/src/composables/useErrorHandling.ts b/frontend/src/composables/useErrorHandling.ts index c708a6b41..be980503f 100644 --- a/frontend/src/composables/useErrorHandling.ts +++ b/frontend/src/composables/useErrorHandling.ts @@ -3,6 +3,7 @@ import { isAxiosError } from "axios"; import type { Composer } from "vue-i18n"; import type { HangarApiException, HangarValidationException, MultiHangarApiException } from "~/types/backend"; import { tryUseNuxtApp } from "#app/nuxt"; +import { isError } from "h3"; export function handleRequestError(err: AxiosError | unknown, msg: string | undefined = undefined, alwaysShowErrorPage = false) { const i18n = tryUseNuxtApp()?.$i18n; @@ -11,8 +12,9 @@ export function handleRequestError(err: AxiosError | unknown, msg: string | unde _handleRequestError(err, i18n); return; } - if (import.meta.env.SSR || alwaysShowErrorPage) { + if (import.meta.server || alwaysShowErrorPage) { _handleRequestError(err, i18n); + return; } const notification = useNotificationStore(); const transformed = transformAxiosError(err); @@ -63,36 +65,45 @@ export function handleRequestError(err: AxiosError | unknown, msg: string | unde function _handleRequestError(err: AxiosError | unknown, i18n?: Composer) { const transformed = transformAxiosError(err); - if (!isAxiosError(err)) { - // everything should be an AxiosError - createError({ + if (!isAxiosError(err) && !isError(err)) { + // everything should be an AxiosError or h3 error + fetchLog("handle not axios error", transformed); + throw createError({ statusCode: 500, }); - fetchLog("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 = - "isMultiException" in err.response.data ? (err.response.data as MultiHangarApiException).exceptions?.[0] : (err.response.data as HangarApiException); - createError({ - statusCode: data?.httpError.statusCode, - statusMessage: data?.message ? (i18n?.te(data.message) ? i18n.t(data.message) : data.message) : undefined, - }); - } else if ("isHangarValidationException" in err.response.data) { - const data = err.response.data as HangarValidationException; - createError({ - statusCode: data.httpError?.statusCode, - statusMessage: data.fieldErrors?.map((f) => f.errorMsg).join(", "), - }); - } else { - createError({ - statusCode: err.response.status, - statusMessage: err.response.statusText, - }); - } + } else if (typeof err.response?.data === "object" && err.response.data) { + _handleErrorResponse(err.response.data, i18n); + } else if (err.cause?.response?.data) { + _handleErrorResponse(err.cause.response.data, i18n); + } else if (err.cause?.statusCode) { + // this error was rethrown, lets inform nuxt + showError(err.cause); } else { - createError({ + throw createError({ statusCode: 500, statusMessage: "Internal Error: " + transformed.code, + cause: err, + }); + } +} + +function _handleErrorResponse(responseData: object, i18n?: Composer) { + if ("isHangarApiException" in responseData) { + const data = "isMultiException" in responseData ? (responseData as MultiHangarApiException).exceptions?.[0] : (responseData as HangarApiException); + throw createError({ + statusCode: data?.httpError.statusCode, + statusMessage: data?.message ? (i18n?.te(data.message) ? i18n.t(data.message) : data.message) : undefined, + }); + } else if ("isHangarValidationException" in responseData) { + const data = responseData as HangarValidationException; + throw createError({ + statusCode: data.httpError?.statusCode, + statusMessage: data.fieldErrors?.map((f) => f.errorMsg).join(", "), + }); + } else { + throw createError({ + statusCode: err.response.status, + statusMessage: err.response.statusText, }); } } diff --git a/frontend/src/composables/useErrorRedirect.ts b/frontend/src/composables/useErrorRedirect.ts index a3d19d5fe..034ef7509 100644 --- a/frontend/src/composables/useErrorRedirect.ts +++ b/frontend/src/composables/useErrorRedirect.ts @@ -1,8 +1,12 @@ import type { RouteLocationNormalizedTyped } from "unplugin-vue-router"; -import type { RouteLocationNamedRaw } from "vue-router"; import type { HangarNuxtError } from "~/types/components/error"; -export function useErrorRedirect(currentRoute: RouteLocationNormalizedTyped, status: number, msg?: string, data?: HangarNuxtError): RouteLocationNamedRaw { +export function useErrorRedirect(currentRoute: RouteLocationNormalizedTyped, status: number, msg?: string, data?: HangarNuxtError) { + const globalError = useError(); + if (globalError.value) { + // we are already in an error state, do nothing + return; + } throw createError({ statusCode: status, message: msg, diff --git a/frontend/src/error.vue b/frontend/src/error.vue index d015bb77f..77c56ba5b 100644 --- a/frontend/src/error.vue +++ b/frontend/src/error.vue @@ -69,7 +69,7 @@ if ( (typeof props.error?.data === "string" && JSON.parse(props.error?.data).logErrorMessage !== false) || (typeof props.error?.data !== "string" && props.error?.data?.logErrorMessage !== false) ) { - console.log("error", text.value, title.value, props.error); + console.log("render error page", text.value, title.value); } try { useHead(useSeo(title.value, null, useRoute(), null)); diff --git a/frontend/src/pages/[user].vue b/frontend/src/pages/[user].vue index 9fc1f2484..8868b6f72 100644 --- a/frontend/src/pages/[user].vue +++ b/frontend/src/pages/[user].vue @@ -7,7 +7,7 @@ const user = ref(); const organization = ref(); const blocking = computed(() => route.name === "user"); if (blocking.value) { - const u = await useUser(route.params.user); + const u = await useUser(route.params.user).catch((err) => handleRequestError(err)); await cb(u); } else { // manually carry nuxt content over cause ???? @@ -15,7 +15,7 @@ if (blocking.value) { useUser(route.params.user).then((u) => nuxtApp.runWithContext(() => cb(u))); } -async function cb(u: Ref) { +async function cb(u: Ref | void) { if (!u || !u.value) { throw useErrorRedirect(route, 404, "Not found"); } else if (route.params.user !== u.value?.name) { diff --git a/frontend/src/pages/[user]/index.vue b/frontend/src/pages/[user]/index.vue index 2f3910f9f..b2587af10 100644 --- a/frontend/src/pages/[user]/index.vue +++ b/frontend/src/pages/[user]/index.vue @@ -41,7 +41,7 @@ const requestParams = computed(() => { return params; }); -const userData = await useUserData(props.user.name, requestParams.value); +const userData = await useUserData(route.params.user, requestParams.value); const { starred, watching, organizations, pinned, organizationVisibility, possibleAlts } = userData.value || { starred: null }; const projects = ref(userData.value?.projects); const orgRoles = useBackendData.orgRoles.filter((role) => role.assignable); @@ -74,7 +74,7 @@ const buttons = computed(() => { return list; }); -const isCurrentUser = computed(() => authStore.user != null && authStore.user.name === props.user.name); +const isCurrentUser = computed(() => authStore.user != null && authStore.user.name === props.user?.name); watchDebounced( requestParams, @@ -85,14 +85,14 @@ watchDebounced( // set the request params await router.replace({ query: { page: page.value, ...paramsWithoutLimit } }); // do the update - projects.value = await useApi("projects", "get", { owner: props.user.name, ...requestParams.value }); + projects.value = await useApi("projects", "get", { owner: props.user?.name, ...requestParams.value }); }, { deep: true, debounce: 250 } ); -const description = (props.user?.tagline ? props.user.tagline + " - " : "") + "Download " + props.user.name + "'s plugins on Hangar."; +const description = (props.user?.tagline ? props.user.tagline + " - " : "") + "Download " + props.user?.name + "'s plugins on Hangar."; useHead( - useSeo(props.user.name, description, route, props.user.avatarUrl, [ + useSeo(props.user?.name, description, route, props.user?.avatarUrl, [ { type: "application/ld+json", children: JSON.stringify({ @@ -100,15 +100,15 @@ useHead( "@type": "ProfilePage", mainEntity: { "@type": "Person", - name: props.user.name, + name: props.user?.name, url: config.publicHost + "/" + route.path, - description: props.user.tagline, - image: props.user.avatarUrl, + description: props.user?.tagline, + image: props.user?.avatarUrl, interactionStatistic: [ { "@type": "InteractionCounter", interactionType: "https://schema.org/CreateAction", - userInteractionCount: props.user.projectCount, + userInteractionCount: props.user?.projectCount, }, { "@type": "InteractionCounter",