Skip to content

fix(pg): strip IPv6 URI brackets from host at connect/lookup, not in the parser - #3698

Open
spokodev wants to merge 1 commit into
brianc:masterfrom
spokodev:fix/pg-connection-string-ipv6-host
Open

fix(pg): strip IPv6 URI brackets from host at connect/lookup, not in the parser#3698
spokodev wants to merge 1 commit into
brianc:masterfrom
spokodev:fix/pg-connection-string-ipv6-host

Conversation

@spokodev

Copy link
Copy Markdown

Problem

pg-connection-string leaves the URI square brackets on the host for IPv6 literals, so the parsed host is unusable:

const parse = require('pg-connection-string')
parse('postgresql://[::1]:5432/mydb').host       // '[::1]'         (should be '::1')
parse('postgresql://[2001:db8::1]/db').host      // '[2001:db8::1]' (should be '2001:db8::1')

The brackets are RFC 3986 / WHATWG URI delimiters for an IPv6 host, not part of the address. libpq stores the bracket-free literal, and pg passes config.host straight to net.connect / dns.lookup:

pg connection-parameters.js → this.host = '[::1]' → net.connect(port, '[::1]')
→ net.isIP('[::1]') === 0 → dns.lookup('[::1]') → ENOTFOUND

So every IPv6 connection string fails to connect today. There is no test or option covering IPv6, so this is an unguarded gap, not intentional.

Fix

Strip a leading [ / trailing ] from the parsed hostname:

-  const hostname = dummyHost ? '' : result.hostname
+  // The WHATWG URL parser keeps the square brackets around an IPv6 literal
+  // (e.g. [::1]); strip them so config.host is a bare address that libpq, and
+  // node's net.connect / dns.lookup, can use directly.
+  const hostname = (dummyHost ? '' : result.hostname).replace(/^\[(.+)\]$/, '$1')

Unconditional replace (no new branch, keeps 100% branch coverage); a no-op for non-bracketed hosts.

Verification

  • New test: postgres://user:pw@[2001:db8::1]:5432/mydb → host 2001:db8::1, port 5432; [::1]::1. Fails on master, passes with the fix.
  • Confirmed unaffected: IPv4 / DNS hostnames, percent-encoded Unix-socket hosts (%2F...), the ?host= query-param override, and a bracketed database name (%5Bweird%5D stays [weird]).
  • Cross-checked against libpq conninfo_uri_parse_options, which advances past [ and NUL-terminates at ] (stores the bare literal).
  • Full suite: 80 passing, nyc check-coverage 100% maintained.

@spokodev
spokodev requested a review from hjr3 as a code owner June 23, 2026 16:05
@zoujimmy82-boop

Copy link
Copy Markdown

Independent confirmation, in case a second data point helps this get looked at. Hit while pointing a local tool's DATABASE_URL at an IPv6 loopback.

pg-connection-string@2.14.0 (currently npm latest, so there is no released version with the fix yet):

const { parse } = require('pg-connection-string')
parse('postgres://u@[::1]:5436/db').host         // '[::1]'
parse('postgres://u@[2001:db8::1]:5432/db').host // '[2001:db8::1]'
parse('postgres://u@127.0.0.1:5436/db').host     // '127.0.0.1'  (unaffected)

End-to-end through pg, the bracketed host reaches dns.lookup verbatim and the connection dies before the socket layer:

new Client({ host: '[::1]', port: 5436, ... })  =>  ENOTFOUND getaddrinfo ENOTFOUND [::1]
new Client({ host: '::1',   port: 5436, ... })  =>  connected, `select 1` returns 1

Same server, same port, only the brackets differ — which isolates it to the parse step rather than to IPv6 connectivity (the server here is Postgres in Docker published on
[::]:5436, listening on ::1).

For anyone arriving from a search: the workaround until this lands is 127.0.0.1, or passing host bare through config instead of a connection string.

@brianc

brianc commented Jul 30, 2026

Copy link
Copy Markdown
Owner

thanks for looking into this - curious if this also impacts libpq / the native bindings? I don't want to break them w/ this change. I'd honestly prefer if this is causing an issue for people this is fixed either in dns.lookup or by removing the brackes in client code. One other option is stripping the [ ] from the host only right before that value is sent to dns.lookup but leave the original intact everywhere else. I don't like modifying the host name supplied by a user, exactly. Feels a bit magical and prone to cause surprises (not the good kind!)

@spokodev
spokodev force-pushed the fix/pg-connection-string-ipv6-host branch from 7e80ab2 to 60d608a Compare July 31, 2026 12:09
@spokodev spokodev changed the title fix(pg-connection-string): strip brackets from IPv6 host fix(pg): strip IPv6 URI brackets from host at connect/lookup, not in the parser Jul 31, 2026
@spokodev

Copy link
Copy Markdown
Author

On the native side: I traced it, and the brackets break libpq today too, so this isn't JS-only. The parsed host reaches dns.lookup in both paths.

  • Native: native/client.js -> ConnectionParameters.getLibpqConnectionString, which calls dns.lookup(this.host) (connection-parameters.js:176) to build hostaddr=. With [::1] that lookup returns ENOTFOUND, so it errors before libpq runs. (net.isIP('[::1]') is 0, net.isIP('::1') is 6; dns.lookup('::1') short-circuits to ::1, dns.lookup('[::1]') gives ENOTFOUND.)
  • JS: no explicit lookup in pg; client.js -> connection.js stream.connect(port, host) resolves inside net.connect, same ENOTFOUND, and [::1] also lands in the TLS servername.

libpq's host keyword doesn't take the brackets (those are URI-only), and pg passes hostaddr (the resolved bare address) alongside it, so a bare host is what libpq wants anyway. Stripping doesn't break native; it fixes it.

I agree the parser shouldn't rewrite the user's host, so I did it your way: reverted the pg-connection-string change and strip only at the two resolver boundaries, leaving connectionParameters.host / client.host intact. Those are Connection.connect() (feeds net.connect plus the TLS servername via upgradeToSSL, and covers the cancel() reconnect since it funnels through the same method) and getLibpqConnectionString (feeds dns.lookup plus the host= keyword). Small stripIpv6Brackets helper, no-op for hostnames, IPv4, and socket paths. Tests at both layers; full pg unit suite green.

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.

3 participants