Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/docs/api/Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import { errors } from 'undici'
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |

Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.

### `ConnectTimeoutError`

When `autoSelectFamily` is enabled and every attempted address fails with a timeout, Node raises an `AggregateError`. Undici surfaces these multi-address timeouts as `ConnectTimeoutError` (so the error shape is the same regardless of whether Node's family-attempt timer or undici's `connectTimeout` wins the race); the original `AggregateError` is preserved on `error.cause`.

### `SocketError`

The `SocketError` has a `.socket` property which holds socket metadata:
Expand Down
29 changes: 27 additions & 2 deletions lib/core/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const net = require('node:net')
const assert = require('node:assert')
const util = require('./util')
const { InvalidArgumentError } = require('./errors')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')

let tls // include tls conditionally since it is not always available

Expand Down Expand Up @@ -142,12 +142,37 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
if (callback) {
const cb = callback
callback = null
cb(err)
cb(maybeNormalizeConnectError(err, this, { timeout, hostname, port }))
}
})

return socket
}
}

// `net.connect` with `autoSelectFamily` raises an `AggregateError` when every
// attempted address fails. If any of those failures is a timeout, surface the
// error as a `ConnectTimeoutError` so callers see the same error regardless of
// which timer (Node's internal one or undici's `connectTimeout`) wins the race.
// The original `AggregateError` is preserved on `.cause`.
function maybeNormalizeConnectError (err, socket, opts) {
if (
err instanceof AggregateError &&
(err.code === 'ETIMEDOUT' || err.errors.some((e) => e != null && e.code === 'ETIMEDOUT'))
) {
let message = 'Connect Timeout Error'
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
} else {
message += ` (attempted address: ${opts.hostname}:${opts.port},`
}
message += ` timeout: ${opts.timeout}ms)`

const wrapped = new ConnectTimeoutError(message)
wrapped.cause = err
return wrapped
}
return err
}

module.exports = buildConnector
37 changes: 37 additions & 0 deletions test/connect-timeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,40 @@ test('connect-timeout', { skip }, async t => {

await t.completed
})

test('autoSelectFamily AggregateError with ETIMEDOUT is normalized to ConnectTimeoutError', { skip }, async t => {
t = tspl(t, { plan: 5 })

const aggregate = new AggregateError([
Object.assign(new Error('connect ETIMEDOUT 127.0.0.1:9000'), { code: 'ETIMEDOUT' }),
Object.assign(new Error('connect ETIMEDOUT ::1:9000'), { code: 'ETIMEDOUT' })
], 'connect ETIMEDOUT')
aggregate.code = 'ETIMEDOUT'

net.connect = function (options) {
const socket = new net.Socket(options)
socket.autoSelectFamilyAttemptedAddresses = ['127.0.0.1:9000', '::1:9000']
setImmediate(() => {
socket.destroy(aggregate)
})
return socket
}

const client = new Client('http://localhost:9000', {
connectTimeout: 1e3
})
after(() => client.close())

client.request({
path: '/',
method: 'GET'
}, (err) => {
t.ok(err instanceof errors.ConnectTimeoutError)
t.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
t.strictEqual(err.message, 'Connect Timeout Error (attempted addresses: 127.0.0.1:9000, ::1:9000, timeout: 1000ms)')
t.ok(err.cause instanceof AggregateError)
t.strictEqual(err.cause, aggregate)
})

await t.completed
})
Loading