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:

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: