diff --git a/apps/web/app/api/apiRouteMiddleware.ts b/apps/web/app/api/apiRouteMiddleware.ts deleted file mode 100644 index 9f888df55d..0000000000 --- a/apps/web/app/api/apiRouteMiddleware.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Params } from "app/_types"; -import { ApiError } from "next/dist/server/api-utils"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; - -export const apiRouteMiddleware = - (handler: (req: NextRequest, { params }: { params: Params }) => Promise) => - async (req: NextRequest, { params }: { params: Params }) => { - try { - return await handler(req, { params }); - } catch (error) { - if (error instanceof ApiError) { - return NextResponse.json({ message: error.message }, { status: error.statusCode }); - } else { - const serverError = getServerErrorFromUnknown(error); - // we don't want to report Bad Request errors to Sentry / console - if (!(serverError.statusCode >= 400 && serverError.statusCode < 500)) { - console.error(error); - const captureException = (await import("@sentry/nextjs")).captureException; - captureException(error); - } - return NextResponse.json( - { - message: serverError.message, - url: serverError.url, - method: serverError.method, - }, - { - status: serverError.statusCode, - } - ); - } - } - }; diff --git a/apps/web/app/api/avatar/[uuid]/route.ts b/apps/web/app/api/avatar/[uuid]/route.ts index 9b39da2233..d3683ec11e 100644 --- a/apps/web/app/api/avatar/[uuid]/route.ts +++ b/apps/web/app/api/avatar/[uuid]/route.ts @@ -1,5 +1,5 @@ import type { Params } from "app/_types"; -import { apiRouteMiddleware } from "app/api/apiRouteMiddleware"; +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; @@ -63,6 +63,6 @@ async function handler(req: NextRequest, { params }: { params: Params }) { }); } -const getHandler = apiRouteMiddleware(handler); +const getHandler = defaultResponderForAppDir(handler); export { getHandler as GET }; diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index cb210a3380..c5d9fa0727 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -1,4 +1,4 @@ -import { apiRouteMiddleware } from "app/api/apiRouteMiddleware"; +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -26,7 +26,7 @@ async function handler(req: NextRequest) { return NextResponse.json(result, { status: statusCode }); } -const deleteHandler = apiRouteMiddleware((req: NextRequest) => handler(req)); -const postHandler = apiRouteMiddleware((req: NextRequest) => handler(req)); +const deleteHandler = defaultResponderForAppDir(handler); +const postHandler = defaultResponderForAppDir(handler); export { deleteHandler as DELETE, postHandler as POST }; diff --git a/apps/web/app/api/customer-card/route.ts b/apps/web/app/api/customer-card/route.ts index e22164094a..b4a1de8266 100644 --- a/apps/web/app/api/customer-card/route.ts +++ b/apps/web/app/api/customer-card/route.ts @@ -1,4 +1,4 @@ -import { apiRouteMiddleware } from "app/api/apiRouteMiddleware"; +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import { createHmac } from "crypto"; import { headers } from "next/headers"; import type { NextRequest } from "next/server"; @@ -540,4 +540,4 @@ async function handler(request: NextRequest) { }); } -export const POST = apiRouteMiddleware(handler); +export const POST = defaultResponderForAppDir(handler); diff --git a/apps/web/app/api/defaultResponderForAppDir.test.ts b/apps/web/app/api/defaultResponderForAppDir.test.ts new file mode 100644 index 0000000000..052a3529b4 --- /dev/null +++ b/apps/web/app/api/defaultResponderForAppDir.test.ts @@ -0,0 +1,77 @@ +import type { NextRequest } from "next/server"; +import { describe, expect, it, vi } from "vitest"; + +import { ErrorCode } from "@calcom/lib/errorCodes"; + +import { TRPCError } from "@trpc/server"; + +import { defaultResponderForAppDir } from "./defaultResponderForAppDir"; + +vi.mock("next/server", () => ({ + NextRequest: class MockNextRequest {}, + NextResponse: { + json: vi.fn((body, init) => ({ + json: vi.fn().mockResolvedValue(body), + status: init?.status || 200, + })), + }, +})); + +describe("defaultResponderForAppDir", () => { + it("should return a JSON response when handler resolves with a result", async () => { + const f = vi.fn().mockResolvedValue({ success: true }); + const req = { method: "GET", url: "/api/test" } as unknown as NextRequest; + const params = {}; + + const response = await defaultResponderForAppDir(f)(req, { params }); + console.log(response); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(json).toEqual({ success: true }); + }); + + it("should return an empty JSON response when handler resolves with no result", async () => { + const f = vi.fn().mockResolvedValue(null); + const req = { method: "GET", url: "/api/test" } as unknown as NextRequest; + const params = {}; + + const response = await defaultResponderForAppDir(f)(req, { params }); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(json).toEqual({}); + }); + + it("should respond with status code 409 for NoAvailableUsersFound", async () => { + const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound)); + const req = { method: "GET", url: "/api/test" } as unknown as NextRequest; + const params = {}; + + const response = await defaultResponderForAppDir(f)(req, { params }); + const json = await response.json(); + + expect(response.status).toBe(409); + expect(json).toEqual({ + message: ErrorCode.NoAvailableUsersFound, + url: undefined, + method: undefined, + }); + }); + + it("should respond with a 429 status code for rate limit errors", async () => { + const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" })); + const req = { method: "POST", url: "/api/test" } as unknown as NextRequest; + const params = {}; + + const response = await defaultResponderForAppDir(f)(req, { params }); + const json = await response.json(); + + expect(response.status).toBe(429); + expect(json).toEqual({ + message: "TOO_MANY_REQUESTS", + url: undefined, + method: undefined, + }); + }); +}); diff --git a/apps/web/app/api/defaultResponderForAppDir.ts b/apps/web/app/api/defaultResponderForAppDir.ts new file mode 100644 index 0000000000..35e9de3ddb --- /dev/null +++ b/apps/web/app/api/defaultResponderForAppDir.ts @@ -0,0 +1,70 @@ +import { wrapApiHandlerWithSentry } from "@sentry/nextjs"; +import type { Params } from "app/_types"; +import { ApiError } from "next/dist/server/api-utils"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; +import { performance } from "@calcom/lib/server/perfObserver"; + +type Handler = ( + req: NextRequest, + { params }: { params: Params } +) => Promise; + +export const defaultResponderForAppDir = ( + handler: Handler, + endpointRoute?: string +) => { + return async (req: NextRequest, { params }: { params: Params }) => { + let ok = false; + try { + performance.mark("Start"); + + const result = endpointRoute + ? await wrapApiHandlerWithSentry(async () => await handler(req, { params }), endpointRoute)() + : await handler(req, { params }); + + ok = true; + if (result) { + return result instanceof Response ? result : NextResponse.json(result); + } + + return NextResponse.json({}); + } catch (error) { + let serverError; + + if (error instanceof ApiError) { + serverError = { + message: error.message, + statusCode: error.statusCode, + url: req.url, + method: req.method, + }; + } else { + serverError = getServerErrorFromUnknown(error); + } + + // Don't report 400-499 errors to Sentry/console + if (!(serverError.statusCode >= 400 && serverError.statusCode < 500)) { + console.error(serverError); + const captureException = (await import("@sentry/nextjs")).captureException; + captureException(error); + } + + return NextResponse.json( + { + message: serverError.message, + url: serverError.url, + method: serverError.method, + }, + { + status: serverError.statusCode, + } + ); + } finally { + performance.mark("End"); + performance.measure(`[${ok ? "OK" : "ERROR"}][${req.method}] '${req.url}'`, "Start", "End"); + } + }; +}; diff --git a/apps/web/app/api/plain-hash/route.ts b/apps/web/app/api/plain-hash/route.ts index 82cf359c31..99e45b69f5 100644 --- a/apps/web/app/api/plain-hash/route.ts +++ b/apps/web/app/api/plain-hash/route.ts @@ -1,4 +1,4 @@ -import { apiRouteMiddleware } from "app/api/apiRouteMiddleware"; +import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import { createHmac } from "crypto"; import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; @@ -79,4 +79,4 @@ async function handler() { return NextResponse.json(response); } -export const POST = apiRouteMiddleware(handler); +export const POST = defaultResponderForAppDir(handler); diff --git a/apps/web/app/api/social/og/image/route.tsx b/apps/web/app/api/social/og/image/route.tsx index 9905ca479a..b2dc9212c9 100644 --- a/apps/web/app/api/social/og/image/route.tsx +++ b/apps/web/app/api/social/og/image/route.tsx @@ -1,4 +1,3 @@ -import { apiRouteMiddleware } from "app/api/apiRouteMiddleware"; import type { NextRequest } from "next/server"; import { ImageResponse } from "next/server"; import type { SatoriOptions } from "satori"; @@ -115,6 +114,4 @@ async function handler(req: NextRequest) { } } -const getHandler = apiRouteMiddleware(handler); - -export { getHandler as GET }; +export { handler as GET }; diff --git a/apps/web/test/lib/plainTiers.test.ts b/apps/web/test/lib/plainTiers.test.ts index 054562ee47..e88a2e0989 100644 --- a/apps/web/test/lib/plainTiers.test.ts +++ b/apps/web/test/lib/plainTiers.test.ts @@ -35,8 +35,8 @@ vi.mock("@lib/buildLegacyCtx", () => ({ }), })); -vi.mock("app/api/apiRouteMiddleware", () => ({ - apiRouteMiddleware: (handler: () => Promise) => handler, +vi.mock("app/api/defaultResponderForAppDir", () => ({ + defaultResponderForAppDir: (handler: () => Promise) => handler, })); vi.mock("@calcom/prisma", () => ({