Skip to content

http: don't inject default :80/:443 in proxy request-line - #29378

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/921cc731/proxy-default-port
Apr 17, 2026
Merged

Jarred-Sumner merged 4 commits into
mainfrom
farm/921cc731/proxy-default-port

Conversation

@robobun

@robobun robobun commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

Fixes #29371.

Repro

// proxy-spy.mjs — logs raw request lines
import net from "node:net";
net.createServer(s => s.on("data", d => {
  console.log(d.toString().split("\r\n")[0]);
  s.end("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nok\n");
})).listen(9999);
$ HTTP_PROXY=http://localhost:9999 bun -e 'await fetch("http://example.com/test", {method:"POST", body:""})'
POST http://example.com:80/test HTTP/1.1   ← Bun
POST http://example.com/test HTTP/1.1      ← curl, Node, expected

Cause

writeProxyRequest in src/http.zig unconditionally wrote :<port> to the absolute-form request-URI, falling back to "80" / "443" when the URL had no explicit port. Per RFC 7230 §5.3.2 the default port should be omitted, and strict proxies (Charles, mitmproxy, corporate middleboxes) refuse to accept :80 as equivalent to no port when matching authorities.

Fix

Only emit :<port> when client.url.getPort() returns a value — i.e. the URL had an explicit port. getPort() returns null for empty port, so no explicit port = no :port in the request-line. Explicit non-default ports (e.g. http://host:8080/) still get written through.

Verification

Regression test at test/regression/issue/29371.test.ts spins up a TCP listener acting as an HTTP proxy and asserts the first line of the forwarded request:

  • no explicit port (http://example.com/test) → POST http://example.com/test HTTP/1.1 ✓
  • explicit non-default port (http://example.com:8080/test) → GET http://example.com:8080/test HTTP/1.1 ✓
  • WHATWG-normalized-away port (http://example.com:80/test → fetch strips :80) → GET http://example.com/test HTTP/1.1 ✓

Fail-before / pass-after: the two no-explicit-port tests fail on main, all three pass with this change. Existing test/js/bun/http/proxy.test.ts has the same pre-existing failures with and without this patch.

When sending a request to an HTTP proxy, the absolute-form request URI
(RFC 7230 §5.3.2) should omit the default port. Bun was unconditionally
writing `:<port>` — falling back to "80"/"443" when the URL had no
explicit port — which turned `http://example.com/path` into
`POST http://example.com:80/path HTTP/1.1` on the wire.

That breaks proxies that do strict Host/authority matching (Charles,
mitmproxy, corporate middleboxes) and diverges from curl and Node's
`http.request`, both of which omit the default port.

Fix: only emit `:<port>` when the original URL had an explicit port.

Fixes #29371
@robobun

robobun commented Apr 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:08 PM PT - Apr 16th, 2026

❌ @robobun, your commit 9684a51 has 2 failures in Build #45963 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29378

That installs a local version of the PR into your bun-29378 executable, so you can run:

bun-29378 --bun

@coderabbitai

coderabbitai Bot commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 463cf7cc-390c-4584-8867-1d77bfca7fa0

📥 Commits

Reviewing files that changed from the base of the PR and between 2bdcdde and 9684a51.

📒 Files selected for processing (2)
  • test/js/bun/perf/static-initializers.test.ts
  • test/js/node/process/process.test.js

Walkthrough

Changed proxy request formatting so the absolute-form URI written by writeProxyRequest() omits default ports and only appends :<port> when the original URL included an explicit port. Added regression tests that verify omission/preservation of ports in proxied request-lines and adjusted unrelated test expectations.

Changes

Cohort / File(s) Summary
HTTP proxy formatting
src/http.zig
Modified writeProxyRequest() to always write hostname and path and to append :<port> only if client.url.getPort() indicates an explicit port was present (prevents injecting :80/:443).
Regression tests (proxy request lines)
test/regression/issue/29371.test.ts
Added a TCP-proxy-based regression test capturing raw proxied request-lines and headers with three cases: omit default :80, preserve explicit :8080, and normalize :80 to omitted form. Ensures proxy is shut down and sockets cleaned up.
Test expectations updates
test/js/bun/static-initializers.test.ts, test/js/node/process/process.test.js
Updated expected static-initializer counts for macOS/arm64 and adjusted the expected mimalloc commit hash in process.versions test fixture.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: stopping the injection of default ports (:80/:443) in proxy request-line URIs, which matches the primary code fix in src/http.zig.
Description check ✅ Passed The description covers both required sections from the template: 'What does this PR do?' (detailed explanation of the fix and cause) and 'How did you verify your code works?' (regression test details).
Linked Issues check ✅ Passed The PR directly addresses issue #29371 by fixing the root cause: stopping the unconditional writing of default ports in the proxy request-line. The change uses client.url.getPort() to conditionally emit : only when explicitly present, matching the required fix.
Out of Scope Changes check ✅ Passed The primary fix in src/http.zig directly targets the proxy port issue. Test updates (mimalloc hash, static-initializer counts) are pre-existing synchronization unrelated to the proxy fix, acknowledged by the author as necessary to unblock CI.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29371.test.ts`:
- Around line 2-8: The file header contains a multi-line bug-context block that
should be trimmed to the project standard: replace the long explanatory prose at
the top of test/regression/issue/29371.test.ts with a single-line GitHub issue
URL comment (keeping the existing issue link), removing the multi-line
paragraph; ensure the file retains the single issue URL comment pattern used
across test/regression/issue/* so only the one-line comment remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: af0df62b-3a44-402b-adda-119897f913d9

📥 Commits

Reviewing files that changed from the base of the PR and between b2d3f5d and 36410f6.

📒 Files selected for processing (2)
  • src/http.zig
  • test/regression/issue/29371.test.ts

Comment thread test/regression/issue/29371.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the Zig change is a clean, minimal fix for a well-documented RFC violation.

Extended reasoning...

Overview

The PR modifies two files: src/http.zig (5 lines removed, 4 added in writeProxyRequest) and a new regression test test/regression/issue/29371.test.ts. The core change removes the unconditional default-port injection in the absolute-form proxy request-URI.

Security risks

None. The change only affects how the port component is formatted in outgoing proxy request-lines. No auth, crypto, or permission-sensitive code is touched.

Level of scrutiny

Low. The fix is a single conditional expression change with clear RFC justification (RFC 7230 §5.3.2). The behavior difference (omitting vs. including the redundant default port) is well-understood, and the fix matches curl and Node.js behavior. The regression test covers all three relevant cases.

Other factors

The only reported issue is a nit in test code (socket.on('error', () => {}) should call socket.destroy() to avoid a theoretical hang in proxy.close()). This is test-code only and an extremely low-probability race on localhost. The inline comment already captures this. The core production change is unambiguously correct.

Comment thread test/regression/issue/29371.test.ts
An empty error handler suppresses the throw but leaves the socket
tracked by the server, so server.close() can't emit 'close'. Call
socket.destroy() on error so proxy.close() always completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
test/regression/issue/29371.test.ts (1)

2-8: ⚠️ Potential issue | 🟡 Minor

Keep only the single issue URL header comment.

Please remove the multi-line bug-context prose at the top and keep just the single URL line to match regression test file convention.

✂️ Suggested edit
 // 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.
Based on learnings: in `test/regression/issue/*`, keep the single-line GitHub issue URL comment and avoid multi-line bug-context prose.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/29371.test.ts` around lines 2 - 8, The top of the
regression test file test/regression/issue/29371.test.ts contains a multi-line
bug-context comment block; replace that block with a single-line GitHub issue
URL comment only (remove all explanatory prose), ensuring the file follows the
test/regression/issue/* convention of keeping just the issue URL comment at the
top.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/regression/issue/29371.test.ts`:
- Around line 2-8: The top of the regression test file
test/regression/issue/29371.test.ts contains a multi-line bug-context comment
block; replace that block with a single-line GitHub issue URL comment only
(remove all explanatory prose), ensuring the file follows the
test/regression/issue/* convention of keeping just the issue URL comment at the
top.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86b8d560-ca66-4c5a-aa92-ceb97e3d2ef6

📥 Commits

Reviewing files that changed from the base of the PR and between 36410f6 and 2bdcdde.

📒 Files selected for processing (1)
  • test/regression/issue/29371.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the socket.destroy() fix addressed my prior feedback and the RFC 7230 §5.3.2 fix in http.zig is correct.

Extended reasoning...

Overview

Two files changed: src/http.zig removes the unconditional default-port injection in writeProxyRequest(), and test/regression/issue/29371.test.ts adds three regression test cases (no explicit port, explicit non-default port, WHATWG-normalized-away port).

Security risks

None. This is a bug fix to HTTP request formatting; it only affects the shape of the absolute-form URI in the proxy request-line.

Level of scrutiny

The zig change is a 9-line diff: removes a dead port-fallback variable, adds a conditional write that mirrors what getPort() already exposes. It precisely implements RFC 7230 §5.3.2 and matches curl/Node behavior. Low risk, well-scoped.

Other factors

My earlier inline comment about the missing socket.destroy() call in the error handler has been resolved — the current diff already contains socket.on("error", () => socket.destroy()), and the inline comment is marked resolved. No bugs were found by the automated system. CI build failures visible in the timeline are from an intermediate autofix.ci commit; the most recent commits on main include both the http.zig fix and the test helper fix.

@robobun

robobun commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator Author

CI failures on 2bdcdde are pre-existing on main, not caused by this change:

  • test/js/node/process/process.test.js — process.versions.mimalloc expected 9a5e1f52... but got a29368ef.... The MIMALLOC_COMMIT in scripts/build/deps/mimalloc.ts was bumped to a29368ef (the child of 9a5e1f52) in the "Un-revert mimalloc upgrade" commit without updating the test. Fails on any lane that actually links mimalloc (windows, alpine).
  • test/js/bun/perf/static-initializers.test.ts — expects 3 initializers on arm64, got 2. Comment in the test refers to MI_OSX_ZONE=ON but scripts/build/deps/mimalloc.ts has it OFF on darwin.

This PR only modifies src/http.zig writeProxyRequest() — nothing that touches mimalloc or static initializers. Happy to add a small test-sync commit if that would unblock merging.

Main's `Un-revert mimalloc upgrade` bumped the mimalloc pin to
a29368ef but left two tests referencing the previous state:

- process.versions.mimalloc expected 9a5e1f52 (parent of a29368ef).
  Fails deterministically on windows/linux/alpine/debian/ubuntu lanes
  that actually link mimalloc.

- static-initializers expected 3/4 initializers assuming
  MI_OSX_ZONE=ON, but scripts/build/deps/mimalloc.ts sets ZONE=OFF
  on darwin (zone override breaks NAPI addons), so the zone-swap
  constructor doesn't run. Actual count is 2 on arm64 / 3 on x64.

Pre-existing on main; included here to unblock CI.
@robobun

robobun commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator Author

Added a second commit (9684a51) to sync two tests that main's "Un-revert mimalloc upgrade" left out of date:

  • process.versions.mimalloc: expected 9a5e1f52, actual a29368ef (parent/child on oven-sh/mimalloc). Deterministic failure on every lane that links mimalloc.
  • static-initializers: comment claimed MI_OSX_ZONE=ON (3/4 initializers) but scripts/build/deps/mimalloc.ts has ZONE=OFF on darwin (zone override breaks NAPI), so actual is 2/3.

Pre-existing on main; folded in here to unblock CI. Happy to split out to a separate PR if preferred.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — previous feedback addressed, fix is correct.

Extended reasoning...

Overview

The PR touches four files: the core HTTP client in src/http.zig, a new regression test in test/regression/issue/29371.test.ts, and two test expectation syncs for mimalloc (process.test.js, static-initializers.test.ts).

Core fix

The change in writeProxyRequest() removes unconditional :port injection, writing it only when client.url.getPort() returns non-null. This is a one-logical-step fix that aligns with RFC 7230 §5.3.2 and matches curl/Node behavior.

My prior feedback

My previous inline comment flagged that socket.on('error', () => {}) in the test helper left sockets untracked after errors, potentially preventing server.close() from emitting 'close'. Commit 2bdcdde addressed this with socket.on('error', () => socket.destroy()); both inline comments are marked resolved.

Security risks

None — this is a request formatting change in client code with no auth, crypto, or permission implications.

Level of scrutiny

Low. The change is minimal, correct, well-tested with three targeted regression cases, and consistent with existing HTTP client patterns.

@robobun

robobun commented Apr 17, 2026

Copy link
Copy Markdown
Collaborator Author

Latest CI failure is test/js/bun/shell/leak.test.ts on alpine-aarch64: TOO many ParsedShellScript or ShellInterpreter objects 11 2. Known-flaky inline-GC test — PR #25880 moved these subcases in-process with a threshold of 3, no await Promise.all(promises) before the assertion, and describe.concurrent around the outer fd leak group. No relation to this PR (src/http.zig proxy request formatting); all 4 runner retries lost to an unlucky alpine-aarch64 host. A maintainer retry of just that step should turn the build green.

@Jarred-Sumner
Jarred-Sumner merged commit d9e397e into main Apr 17, 2026
60 of 61 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/921cc731/proxy-default-port branch April 17, 2026 00:46
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
)

Fixes oven-sh#29371.

## Repro

```js
// proxy-spy.mjs — logs raw request lines
import net from "node:net";
net.createServer(s => s.on("data", d => {
  console.log(d.toString().split("\r\n")[0]);
  s.end("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nok\n");
})).listen(9999);
```

```console
$ HTTP_PROXY=http://localhost:9999 bun -e 'await fetch("http://example.com/test", {method:"POST", body:""})'
POST http://example.com:80/test HTTP/1.1   ← Bun
POST http://example.com/test HTTP/1.1      ← curl, Node, expected
```

## Cause

`writeProxyRequest` in `src/http.zig` unconditionally wrote `:<port>` to
the absolute-form request-URI, falling back to `"80"` / `"443"` when the
URL had no explicit port. Per RFC 7230 §5.3.2 the default port should be
omitted, and strict proxies (Charles, mitmproxy, corporate middleboxes)
refuse to accept `:80` as equivalent to no port when matching
authorities.

## Fix

Only emit `:<port>` when `client.url.getPort()` returns a value — i.e.
the URL had an explicit port. `getPort()` returns `null` for empty
`port`, so no explicit port = no `:port` in the request-line. Explicit
non-default ports (e.g. `http://host:8080/`) still get written through.

## Verification

Regression test at `test/regression/issue/29371.test.ts` spins up a TCP
listener acting as an HTTP proxy and asserts the first line of the
forwarded request:

- no explicit port (`http://example.com/test`) → `POST
http://example.com/test HTTP/1.1` ✓
- explicit non-default port (`http://example.com:8080/test`) → `GET
http://example.com:8080/test HTTP/1.1` ✓
- WHATWG-normalized-away port (`http://example.com:80/test` → fetch
strips `:80`) → `GET http://example.com/test HTTP/1.1` ✓

Fail-before / pass-after: the two no-explicit-port tests fail on `main`,
all three pass with this change. Existing
`test/js/bun/http/proxy.test.ts` has the same pre-existing failures with
and without this patch.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
)

Fixes oven-sh#29371.

## Repro

```js
// proxy-spy.mjs — logs raw request lines
import net from "node:net";
net.createServer(s => s.on("data", d => {
  console.log(d.toString().split("\r\n")[0]);
  s.end("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nok\n");
})).listen(9999);
```

```console
$ HTTP_PROXY=http://localhost:9999 bun -e 'await fetch("http://example.com/test", {method:"POST", body:""})'
POST http://example.com:80/test HTTP/1.1   ← Bun
POST http://example.com/test HTTP/1.1      ← curl, Node, expected
```

## Cause

`writeProxyRequest` in `src/http.zig` unconditionally wrote `:<port>` to
the absolute-form request-URI, falling back to `"80"` / `"443"` when the
URL had no explicit port. Per RFC 7230 §5.3.2 the default port should be
omitted, and strict proxies (Charles, mitmproxy, corporate middleboxes)
refuse to accept `:80` as equivalent to no port when matching
authorities.

## Fix

Only emit `:<port>` when `client.url.getPort()` returns a value — i.e.
the URL had an explicit port. `getPort()` returns `null` for empty
`port`, so no explicit port = no `:port` in the request-line. Explicit
non-default ports (e.g. `http://host:8080/`) still get written through.

## Verification

Regression test at `test/regression/issue/29371.test.ts` spins up a TCP
listener acting as an HTTP proxy and asserts the first line of the
forwarded request:

- no explicit port (`http://example.com/test`) → `POST
http://example.com/test HTTP/1.1` ✓
- explicit non-default port (`http://example.com:8080/test`) → `GET
http://example.com:8080/test HTTP/1.1` ✓
- WHATWG-normalized-away port (`http://example.com:80/test` → fetch
strips `:80`) → `GET http://example.com/test HTTP/1.1` ✓

Fail-before / pass-after: the two no-explicit-port tests fail on `main`,
all three pass with this change. Existing
`test/js/bun/http/proxy.test.ts` has the same pre-existing failures with
and without this patch.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun fetch inserts explicit :80 into proxy request line for HTTP URLs

2 participants