Files
bun.sh/test/regression/issue/26143.test.ts
robobun 2a483631fb fix(http): allow body on GET/HEAD/OPTIONS requests for Node.js compatibility (#26145)
## 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>
2026-01-15 17:46:07 -08:00

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();
}
});
});