mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary
Fixed `http.request()` and `https.request()` hanging indefinitely when a
GET request includes a body (via `req.write()`).
### Approach
Instead of adding a public `allowGetBody` option to `fetch()`, this PR
creates a dedicated internal function `nodeHttpClient` that:
- Uses a comptime parameter to avoid code duplication
- Allows body on GET/HEAD/OPTIONS requests (Node.js behavior)
- Is only accessible internally via `$newZigFunction`
- Keeps the public `Bun.fetch()` API unchanged (Web Standards compliant)
### Implementation
1. **fetch.zig**: Refactored to use `fetchImpl(comptime allow_get_body:
bool, ...)` shared implementation
- `Bun__fetch_()` calls `fetchImpl(false, ...)` - validates body on
GET/HEAD/OPTIONS
- `nodeHttpClient()` calls `fetchImpl(true, ...)` - allows body on
GET/HEAD/OPTIONS
2. **_http_client.ts**: Uses `$newZigFunction("fetch.zig",
"nodeHttpClient", 2)` for HTTP requests
## Test plan
- [x] Added regression test at `test/regression/issue/26143.test.ts`
- [x] Test verifies GET requests with body complete successfully
- [x] Test verifies HEAD requests with body complete successfully
- [x] Test verifies `Bun.fetch()` still throws on GET with body (Web
Standards)
- [x] Test fails on current release (v1.3.6) and passes with this fix
Fixes #26143
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
describe("issue #26143 - https GET request with body hangs", () => {
|
|
test("http.request GET with body should complete", async () => {
|
|
const http = require("http");
|
|
|
|
// Use Node.js-style http.createServer which properly handles bodies on all methods
|
|
const server = http.createServer((req: any, res: any) => {
|
|
let body = "";
|
|
req.on("data", (chunk: string) => {
|
|
body += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ received: body }));
|
|
});
|
|
});
|
|
|
|
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
const port = server.address().port;
|
|
|
|
try {
|
|
const result = await new Promise<{ status: number; data: string }>((resolve, reject) => {
|
|
const options = {
|
|
hostname: "localhost",
|
|
port,
|
|
path: "/test",
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": 2,
|
|
},
|
|
};
|
|
|
|
const req = http.request(options, (res: any) => {
|
|
let data = "";
|
|
res.on("data", (chunk: string) => {
|
|
data += chunk;
|
|
});
|
|
res.on("end", () => {
|
|
resolve({ status: res.statusCode, data });
|
|
});
|
|
});
|
|
|
|
req.on("error", reject);
|
|
req.write("{}");
|
|
req.end();
|
|
});
|
|
|
|
expect(result.status).toBe(200);
|
|
expect(result.data).toContain('"received":"{}"');
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("GET request without body should still work", async () => {
|
|
const http = require("http");
|
|
|
|
const server = http.createServer((req: any, res: any) => {
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ method: req.method }));
|
|
});
|
|
|
|
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
const port = server.address().port;
|
|
|
|
try {
|
|
const result = await new Promise<{ status: number; data: string }>((resolve, reject) => {
|
|
const options = {
|
|
hostname: "localhost",
|
|
port,
|
|
path: "/test",
|
|
method: "GET",
|
|
};
|
|
|
|
const req = http.request(options, (res: any) => {
|
|
let data = "";
|
|
res.on("data", (chunk: string) => {
|
|
data += chunk;
|
|
});
|
|
res.on("end", () => {
|
|
resolve({ status: res.statusCode, data });
|
|
});
|
|
});
|
|
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
|
|
expect(result.status).toBe(200);
|
|
expect(result.data).toContain('"method":"GET"');
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("HEAD request with body should complete", async () => {
|
|
const http = require("http");
|
|
|
|
const server = http.createServer((req: any, res: any) => {
|
|
let body = "";
|
|
req.on("data", (chunk: string) => {
|
|
body += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
res.writeHead(200, { "X-Custom": "header", "X-Body-Received": body });
|
|
res.end();
|
|
});
|
|
});
|
|
|
|
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
const port = server.address().port;
|
|
|
|
try {
|
|
const result = await new Promise<{ status: number; header: string | undefined }>((resolve, reject) => {
|
|
const options = {
|
|
hostname: "localhost",
|
|
port,
|
|
path: "/test",
|
|
method: "HEAD",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": 2,
|
|
},
|
|
};
|
|
|
|
const req = http.request(options, (res: any) => {
|
|
res.on("data", () => {});
|
|
res.on("end", () => {
|
|
resolve({ status: res.statusCode, header: res.headers["x-custom"] });
|
|
});
|
|
});
|
|
|
|
req.on("error", reject);
|
|
req.write("{}");
|
|
req.end();
|
|
});
|
|
|
|
expect(result.status).toBe(200);
|
|
expect(result.header).toBe("header");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("Bun.fetch without allowGetBody should still throw", async () => {
|
|
const http = require("http");
|
|
|
|
const server = http.createServer((req: any, res: any) => {
|
|
res.writeHead(200);
|
|
res.end();
|
|
});
|
|
|
|
await new Promise<void>(resolve => server.listen(0, resolve));
|
|
const port = server.address().port;
|
|
|
|
try {
|
|
// Without allowGetBody, this should throw
|
|
expect(async () => {
|
|
await fetch(`http://localhost:${port}/test`, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": "2",
|
|
},
|
|
body: "{}",
|
|
});
|
|
}).toThrow("fetch() request with GET/HEAD/OPTIONS method cannot have body.");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|