current commit
260719.1 · refactor: replace tspl with node:test t.plan in connect-timeout tests (#5518)
'use strict'

const { test, describe } = require('node:test')
const { Client, Pool, errors } = require('..')
const net = require('node:net')
const assert = require('node:assert')

const skip = !!process.env.CITGM

// Using describe instead of test to avoid the timeout
describe('prioritize socket errors over timeouts', { skip }, async () => {
  const client = new Pool('http://foorbar.invalid:1234', { connectTimeout: 1 })
  const { promise, resolve } = Promise.withResolvers()

  client.request({ method: 'GET', path: '/foobar' })
    .then(() => {
      assert.fail('should have errored')
      resolve()
    })
    .catch((err) => {
      assert.strictEqual(err.code, 'ENOTFOUND')
      assert.strictEqual(err.code !== 'UND_ERR_CONNECT_TIMEOUT', true)
      resolve()
    })

  // block for 1s which is enough for the dns lookup to complete and the
  // Timeout to fire
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(1000))

  await promise
})

// mock net.connect to avoid the dns lookup
net.connect = function (options) {
  return new net.Socket(options)
}

async function assertConnectTimeout (dispatcher, t) {
  t.plan(3)

  await new Promise((resolve, reject) => {
    // Connection timeouts use FastTimers, which have a deliberately low
    // resolution, and Windows adds an extra setImmediate before timing out.
    const timeout = setTimeout(() => {
      dispatcher.destroy()
      reject(new Error('connect-timeout callback did not fire'))
    }, 5e3)

    dispatcher.request({
      path: '/',
      method: 'GET'
    }, (err) => {
      try {
        t.assert.ok(err instanceof errors.ConnectTimeoutError)
        t.assert.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
        t.assert.strictEqual(err.message, 'Connect Timeout Error (attempted address: localhost:9000, timeout: 1000ms)')
        resolve()
      } catch (error) {
        reject(error)
      } finally {
        clearTimeout(timeout)
      }
    })
  })
}

test('connect-timeout (Client)', { skip }, async t => {
  const client = new Client('http://localhost:9000', {
    connectTimeout: 1e3
  })
  t.after(() => client.close().catch(() => {}))

  await assertConnectTimeout(client, t)
})

test('connect-timeout (Pool)', { skip }, async t => {
  const client = new Pool('http://localhost:9000', {
    connectTimeout: 1e3
  })
  t.after(() => client.close().catch(() => {}))

  await assertConnectTimeout(client, t)
})

test('autoSelectFamily AggregateError with ETIMEDOUT is normalized to ConnectTimeoutError', { skip }, async t => {
  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
  })
  t.after(() => client.close().catch(() => {}))

  await new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error('timeout waiting for callback'))
    }, 5e3)

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