* refactor: apply biome formatting to small packages + packages/lib Format packages/sms, packages/prisma, packages/platform/libraries, packages/platform/examples, packages/platform/types, packages/emails, and packages/lib. Excludes packages/platform/examples/base/src/pages/[bookingUid].tsx due to pre-existing lint errors. Co-Authored-By: Claude Opus 4.6 <[email protected]> * revert: remove packages/platform formatting changes Revert biome formatting for packages/platform as requested. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
214 lines
6.9 KiB
TypeScript
214 lines
6.9 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");
|
|
}
|
|
|
|
// Fallback to code check when instanceof fails due to different Prisma client instances
|
|
function isPrismaError(cause: unknown): cause is Prisma.PrismaClientKnownRequestError {
|
|
return (
|
|
cause instanceof Prisma.PrismaClientKnownRequestError ||
|
|
(cause instanceof Error &&
|
|
"code" in cause &&
|
|
typeof cause.code === "string" &&
|
|
cause.code.startsWith("P"))
|
|
);
|
|
}
|
|
|
|
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:
|
|
case ErrorCode.CollectCardFailure:
|
|
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 });
|
|
}
|
|
|
|
export { isPrismaError };
|