Merged in chore/add-error-details-for-sentry (pull request #3378)
Include more details when throwing errors for debugging in Sentry * WIP throw errors with more details for debugging in Sentry * Fix throwing response-data * Clearer message when a response fails * Add message to errors * better typings * . * Try to send profileID and membershipNumber to Sentry when we fail to parse the apiResponse * rename notFound -> notFoundError * Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/add-error-details-for-sentry Approved-by: Linus Flood
This commit is contained in:
@@ -1,66 +1,88 @@
|
||||
import { TRPCError } from "@trpc/server"
|
||||
|
||||
export function gatewayTimeout(cause?: unknown) {
|
||||
import { getResponseBody } from "./utils/getResponseBody"
|
||||
|
||||
type CustomCause = {
|
||||
message: string
|
||||
errorDetails: Record<string, unknown>
|
||||
}
|
||||
|
||||
type ResponseLike = {
|
||||
status: number
|
||||
statusText: string
|
||||
body: string | Record<string, unknown>
|
||||
url?: string
|
||||
}
|
||||
|
||||
type TRPCCause = ResponseLike | CustomCause | Error | string
|
||||
|
||||
export function isCustomCause(cause: unknown): cause is CustomCause {
|
||||
return (
|
||||
!!cause &&
|
||||
typeof cause === "object" &&
|
||||
"message" in cause &&
|
||||
"errorDetails" in cause &&
|
||||
(cause as CustomCause).errorDetails !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
export function isTRPCError(error: unknown): error is TRPCError {
|
||||
return error instanceof Error && error.name === "TRPCError"
|
||||
}
|
||||
|
||||
export function gatewayTimeout(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "GATEWAY_TIMEOUT",
|
||||
message: `Gateway Timeout`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function unauthorizedError(cause?: unknown) {
|
||||
export function unauthorizedError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: `Unauthorized`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function forbiddenError(cause?: unknown) {
|
||||
export function forbiddenError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: `Forbidden`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function conflictError(cause?: unknown) {
|
||||
export function conflictError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Conflict`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function badRequestError(cause?: unknown) {
|
||||
export function badRequestError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Bad request`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function notFound(cause?: unknown) {
|
||||
export function notFoundError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Not found`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function unprocessableContent(cause?: unknown) {
|
||||
export function unprocessableContent(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "UNPROCESSABLE_CONTENT",
|
||||
message: "Unprocessable content",
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
export function internalServerError(cause?: unknown) {
|
||||
export function internalServerError(cause?: TRPCCause, message: string = "") {
|
||||
return new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Internal Server Error`,
|
||||
cause,
|
||||
cause: harmonizeCause(cause, message),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,20 +128,137 @@ export function httpStatusByErrorCode(error: TRPCError) {
|
||||
}
|
||||
}
|
||||
|
||||
export function serverErrorByStatus(status: number, cause?: unknown) {
|
||||
function errorCodeByHttpStatus(status: number): TRPCError["code"] {
|
||||
switch (status) {
|
||||
case 400:
|
||||
return "BAD_REQUEST"
|
||||
case 401:
|
||||
return unauthorizedError(cause)
|
||||
return "UNAUTHORIZED"
|
||||
case 403:
|
||||
return forbiddenError(cause)
|
||||
return "FORBIDDEN"
|
||||
case 404:
|
||||
return notFound(cause)
|
||||
return "NOT_FOUND"
|
||||
case 409:
|
||||
return conflictError(cause)
|
||||
return "CONFLICT"
|
||||
case 422:
|
||||
return unprocessableContent(cause)
|
||||
return "UNPROCESSABLE_CONTENT"
|
||||
case 504:
|
||||
return "GATEWAY_TIMEOUT"
|
||||
case 500:
|
||||
default:
|
||||
return internalServerError(cause)
|
||||
return "INTERNAL_SERVER_ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
export function serverErrorByStatus(
|
||||
status: number,
|
||||
cause: CustomCause | Error | string
|
||||
): TRPCError
|
||||
export function serverErrorByStatus(
|
||||
status: number,
|
||||
cause: ResponseLike,
|
||||
message: string
|
||||
): TRPCError
|
||||
export function serverErrorByStatus(
|
||||
status: number,
|
||||
cause?: TRPCCause,
|
||||
message?: string
|
||||
) {
|
||||
switch (status) {
|
||||
case 401:
|
||||
return unauthorizedError(cause, message)
|
||||
case 403:
|
||||
return forbiddenError(cause, message)
|
||||
case 404:
|
||||
return notFoundError(cause, message)
|
||||
case 409:
|
||||
return conflictError(cause, message)
|
||||
case 422:
|
||||
return unprocessableContent(cause, message)
|
||||
case 500:
|
||||
return internalServerError(cause, message)
|
||||
case 504:
|
||||
return gatewayTimeout(cause, message)
|
||||
default:
|
||||
return internalServerError(cause, message)
|
||||
}
|
||||
}
|
||||
|
||||
function harmonizeCause(
|
||||
cause: TRPCCause | undefined,
|
||||
message: string = ""
|
||||
): CustomCause | Error | undefined {
|
||||
if (!cause) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isResponseLike(cause)) {
|
||||
return {
|
||||
message: message || `HTTP Error ${cause.status}: ${cause.statusText}`,
|
||||
errorDetails: {
|
||||
status: cause.status,
|
||||
statusText: cause.statusText || errorCodeByHttpStatus(cause.status),
|
||||
body: truncate(cause.body, 200), // Avoids issues in Sentry with large bodies
|
||||
url: cause.url,
|
||||
},
|
||||
} satisfies CustomCause
|
||||
}
|
||||
|
||||
if (typeof cause === "string") {
|
||||
return { message: cause, errorDetails: {} } satisfies CustomCause
|
||||
}
|
||||
|
||||
return cause
|
||||
}
|
||||
|
||||
export async function extractResponseDetails(
|
||||
response: Response
|
||||
): Promise<ResponseLike> {
|
||||
const body = await getResponseBody(response)
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body,
|
||||
url: response.url,
|
||||
}
|
||||
}
|
||||
|
||||
function isResponseLike(cause: TRPCCause): cause is ResponseLike {
|
||||
if (typeof cause !== "object" || !cause) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("status" in cause) || typeof cause.status !== "number") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("statusText" in cause) || typeof cause.statusText !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("body" in cause)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function truncate(
|
||||
str: string | Record<string, unknown>,
|
||||
maxLength: number
|
||||
): string {
|
||||
if (typeof str !== "string") {
|
||||
str = JSON.stringify(str)
|
||||
}
|
||||
|
||||
if (str.length <= maxLength) {
|
||||
return str
|
||||
}
|
||||
const originalLength = str.length
|
||||
|
||||
return (
|
||||
str.slice(0, maxLength) +
|
||||
`... [truncated, original length: ${originalLength}]`
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user