Skip to content

Commit 3b0bb25

Browse files
fix: status code or router/embed rate limiting (calcom#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 <hariombalhara@gmail.com>
1 parent d27490e commit 3b0bb25

4 files changed

Lines changed: 40 additions & 4 deletions

File tree

apps/web/pages/router/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function Router({ form, message, errorMessage }: inferSSRProps<ty
1212
return (
1313
<>
1414
<Head>
15-
<title>{form.name} | Cal.com Forms</title>
15+
<title>{form?.name} | Cal.com Forms</title>
1616
</Head>
1717
<div className="mx-auto my-0 max-w-3xl md:my-24">
1818
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs";
22
import type { GetServerSidePropsContext } from "next";
33

4-
import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl";
4+
import { getRoutedUrl, hasEmbedPath } from "@calcom/lib/server/getRoutedUrl";
5+
6+
import { TRPCError } from "@trpc/server";
57

68
export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps(
79
context: GetServerSidePropsContext
810
) {
9-
return await getRoutedUrl(context);
11+
try {
12+
return await getRoutedUrl(context);
13+
} catch (error) {
14+
if (error instanceof TRPCError && error.code === "TOO_MANY_REQUESTS") {
15+
context.res.statusCode = 429;
16+
const isEmbed = hasEmbedPath(context.req.url || "");
17+
18+
return {
19+
props: {
20+
isEmbed,
21+
form: null,
22+
message: null,
23+
errorMessage: error.message,
24+
},
25+
};
26+
}
27+
throw error;
28+
}
1029
},
1130
"/router");

packages/lib/server/getRoutedUrl.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "@calcom/lib/__mocks__/logger";
22

3+
import { createHash } from "crypto";
34
import type { GetServerSidePropsContext } from "next";
45
import { beforeEach, describe, expect, it, vi } from "vitest";
56

@@ -12,12 +13,14 @@ import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substit
1213
import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
1314
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
1415
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
16+
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
1517
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
1618
import { UserRepository } from "@calcom/lib/server/repository/user";
1719

1820
import { getRoutedUrl } from "./getRoutedUrl";
1921

2022
// Mock dependencies
23+
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
2124
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
2225
vi.mock("@calcom/lib/server/repository/routingForm");
2326
vi.mock("@calcom/lib/server/repository/user");
@@ -112,6 +115,7 @@ describe("getRoutedUrl", () => {
112115
it("should return notFound if form is not found", async () => {
113116
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
114117
const context = mockContext({});
118+
115119
const result = await getRoutedUrl(context);
116120
expect(result).toEqual({ notFound: true });
117121
expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
@@ -259,6 +263,19 @@ describe("getRoutedUrl", () => {
259263
);
260264
});
261265

266+
it("should throw an error if rate limit is exceeded", async () => {
267+
vi.mocked(checkRateLimitAndThrowError).mockRejectedValue(new Error("Rate limit exceeded"));
268+
const context = mockContext({ email: "test@cal.com" });
269+
const expectedHash = createHash("sha256")
270+
.update(JSON.stringify({ email: "test@cal.com" }))
271+
.digest("hex");
272+
273+
await expect(getRoutedUrl(context)).rejects.toThrow("Rate limit exceeded");
274+
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
275+
identifier: `form:form-id:hash:${expectedHash}`,
276+
});
277+
});
278+
262279
describe("Dry Run", () => {
263280
it("should call handleResponse with isPreview:true", async () => {
264281
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);

packages/lib/server/getRoutedUrl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const getDeterministicHashForResponse = (fieldsResponses: Record<string, unknown
4444
return hash;
4545
};
4646

47-
function hasEmbedPath(pathWithQuery: string) {
47+
export function hasEmbedPath(pathWithQuery: string) {
4848
const onlyPath = pathWithQuery.split("?")[0];
4949
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
5050
}

0 commit comments

Comments
 (0)