diff --git a/packages/app-store/_utils/stripe.types.ts b/packages/app-store/_utils/stripe.types.ts new file mode 100644 index 0000000000..d9f6a04692 --- /dev/null +++ b/packages/app-store/_utils/stripe.types.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; + +// Copied from node_modules/stripe/types/Errors.d.ts because stripe lib doesn't expose these types +// Ref: https://github.com/stripe/stripe-node/blob/master/types/Errors.d.ts#L194 +interface StripeInvalidRequestError extends Error { + readonly type: "StripeInvalidRequestError"; + readonly rawType: "invalid_request_error"; + /** + * A human-readable message giving more details about the error. For card errors, these messages can + * be shown to your users. + */ + readonly message: string; + /** + * For card errors, a short string describing the kind of card error that occurred. + * + * @docs https://stripe.com/docs/error-codes + */ + readonly code?: string; + + /** + * A URL to more information about the error code reported. + * + * @docs https://stripe.com/docs/error-codes + */ + readonly doc_url?: string; + + /** + * Typically a 4xx or 5xx. + */ + readonly statusCode?: number; + + readonly raw: unknown; + + readonly headers: { + [key: string]: string; + }; + + readonly requestId: string; + + /** + * The parameter the error relates to if the error is parameter-specific. You can use this to display a + * message near the correct form field, for example. + */ + readonly param?: string; +} + +const errorSchema = z.object({ + name: z.string(), + message: z.string(), + stack: z.string().optional(), +}); + +export const stripeInvalidRequestErrorSchema = errorSchema.extend({ + type: z.literal("StripeInvalidRequestError"), + rawType: z.literal("invalid_request_error"), + code: z.string().optional(), + doc_url: z.string().optional(), + statusCode: z.number().optional(), + raw: z.unknown(), + headers: z.record(z.string()), + requestId: z.string(), + param: z.string().optional(), +}); diff --git a/packages/lib/server/getServerErrorFromUnknown.test.ts b/packages/lib/server/getServerErrorFromUnknown.test.ts new file mode 100644 index 0000000000..d04edf37f0 --- /dev/null +++ b/packages/lib/server/getServerErrorFromUnknown.test.ts @@ -0,0 +1,34 @@ +import { expect, it, describe } from "vitest"; + +import { HttpError } from "../http-error"; +import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; + +describe("getServerErrorFromUnknown", () => { + it("should handle a StripeInvalidRequestError", () => { + const stripeError = { + name: "StripeInvalidRequestError", + type: "StripeInvalidRequestError", + rawType: "invalid_request_error", + message: "No such customer: cus_12345; a valid customer ID is required.", + code: "resource_missing", + doc_url: "https://stripe.com/docs/error-codes/resource-missing", + statusCode: 400, + raw: { + error: { type: "invalid_request_error", message: "No such customer" }, + }, + headers: { + "content-type": "application/json", + "request-id": "req_abc123xyz", + }, + requestId: "req_abc123xyz", + param: "customer", + }; + Object.setPrototypeOf(stripeError, Error.prototype); + const result = getServerErrorFromUnknown(stripeError); + expect(result).toBeInstanceOf(HttpError); + expect(result.statusCode).toBe(400); + expect(result.message).toBe(stripeError.message); + expect(result.cause).toEqual(stripeError); + expect(result.name).toBe("HttpError"); + }); +}); diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts index 400829b5b0..23ee3b333f 100644 --- a/packages/lib/server/getServerErrorFromUnknown.ts +++ b/packages/lib/server/getServerErrorFromUnknown.ts @@ -1,8 +1,8 @@ import { Prisma } from "@prisma/client"; -import Stripe from "stripe"; import type { ZodIssue } from "zod"; import { ZodError } from "zod"; +import { stripeInvalidRequestErrorSchema } from "@calcom/app-store/_utils/stripe.types"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { TRPCError } from "@trpc/server"; @@ -56,8 +56,9 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError { if (isPrismaError(cause)) { return getServerErrorFromPrismaError(cause); } - if (cause instanceof Stripe.errors.StripeInvalidRequestError) { - return getHttpError({ statusCode: 400, cause }); + const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause); + if (parsedStripeError.success) { + return getHttpError({ statusCode: 400, cause: parsedStripeError.data }); } if (cause instanceof HttpError) { const redactedCause = redactError(cause);