Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
trentm opened this issue Oct 20, 2021 · 12 comments
Closed

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

trentm opened this issue Oct 20, 2021 · 12 comments

Comments

@trentm
Copy link
Contributor

trentm commented Oct 20, 2021

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

@trentm
Copy link
Contributor Author

trentm commented Oct 20, 2021

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.

@richardlau
Copy link
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.

trentm added a commit to elastic/apm-agent-nodejs that referenced this issue Oct 20, 2021
@trentm
Copy link
Contributor Author

trentm commented Oct 20, 2021

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.

@trentm trentm closed this as completed Oct 20, 2021
sjrd added a commit to sjrd/scala-js-js-envs that referenced this issue Nov 2, 2021
Node.js 17 switched from resolving host names to IPv4 by default to
resolving to the order given by the OS. This was an intended change
done in nodejs/node#39987, which
nevertheless caused issues in downstream projects, as reported in
nodejs/node#40537.

scalajs-env-nodejs hit that issue, as the JVM server opened on IPv4
by default (apparently this is what the JVM does), but the client
tried to connect via IPv6 with Node.js 17. We fix the issue by
forcing the use of IPv4 in the Node.js client, as well as on the
JVM server for good measure.
markdoliner-doma added a commit to StatesTitle/thrift-server that referenced this issue Jul 6, 2022
We [recently added some better exception handling](#9) but unfortunately printing the response body isn't working because [`response.text()` is a Promise](https://github.github.io/fetch/#response-body). The message looks like this: `ThriftRequestError: Thrift request failed: HTTP 503 Service Unavailable: [object Promise]`

The fix is to use `.then()` to get the body.

I have not tested this. I did run `npm run test` and some of the tests pass, but 75 out of 108 thrift-integration tests fail. I'm seeing this error a lot:
```
request to http://localhost:8090/thrift failed, reason: connect ECONNREFUSED ::1:8090
```
I think it also might have been happening before this change. It looks unrelated. It seems like it's happening because [Hapi listens on 127.0.0.1](https://hapi.dev/api/?v=20.2.2#-serveroptionsaddress) and [Node v17 and higher stopped forcing IPv64addresses to be listed first when resolving domains](nodejs/node#40537 (comment)) so now IPv6 addresses can be returned first. I'll probably post a separate PR for that, but for now I'd prefer not to block this change.
markdoliner-doma added a commit to StatesTitle/thrift-server that referenced this issue Jul 6, 2022
And specify it as "localhost" for the servers that are started by the integration tests.

Some of the integration tests were failing for me because:
- My laptop is configured with both IPv4 and IPv6 for localhost (and non-localhost, too, but that's not relevant here).
- Hapi binds to 0.0.0.0 by default ([API docs](https://hapi.dev/api/?v=20.2.2#-serveroptionsaddress), [code reference](https://github.com/hapijs/hapi/blob/b8ba0adc7c3255995cb56a9a740c4f9750b80e6b/lib/core.js#L339)).
- That's an IPv4 address so the server will only accept connections over IPv4 and not IPv6.
- Our integration test config specifies `localhost` when creating the clients for connecting to these servers.
- `localhost` can resolve to an IPv6 address.
- Node before v17 sorted IP addresses from the name resolver so IPv4 addresses were first but Node v17 and newer returns them in the order returned by the resolver ([GitHub comment](nodejs/node#40537 (comment)), [relevant PR](nodejs/node#39987), it's the first "Other Notable Changes" in [the v17.0.0 release notes](https://nodejs.org/en/blog/release/v17.0.0/#other-notable-changes)). Apparently on my laptop an IPv6 address is returned before an IPv4 address.

Alternatively I could have changed the test config so clients connect to 127.0.0.1. I like this change because:
- It's a good idea to allow users to specify the address to bind to.
- Now the test servers will bind to localhost instead of 0.0.0.0 and it's nice to avoid accepting outside connections when possible.
markdoliner-doma added a commit to StatesTitle/thrift-server that referenced this issue Jul 6, 2022
And specify it as "localhost" for the servers that are started by the integration tests.

Some of the integration tests were failing for me because:
- My laptop is configured with both IPv4 and IPv6 for localhost (and non-localhost, too, but that's not relevant here).
- Hapi binds to 0.0.0.0 by default ([API docs](https://hapi.dev/api/?v=20.2.2#-serveroptionsaddress), [code reference](https://github.com/hapijs/hapi/blob/b8ba0adc7c3255995cb56a9a740c4f9750b80e6b/lib/core.js#L339)).
- That's an IPv4 address so the server will only accept connections over IPv4 and not IPv6.
- Our integration test config specifies `localhost` when creating the clients for connecting to these servers.
- `localhost` can resolve to an IPv6 address.
- Node before v17 sorted IP addresses from the name resolver so IPv4 addresses were first but Node v17 and newer returns them in the order returned by the resolver ([GitHub comment](nodejs/node#40537 (comment)), [relevant PR](nodejs/node#39987), it's the first "Other Notable Changes" in [the v17.0.0 release notes](https://nodejs.org/en/blog/release/v17.0.0/#other-notable-changes)). Apparently on my laptop an IPv6 address is returned before an IPv4 address.

Alternatively I could have changed the test config so clients connect to 127.0.0.1. I like this change because:
- It's nice to allow users to specify the address to bind to anyway.
- Now the test servers will bind to localhost instead of 0.0.0.0 and it's nice to avoid accepting outside connections when possible.
@zihotki
Copy link

zihotki commented Sep 5, 2023

I've stumbled upon this issue as well. The issue happened on Windows pc. The fixes mentioned above don't work for me (or not feasible) even using v20.

Windows favours IPv6 by default (though I'm not completely sure if it's overall default or it's default for our org) and looks like Node on Windows follows that behaviour. In our situation the following worked:

netsh interface ipv6 set prefix ::ffff:0:0/96 60 4

This command changes precedence rules for dns resolution in a way so IPv4 addresses will have higher precedence. Which means if there are IPv4 and IPv6 address for localhost, it'll pick IPv4. After this change the problem disappeared.

rak-phillip added a commit to rak-phillip/rancher-desktop that referenced this issue Oct 9, 2023
This resolves an error we were experiencing in the test

```
FetchError: request to http://localhost:6109/list failed, reason: connect ECONNREFUSED ::1:6109
```

Node.js used to favor IPv4 but that changed in v17

> 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.

nodejs/node#40537 (comment)

Signed-off-by: Phillip Rak <rak.phillip@gmail.com>
rak-phillip added a commit to rak-phillip/rancher-desktop that referenced this issue Nov 1, 2023
This resolves an error we were experiencing in the test

```
FetchError: request to http://localhost:6109/list failed, reason: connect ECONNREFUSED ::1:6109
```

Node.js used to favor IPv4 but that changed in v17

> 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.

nodejs/node#40537 (comment)

Signed-off-by: Phillip Rak <rak.phillip@gmail.com>
s22su added a commit to salemove/broccoli that referenced this issue Nov 23, 2023
This fixes using broccoli locally with nodejs 18.x. Since node 17.x it
favours IPv6 over IPv4: nodejs/node#40537
@undergroundwires
Copy link

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

undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Mar 30, 2024
This commit upgrades Node.js version to v20.x in CI/CD environment.

Previously used Node 18.x is moving towards end-of-life, with a planned
date of 2025-04-30. In contrast, Node 20.x has been offering long-term
support (LTS) since 2023-10-24. This makes Node 20.x a stable and
recommended version for production environments.

This commit also configures `actions/setup-node` with the
`check-latest` flag to always use the latest Node 20.x version, keeping
CI/CD setup up-to-date with minimal maintenance.
Details:
- actions/setup-node#165
- actions/setup-node#160

Using Node 20.x in CI/CD environments provides better compatibility with
Electron v29.0 which moves to Node 20.x.
Details:
- electron/electron#40343

This upgrade improves network connection handling in CI/CD pipelines
(where issues occur due to GitHub runners not supporting IPv6).
Details:
- actions/runner#3138
- actions/runner-images#668
- actions/runner#3213
- actions/runner-images#9540

Node 20.x adopts the Happy Eyeballs algorithm for improved IPv6
connectivity.
- nodejs/node#40702
- nodejs/node#41625
- nodejs/node#44731

This mitigates issues like `UND_ERR_CONNECT_TIMEOUT` and localhost DNS
resolution in CI/CD environments:
Details:
- nodejs/node#40537
- actions/runner#3213
- actions/runner-images#9540

Node 20 introduces `setDefaultAutoSelectFamily`, a global function from
Node 19.4.0, enabling better IPv4 support, especially in environments
with limited or problematic IPv6 support.
Details:
- nodejs/node#45777

Node 20.x defaults to the new `autoSelectFamily`, improving network
connection reliability in GitHub runners lacking full IPv6 support.
Details:
- nodejs/node#46790
undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Mar 30, 2024
This commit upgrades Node.js version to v20.x in CI/CD environment.

Previously used Node 18.x is moving towards end-of-life, with a planned
date of 2025-04-30. In contrast, Node 20.x has been offering long-term
support (LTS) since 2023-10-24. This makes Node 20.x a stable and
recommended version for production environments.

This commit also configures `actions/setup-node` with the
`check-latest` flag to always use the latest Node 20.x version, keeping
CI/CD setup up-to-date with minimal maintenance.
Details:
- actions/setup-node#165
- actions/setup-node#160

Using Node 20.x in CI/CD environments provides better compatibility with
Electron v29.0 which moves to Node 20.x.
Details:
- electron/electron#40343

This upgrade improves network connection handling in CI/CD pipelines
(where issues occur due to GitHub runners not supporting IPv6).
Details:
- actions/runner#3138
- actions/runner-images#668
- actions/runner#3213
- actions/runner-images#9540

Node 20.x adopts the Happy Eyeballs algorithm for improved IPv6
connectivity.
- nodejs/node#40702
- nodejs/node#41625
- nodejs/node#44731

This mitigates issues like `UND_ERR_CONNECT_TIMEOUT` and localhost DNS
resolution in CI/CD environments:
Details:
- nodejs/node#40537
- actions/runner#3213
- actions/runner-images#9540

Node 20 introduces `setDefaultAutoSelectFamily`, a global function from
Node 19.4.0, enabling better IPv4 support, especially in environments
with limited or problematic IPv6 support.
Details:
- nodejs/node#45777

Node 20.x defaults to the new `autoSelectFamily`, improving network
connection reliability in GitHub runners lacking full IPv6 support.
Details:
- nodejs/node#46790
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

No branches or pull requests

7 participants