sensitivity to order of connection errors (original) (raw)
Bug Description
When making requests that attempt connection to multiple ip addresses (like a ipv4 and a ipv6), then rejection is quite volatile:
- when last attempted socket ended with timeout then it always throws
ConnectTimeoutError, hiding reasons of previous unsuccessfull connection attempts - otherwise a AggregateError is throws with raw errors from
node:netinside
Reproducible By
setup firewall
sudo iptables -A INPUT -p tcp --dport 3112 -j DROP
sudo ip6tables -A INPUT -p tcp --dport 3112 -j REJECT --reject-with tcp-reset
sudo iptables -A INPUT -p tcp --dport 3113 -j REJECT --reject-with tcp-reset
sudo ip6tables -A INPUT -p tcp --dport 3113 -j DROP
setup address that resolves to localhost on both families: write to /etc/hosts:
::1 example.com
127.0.0.1 example.com
run in node
await require('undici').request('http://foofoo.in:3112')
await require('undici').request('http://foofoo.in:3113')
Expected Behavior
I would expect both requests to reject similar payload
Logs & Screenshots
> await require('undici').request('http://example.com:3112')
Uncaught:
ConnectTimeoutError: Connect Timeout Error (attempted addresses: ::1:3112, 127.0.0.1:3112, timeout: 10000ms)
at onConnectTimeout (/home/konrad/erli/services/product/node_modules/undici/lib/core/util.js:894:19)
at Immediate._onImmediate (/home/konrad/erli/services/product/node_modules/undici/lib/core/util.js:863:11)
at process.processImmediate (node:internal/timers:504:21)
at process.topLevelDomainCallback (node:domain:161:15)
at process.callbackTrampoline (node:internal/async_hooks:128:24) {
code: 'UND_ERR_CONNECT_TIMEOUT'
}
> await require('undici').request('http://example.com:3113')
Uncaught AggregateError [ETIMEDOUT]:
at internalConnectMultiple (nodeš„
1134:18)
at afterConnectMultiple (nodeš„
1715:7)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
code: 'ETIMEDOUT',
[errors]: [
Error: connect ETIMEDOUT ::1:3113
at createConnectionError (nodeš„
1678:14)
at Timeout.internalConnectMultipleTimeout (nodeš„
1737:38)
at listOnTimeout (node:internal/timers:607:11)
at process.processTimers (node:internal/timers:541:7) {
errno: -110,
code: 'ETIMEDOUT',
syscall: 'connect',
address: '::1',
port: 3113
},
Error: connect ECONNREFUSED 127.0.0.1:3113
at createConnectionError (nodeš„
1678:14)
at afterConnectMultiple (nodeš„
1708:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 3113
}
]
}
Environment
Ubuntu 24.04, Node 24.12, undici 8.1.0 (and 7.18)
Additional context
I had retrying/error handling logic for UND_ERR_CONNECT_TIMEOUT. When ipv6 is enabled but not functional, the errors that would normally be UND_ERR_CONNECT_TIMEOUT changed into AggregateError with ETIMEDOUT and ENETUNREACH, and I didn't handle them properly (not to mention that they are harder to handle).
I see a lot of effort was put into repacking those netive errors errors from lib/core/errors.js, it would be great if one could rely on handling just those errors, to have no edge cases or have a definite guide that covers them.
Perhaps one of following proposals should be considered:
- ENETUNREACH should be suppressed when other errors exist, as ENETUNREACH is usually of lesser importance⦠It would protect those migrating from ipv4 to dual stack servers from unexpected error format change.
- some heuristic that packs those errors together but still exposes the details
- it should be documented that undici can reject with
AggregateError, but undici should replaceETIMEDOUTwithUND_ERR_CONNECT_TIMEOUTinsideAggregateError