diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index c9ba919564..611bb69b66 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -32,6 +32,7 @@
"verify_email_change_success_toast": "Updated your email to {{email}}",
"verify_email_change_failure_toast": "Failed to update email.",
"change_of_email": "Verify your new {{appName}} email address",
+ "trace_reference_id": "Trace reference ID",
"change_of_email_toast": "We have sent a verification link to {{email}}. We will update your email address once you click this link.",
"copy_somewhere_safe": "Save this API key somewhere safe. You will not be able to view it again.",
"verify_email_email_body": "Please verify your email address by clicking the button below.",
diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
index a9abddc0cb..49c1063708 100644
--- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
+++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
@@ -323,6 +323,12 @@ const getError = ({
return error?.message ? (
<>
{responseVercelIdHeader ?? ""} {t(messageKey, { date, count })}
+ {error.data?.traceId && (
+
+ {t("trace_reference_id")}:
+ {error.data.traceId}
+
+ )}
>
) : (
<>{t("can_you_try_again")}>
diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts
index 37095979b7..80877bdcfe 100644
--- a/packages/features/bookings/Booker/components/hooks/useBookings.ts
+++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts
@@ -400,6 +400,7 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
const error = err as Error & {
data: { rescheduleUid: string; startTime: string; attendees: string[] };
+ traceId?: string;
};
if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
@@ -521,6 +522,11 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin
: event?.data?.forwardParamsSuccessRedirect,
});
},
+ onError: (err, _, ctx) => {
+ console.error("Error creating recurring booking", err);
+ // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- It is only called when user takes an action in embed
+ bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
+ },
});
const handleBookEvent = useHandleBookEvent({
diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts
index 9201950b05..9b1de598fb 100644
--- a/packages/features/bookings/lib/service/RegularBookingService.ts
+++ b/packages/features/bookings/lib/service/RegularBookingService.ts
@@ -580,7 +580,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "email_verification_required",
- data: { traceId: traceContext.traceId },
});
}
@@ -590,7 +589,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "invalid_verification_code",
- data: { traceId: traceContext.traceId },
});
}
}
@@ -608,14 +606,12 @@ async function handler(
throw new HttpError({
statusCode: 404,
message: "event_type_not_found",
- data: { traceId: traceContext.traceId },
});
if (eventType.seatsPerTimeSlot && eventType.recurringEvent) {
throw new HttpError({
statusCode: 400,
message: "recurring_event_seats_error",
- data: { traceId: traceContext.traceId },
});
}
@@ -757,7 +753,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "Missing routingFormResponseId",
- data: { traceId: traceContext.traceId },
});
}
routingFormResponse = await deps.prismaClient.app_RoutingForms_FormResponse.findUnique({
@@ -1858,7 +1853,6 @@ async function handler(
throw new HttpError({
statusCode: 409,
message: ErrorCode.BookingConflict,
- data: { traceId: traceContext.traceId },
});
}
throw err;
@@ -2105,7 +2099,6 @@ async function handler(
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking failed",
- data: { traceId: traceContext.traceId },
};
tracingLogger.error(
@@ -2329,7 +2322,6 @@ async function handler(
throw new HttpError({
statusCode: 400,
message: "Missing payment credentials",
- data: { traceId: traceContext.traceId },
});
}
diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts
index 454a1e7673..f28e9b064d 100644
--- a/packages/lib/server/defaultResponder.ts
+++ b/packages/lib/server/defaultResponder.ts
@@ -61,7 +61,7 @@ export function defaultResponder(
const statusCode = getHTTPStatusCodeFromError(err);
error = new HttpError({ statusCode, message: err.message });
} else {
- error = getServerErrorFromUnknown(err);
+ error = getServerErrorFromUnknown(tracedError);
}
// we don't want to report Bad Request errors to Sentry / console
if (!(error.statusCode >= 400 && error.statusCode < 500)) {
diff --git a/packages/lib/server/getServerErrorFromUnknown.test.ts b/packages/lib/server/getServerErrorFromUnknown.test.ts
index 10996e37e4..b922312b0b 100644
--- a/packages/lib/server/getServerErrorFromUnknown.test.ts
+++ b/packages/lib/server/getServerErrorFromUnknown.test.ts
@@ -1,9 +1,14 @@
+import { Prisma } from "@calcom/prisma/client";
import { describe, expect, test } from "vitest";
+import { ZodError } from "zod";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
+import { TRPCError } from "@trpc/server";
+
import { HttpError } from "../http-error";
+import { TracedError } from "../tracing/error";
import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";
const test400Codes = [
@@ -113,6 +118,135 @@ describe("getServerErrorFromUnknown", () => {
});
});
+describe("TracedError handling", () => {
+ test("should extract traceId and tracedData from TracedError", () => {
+ const originalError = new Error("Original error message");
+ const tracedData = { userId: "123", operation: "booking" };
+ const traceContext = {
+ traceId: "trace_abc123",
+ spanId: "span_123",
+ operation: "test_operation",
+ };
+
+ const tracedError = new TracedError(originalError, traceContext, tracedData);
+
+ const result = getServerErrorFromUnknown(tracedError);
+
+ expect(result.statusCode).toBe(500);
+ expect(result.message).toBe("Original error message");
+ expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
+ });
+
+ test("should handle TracedError wrapping ErrorWithCode", () => {
+ const originalError = new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
+ const tracedData = { bookingId: "456" };
+ const traceContext = {
+ traceId: "trace_def456",
+ spanId: "span_456",
+ operation: "booking_lookup",
+ };
+
+ const tracedError = new TracedError(originalError, traceContext, tracedData);
+
+ const result = getServerErrorFromUnknown(tracedError);
+
+ expect(result.statusCode).toBe(404);
+ expect(result.message).toBe("Booking not found");
+ expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
+ });
+});
+
+describe("TRPCError handling", () => {
+ test("should handle TRPCError with BAD_REQUEST", () => {
+ const trpcError = new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid input data",
+ });
+
+ const result = getServerErrorFromUnknown(trpcError);
+
+ expect(result.statusCode).toBe(400);
+ expect(result.message).toBe("Invalid input data");
+ expect(result.data).toBeUndefined();
+ });
+
+ test("should handle TracedError wrapping TRPCError", () => {
+ const trpcError = new TRPCError({
+ code: "NOT_FOUND",
+ message: "Resource not found",
+ });
+ const tracedData = { resourceId: "789" };
+ const traceContext = {
+ traceId: "trace_trpc123",
+ spanId: "span_trpc123",
+ operation: "resource_lookup",
+ };
+
+ const tracedError = new TracedError(trpcError, traceContext, tracedData);
+
+ const result = getServerErrorFromUnknown(tracedError);
+
+ expect(result.statusCode).toBe(404);
+ expect(result.message).toBe("Resource not found");
+ expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId });
+ });
+});
+
+describe("ZodError handling", () => {
+ test("should handle ZodError with validation issues", () => {
+ const zodError = new ZodError([
+ {
+ code: "invalid_type",
+ expected: "string",
+ received: "number",
+ path: ["name"],
+ message: "Expected string, received number",
+ },
+ ]);
+
+ const result = getServerErrorFromUnknown(zodError);
+
+ expect(result.statusCode).toBe(400);
+ expect(result.message).toBe("invalid_type in 'name': Expected string, received number");
+ expect(result.cause).toBe(zodError);
+ });
+});
+
+describe("Prisma error handling", () => {
+ test("should handle Prisma P2025 error (record not found)", () => {
+ const prismaError = new Error("Record to delete does not exist.") as any;
+ prismaError.code = "P2025";
+ prismaError.clientVersion = "5.0.0";
+ Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);
+
+ const result = getServerErrorFromUnknown(prismaError);
+
+ expect(result.statusCode).toBe(404);
+ expect(result.message).toBe("Record to delete does not exist.");
+ expect(result.cause).toBe(prismaError);
+ });
+
+ test("should handle other Prisma errors as 400", () => {
+ const prismaError = new Error("Foreign key constraint failed") as any;
+ prismaError.code = "P2003";
+ prismaError.clientVersion = "5.0.0";
+ Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype);
+
+ const result = getServerErrorFromUnknown(prismaError);
+
+ expect(result.statusCode).toBe(400);
+ expect(result.message).toBe("Foreign key constraint failed");
+ });
+});
+
+test("should handle unknown type fallback", () => {
+ const result = getServerErrorFromUnknown({ someProperty: "value" });
+
+ expect(result.statusCode).toBe(500);
+ expect(result.message).toBe("Unhandled error of type 'object'. Please reach out for our customer support.");
+ expect(result.data).toBeUndefined();
+});
+
test400Codes.forEach((errorCode) => {
test(`${errorCode} should return 400 Bad Request`, () => {
const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`);
diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts
index 197e7bd076..41ba8e0451 100644
--- a/packages/lib/server/getServerErrorFromUnknown.ts
+++ b/packages/lib/server/getServerErrorFromUnknown.ts
@@ -5,9 +5,13 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import { Prisma } from "@calcom/prisma/client";
+import { TRPCError } from "@trpc/server";
+import { getHTTPStatusCodeFromError } from "@trpc/server/http";
+
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;
@@ -40,61 +44,85 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
* For client-side code, use getErrorFromUnknown from @calcom/lib/errors instead.
*/
export function getServerErrorFromUnknown(cause: unknown): HttpError {
+ let traceId: string | undefined;
+ let tracedData: Record | undefined;
+
+ if (cause instanceof TracedError) {
+ traceId = cause.traceId;
+ tracedData = cause.data;
+ cause = cause.originalError;
+ }
+
+ if (cause instanceof TRPCError) {
+ const statusCode = getHTTPStatusCodeFromError(cause);
+ return new HttpError({
+ statusCode,
+ message: cause.message,
+ cause,
+ data: traceId ? { ...tracedData, traceId } : undefined,
+ });
+ }
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);
+ return getServerErrorFromPrismaError(cause, traceId, tracedData);
}
const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause);
if (parsedStripeError.success) {
- return getHttpError({ statusCode: 400, cause: parsedStripeError.data });
+ 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: cause.data,
+ data: traceId ? { ...cause.data, ...tracedData, traceId } : cause.data,
cause,
});
}
if (cause instanceof HttpError) {
- const redactedCause = redactError(cause);
- return {
- ...redactedCause,
- name: cause.name,
+ const originalData = cause.data;
+ return new HttpError({
+ statusCode: cause.statusCode,
message: cause.message ?? "",
cause: cause.cause,
url: cause.url,
- statusCode: cause.statusCode,
method: cause.method,
- };
+ data: traceId ? { ...originalData, ...tracedData, traceId } : originalData,
+ });
}
if (cause instanceof Error) {
const statusCode = getHttpStatusCode(cause);
- return getHttpError({ statusCode, 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,
});
}
@@ -152,14 +180,33 @@ export function getHttpStatusCode(cause: Error | ErrorWithCode): number {
}
}
-function getHttpError({ statusCode, cause }: { statusCode: number; cause: T }) {
+function getHttpError({
+ statusCode,
+ cause,
+ traceId,
+ tracedData,
+}: {
+ statusCode: number;
+ cause: T;
+ traceId?: string;
+ tracedData?: Record;
+}) {
const redacted = redactError(cause);
- return new HttpError({ statusCode, message: redacted.message, cause: redacted });
+ return new HttpError({
+ statusCode,
+ message: redacted.message,
+ cause: redacted,
+ data: traceId ? { ...tracedData, traceId } : undefined,
+ });
}
-function getServerErrorFromPrismaError(cause: Prisma.PrismaClientKnownRequestError) {
+function getServerErrorFromPrismaError(
+ cause: Prisma.PrismaClientKnownRequestError,
+ traceId?: string,
+ tracedData?: Record
+) {
if (cause.code === "P2025") {
- return getHttpError({ statusCode: 404, cause });
+ return getHttpError({ statusCode: 404, cause, traceId, tracedData });
}
- return getHttpError({ statusCode: 400, cause });
+ return getHttpError({ statusCode: 400, cause, traceId, tracedData });
}