chore: write defaultResponder for App Router APIs (#19727)
* create defaultResponderForAppDir * rename all usages * finish * fix
This commit is contained in:
@@ -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<NextResponse | Response>) =>
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<T extends NextResponse | Response = NextResponse> = (
|
||||
req: NextRequest,
|
||||
{ params }: { params: Params }
|
||||
) => Promise<T>;
|
||||
|
||||
export const defaultResponderForAppDir = <T extends NextResponse | Response = NextResponse>(
|
||||
handler: Handler<T>,
|
||||
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");
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -35,8 +35,8 @@ vi.mock("@lib/buildLegacyCtx", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("app/api/apiRouteMiddleware", () => ({
|
||||
apiRouteMiddleware: (handler: () => Promise<Response>) => handler,
|
||||
vi.mock("app/api/defaultResponderForAppDir", () => ({
|
||||
defaultResponderForAppDir: (handler: () => Promise<Response>) => handler,
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
|
||||
Reference in New Issue
Block a user