diff --git a/src/server/next-compat.test.ts b/src/server/next-compat.test.ts index 733738de5..262c1f852 100644 --- a/src/server/next-compat.test.ts +++ b/src/server/next-compat.test.ts @@ -46,6 +46,55 @@ describe("next-compat", () => { expect(parsed).toEqual({ foo: "bar" }); }); + it("should not throw when the input body is already used", async () => { + const plainReq = new Request("https://example.com/api/data", { + method: "POST", + body: JSON.stringify({ foo: "bar" }) + }); + + await plainReq.text(); // consume body + + const nextReq = toNextRequest(plainReq); + expect(nextReq).toBeInstanceOf(NextRequest); + expect(nextReq.method).toBe("POST"); + }); + + it("should handle a Request instance without a clone function", () => { + const req = new Request("https://example.com/no-clone", { + method: "GET" + }); + Object.defineProperty(req, "clone", { value: undefined }); + + const nextReq = toNextRequest(req); + expect(nextReq).toBeInstanceOf(NextRequest); + expect(nextReq.url).toBe("https://example.com/no-clone"); + }); + + it("should tolerate inputs without a body property", () => { + const fakeReq: any = { + url: "https://example.com/no-body", + method: "GET", + headers: new Headers() + }; + + const nextReq = toNextRequest(fakeReq); + expect(nextReq).toBeInstanceOf(NextRequest); + expect(nextReq.url).toBe("https://example.com/no-body"); + }); + + it("should skip locked bodies when rebuilding requests", () => { + const fakeReq: any = { + url: "https://example.com/locked-body", + method: "POST", + headers: new Headers(), + body: { locked: true } + }; + + const nextReq = toNextRequest(fakeReq); + expect(nextReq).toBeInstanceOf(NextRequest); + expect(nextReq.url).toBe("https://example.com/locked-body"); + }); + it("should set duplex to 'half' if not provided", () => { const req = new Request("https://example.com", { method: "GET" }); const nextReq = toNextRequest(req); diff --git a/src/server/next-compat.ts b/src/server/next-compat.ts index 414279c9f..9265b755c 100644 --- a/src/server/next-compat.ts +++ b/src/server/next-compat.ts @@ -42,6 +42,41 @@ function collectFromNextUrl(input: Request): NextConfig | undefined { return config && Object.keys(config).length ? config : undefined; } +function tryCloneRequest(input: Request): Request | null { + if (typeof (input as any).clone !== "function") { + return null; + } + + try { + return input.clone(); + } catch { + return null; + } +} + +function getSafeBody(input: any): BodyInit | null | undefined { + if (!("body" in input)) { + return undefined; + } + + const body = input.body; + + if (body == null) { + return body; + } + + if (input.bodyUsed) { + return undefined; + } + + const locked = (body as any).locked; + if (typeof locked === "boolean" && locked) { + return undefined; + } + + return body as BodyInit; +} + /** * Normalize a Request or NextRequest to a NextRequest instance. * Ensures consistent behavior across Next.js 15 (Edge) and 16 (Node Proxy). @@ -54,18 +89,22 @@ export function toNextRequest(input: Request | NextRequest): NextRequest { const nextConfig = collectFromNextUrl(input); + const source = + input instanceof Request ? (tryCloneRequest(input) ?? input) : input; + const body = getSafeBody(source); + const init: any = { - method: input.method, - headers: input.headers, - body: input.body as any, - duplex: (input as any).duplex ?? "half" + method: source.method, + headers: source.headers, + duplex: (source as any).duplex ?? "half", + ...(body !== undefined ? { body } : {}) }; if (nextConfig) { init.nextConfig = nextConfig; } - return new NextRequest(input.url, init); + return new NextRequest(source.url, init); } /**