Skip to content

fix(pg-connection-string): strip brackets from IPv6 host - #3698

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

fix(pg-connection-string): strip brackets from IPv6 host#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.

The WHATWG URL parser keeps the square brackets around an IPv6 literal,
so parse('postgresql://[::1]:5432/db').host returned '[::1]'. libpq
stores the address without the brackets, and consumers pass config.host
straight to net.connect / dns.lookup, which treat '[::1]' as a hostname
and fail with ENOTFOUND. Strip the brackets so IPv6 connection strings
resolve to the bare address.
@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!)

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