Skip to content

Commit 55fbdb9

Browse files
committed
fix: avoid reusing disturbed request bodies
1 parent 7a15b30 commit 55fbdb9

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

src/server/next-compat.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ describe("next-compat", () => {
4646
expect(parsed).toEqual({ foo: "bar" });
4747
});
4848

49+
it("should not throw when the input body is already used", async () => {
50+
const plainReq = new Request("https://example.com/api/data", {
51+
method: "POST",
52+
body: JSON.stringify({ foo: "bar" })
53+
});
54+
55+
await plainReq.text(); // consume body
56+
57+
const nextReq = toNextRequest(plainReq);
58+
expect(nextReq).toBeInstanceOf(NextRequest);
59+
expect(nextReq.method).toBe("POST");
60+
});
61+
4962
it("should set duplex to 'half' if not provided", () => {
5063
const req = new Request("https://example.com", { method: "GET" });
5164
const nextReq = toNextRequest(req);

src/server/next-compat.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,41 @@ function collectFromNextUrl(input: Request): NextConfig | undefined {
4242
return config && Object.keys(config).length ? config : undefined;
4343
}
4444

45+
function tryCloneRequest(input: Request): Request | null {
46+
if (typeof (input as any).clone !== "function") {
47+
return null;
48+
}
49+
50+
try {
51+
return input.clone();
52+
} catch {
53+
return null;
54+
}
55+
}
56+
57+
function getSafeBody(input: any): BodyInit | null | undefined {
58+
if (!("body" in input)) {
59+
return undefined;
60+
}
61+
62+
const body = input.body;
63+
64+
if (body == null) {
65+
return body;
66+
}
67+
68+
if (input.bodyUsed) {
69+
return undefined;
70+
}
71+
72+
const locked = (body as any).locked;
73+
if (typeof locked === "boolean" && locked) {
74+
return undefined;
75+
}
76+
77+
return body as BodyInit;
78+
}
79+
4580
/**
4681
* Normalize a Request or NextRequest to a NextRequest instance.
4782
* Ensures consistent behavior across Next.js 15 (Edge) and 16 (Node Proxy).
@@ -54,18 +89,22 @@ export function toNextRequest(input: Request | NextRequest): NextRequest {
5489

5590
const nextConfig = collectFromNextUrl(input);
5691

92+
const source =
93+
input instanceof Request ? (tryCloneRequest(input) ?? input) : input;
94+
const body = getSafeBody(source);
95+
5796
const init: any = {
58-
method: input.method,
59-
headers: input.headers,
60-
body: input.body as any,
61-
duplex: (input as any).duplex ?? "half"
97+
method: source.method,
98+
headers: source.headers,
99+
duplex: (source as any).duplex ?? "half",
100+
...(body !== undefined ? { body } : {})
62101
};
63102

64103
if (nextConfig) {
65104
init.nextConfig = nextConfig;
66105
}
67106

68-
return new NextRequest(input.url, init);
107+
return new NextRequest(source.url, init);
69108
}
70109

71110
/**

0 commit comments

Comments
 (0)