Skip to content

"localhost" favours IPv6 in node v17, used to favour IPv4 #40537

Description

@trentm

Version

v17.0.0

Platform

Darwin pink.local 20.6.0 Darwin Kernel Version 20.6.0: Mon Aug 30 06:12:21 PDT 2021; root:xnu-7195.141.6~3/RELEASE_X86_64 x86_64

Subsystem

No response

What steps will reproduce the bug?

var http = require('http')
var server = http.createServer(function (req, res) { /* ... */ })
server.listen(3000, 'localhost', function (_err) { 
  console.log('server listening: %s', server.address())
})

I'm not sure what the underlying mechanism is, but it looks to me like the resolution of "localhost" for server.listen(PORT, HOST, ...) and for http.request('http://localhost/...', ...) changed from favouring IPv4 in node v16 and earlier, to favouring IPv6. The result is some possibly confusing breakages when using IPv4 values such as 127.0.0.1 and 0.0.0.0. For example, the following script errors with node v17, but succeeds with node v16:

// example-localhost-means-which-ipv.js
var http = require('http')

var server = http.createServer(function (req, res) {
  req.on('data', function (chunk) {
    console.log('server req data: %s', chunk)
  })
  req.on('end', function () {
    console.log('server req end')
    res.end('pong')
  })
})

// Listen on IPv4 address.
var theHost = '127.0.0.1'
server.listen(3000, theHost, function (_err) {
  console.log('server listening: %s', server.address())

  // GET from localhost.
  var theUrl = 'http://localhost:3000/ping'
  console.log('client req: GET %s', theUrl)
  http.get(theUrl, function (res) {
    console.log('client res:', res.statusCode, res.headers)
    res.on('data', (chunk) => {
      console.log('client data: %s', chunk)
    })
    res.on('end', () => {
      console.log('client end')
      server.close()
    })
  })
})
% node --version
v17.0.0

% node example-localhost-means-which-ipv.js
server listening: { address: '127.0.0.1', family: 'IPv4', port: 3000 }
client req: GET http://localhost:3000/ping
node:events:368
      throw er; // Unhandled 'error' event
      ^

Error: connect ECONNREFUSED ::1:3000
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
Emitted 'error' event on ClientRequest instance at:
    at Socket.socketErrorListener (node:_http_client:447:9)
    at Socket.emit (node:events:390:28)
    at emitErrorNT (node:internal/streams/destroy:164:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -61,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '::1',
  port: 3000
}

Node.js v17.0.0

% nvm use 16
Now using node v16.11.1 (npm v8.0.0)

% node example-localhost-means-which-ipv.js
server listening: { address: '127.0.0.1', family: 'IPv4', port: 3000 }
client req: GET http://localhost:3000/ping
server req end
client res: 200 {
  date: 'Wed, 20 Oct 2021 21:01:56 GMT',
  connection: 'close',
  'content-length': '4'
}
client data: pong
client end

Similarly, if we reverse things to listen on 'localhost' and GET from 'http://[::1]:3000/ping' (IPv6), then it fails on node v16 and passes on node v17.

As I said above, I don't know the mechanism for selecting IPv4 or IPv6. I suspect this isn't a "bug", except perhaps a lack of documentation? I don't see any mention of localhost of IPv6 in the changelog. Thanks.

How often does it reproduce? Is there a required condition?

Everytime. While I tested mostly on macOS, I believe I was seeing this in GitHub Action tests running on linux containers.

What is the expected behavior?

No response

What do you see instead?

No response

Additional information

No response

Activity

  1. trentm commented on Oct 20, 2021

    @trentm
    ContributorAuthor

    If it helps, I happened upon this is tests using Hapi. A Hapi Server listen address defaults to '0.0.0.0' which pins it to IPv4. When my test made a client request using localhost I saw the above ECONNREFUSED ::1:$PORT.

  2. richardlau commented on Oct 20, 2021

    @richardlau
    Member

    It's #39987 -- Node.js now returns IP addresses in the order they are returned from the name resolver/DNS. There's a --dns-result-order command line option you can set to control the behaviour -- the difference in Node.js 17 is the default changed.

  3. trentm commented on Oct 20, 2021

    @trentm
    ContributorAuthor

    Ah, which is the first "Other notable changes" in the release notes. 🤦 Thanks for the pointer! I'll close this, as it is definitely intended behaviour. A mention of this in some sort of "node v17 migration guide" would likely be helpful to others.

  4. added a commit that references this issue on Nov 2, 2021
  5. macrozone commented on Sep 5, 2022

    @macrozone

    Just run into this.

    one fix is to do this:

    
    import dns from 'node:dns';
    dns.setDefaultResultOrder('ipv4first');
    

    which reverts the old order

  6. 17 remaining items

  7. undergroundwires commented on Mar 30, 2024

    @undergroundwires

    Here's my workaround (open-source and documented) that I hope that can help you too:

    After days of research and trial/error, this is how I got this working:

    1. Create a script called force-ipv4.sh, that configures system to prefer IPv4 over IPv6, call it to configure the machine. It was not easy to find a reliable cross-platform solution and I went Cloudflare WARP for DNS resolution along with some system configurations.
    2. To easily use the script in GitHub workflows, create GitHub action called force-ipv4 and call it in GitHub runners.
    3. Fixes the IPv6 request issues, and you can happily run e.g. fetch API from Node.

    Related commit introducing this fix: undergroundwires/privacy.sexy@52fadcd

  8. added a commit that references this issue on May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions