Skip to content

fix: strip IPv6 brackets from outgoing.hostname to fix ENOTFOUND#53

Open
cpetit-sw wants to merge 1 commit intosagemathinc:mainfrom
cpetit-sw:fix/ipv6-hostname-brackets
Open

fix: strip IPv6 brackets from outgoing.hostname to fix ENOTFOUND#53
cpetit-sw wants to merge 1 commit intosagemathinc:mainfrom
cpetit-sw:fix/ipv6-hostname-brackets

Conversation

@cpetit-sw
Copy link

@cpetit-sw cpetit-sw commented Feb 28, 2026

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]:8888 or http://[2a05:d018::1]:8888), the proxy fails with:

Error: getaddrinfo ENOTFOUND [::1]

The WHATWG URL spec serialises IPv6 hostnames with square brackets:

new URL("http://[::1]:8888/").hostname // → "[::1]"  (brackets included)
new URL("http://[::1]:8888/").host     // → "[::1]:8888"

setupOutgoing copies target.hostname (= "[::1]") directly to outgoing.hostname, which is then passed to http.request. Node.js forwards this value to dns.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.hostname when present. outgoing.host ("[::1]:8888") is already in the correct RFC 2732 format for HTTP Host headers and is left unchanged.

Also fixes hasPort() which used indexOf(":") and therefore always returned true for 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.ts covering:

  1. Basic forwarding to an IPv6 backend (the ENOTFOUND regression)
  2. changeOrigin: true — verifies the Host header is [::1]:port (RFC 2732 bracket notation) for non-standard ports

Full 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 as http://[ipv6]:port and forwards them through http-proxy-3, hitting this bug.

Fixes #52

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
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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", () => {

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Author

@cpetit-sw cpetit-sw Mar 2, 2026

Choose a reason for hiding this comment

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

let's wait for the tests here then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fails to connect to IPv6 addresses

1 participant