* refactor: replace TRPCError with ErrorWithCode in packages/features This refactor moves error handling from throwing TRPCError directly in packages/features to throwing ErrorWithCode instead. The conversion to TRPCError now happens at the TRPC layer. Changes: - Add generic ErrorCode values (Unauthorized, Forbidden, NotFound, BadRequest, InternalServerError) to errorCodes.ts - Update getServerErrorFromUnknown to map new ErrorCodes to proper HTTP status codes - Create toTRPCError helper in packages/trpc/server/lib - Create errorMappingMiddleware in packages/trpc/server/middlewares - Migrate TRPCError throws in packages/features to ErrorWithCode: - teamService.ts - getEventTypeById.ts - eventTypeRepository.ts - OrganizationPermissionService.ts - OrganizationPaymentService.ts - sso.ts - handleCreatePhoneCall.ts - userCanCreateTeamGroupMapping.ts This improves separation of concerns by making packages/features transport-agnostic, allowing the same feature code to be reused from tRPC, API routes, workers, etc. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: remove isTrpcCall parameter and fix lint warning - Remove isTrpcCall parameter from get.handler.ts call since the feature layer no longer needs to know about tRPC - Fix unsafe optional chaining lint warning in getEventTypesByViewer.ts by precomputing usersSource variable - Complete migration of getEventTypesByViewer.ts to ErrorWithCode Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * revert * add eslint rule * add comment * fix: add isTrpcCall back to getEventTypeById interface The user reverted the removal of isTrpcCall parameter from the handler, so we need to add it back to the interface to fix the type error. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * test: update teamService tests to expect ErrorWithCode instead of TRPCError Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor * wip * feat: integrate errorMappingMiddleware into base TRPC procedure Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * connect middlewares * revert * revert * refactor * rename * fix: handle ErrorWithCode in teams server-page error handling The error handling was checking for TRPCError, but teamService now throws ErrorWithCode. This caused the 'This invitation is not for your account' error message to not be displayed when a wrong user tries to use an invitation link. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * fix * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
166 lines
5.3 KiB
TypeScript
166 lines
5.3 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";
|
|
|
|
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, tRPC handlers, webhooks, and server-side services.
|
|
* For client-side code, use getErrorFromUnknown from @calcom/lib/errors instead.
|
|
*/
|
|
export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
|
if (isZodError(cause)) {
|
|
return new HttpError({
|
|
statusCode: 400,
|
|
message: parseZodErrorIssues(cause.issues),
|
|
cause,
|
|
});
|
|
}
|
|
if (cause instanceof SyntaxError) {
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: "Unexpected error, please reach out for our customer support.",
|
|
});
|
|
}
|
|
if (isPrismaError(cause)) {
|
|
return getServerErrorFromPrismaError(cause);
|
|
}
|
|
const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause);
|
|
if (parsedStripeError.success) {
|
|
return getHttpError({ statusCode: 400, cause: parsedStripeError.data });
|
|
}
|
|
if (cause instanceof ErrorWithCode) {
|
|
const statusCode = getHttpStatusCode(cause);
|
|
return new HttpError({
|
|
statusCode,
|
|
message: cause.message ?? "",
|
|
data: cause.data,
|
|
cause,
|
|
});
|
|
}
|
|
if (cause instanceof HttpError) {
|
|
const redactedCause = redactError(cause);
|
|
return {
|
|
...redactedCause,
|
|
name: cause.name,
|
|
message: cause.message ?? "",
|
|
cause: cause.cause,
|
|
url: cause.url,
|
|
statusCode: cause.statusCode,
|
|
method: cause.method,
|
|
};
|
|
}
|
|
if (cause instanceof Error) {
|
|
const statusCode = getHttpStatusCode(cause);
|
|
return getHttpError({ statusCode, cause });
|
|
}
|
|
if (typeof cause === "string") {
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: cause,
|
|
});
|
|
}
|
|
|
|
return new HttpError({
|
|
statusCode: 500,
|
|
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
|
|
});
|
|
}
|
|
|
|
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 }: { statusCode: number; cause: T }) {
|
|
const redacted = redactError(cause);
|
|
return new HttpError({ statusCode, message: redacted.message, cause: redacted });
|
|
}
|
|
|
|
function getServerErrorFromPrismaError(cause: Prisma.PrismaClientKnownRequestError) {
|
|
if (cause.code === "P2025") {
|
|
return getHttpError({ statusCode: 404, cause });
|
|
}
|
|
return getHttpError({ statusCode: 400, cause });
|
|
}
|