Description
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