From 6a532c2271a0fe91f5ede51a851ba87e54b85629 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Mon, 26 Jan 2026 00:02:30 +0900 Subject: [PATCH] fix: return 400 instead of 500 for invalid email in iCal generation (#27217) * fix: return 400 instead of 500 for invalid email in iCal generation Co-Authored-By: benny@cal.com * test: add unit tests for error handling in generateIcsString Co-Authored-By: benny@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/emails/lib/generateIcsString.test.ts | 90 ++++++++++++++++++- packages/emails/lib/generateIcsString.ts | 9 +- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/emails/lib/generateIcsString.test.ts b/packages/emails/lib/generateIcsString.test.ts index d7b9887bc4..a023254803 100644 --- a/packages/emails/lib/generateIcsString.test.ts +++ b/packages/emails/lib/generateIcsString.test.ts @@ -1,6 +1,8 @@ -import { describe, expect } from "vitest"; +import { describe, expect, vi } from "vitest"; import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import { ErrorWithCode } from "@calcom/lib/errors"; import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder"; import { buildVideoCallData } from "@calcom/lib/test/builder"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -166,4 +168,90 @@ describe("generateIcsString", () => { expect(icsString).toEqual(expect.stringContaining(`LOCATION:${event.location}`)); }); }); + describe("error handling", () => { + test("throws ErrorWithCode.BadRequest when ics library returns ValidationError", async () => { + // Mock the ics library to return a ValidationError + const ics = await import("ics"); + const createEventSpy = vi.spyOn(ics, "createEvent"); + + // Simulate a Yup ValidationError (which has name: "ValidationError") + const validationError = { + name: "ValidationError", + message: "attendees[0].email must be a valid email", + errors: ["attendees[0].email must be a valid email"], + }; + + createEventSpy.mockReturnValueOnce({ + error: validationError as unknown as Error, + value: undefined, + }); + + const event = buildCalendarEvent({ + iCalSequence: 0, + attendees: [buildPerson()], + }); + + expect(() => + generateIcsString({ + event, + status: "CONFIRMED", + }) + ).toThrow(ErrorWithCode); + + try { + generateIcsString({ + event, + status: "CONFIRMED", + }); + } catch (error) { + expect(error).toBeInstanceOf(ErrorWithCode); + expect((error as ErrorWithCode).code).toBe(ErrorCode.BadRequest); + expect((error as ErrorWithCode).message).toBe("attendees[0].email must be a valid email"); + } + + createEventSpy.mockRestore(); + }); + + test("re-throws non-ValidationError errors as-is", async () => { + const ics = await import("ics"); + const createEventSpy = vi.spyOn(ics, "createEvent"); + + // Simulate a different type of error + const genericError = new Error("Some other error"); + + createEventSpy.mockReturnValueOnce({ + error: genericError, + value: undefined, + }); + + const event = buildCalendarEvent({ + iCalSequence: 0, + attendees: [buildPerson()], + }); + + expect(() => + generateIcsString({ + event, + status: "CONFIRMED", + }) + ).toThrow(genericError); + + createEventSpy.mockRestore(); + }); + + test("returns ics string when there is no error", () => { + const event = buildCalendarEvent({ + iCalSequence: 0, + attendees: [buildPerson()], + }); + + const icsString = generateIcsString({ + event, + status: "CONFIRMED", + }); + + expect(icsString).toBeDefined(); + expect(typeof icsString).toBe("string"); + }); + }); }); diff --git a/packages/emails/lib/generateIcsString.ts b/packages/emails/lib/generateIcsString.ts index 981834f2c8..658fb35cdf 100644 --- a/packages/emails/lib/generateIcsString.ts +++ b/packages/emails/lib/generateIcsString.ts @@ -6,6 +6,8 @@ import { RRule } from "rrule"; import { getRichDescription } from "@calcom/lib/CalEventParser"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import { ErrorWithCode } from "@calcom/lib/errors"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; export enum BookingAction { @@ -55,7 +57,7 @@ const generateIcsString = ({ status: EventStatus; partstat?: ParticipationStatus; t?: TFunction; -}) => { +}): string | undefined => { const location = getVideoCallUrlFromCalEvent(event) || event.location; // Taking care of recurrence rule @@ -111,6 +113,11 @@ const generateIcsString = ({ busyStatus: "BUSY", }); if (icsEvent.error) { + // The ics library throws Yup ValidationError objects (not Error instances) for invalid data like invalid email formats. + // Convert these to ErrorWithCode.BadRequest so they return 400 instead of falling through to a generic 500. + if (icsEvent.error.name === "ValidationError") { + throw new ErrorWithCode(ErrorCode.BadRequest, icsEvent.error.message); + } throw icsEvent.error; } return icsEvent.value;