Files
bun.sh/test/js/bun/http/proxy.test.js
Jarred Sumner 39e2c22e1a fix(http): disable keep-alive on proxy authentication failure (407) (#25884)
## Summary

- Disable HTTP keep-alive when a proxy returns a 407 (Proxy
Authentication Required) status code
- This prevents subsequent requests from trying to reuse a connection
that the proxy server has closed
- Refactored proxy tests to use `describe.concurrent` and async
`Bun.spawn` patterns

## Test plan

- [x] Added test `simultaneous proxy auth failures should not hang` that
verifies multiple concurrent requests with invalid proxy credentials
complete without hanging
- [x] Existing proxy tests pass

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-01-07 20:49:30 -08:00

261 lines
7.5 KiB
JavaScript

import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import fs from "fs";
import { bunEnv, bunExe, gc } from "harness";
import path from "path";
let proxy, auth_proxy, server;
beforeAll(() => {
proxy = Bun.serve({
port: 0,
async fetch(request) {
// if is not an proxy connection just drop it
if (!request.headers.has("proxy-connection")) {
return new Response("Bad Request", { status: 400 });
}
// simple http proxy
if (request.url.startsWith("http://")) {
return await fetch(request.url, {
method: request.method,
body: await request.text(),
});
}
// no TLS support here
return new Response("Bad Request", { status: 400 });
},
});
auth_proxy = Bun.serve({
port: 0,
async fetch(request) {
// if is not an proxy connection just drop it
if (!request.headers.has("proxy-connection")) {
return new Response("Bad Request", { status: 400 });
}
if (!request.headers.has("proxy-authorization")) {
return new Response("Proxy Authentication Required", { status: 407 });
}
const auth = Buffer.from(
request.headers.get("proxy-authorization").replace("Basic ", "").trim(),
"base64",
).toString("utf8");
if (auth !== "squid_user:ASD123@123asd") {
return new Response("Forbidden", { status: 403 });
}
// simple http proxy
if (request.url.startsWith("http://")) {
return await fetch(request.url, {
method: request.method,
body: await request.text(),
});
}
// no TLS support here
return new Response("Bad Request", { status: 400 });
},
});
server = Bun.serve({
port: 0,
async fetch(request) {
if (request.method === "POST") {
const text = await request.text();
return new Response(text, { status: 200 });
}
return new Response("Hello, World", { status: 200 });
},
});
});
afterAll(() => {
server.stop(true);
proxy.stop(true);
auth_proxy.stop(true);
});
const test = process.env.PROXY_URL ? it : it.skip;
describe.concurrent(() => {
test("should be able to post on TLS", async () => {
const data = JSON.stringify({
"name": "bun",
});
const result = await fetch("https://httpbin.org/post", {
method: "POST",
proxy: process.env.PROXY_URL,
verbose: true,
headers: {
"Content-Type": "application/json",
},
body: data,
}).then(res => res.json());
expect(result.data).toBe(data);
});
test("should be able to post bigger on TLS", async () => {
const data = fs.readFileSync(path.join(import.meta.dir, "fetch.json")).toString("utf8");
const result = await fetch("https://httpbin.org/post", {
method: "POST",
proxy: process.env.PROXY_URL,
verbose: true,
headers: {
"Content-Type": "application/json",
},
body: data,
}).then(res => res.json());
expect(result.data).toBe(data);
});
describe("proxy non-TLS", async () => {
let url;
let auth_proxy_url;
let proxy_url;
const requests = [
() => [new Request(url), auth_proxy_url],
() => [
new Request(url, {
method: "POST",
body: "Hello, World",
}),
auth_proxy_url,
],
() => [url, auth_proxy_url],
() => [new Request(url), proxy_url],
() => [
new Request(url, {
method: "POST",
body: "Hello, World",
}),
proxy_url,
],
() => [url, proxy_url],
];
beforeAll(() => {
url = `http://localhost:${server.port}`;
auth_proxy_url = `http://squid_user:ASD123%40123asd@localhost:${auth_proxy.port}`;
proxy_url = `localhost:${proxy.port}`;
});
for (let callback of requests) {
test(async () => {
const [request, proxy] = callback();
gc();
const response = await fetch(request, { verbose: true, proxy });
gc();
const text = await response.text();
gc();
expect(text).toBe("Hello, World");
});
}
});
it("proxy non-TLS auth can fail", async () => {
const url = `http://localhost:${server.port}`;
{
try {
const response = await fetch(url, {
verbose: true,
proxy: `http://localhost:${auth_proxy.port}`,
});
expect(response.status).toBe(407);
} catch (err) {
expect(true).toBeFalsy();
}
}
{
try {
const response = await fetch(url, {
verbose: true,
proxy: `http://squid_user:asdf123@localhost:${auth_proxy.port}`,
});
expect(response.status).toBe(403);
} catch (err) {
expect(true).toBeFalsy();
}
}
});
it("simultaneous proxy auth failures should not hang", async () => {
const url = `http://localhost:${server.port}`;
const invalidProxy = `http://localhost:${auth_proxy.port}`;
// First batch: 5 simultaneous fetches with invalid credentials
const firstBatch = await Promise.all([
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
]);
expect(firstBatch.map(r => r.status)).toEqual([407, 407, 407, 407, 407]);
await Promise.all(firstBatch.map(r => r.text())).catch(() => {});
// Second batch: immediately send another 5
// Before the fix, these would hang due to keep-alive on failed proxy connections
const secondBatch = await Promise.all([
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
fetch(url, { proxy: invalidProxy }),
]);
expect(secondBatch.map(r => r.status)).toEqual([407, 407, 407, 407, 407]);
await Promise.all(secondBatch.map(r => r.text())).catch(() => {});
});
it.each([
[undefined, undefined],
["", ""],
["''", "''"],
['""', '""'],
])("test proxy env, http_proxy=%s https_proxy=%s", async (http_proxy, https_proxy) => {
const { exited, stderr: stream } = Bun.spawn({
cmd: [bunExe(), "-e", 'await fetch("https://example.com")'],
env: {
...bunEnv,
http_proxy: http_proxy,
https_proxy: https_proxy,
},
stdout: "inherit",
stderr: "pipe",
});
const [exitCode, stderr] = await Promise.all([exited, stream.text()]);
expect(stderr.includes("FailedToOpenSocket: Was there a typo in the url or port?")).toBe(false);
expect(exitCode).toBe(0);
});
it.each([
// Empty entries in NO_PROXY should not cause out-of-bounds access
["localhost, , example.com"],
[",localhost,example.com"],
["localhost,example.com,"],
[" , , "],
[",,,"],
[". , .. , ..."],
])("NO_PROXY with empty entries does not crash: %s", async no_proxy => {
// We just need to verify parsing NO_PROXY doesn't crash.
// The fetch target doesn't matter - NO_PROXY parsing happens before the connection.
const { exited, stderr: stream } = Bun.spawn({
cmd: [bunExe(), "-e", `fetch("http://localhost:1").catch(() => {})`],
env: {
...bunEnv,
http_proxy: "http://127.0.0.1:1",
NO_PROXY: no_proxy,
},
stderr: "pipe",
});
const [exitCode, stderr] = await Promise.all([exited, stream.text()]);
if (exitCode !== 0) {
console.error("stderr:", stderr);
}
expect(exitCode).toBe(0);
});
});