Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/http.zig
Original file line number Diff line number Diff line change
Expand Up @@ -389,19 +389,19 @@ fn writeProxyRequest(
request: picohttp.Request,
client: *HTTPClient,
) !void {
var port: []const u8 = undefined;
if (client.url.getPort()) |_| {
port = client.url.port;
} else {
port = if (client.url.isHTTPS()) "443" else "80";
}

_ = writer.write(request.method) catch 0;
// will always be http:// here, https:// needs CONNECT tunnel
_ = writer.write(" http://") catch 0;
_ = writer.write(client.url.hostname) catch 0;
_ = writer.write(":") catch 0;
_ = writer.write(port) catch 0;
// Only include the port in the absolute-form request URI when the
// original URL had an explicit port. RFC 7230 §5.3.2 treats the default
// port as redundant, and writing `:80`/`:443` here breaks proxies that
// do strict Host/authority matching (e.g. Charles, mitmproxy). Matches
// curl and Node.js `http.request` behavior.
if (client.url.getPort()) |_| {
_ = writer.write(":") catch 0;
_ = writer.write(client.url.port) catch 0;
}
_ = writer.write(request.path) catch 0;
_ = writer.write(" HTTP/1.1\r\nProxy-Connection: Keep-Alive\r\n") catch 0;

Expand Down
9 changes: 5 additions & 4 deletions test/js/bun/perf/static-initializers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ describe("static initializers", () => {
.map(a => a.trim())
.filter(line => line.includes("running initializer") && line.includes(bunExe()));

// mimalloc v3 with MI_OSX_ZONE=ON contributes three: __GLOBAL__sub_I_static.c,
// mi_process_attach, and _mi_macos_override_malloc (the zone-swap constructor).
// On x86_64 there is also ___cpu_indicator_init from CPU feature detection.
// mimalloc v3 on darwin (MI_OSX_ZONE=OFF in scripts/build/deps/mimalloc.ts —
// zone override breaks NAPI addons) contributes two: __GLOBAL__sub_I_static.c
// and mi_process_attach. On x86_64 there is also ___cpu_indicator_init from
// CPU feature detection.
expect(
bunInitializers.length,
`Do not add static initializers to Bun. Static initializers are called when Bun starts up, regardless of whether you use the variables or not. This makes Bun slower.`,
).toBe(process.arch === "arm64" ? 3 : 4);
).toBe(process.arch === "arm64" ? 2 : 3);
});
});
2 changes: 1 addition & 1 deletion test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ it("process.versions", () => {
const expectedVersions = {
boringssl: "0c5fce43b7ed5eb6001487ee48ac65766f5ddcd1",
libarchive: "ded82291ab41d5e355831b96b0e1ff49e24d8939",
mimalloc: "9a5e1f52cdf4662f9590b69de104a4469140796f",
mimalloc: "a29368ef60d5c90bd760ff42a36ad4ad919a9ad7",
picohttpparser: "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
zlib: "886098f3f339617b4243b286f5ed364b9989e245",
tinycc: "12882eee073cfe5c7621bcfadf679e1372d4537b",
Expand Down
114 changes: 114 additions & 0 deletions test/regression/issue/29371.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// https://github.com/oven-sh/bun/issues/29371
//
// Bun was unconditionally inserting `:80` (or `:443`) into the absolute-form
// request-URI sent to an HTTP proxy, even when the target URL had no explicit
// port. That turned e.g. `http://example.com/path` into
// `POST http://example.com:80/path HTTP/1.1`, which breaks proxies that do
// strict Host/authority matching. Per RFC 7230 §5.3.2 the default port should
// be omitted; curl and Node's `http.request` both do this.
Comment thread
robobun marked this conversation as resolved.

import { expect, test } from "bun:test";
import { once } from "node:events";
import net from "node:net";

function createCapturingProxy() {
const requests: string[] = [];
const server = net.createServer((socket: net.Socket) => {
let buf = "";
socket.on("data", chunk => {
buf += chunk.toString("utf8");
// Capture the request-line + headers on the first request we see.
const headerEnd = buf.indexOf("\r\n\r\n");
if (headerEnd !== -1) {
requests.push(buf.slice(0, headerEnd));
socket.end("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok");
buf = "";
}
});
Comment thread
robobun marked this conversation as resolved.
// If the socket errors (e.g. client RSTs mid-write), destroy it so
// server.close() can drop its tracking and emit 'close'. A bare
// empty handler leaves the socket tracked forever.
socket.on("error", () => socket.destroy());
});
return {
server,
requests,
async listen() {
server.listen(0);
await once(server, "listening");
return (server.address() as net.AddressInfo).port;
},
async close() {
server.close();
await once(server, "close");
},
};
}

test("proxy request-line omits default :80 for http:// without explicit port", async () => {
const proxy = createCapturingProxy();
const port = await proxy.listen();
try {
const res = await fetch("http://example.com/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hello: "world" }),
proxy: `http://localhost:${port}`,
keepalive: false,
});
expect(res.status).toBe(200);
await res.text();

expect(proxy.requests.length).toBeGreaterThanOrEqual(1);
const requestLine = proxy.requests[0].split("\r\n")[0];
// Must NOT contain the injected default port.
expect(requestLine).toBe("POST http://example.com/test HTTP/1.1");
expect(requestLine).not.toContain(":80");
} finally {
await proxy.close();
}
});

test("proxy request-line keeps explicit non-default port for http://host:PORT/", async () => {
const proxy = createCapturingProxy();
const port = await proxy.listen();
try {
const res = await fetch("http://example.com:8080/test", {
method: "GET",
proxy: `http://localhost:${port}`,
keepalive: false,
});
expect(res.status).toBe(200);
await res.text();

expect(proxy.requests.length).toBeGreaterThanOrEqual(1);
const requestLine = proxy.requests[0].split("\r\n")[0];
// Explicit non-default port must be preserved.
expect(requestLine).toBe("GET http://example.com:8080/test HTTP/1.1");
} finally {
await proxy.close();
}
});

test("proxy request-line strips explicit :80 that fetch normalized away", async () => {
// Even if the user writes `:80`, WHATWG URL normalization in fetch() drops
// it before the URL reaches the HTTP client. The request-line must match
// that normalized form — no phantom `:80` reappearing on the wire.
const proxy = createCapturingProxy();
const port = await proxy.listen();
try {
const res = await fetch("http://example.com:80/test", {
method: "GET",
proxy: `http://localhost:${port}`,
keepalive: false,
});
expect(res.status).toBe(200);
await res.text();

expect(proxy.requests.length).toBeGreaterThanOrEqual(1);
const requestLine = proxy.requests[0].split("\r\n")[0];
expect(requestLine).toBe("GET http://example.com/test HTTP/1.1");
} finally {
await proxy.close();
}
});
Loading