From ffe9d43d09d90b63404ebd632027450f3e8f692c Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Sat, 21 Jun 2025 09:26:23 +0300 Subject: [PATCH] chore: better trpc error handling (#21948) --- .../v2/src/filters/trpc-exception.filter.ts | 172 ++++++++++-------- .../controllers/slots.controller.ts | 7 + 2 files changed, 108 insertions(+), 71 deletions(-) diff --git a/apps/api/v2/src/filters/trpc-exception.filter.ts b/apps/api/v2/src/filters/trpc-exception.filter.ts index 2d7c13264a..4a77cba3b1 100644 --- a/apps/api/v2/src/filters/trpc-exception.filter.ts +++ b/apps/api/v2/src/filters/trpc-exception.filter.ts @@ -6,6 +6,100 @@ import { ERROR_STATUS } from "@calcom/platform-constants"; import { TRPCError } from "@calcom/platform-libraries"; import { Response } from "@calcom/platform-types"; +export type TRPCErrorCode = TRPCError["code"]; + +export interface ErrorDefinition { + statusCode: number; + message: string; +} + +// Define the specific TRPC error codes +export const TRPC_ERROR_CODE = { + UNAUTHORIZED: "UNAUTHORIZED", + FORBIDDEN: "FORBIDDEN", + NOT_FOUND: "NOT_FOUND", + INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR", + BAD_REQUEST: "BAD_REQUEST", + CONFLICT: "CONFLICT", + TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS", + PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE", + CLIENT_CLOSED_REQUEST: "CLIENT_CLOSED_REQUEST", + METHOD_NOT_SUPPORTED: "METHOD_NOT_SUPPORTED", + NOT_IMPLEMENTED: "NOT_IMPLEMENTED", + PRECONDITION_FAILED: "PRECONDITION_FAILED", + TIMEOUT: "TIMEOUT", + UNPROCESSABLE_CONTENT: "UNPROCESSABLE_CONTENT", + PARSE_ERROR: "PARSE_ERROR", +} as const; // `as const` ensures literal types, improving type inference + +// Map TRPC error codes to their HTTP status codes and default messages +export const TRPC_ERROR_MAP: Record<(typeof TRPC_ERROR_CODE)[keyof typeof TRPC_ERROR_CODE], ErrorDefinition> = + { + [TRPC_ERROR_CODE.UNAUTHORIZED]: { + statusCode: 401, + message: "You are not authorized to access this resource", + }, + [TRPC_ERROR_CODE.FORBIDDEN]: { + statusCode: 403, + message: "You don't have necessary permissions to access this resource", + }, + [TRPC_ERROR_CODE.NOT_FOUND]: { + statusCode: 404, + message: "The requested resource was not found", + }, + [TRPC_ERROR_CODE.INTERNAL_SERVER_ERROR]: { + statusCode: 500, + message: "An unexpected error occurred on the server. Please try again later", + }, + [TRPC_ERROR_CODE.BAD_REQUEST]: { + statusCode: 400, + message: "Bad request: Please check your request data and try again", + }, + [TRPC_ERROR_CODE.CONFLICT]: { + statusCode: 409, + message: "Could not process the request due to a resource conflict with the current state", + }, + [TRPC_ERROR_CODE.TOO_MANY_REQUESTS]: { + statusCode: 429, + message: "You have exceeded the allowed number of requests", + }, + [TRPC_ERROR_CODE.PAYLOAD_TOO_LARGE]: { + statusCode: 413, + message: "The request payload is too large", + }, + [TRPC_ERROR_CODE.CLIENT_CLOSED_REQUEST]: { + statusCode: 499, + message: "The client closed the connection before the server could finish processing the request", + }, + [TRPC_ERROR_CODE.METHOD_NOT_SUPPORTED]: { + statusCode: 405, + message: "The requested method is not supported for this resource", + }, + [TRPC_ERROR_CODE.NOT_IMPLEMENTED]: { + statusCode: 501, + message: "The requested method is not implemented on the server", + }, + [TRPC_ERROR_CODE.PRECONDITION_FAILED]: { + statusCode: 412, + message: "The server does not meet one of the preconditions that the requester put on the request", + }, + [TRPC_ERROR_CODE.TIMEOUT]: { + statusCode: 408, + message: "The request took too long to complete. Please try again later", + }, + [TRPC_ERROR_CODE.UNPROCESSABLE_CONTENT]: { + statusCode: 422, + message: + "The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect", + }, + [TRPC_ERROR_CODE.PARSE_ERROR]: { + statusCode: 400, + message: "The request could not be parsed due to invalid syntax.", + }, + }; + +// --- TRPCExceptionFilter (updated) --- + @Catch(TRPCError) export class TRPCExceptionFilter implements ExceptionFilter { private readonly logger = new Logger("TRPCExceptionFilter"); @@ -15,77 +109,13 @@ export class TRPCExceptionFilter implements ExceptionFilter { const response = ctx.getResponse(); const request = ctx.getRequest(); - let statusCode = 500; - let errorMessage = exception.message; + const errorDefinition = TRPC_ERROR_MAP[exception.code as keyof typeof TRPC_ERROR_CODE] || { + statusCode: 500, + message: exception.message, + }; - switch (exception.code) { - case "UNAUTHORIZED": - statusCode = 401; - errorMessage = "You are not authorized to access this resource"; - break; - case "FORBIDDEN": - statusCode = 403; - errorMessage = "You don't have necessary permissions to access this resource"; - break; - case "NOT_FOUND": - statusCode = 404; - errorMessage = "The requested resource was not found"; - break; - case "INTERNAL_SERVER_ERROR": - statusCode = 500; - errorMessage = "An unexpected error occurred on the server. Please try again later"; - break; - case "BAD_REQUEST": - statusCode = 400; - errorMessage = "Bad request: Please check your request data and try again"; - break; - case "CONFLICT": - statusCode = 409; - errorMessage = "Could not process the request due to a resource conflict with the current state"; - break; - case "TOO_MANY_REQUESTS": - statusCode = 429; - errorMessage = "You have exceeded the allowed number of requests"; - break; - case "PAYLOAD_TOO_LARGE": - statusCode = 413; - errorMessage = "The request payload is too large"; - break; - case "CLIENT_CLOSED_REQUEST": - statusCode = 499; - errorMessage = - "The client closed the connection before the server could finish processing the request"; - break; - case "METHOD_NOT_SUPPORTED": - statusCode = 405; - errorMessage = "The requested method is not supported for this resource"; - case "NOT_IMPLEMENTED": - statusCode = 501; - errorMessage = "The requested method is not implemented on the server"; - break; - case "PRECONDITION_FAILED": - statusCode = 412; - errorMessage = - "The server does not meet one of the preconditions that the requester put on the request"; - break; - case "TIMEOUT": - statusCode = 408; - errorMessage = "The request took too long to complete. Please try again later"; - break; - case "UNPROCESSABLE_CONTENT": - statusCode = 422; - errorMessage = - "The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect"; - break; - case "PARSE_ERROR": - statusCode = 400; - errorMessage = "The request could not be parsed due to invalid syntax."; - break; - default: - statusCode = 500; - errorMessage = exception.message; - break; - } + const statusCode = errorDefinition.statusCode; + const errorMessage = errorDefinition.message; const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; response.setHeader("X-Request-Id", requestId.toString()); @@ -103,7 +133,7 @@ export class TRPCExceptionFilter implements ExceptionFilter { status: ERROR_STATUS, timestamp: new Date().toISOString(), path: request.url, - error: { code: exception.name, message: errorMessage }, + error: { code: exception.name, message: errorMessage }, // exception.name is usually "TRPCError" }); } } diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts index cb4dc7b808..7197f25b30 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts @@ -1,3 +1,4 @@ +import { TRPC_ERROR_CODE, TRPC_ERROR_MAP, TRPCErrorCode } from "@/filters/trpc-exception.filter"; import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service"; import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service"; import { SlotsWorkerService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-worker.service"; @@ -15,6 +16,7 @@ import { VERSION_2024_06_11, VERSION_2024_08_13, } from "@calcom/platform-constants"; +import { TRPCError } from "@calcom/platform-libraries"; import { getAvailableSlots } from "@calcom/platform-libraries/slots"; import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types"; import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types"; @@ -209,7 +211,12 @@ export class SlotsController_2024_04_15 { "Invalid time range given - check the 'startTime' and 'endTime' query parameters." ); } + + if (TRPC_ERROR_MAP[error.message as keyof typeof TRPC_ERROR_CODE]) { + throw new TRPCError({ code: error.message as TRPCErrorCode }); + } } + throw error; } }