You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Conditional unhandled 'error' event when http.request with .lookup #48771
$ sudo ip route del blackhole 192.168.144.2
$ curl -s 192.168.144.2 >/dev/null &&echo OK
OK
$ node ./src/extra/reproduce.cjs &&echo OK
^C
$ sudo ip route add blackhole 192.168.144.2
$ node ./src/extra/reproduce.cjs &&echo OK
{
err: Error: connect EINVAL 192.168.144.2:80 - Local (0.0.0.0:0)
at internalConnect (node:net:1087:16)
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at emitLookup (node:net:1478:9)
at lookup (/workspaces/loynoir/repo/reproduce-node-48771/src/extra/reproduce.cjs:2:32)
at emitLookup (node:net:1402:5)
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at lookupAndConnectMultiple (node:net:1401:3)
at node:net:1347:7
at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18)
at lookupAndConnect (node:net:1346:5) {
errno: -22,
code: 'EINVAL',
syscall: 'connect',
address: '192.168.144.2',
port: 80
}
}
OK
FYI I tested if my network family autoselection was involved into this. Apparently is not: if you change lines 19 and 23 in `` in your repro repo to this:
to accomodate for both single and multiple DNS lookup, then the problem will happen even if you use --no-network-family-autoselection.
In macOS in order to reproduce the problem you can route that IP to something unreachable. For instance:
sudo route add 192.168.144.2 10.3.0.1
(10.3.0.1 is unreachable from my system, you might have to use another IP).
With the unreachable IP setup, I tried the narrow down example in #48771 (comment) and it worked, so it seems like the problem is not on the net module but rather in http which is not setting the error listener fast enough.
It looks like a bug, unfortunately, I don't have time to dig deep on how to fix it. I've never seen this problem happen in practice, but I guess it can happen if the kernel is really fast in responding.
A quick code review spotted the problem: when a socket is assigned to a ClientRequest, we defer to the next tick setting an error handler:
@mcollina I think the only way to fix this is to defer the error emitting (self.destroy(ex)) in internalConnect and internalConnectMultiple via nextTick. WDYT?
@mcollina I think the only way to fix this is to defer the error emitting (self.destroy(ex)) in internalConnect and internalConnectMultiple via nextTick. WDYT?
To fix the issue, I made a modification to the onSocket function. I checked for the presence of an error (err) and if it exists, I immediately emitted the 'error' event using this.emit('error', err) and then called this.destroy() to terminate the request. This way, the error is handled synchronously.
Is the proposed modification a correct and appropriate solution to this issue?
Is there any potential downside or side effect to this modification that I should be aware of?
Looking forward to your feedback on the proposed solution. If this approach is deemed appropriate, I can submit a pull request with the suggested changes.
Activity
bnoordhuis commentedon Jul 15, 2023
I'm not able to reproduce. Let's try to reduce the test case. What happens when you run this?
loynoir commentedon Jul 15, 2023
bnoordhuis commentedon Jul 15, 2023
Okay, so that works for you as well. Please try expanding the test until you have a minimal reproducer.
loynoir commentedon Jul 15, 2023
@bnoordhuis
https://github.com/loynoir/reproduce-node-48771
I think this is the minimal reproduce.
I guess, some node code is not wrap with
process.nextTick.So, if user not use
In some situation, there is unhandled 'error' event within http.request, and leads to
Within script, like https://github.com/loynoir/reproduce-node-48771, http.request throw an un-catch-able error
Within worker, like error caught, but exitCode is not zero #48756, http.request throw an error, although error is caught, but worker exitCode is not zero.
ShogunPanda commentedon Jul 18, 2023
FYI I tested if my network family autoselection was involved into this. Apparently is not: if you change lines 19 and 23 in `` in your repro repo to this:
to accomodate for both single and multiple DNS lookup, then the problem will happen even if you use
--no-network-family-autoselection.In macOS in order to reproduce the problem you can route that IP to something unreachable. For instance:
(10.3.0.1 is unreachable from my system, you might have to use another IP).
With the unreachable IP setup, I tried the narrow down example in #48771 (comment) and it worked, so it seems like the problem is not on the
netmodule but rather inhttpwhich is not setting the error listener fast enough.@mcollina Any thoughts on this?
mcollina commentedon Jul 19, 2023
It looks like a bug, unfortunately, I don't have time to dig deep on how to fix it. I've never seen this problem happen in practice, but I guess it can happen if the kernel is really fast in responding.
A quick code review spotted the problem: when a socket is assigned to a ClientRequest, we defer to the next tick setting an error handler:
node/lib/_http_client.js
Lines 860 to 900 in a2fc4a3
The
'error'handler is set intickOnSocket:node/lib/_http_client.js
Line 822 in a2fc4a3
Deferring by a
nextTickis fine for every I/O but not a synchronous DNS error:node/lib/net.js
Lines 1414 to 1418 in a2fc4a3
Can that happen?
As a side note, I'd recommend you to use
undicias it should better handle this case (and be easier to fix).loynoir commentedon Jul 19, 2023
@mcollina
When I use
.lookupwithoutnextTick, I did see node.jshttp.requestthrow un-catch-able error.Although I cannot reproduce same error, I found
ip-blackholemay also lethttp.requestthrow un-catch-able error.ShogunPanda commentedon Jul 21, 2023
@mcollina I think the only way to fix this is to defer the error emitting (
self.destroy(ex)) ininternalConnectandinternalConnectMultiplevianextTick. WDYT?loynoir commentedon Aug 2, 2023
Verified
undicidon't have this bug.Error is caught within
undici.$ sudo ip route add blackhole 192.168.144.4 $ cat reproduce.mjs import { Agent } from 'undici' try { await fetch('http://example.com', { dispatcher: new Agent({ connect: { lookup: (hostname, options, callback) => { // node <20 // callback(null, '192.168.144.4', 4) // node 20 callback(null, [{ address: '192.168.144.4', family: 4 }]) } } }) }) } catch (caught) { console.warn({ caught }) } $ node reproduce.mjs { caught: TypeError: fetch failed at Object.fetch (node:internal/deps/undici/undici:11576:11) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async file:///tmp/tmp.ogEc9I0rez/reproduce.mjs:4:5 { cause: Error: connect EINVAL 192.168.144.4:80 - Local (0.0.0.0:0) at internalConnect (node:net:1087:16) at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18) at emitLookup (node:net:1478:9) at lookup (file:///tmp/tmp.ogEc9I0rez/reproduce.mjs:11:21) at emitLookup (node:net:1402:5) at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18) at lookupAndConnectMultiple (node:net:1401:3) at node:net:1347:7 at defaultTriggerAsyncIdScope (node:internal/async_hooks:464:18) at lookupAndConnect (node:net:1346:5) { errno: -22, code: 'EINVAL', syscall: 'connect', address: '192.168.144.4', port: 80 } } }mcollina commentedon Aug 2, 2023
@ShogunPanda yes
mertcanaltin commentedon Aug 2, 2023
@mcollina hello
To fix the issue, I made a modification to the onSocket function. I checked for the presence of an error (err) and if it exists, I immediately emitted the 'error' event using this.emit('error', err) and then called this.destroy() to terminate the request. This way, the error is handled synchronously.
Modified Code:
Open Questions:
Is the proposed modification a correct and appropriate solution to this issue?
Is there any potential downside or side effect to this modification that I should be aware of?
Looking forward to your feedback on the proposed solution. If this approach is deemed appropriate, I can submit a pull request with the suggested changes.
Thanks!
33 remaining items
net: defer synchronous destroy calls in internalConnect
http: attach error handler to socket synchronously in onSocket
net: defer self.destroy calls to nextTick
http: use private symbol for immediateError property
fix: rebase fixes