refactor: consolidate error handlers to use getServerErrorFromUnknown (#25114)

* refactor: consolidate error handlers to use getServerErrorFromUnknown

- Migrate server-only code to use getServerErrorFromUnknown for better error handling
- Add JSDoc documentation to both getErrorFromUnknown and getServerErrorFromUnknown
- Update webhook handlers, payment services, email service, and booking service
- Keep getErrorFromUnknown for client-side and isomorphic code
- Improve error message extraction by using err.cause?.stack instead of err.stack
- Fix ESLint warnings: replace 'any' with 'unknown' types, fix hasOwnProperty usage

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* Update packages/app-store/paypal/api/webhook.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* refactor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Benny Joo
2025-11-28 09:14:06 +01:00
committed by GitHub
co-authored by benny@cal.com <sldisek783@gmail.com> cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
parent 724204843a
commit e6ba89961c
12 changed files with 61 additions and 38 deletions
@@ -4,8 +4,8 @@ import type Stripe from "stripe";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { prisma } from "@calcom/prisma";
export const config = {
@@ -108,13 +108,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
if (!err.message.includes("No credential found with subscription ID")) {
console.error(`Webhook Error: ${err.message}`);
}
res.status(err.statusCode ?? 500).send({
res.status(err.statusCode).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
return;
}
+4 -4
View File
@@ -6,8 +6,8 @@ import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePa
import { albyCredentialKeysSchema } from "@calcom/app-store/alby/lib";
import parseInvoice from "@calcom/app-store/alby/lib/parseInvoice";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import prisma from "@calcom/prisma";
export const config = {
@@ -90,11 +90,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return await handlePaymentSuccess(payment.id, payment.bookingId);
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
return res.status(err.statusCode || 500).send({
return res.status(err.statusCode).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
}
}
@@ -5,8 +5,8 @@ import { z } from "zod";
import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { PrismaBookingPaymentRepository as BookingPaymentRepository } from "@calcom/lib/server/repository/PrismaBookingPaymentRepository";
import appConfig from "../config.json";
@@ -90,11 +90,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await handlePaymentSuccess(payment.id, payment.bookingId);
return res.status(200).json({ success: true });
} catch (_err) {
const err = getErrorFromUnknown(_err);
const statusCode = err instanceof HttpCode ? err.statusCode : 500;
return res.status(statusCode).send({
const err = getServerErrorFromUnknown(_err);
return res.status(err.statusCode).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
}
}
+3 -3
View File
@@ -4,8 +4,8 @@ import type z from "zod";
import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import prisma from "@calcom/prisma";
import type { hitpayCredentialKeysSchema } from "../lib/hitpayCredentialKeysSchema";
@@ -111,11 +111,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
return await handlePaymentSuccess(payment.id, payment.bookingId);
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
return res.status(200).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
}
}
+3 -3
View File
@@ -6,8 +6,8 @@ import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePa
import { paypalCredentialKeysSchema } from "@calcom/app-store/paypal/lib";
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import prisma from "@calcom/prisma";
export const config = {
@@ -92,11 +92,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return await handlePaypalPaymentSuccess(parsedPayload, bodyAsString, parseHeaders.data);
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(200).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
return;
}
@@ -5,9 +5,9 @@ import z from "zod";
import { sendAwaitingPaymentEmailAndSMS } from "@calcom/emails/email-manager";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { ErrorWithCode } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import type { Booking, Payment, PaymentOption, Prisma } from "@calcom/prisma/client";
@@ -368,7 +368,7 @@ export class PaymentService implements IAbstractPaymentService {
});
return updatedPayment;
} catch (e) {
const err = getErrorFromUnknown(e);
const err = getServerErrorFromUnknown(e);
throw err;
}
}
+6 -6
View File
@@ -3,9 +3,9 @@ import queue from "queue";
import dayjs from "@calcom/dayjs";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -100,7 +100,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
if (!event.data.hasOwnProperty(parameterFilter)) {
if (!Object.prototype.hasOwnProperty.call(event.data, parameterFilter)) {
res.status(500).json({ message: "Selected param not available" });
return;
}
@@ -141,7 +141,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);
}
await q.start();
} catch (error) {
} catch {
throw new Error("Failed to reschedule bookings");
}
}
@@ -155,11 +155,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
return res.status(200).json({ body: req.body });
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(err.statusCode ?? 500).send({
res.status(err.statusCode).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
return;
}
+2 -2
View File
@@ -3,9 +3,9 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
import { serverConfig } from "@calcom/lib/serverConfig";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { setTestEmail } from "@calcom/lib/testEmails";
import { prisma } from "@calcom/prisma";
@@ -77,7 +77,7 @@ export default class BaseEmail {
payloadWithUnEscapedSubject,
(_err, info) => {
if (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
this.printNodeMailerError(err);
reject(err);
} else {
@@ -61,7 +61,7 @@ import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown, ErrorWithCode } from "@calcom/lib/errors";
import { ErrorWithCode } from "@calcom/lib/errors";
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
@@ -69,6 +69,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { criticalLogger } from "@calcom/lib/logger.server";
import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { distributedTracing } from "@calcom/lib/tracing/factory";
@@ -1844,9 +1845,9 @@ async function handler(
};
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
tracingLogger.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
if (err.code === "P2002") {
if (err.cause && typeof err.cause === "object" && "code" in err.cause && err.cause.code === "P2002") {
throw new HttpError({
statusCode: 409,
message: ErrorCode.BookingConflict,
+4 -4
View File
@@ -14,10 +14,10 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -180,11 +180,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
const err = getServerErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(err.statusCode ?? 500).send({
res.status(err.statusCode).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
stack: IS_PRODUCTION ? undefined : err.cause?.stack,
});
return;
}
+19 -2
View File
@@ -13,17 +13,34 @@ export class ErrorWithCode extends Error {
get(_, prop: string) {
if (prop in ErrorCode) {
const code = ErrorCode[prop as keyof typeof ErrorCode];
return (message?: string, data?: Record<string, any>) => new ErrorWithCode(code, message, data);
return (message?: string, data?: Record<string, unknown>) => new ErrorWithCode(code, message, data);
}
throw new Error(`Unknown error code: ${prop}`);
},
}) as unknown as Record<
keyof typeof ErrorCode,
(message?: string, data?: Record<string, any>) => ErrorWithCode
(message?: string, data?: Record<string, unknown>) => ErrorWithCode
>;
}
}
/**
* Converts unknown error types to Error objects.
*
* @deprecated For server-side code, use `getServerErrorFromUnknown` from `@calcom/lib/server/getServerErrorFromUnknown` instead.
* This function should only be used in client-side or isomorphic code (React components, shared utilities).
*
* Use this function when:
* - You're in a React component that runs on the client
* - You're in shared/isomorphic code that cannot import server-only dependencies
* - You only need a basic Error object without HTTP status code mapping
*
* For server-side error handling (API routes, tRPC handlers, webhooks), use `getServerErrorFromUnknown`
* which provides proper HTTP status code mapping, error redaction, and handles Zod/Prisma/Stripe errors.
*
* @param cause - The unknown error to convert
* @returns An Error object with optional statusCode and code properties
*/
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: string } {
if (cause instanceof Error) {
return cause;
@@ -33,6 +33,12 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
.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({