mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary Fixes a bug where sequential HTTP requests with proxy-style absolute URLs (e.g. `GET http://example.com/path HTTP/1.1`) hang on the 2nd+ request when using keep-alive connections. ## Root Cause In `packages/bun-uws/src/HttpParser.h`, the parser was treating proxy-style absolute URLs identically to `CONNECT` method requests — setting `isConnectRequest = true` and entering tunnel mode. This flag was never reset between requests on the same keep-alive connection, so the 2nd+ request was swallowed as raw tunnel data instead of being parsed as HTTP. ## Fix 3-line change in `HttpParser.h:569`: - **`isConnect`**: Now only matches actual `CONNECT` method requests (removed `isHTTPorHTTPSPrefixForProxies` from the condition) - **`isProxyStyleURL`**: New variable that detects `http://`/`https://` prefixes and accepts them as valid request targets — without triggering tunnel mode ## Who was affected - Any Bun HTTP server (`Bun.serve()` or `node:http createServer`) receiving proxy-style requests on keep-alive connections - HTTP proxy servers built with Bun could only handle one request per connection - Bun's own HTTP client making sequential requests through an HTTP proxy backed by a Bun server ## Test Added `test/js/node/http/node-http-proxy-url.test.ts` with 3 test cases: 1. Sequential GET requests with absolute URL paths 2. Sequential POST requests with absolute URL paths 3. Mixed normal and proxy-style URLs Tests run under both Node.js and Bun for compatibility verification. - ❌ Fails with system bun (2/3 tests timeout on 2nd request) - ✅ Passes with debug build (3/3 tests pass) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
162 lines
6.0 KiB
TypeScript
162 lines
6.0 KiB
TypeScript
/**
|
|
* All tests in this file should also run in Node.js.
|
|
*
|
|
* Do not add any tests that only run in Bun.
|
|
*/
|
|
|
|
import { describe, test } from "node:test";
|
|
import assert from "node:assert";
|
|
import { Agent, createServer, request as httpRequest } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
|
|
// Helper to make a request and get the response.
|
|
// Uses a shared agent so that all requests go through the same TCP connection,
|
|
// which is critical for actually testing the keep-alive / proxy-URL bug.
|
|
function makeRequest(
|
|
port: number,
|
|
path: string,
|
|
agent: Agent,
|
|
): Promise<{ statusCode: number; body: string; url: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = httpRequest({ host: "127.0.0.1", port, path, method: "GET", agent }, res => {
|
|
let body = "";
|
|
res.on("data", chunk => {
|
|
body += chunk;
|
|
});
|
|
res.on("end", () => {
|
|
resolve({ statusCode: res.statusCode!, body, url: path });
|
|
});
|
|
});
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function listenOnRandomPort(server: ReturnType<typeof createServer>): Promise<number> {
|
|
return new Promise((resolve) => {
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const addr = server.address() as AddressInfo;
|
|
resolve(addr.port);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("HTTP server with proxy-style absolute URLs", () => {
|
|
test("sequential GET requests with absolute URL paths don't hang", async () => {
|
|
const agent = new Agent({ keepAlive: true, maxSockets: 1 });
|
|
const server = createServer((req, res) => {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.end(req.url);
|
|
});
|
|
|
|
const port = await listenOnRandomPort(server);
|
|
|
|
try {
|
|
// Make 3 sequential requests with proxy-style absolute URLs
|
|
// Before the fix, request 2 would hang because the parser entered tunnel mode
|
|
const r1 = await makeRequest(port, "http://example.com/test1", agent);
|
|
assert.strictEqual(r1.statusCode, 200);
|
|
assert.ok(r1.body.includes("example.com"), `Expected body to contain "example.com", got: ${r1.body}`);
|
|
assert.ok(r1.body.includes("/test1"), `Expected body to contain "/test1", got: ${r1.body}`);
|
|
|
|
const r2 = await makeRequest(port, "http://example.com/test2", agent);
|
|
assert.strictEqual(r2.statusCode, 200);
|
|
assert.ok(r2.body.includes("example.com"), `Expected body to contain "example.com", got: ${r2.body}`);
|
|
assert.ok(r2.body.includes("/test2"), `Expected body to contain "/test2", got: ${r2.body}`);
|
|
|
|
const r3 = await makeRequest(port, "http://other.com/test3", agent);
|
|
assert.strictEqual(r3.statusCode, 200);
|
|
assert.ok(r3.body.includes("other.com"), `Expected body to contain "other.com", got: ${r3.body}`);
|
|
assert.ok(r3.body.includes("/test3"), `Expected body to contain "/test3", got: ${r3.body}`);
|
|
} finally {
|
|
agent.destroy();
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("sequential POST requests with absolute URL paths don't hang", async () => {
|
|
const agent = new Agent({ keepAlive: true, maxSockets: 1 });
|
|
const server = createServer((req, res) => {
|
|
let body = "";
|
|
req.on("data", chunk => {
|
|
body += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.end(`${req.method} ${req.url} body=${body}`);
|
|
});
|
|
});
|
|
|
|
const port = await listenOnRandomPort(server);
|
|
|
|
try {
|
|
for (let i = 1; i <= 3; i++) {
|
|
const result = await new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
|
|
const req = httpRequest(
|
|
{
|
|
host: "127.0.0.1",
|
|
port,
|
|
path: `http://example.com/post${i}`,
|
|
method: "POST",
|
|
headers: { "Content-Type": "text/plain" },
|
|
agent,
|
|
},
|
|
res => {
|
|
let body = "";
|
|
res.on("data", chunk => {
|
|
body += chunk;
|
|
});
|
|
res.on("end", () => {
|
|
resolve({ statusCode: res.statusCode!, body });
|
|
});
|
|
},
|
|
);
|
|
req.on("error", reject);
|
|
req.write(`data${i}`);
|
|
req.end();
|
|
});
|
|
assert.strictEqual(result.statusCode, 200);
|
|
assert.ok(result.body.includes(`/post${i}`), `Expected body to contain "/post${i}", got: ${result.body}`);
|
|
assert.ok(result.body.includes(`body=data${i}`), `Expected body to contain "body=data${i}", got: ${result.body}`);
|
|
}
|
|
} finally {
|
|
agent.destroy();
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("mixed normal and proxy-style URLs work sequentially", async () => {
|
|
const agent = new Agent({ keepAlive: true, maxSockets: 1 });
|
|
const server = createServer((req, res) => {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.end(req.url);
|
|
});
|
|
|
|
const port = await listenOnRandomPort(server);
|
|
|
|
try {
|
|
// Mix of normal and proxy-style URLs
|
|
const r1 = await makeRequest(port, "/normal1", agent);
|
|
assert.strictEqual(r1.statusCode, 200);
|
|
assert.ok(r1.body.includes("/normal1"), `Expected body to contain "/normal1", got: ${r1.body}`);
|
|
|
|
const r2 = await makeRequest(port, "http://example.com/proxy1", agent);
|
|
assert.strictEqual(r2.statusCode, 200);
|
|
assert.ok(r2.body.includes("example.com"), `Expected body to contain "example.com", got: ${r2.body}`);
|
|
assert.ok(r2.body.includes("/proxy1"), `Expected body to contain "/proxy1", got: ${r2.body}`);
|
|
|
|
const r3 = await makeRequest(port, "/normal2", agent);
|
|
assert.strictEqual(r3.statusCode, 200);
|
|
assert.ok(r3.body.includes("/normal2"), `Expected body to contain "/normal2", got: ${r3.body}`);
|
|
|
|
const r4 = await makeRequest(port, "http://other.com/proxy2", agent);
|
|
assert.strictEqual(r4.statusCode, 200);
|
|
assert.ok(r4.body.includes("other.com"), `Expected body to contain "other.com", got: ${r4.body}`);
|
|
assert.ok(r4.body.includes("/proxy2"), `Expected body to contain "/proxy2", got: ${r4.body}`);
|
|
} finally {
|
|
agent.destroy();
|
|
server.close();
|
|
}
|
|
});
|
|
});
|