From 3b0bb2525132c2f4ca62628a59b557e8565c3328 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:58:44 +0530 Subject: [PATCH] fix: status code or router/embed rate limiting (#21968) * fix: status code or router embed rate limiting * fix: type err * fix: handle api v2 * fix: status code * chore: remove unused * chore: update unit test * chore: unit test * refactor: make res required * chore: remove unused * chore: add a wrapper in getserversideprops * chore: remlove log * Add ratelLimiting unit test --------- Co-authored-by: Hariom --- apps/web/pages/router/index.tsx | 2 +- .../server/lib/router/getServerSideProps.ts | 23 +++++++++++++++++-- packages/lib/server/getRoutedUrl.test.ts | 17 ++++++++++++++ packages/lib/server/getRoutedUrl.ts | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/web/pages/router/index.tsx b/apps/web/pages/router/index.tsx index e309c19f0a..8aef863516 100644 --- a/apps/web/pages/router/index.tsx +++ b/apps/web/pages/router/index.tsx @@ -12,7 +12,7 @@ export default function Router({ form, message, errorMessage }: inferSSRProps - {form.name} | Cal.com Forms + {form?.name} | Cal.com Forms
diff --git a/apps/web/server/lib/router/getServerSideProps.ts b/apps/web/server/lib/router/getServerSideProps.ts index 47990b46f9..1f30942e37 100644 --- a/apps/web/server/lib/router/getServerSideProps.ts +++ b/apps/web/server/lib/router/getServerSideProps.ts @@ -1,11 +1,30 @@ import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs"; import type { GetServerSidePropsContext } from "next"; -import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl"; +import { getRoutedUrl, hasEmbedPath } from "@calcom/lib/server/getRoutedUrl"; + +import { TRPCError } from "@trpc/server"; export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps( context: GetServerSidePropsContext ) { - return await getRoutedUrl(context); + try { + return await getRoutedUrl(context); + } catch (error) { + if (error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS") { + context.res.statusCode = 429; + const isEmbed = hasEmbedPath(context.req.url || ""); + + return { + props: { + isEmbed, + form: null, + message: null, + errorMessage: error.message, + }, + }; + } + throw error; + } }, "/router"); diff --git a/packages/lib/server/getRoutedUrl.test.ts b/packages/lib/server/getRoutedUrl.test.ts index 862b036b1a..f595b0f512 100644 --- a/packages/lib/server/getRoutedUrl.test.ts +++ b/packages/lib/server/getRoutedUrl.test.ts @@ -1,5 +1,6 @@ import "@calcom/lib/__mocks__/logger"; +import { createHash } from "crypto"; import type { GetServerSidePropsContext } from "next"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -12,12 +13,14 @@ import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substit import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm"; import { UserRepository } from "@calcom/lib/server/repository/user"; import { getRoutedUrl } from "./getRoutedUrl"; // Mock dependencies +vi.mock("@calcom/lib/checkRateLimitAndThrowError"); vi.mock("@calcom/app-store/routing-forms/lib/handleResponse"); vi.mock("@calcom/lib/server/repository/routingForm"); vi.mock("@calcom/lib/server/repository/user"); @@ -112,6 +115,7 @@ describe("getRoutedUrl", () => { it("should return notFound if form is not found", async () => { vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null); const context = mockContext({}); + const result = await getRoutedUrl(context); expect(result).toEqual({ notFound: true }); expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id"); @@ -259,6 +263,19 @@ describe("getRoutedUrl", () => { ); }); + it("should throw an error if rate limit is exceeded", async () => { + vi.mocked(checkRateLimitAndThrowError).mockRejectedValue(new Error("Rate limit exceeded")); + const context = mockContext({ email: "test@cal.com" }); + const expectedHash = createHash("sha256") + .update(JSON.stringify({ email: "test@cal.com" })) + .digest("hex"); + + await expect(getRoutedUrl(context)).rejects.toThrow("Rate limit exceeded"); + expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({ + identifier: `form:form-id:hash:${expectedHash}`, + }); + }); + describe("Dry Run", () => { it("should call handleResponse with isPreview:true", async () => { vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never); diff --git a/packages/lib/server/getRoutedUrl.ts b/packages/lib/server/getRoutedUrl.ts index ff03920863..1b3db54562 100644 --- a/packages/lib/server/getRoutedUrl.ts +++ b/packages/lib/server/getRoutedUrl.ts @@ -44,7 +44,7 @@ const getDeterministicHashForResponse = (fieldsResponses: Record