fix: strip IPv6 brackets from outgoing.hostname to fix ENOTFOUND#53
fix: strip IPv6 brackets from outgoing.hostname to fix ENOTFOUND#53cpetit-sw wants to merge 1 commit intosagemathinc:mainfrom
Conversation
The WHATWG URL spec serialises IPv6 hostnames with square brackets,
e.g. new URL("http://[::1]/").hostname === "[::1]". When setupOutgoing
copies this value to outgoing.hostname, Node.js passes the bracketed
form to dns.lookup / getaddrinfo, which fails with ENOTFOUND because
getaddrinfo expects a bare address ("::1"), not "[::1]".
Fix: after copying target properties, strip the brackets from
outgoing.hostname when present. outgoing.host ("[::1]:port") is
already in the correct RFC 2732 format for HTTP Host headers and is
left unchanged.
Also fix hasPort() to correctly handle IPv6 bracket notation:
a plain indexOf(":") falsely matches colons inside the IPv6 address
itself; the port is present only if there are characters after "]".
Adds a regression test (lib/test/http/ipv6-proxy.test.ts) covering
both basic forwarding to an IPv6 backend and the changeOrigin Host
header format.
Fixes sagemathinc#52
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe320bfec7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Listen on IPv6 loopback and return the assigned port. | ||
| async function listenIPv6(server: http.Server): Promise<number> { | ||
| return new Promise((resolve, reject) => { | ||
| server.listen(0, "::1", () => { |
There was a problem hiding this comment.
Skip IPv6 tests when loopback is unavailable
Binding the backend to ::1 unconditionally makes this test fail on environments where IPv6 loopback is disabled (common in some CI containers/hosts), so the suite can regress even though proxy behavior is unchanged. Because listenIPv6 rejects on socket bind errors, these tests should gate on IPv6 availability (or skip on EAFNOSUPPORT/EADDRNOTAVAIL) instead of hard-failing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
let's wait for the tests here then.
Context
I'm not a TypeScript developer — I'm a Site Reliability Engineer who ran into this bug while operating a JupyterHub deployment on an IPv6-only Kubernetes cluster. Every time a user tried to launch a notebook, the proxy would return a 503 with
getaddrinfo ENOTFOUND [2a05:...]in the logs. After digging into the stack (KubeSpawner → configurable-http-proxy → http-proxy-3), I traced the root cause to this library and found the open issue #52.This fix and the accompanying test were developed with AI assistance (Claude). I've done my best to make sure the fix is correct and minimal, but please review carefully — I'm happy to iterate.
Problem
When proxying to an IPv6 target (e.g.
http://[::1]:8888orhttp://[2a05:d018::1]:8888), the proxy fails with:The WHATWG URL spec serialises IPv6 hostnames with square brackets:
setupOutgoingcopiestarget.hostname(="[::1]") directly tooutgoing.hostname, which is then passed tohttp.request. Node.js forwards this value todns.lookup/getaddrinfo, which expects a bare address (::1), not the bracketed form — hence ENOTFOUND.This is the root cause reported in #52.
Fix
After copying the target properties, strip brackets from
outgoing.hostnamewhen present.outgoing.host("[::1]:8888") is already in the correct RFC 2732 format for HTTPHostheaders and is left unchanged.Also fixes
hasPort()which usedindexOf(":")and therefore always returnedtruefor any IPv6 address (even without a port), because IPv6 addresses contain colons. The correct check looks for characters after the closing].Tests
New test file
lib/test/http/ipv6-proxy.test.tscovering:changeOrigin: true— verifies theHostheader is[::1]:port(RFC 2732 bracket notation) for non-standard portsFull test suite: 302 passed, 0 failures.
Real-world context
Triggered in production on an IPv6-only Kubernetes cluster: KubeSpawner registers notebook pods using
pod.status.podIP(an IPv6 address), configurable-http-proxy (JupyterHub) receives routes ashttp://[ipv6]:portand forwards them throughhttp-proxy-3, hitting this bug.Fixes #52