* refactor: remove TRPCError handling from getServerErrorFromUnknown This removes the @trpc/server dependency from packages/lib by removing TRPCError handling from getServerErrorFromUnknown.ts. packages/lib is meant to be a general utility library that other packages depend on, so it shouldn't import tRPC. The callers that need to handle TRPCError (like onErrorHandler.ts and defaultResponder.ts) already handle it explicitly before calling getServerErrorFromUnknown, so this change has no behavioral impact. Changes: - Remove TRPCError and getHTTPStatusCodeFromError imports - Remove TRPCError instanceof check from getServerErrorFromUnknown - Update docstring to note that TRPCError is not handled - Remove TRPCError-related tests (they belong in the tRPC layer) - Fix lint warnings in Prisma error tests (replace 'as any' with proper type) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * wip --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
204 lines
6.6 KiB
TypeScript
204 lines
6.6 KiB
TypeScript
import type { ZodIssue } from "zod";
|
|
import { ZodError } from "zod";
|
|
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
|
import { Prisma } from "@calcom/prisma/client";
|
|
|
|
import { HttpError } from "../http-error";
|
|
import { redactError } from "../redactError";
|
|
import { stripeInvalidRequestErrorSchema } from "../stripe-error";
|
|
import { TracedError } from "../tracing/error";
|
|
|
|
function hasName(cause: unknown): cause is { name: string } {
|
|
return !!cause && typeof cause === "object" && "name" in cause;
|
|
}
|
|
|
|
function isZodError(cause: unknown): cause is ZodError {
|
|
return cause instanceof ZodError || (hasName(cause) && cause.name === "ZodError");
|
|
}
|
|
|
|
function isPrismaError(cause: unknown): cause is Prisma.PrismaClientKnownRequestError {
|
|
return cause instanceof Prisma.PrismaClientKnownRequestError;
|
|
}
|
|
|
|
function parseZodErrorIssues(issues: ZodIssue[]): string {
|
|
return issues
|
|
.map((i) =>
|
|
i.code === "invalid_union"
|
|
? i.unionErrors.map((ue) => parseZodErrorIssues(ue.issues)).join("; ")
|
|
: i.code === "unrecognized_keys"
|
|
? i.message
|
|
: `${i.path.length ? `${i.code} in '${i.path}': ` : ""}${i.message}`
|
|
)
|
|
.join("; ");
|
|
}
|
|
|
|
/**
|
|
* Converts unknown error types to HttpError with proper status code mapping and error redaction.
|
|
* SERVER-ONLY: This function imports Prisma and Stripe schemas and should only be used in server-side code.
|
|
* Use in API routes, webhooks, and server-side services.
|
|
* For client-side code, use getErrorFromUnknown from @calcom/lib/errors instead.
|
|
*
|
|
* NOTE: This function does NOT handle TRPCError. Callers that need to handle TRPCError should do so
|
|
* explicitly before calling this function (see onErrorHandler.ts and defaultResponder.ts for examples).
|
|
*/
|
|
export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
|
let traceId: string | undefined;
|
|
let tracedData: Record<string, unknown> | undefined;
|
|
|
|
if (cause instanceof TracedError) {
|
|
traceId = cause.traceId;
|
|
tracedData = cause.data;
|
|
cause = cause.originalError;
|
|
}
|
|
|
|
if (isZodError(cause)) {
|
|
return new HttpError({
|
|
statusCode: 400,
|
|
message: parseZodErrorIssues(cause.issues),
|
|
cause,
|
|
data: traceId ? { ...tracedData, traceId } : undefined,
|
|
});
|
|
}
|
|
if (cause instanceof SyntaxError) {
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: "Unexpected error, please reach out for our customer support.",
|
|
cause,
|
|
data: traceId ? { ...tracedData, traceId } : undefined,
|
|
});
|
|
}
|
|
if (isPrismaError(cause)) {
|
|
return getServerErrorFromPrismaError(cause, traceId, tracedData);
|
|
}
|
|
const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause);
|
|
if (parsedStripeError.success) {
|
|
const stripeErrorObj = new Error(parsedStripeError.data.message || "Stripe error");
|
|
stripeErrorObj.name = parsedStripeError.data.type || "StripeInvalidRequestError";
|
|
return getHttpError({ statusCode: 400, cause: stripeErrorObj, traceId, tracedData });
|
|
}
|
|
if (cause instanceof ErrorWithCode) {
|
|
const statusCode = getHttpStatusCode(cause);
|
|
return new HttpError({
|
|
statusCode,
|
|
message: cause.message ?? "",
|
|
data: traceId ? { ...cause.data, ...tracedData, traceId } : cause.data,
|
|
cause,
|
|
});
|
|
}
|
|
if (cause instanceof HttpError) {
|
|
const originalData = cause.data;
|
|
return new HttpError({
|
|
statusCode: cause.statusCode,
|
|
message: cause.message ?? "",
|
|
cause: cause.cause,
|
|
url: cause.url,
|
|
method: cause.method,
|
|
data: traceId ? { ...originalData, ...tracedData, traceId } : originalData,
|
|
});
|
|
}
|
|
if (cause instanceof Error) {
|
|
const statusCode = getHttpStatusCode(cause);
|
|
return getHttpError({ statusCode, cause, traceId, tracedData });
|
|
}
|
|
if (typeof cause === "string") {
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: cause,
|
|
data: traceId ? { ...tracedData, traceId } : undefined,
|
|
});
|
|
}
|
|
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
|
|
data: traceId ? { ...tracedData, traceId } : tracedData,
|
|
});
|
|
}
|
|
|
|
export function getHttpStatusCode(cause: Error | ErrorWithCode): number {
|
|
const errorCode = cause instanceof ErrorWithCode ? cause.code : cause.message;
|
|
|
|
switch (errorCode) {
|
|
// Generic HTTP error codes
|
|
case ErrorCode.BadRequest:
|
|
return 400;
|
|
case ErrorCode.Unauthorized:
|
|
return 401;
|
|
case ErrorCode.Forbidden:
|
|
return 403;
|
|
case ErrorCode.NotFound:
|
|
return 404;
|
|
case ErrorCode.InternalServerError:
|
|
return 500;
|
|
|
|
// Domain-specific error codes
|
|
// 400 Bad Request
|
|
case ErrorCode.RequestBodyWithouEnd:
|
|
case ErrorCode.MissingPaymentCredential:
|
|
case ErrorCode.MissingPaymentAppId:
|
|
case ErrorCode.AvailabilityNotFoundInSchedule:
|
|
case ErrorCode.CancelledBookingsCannotBeRescheduled:
|
|
case ErrorCode.BookingTimeOutOfBounds:
|
|
case ErrorCode.BookingNotAllowedByRestrictionSchedule:
|
|
case ErrorCode.BookerLimitExceeded:
|
|
case ErrorCode.BookerLimitExceededReschedule:
|
|
case ErrorCode.EventTypeNoHosts:
|
|
case ErrorCode.RequestBodyInvalid:
|
|
case ErrorCode.ChargeCardFailure:
|
|
return 400;
|
|
// 409 Conflict
|
|
case ErrorCode.NoAvailableUsersFound:
|
|
case ErrorCode.FixedHostsUnavailableForBooking:
|
|
case ErrorCode.RoundRobinHostsUnavailableForBooking:
|
|
case ErrorCode.AlreadySignedUpForBooking:
|
|
case ErrorCode.BookingSeatsFull:
|
|
case ErrorCode.NotEnoughAvailableSeats:
|
|
case ErrorCode.BookingConflict:
|
|
case ErrorCode.PaymentCreationFailure:
|
|
return 409;
|
|
// 404 Not Found
|
|
case ErrorCode.EventTypeNotFound:
|
|
case ErrorCode.BookingNotFound:
|
|
case ErrorCode.RestrictionScheduleNotFound:
|
|
return 404;
|
|
case ErrorCode.UnableToSubscribeToThePlatform:
|
|
case ErrorCode.UpdatingOauthClientError:
|
|
case ErrorCode.CreatingOauthClientError:
|
|
default:
|
|
return 500;
|
|
}
|
|
}
|
|
|
|
function getHttpError<T extends Error>({
|
|
statusCode,
|
|
cause,
|
|
traceId,
|
|
tracedData,
|
|
}: {
|
|
statusCode: number;
|
|
cause: T;
|
|
traceId?: string;
|
|
tracedData?: Record<string, unknown>;
|
|
}) {
|
|
const redacted = redactError(cause);
|
|
return new HttpError({
|
|
statusCode,
|
|
message: redacted.message,
|
|
cause: redacted,
|
|
data: traceId ? { ...tracedData, traceId } : undefined,
|
|
});
|
|
}
|
|
|
|
function getServerErrorFromPrismaError(
|
|
cause: Prisma.PrismaClientKnownRequestError,
|
|
traceId?: string,
|
|
tracedData?: Record<string, unknown>
|
|
) {
|
|
if (cause.code === "P2025") {
|
|
return getHttpError({ statusCode: 404, cause, traceId, tracedData });
|
|
}
|
|
return getHttpError({ statusCode: 400, cause, traceId, tracedData });
|
|
}
|