current commit
261105.4 · fix(test): compare proxy-agent headers as plain objects (#6122)
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const assert = require('node:assert')
const { test, after } = require('node:test')
const diagnosticsChannel = require('node:diagnostics_channel')
const { request, fetch, Headers, setGlobalDispatcher, getGlobalDispatcher } = require('..')
const { InvalidArgumentError, ConnectTimeoutError, SecureProxyConnectionError } = require('../lib/core/errors')
const ProxyAgent = require('../lib/dispatcher/proxy-agent')
const Pool = require('../lib/dispatcher/pool')
const { createServer } = require('node:http')
const https = require('node:https')
const http2 = require('node:http2')
const net = require('node:net')
const { Socket } = require('node:net')
const { createProxy } = require('proxy')

const certs = (() => {
  const forge = require('node-forge')
  const createCert = (cn, issuer, keyLength = 2048) => {
    const keys = forge.pki.rsa.generateKeyPair(keyLength)
    const cert = forge.pki.createCertificate()
    cert.publicKey = keys.publicKey
    cert.serialNumber = '' + Date.now()
    cert.validity.notBefore = new Date()
    cert.validity.notAfter = new Date()
    cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)

    const attrs = [{
      name: 'commonName',
      value: cn
    }]
    cert.setSubject(attrs)
    const isCa = issuer === undefined
    cert.setExtensions([{
      name: 'basicConstraints',
      cA: isCa
    }, {
      name: 'keyUsage',
      keyCertSign: true,
      digitalSignature: true,
      nonRepudiation: true,
      keyEncipherment: true,
      dataEncipherment: true
    }, {
      name: 'extKeyUsage',
      serverAuth: true,
      clientAuth: true,
      codeSigning: true,
      emailProtection: true,
      timeStamping: true
    }, {
      name: 'nsCertType',
      client: true,
      server: true,
      email: true,
      objsign: true,
      sslCA: isCa,
      emailCA: isCa,
      objCA: isCa
    }])

    const alg = forge.md.sha256.create()
    if (issuer !== undefined) {
      cert.setIssuer(issuer.certificate.subject.attributes)
      cert.sign(issuer.privateKey, alg)
    } else {
      cert.setIssuer(attrs)
      cert.sign(keys.privateKey, alg)
    }
    return {
      privateKey: keys.privateKey,
      publicKey: keys.publicKey,
      certificate: cert
    }
  }

  const root = createCert('CA')
  const server = createCert('agent1', root)
  const client = createCert('client', root)
  const proxy = createCert('proxy', root)

  return {
    root: {
      key: forge.pki.privateKeyToPem(root.privateKey),
      crt: forge.pki.certificateToPem(root.certificate)
    },
    server: {
      key: forge.pki.privateKeyToPem(server.privateKey),
      crt: forge.pki.certificateToPem(server.certificate)
    },
    client: {
      key: forge.pki.privateKeyToPem(client.privateKey),
      crt: forge.pki.certificateToPem(client.certificate)
    },
    proxy: {
      key: forge.pki.privateKeyToPem(proxy.privateKey),
      crt: forge.pki.certificateToPem(proxy.certificate)
    }
  }
})()

test('should throw error when no uri is provided', (t) => {
  t = tspl(t, { plan: 2 })
  t.throws(() => new ProxyAgent(), InvalidArgumentError)
  t.throws(() => new ProxyAgent({}), InvalidArgumentError)
})

test('using auth in combination with token should throw', (t) => {
  t = tspl(t, { plan: 1 })
  t.throws(() => new ProxyAgent({
    auth: 'foo',
    token: 'Bearer bar',
    uri: 'http://example.com'
  }),
  InvalidArgumentError
  )
})

test('should accept string, URL and object as options', (t) => {
  t = tspl(t, { plan: 3 })
  t.doesNotThrow(() => new ProxyAgent('http://example.com'))
  t.doesNotThrow(() => new ProxyAgent(new URL('http://example.com')))
  t.doesNotThrow(() => new ProxyAgent({ uri: 'http://example.com' }))
})

test('ProxyAgent forwards connectTimeout to the proxy connector', async (t) => {
  t = tspl(t, { plan: 4 })

  const originalConnect = net.connect
  let connect
  let socket
  const proxyAgent = new ProxyAgent({
    uri: 'http://localhost:9000',
    connectTimeout: 1e3,
    clientFactory (_origin, options) {
      connect = options.connect
      return {
        close () {
          return Promise.resolve()
        },
        destroy () {
          return Promise.resolve()
        }
      }
    }
  })

  after(() => proxyAgent.close())

  try {
    net.connect = function (options) {
      return new net.Socket(options)
    }

    t.ok(typeof connect === 'function')

    const timeout = setTimeout(() => {
      if (socket && !socket.destroyed) {
        socket.destroy()
      }
      t.fail('connectTimeout was not forwarded to the proxy connector')
    }, 2e3)

    await new Promise((resolve, reject) => {
      socket = connect({ hostname: 'localhost', protocol: 'http:', port: 9000 }, (err) => {
        try {
          t.ok(err instanceof ConnectTimeoutError)
          t.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT')
          t.strictEqual(err.message, 'Connect Timeout Error (attempted address: localhost:9000, timeout: 1000ms)')
          clearTimeout(timeout)
          resolve()
        } catch (error) {
          clearTimeout(timeout)
          reject(error)
        }
      })
    })
  } finally {
    net.connect = originalConnect
    if (socket && !socket.destroyed) {
      socket.destroy()
    }
  }
})

test('use proxy-agent to connect through proxy (keep alive)', async (t) => {
  t = tspl(t, { plan: 10 })
  const server = await buildServer()
  const proxy = await buildProxy()
  delete proxy.authenticate

  let _socket, _connectParams
  diagnosticsChannel.channel('undici:proxy:connected').subscribe(({ socket, connectParams }) => {
    _socket = socket
    _connectParams = connectParams
  })

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTunnel: true
  })
  const parsedOrigin = new URL(serverUrl)

  proxy.on('connect', (msg) => {
    t.strictEqual(msg.headers['proxy-connection'], 'keep-alive')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')
  t.ok(_socket instanceof Socket)
  t.equal(_connectParams.origin, proxyUrl)
  t.equal(_connectParams.path, serverUrl.replace('http://', ''))
  t.equal(_connectParams.headers['proxy-connection'], 'keep-alive')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()
  delete proxy.authenticate

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const parsedOrigin = new URL(serverUrl)

  proxy.on('connect', () => {
    t.ok(true, 'should connect to proxy')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy agent to connect through proxy using Pool', async (t) => {
  t = tspl(t, { plan: 3 })
  const server = await buildServer()
  const proxy = await buildProxy()
  let resolveFirstConnect
  let connectCount = 0

  proxy.authenticate = async function (req) {
    if (++connectCount === 2) {
      t.ok(true, 'second connect should arrive while first is still inflight')
      resolveFirstConnect()
      return true
    } else {
      await new Promise((resolve) => {
        resolveFirstConnect = resolve
      })
      return true
    }
  }

  server.on('request', (req, res) => {
    res.end()
  })

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const clientFactory = (url, options) => {
    return new Pool(url, options)
  }
  const proxyAgent = new ProxyAgent({ auth: Buffer.from('user:pass').toString('base64'), uri: proxyUrl, clientFactory })
  const firstRequest = request(`${serverUrl}`, { dispatcher: proxyAgent })
  const secondRequest = await request(`${serverUrl}`, { dispatcher: proxyAgent })
  t.strictEqual((await firstRequest).statusCode, 200)
  t.strictEqual(secondRequest.statusCode, 200)
  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy using path with params', async (t) => {
  t = tspl(t, { plan: 5 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
  const parsedOrigin = new URL(serverUrl)

  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })
  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy using path with params with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const parsedOrigin = new URL(serverUrl)

  proxy.on('connect', () => {
    t.ok(true, 'should call proxy')
  })
  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy with basic auth in URL', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`)
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req, fn) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy with username-only auth in URL', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = new URL(`http://user:@localhost:${proxy.address().port}`)
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy with username-only auth in URL without colon', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = new URL(`http://user@localhost:${proxy.address().port}`)
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy with basic auth in URL with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 7 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = new URL(`http://user:pass@localhost:${proxy.address().port}`)
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req, fn) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.ok(true, 'proxy should be called')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent to connect through proxy with username-only auth in URL with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 7 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = new URL(`http://user:@localhost:${proxy.address().port}`)
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.ok(true, 'proxy should be called')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with auth', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    auth: Buffer.from('user:pass').toString('base64'),
    uri: proxyUrl,
    proxyTunnel: false
  })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with auth with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 7 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    auth: Buffer.from('user:pass').toString('base64'),
    uri: proxyUrl,
    proxyTunnel: true
  })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.ok(true, 'proxy should be called')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with token', async (t) => {
  t = tspl(t, { plan: 6 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    token: `Bearer ${Buffer.from('user:pass').toString('base64')}`,
    uri: proxyUrl,
    proxyTunnel: false
  })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with token with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 7 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    token: `Bearer ${Buffer.from('user:pass').toString('base64')}`,
    uri: proxyUrl,
    proxyTunnel: true
  })
  const parsedOrigin = new URL(serverUrl)

  proxy.authenticate = function (req) {
    t.ok(true, 'authentication should be called')
    return req.headers['proxy-authorization'] === `Bearer ${Buffer.from('user:pass').toString('base64')}`
  }
  proxy.on('connect', () => {
    t.ok(true, 'proxy should be called')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar', { dispatcher: proxyAgent })
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with custom headers', async (t) => {
  t = tspl(t, { plan: 1 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTunnel: false,
    headers: {
      'User-Agent': 'Foobar/1.0.0'
    }
  })

  proxy.on('connect', (req) => {
    t.fail('proxy tunnel should not be established')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.headers['user-agent'], 'BarBaz/1.0.0')
    res.end()
  })

  await request(serverUrl + '/hello?foo=bar', {
    headers: { 'user-agent': 'BarBaz/1.0.0' },
    dispatcher: proxyAgent
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with custom headers with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 2 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    headers: {
      'User-Agent': 'Foobar/1.0.0'
    },
    proxyTunnel: true
  })

  proxy.on('connect', (req) => {
    t.strictEqual(req.headers['user-agent'], 'Foobar/1.0.0')
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.headers['user-agent'], 'BarBaz/1.0.0')
    res.end()
  })

  await request(serverUrl + '/hello?foo=bar', {
    headers: { 'user-agent': 'BarBaz/1.0.0' },
    dispatcher: proxyAgent
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

for (const proxyTunnel of [false, true]) {
  test(`use proxy-agent with a headers object reused across origins (proxyTunnel: ${proxyTunnel})`, async (t) => {
    const serverA = await buildServer()
    const serverB = await buildServer()
    const proxy = await buildProxy()
    const proxyAgent = new ProxyAgent({ uri: `http://localhost:${proxy.address().port}`, proxyTunnel })
    t.after(() => {
      serverA.close()
      serverB.close()
      proxy.close()
      return proxyAgent.close()
    })

    const hosts = []
    const onRequest = (req, res) => {
      hosts.push(req.headers.host)
      res.end()
    }
    serverA.on('request', onRequest)
    serverB.on('request', onRequest)

    const headers = { 'x-foo': 'bar' }
    const hostA = `localhost:${serverA.address().port}`
    const hostB = `localhost:${serverB.address().port}`

    for (const host of [hostA, hostB]) {
      const { body } = await request(`http://${host}/`, { dispatcher: proxyAgent, headers })
      await body.dump()
    }

    assert.deepStrictEqual(hosts, [hostA, hostB])
    assert.deepStrictEqual(headers, { 'x-foo': 'bar' })
  })

  test(`use proxy-agent with a Host header in any case (proxyTunnel: ${proxyTunnel})`, async (t) => {
    const server = await buildServer()
    const proxy = await buildProxy()
    const proxyAgent = new ProxyAgent({ uri: `http://localhost:${proxy.address().port}`, proxyTunnel })
    t.after(() => {
      server.close()
      proxy.close()
      return proxyAgent.close()
    })

    const host = `localhost:${server.address().port}`
    server.on('request', (req, res) => {
      res.end(req.headers.host)
    })

    const { body } = await request(`http://${host}/`, {
      dispatcher: proxyAgent,
      headers: { HOST: 'custom.example' }
    })

    assert.strictEqual(await body.text(), 'custom.example')
  })
}

test('use proxy-agent with repeated iterable headers', async (t) => {
  t = tspl(t, { plan: 1 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent(proxyUrl)

  server.on('request', (req, res) => {
    t.strictEqual(req.headers['x-duplicate'], 'first, second')
    res.end()
  })

  const headers = {
    * [Symbol.iterator] () {
      yield ['x-duplicate', 'first']
      yield ['x-duplicate', 'second']
    }
  }

  await request(serverUrl, { dispatcher: proxyAgent, headers })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('sending proxy-authorization in request headers should throw', async (t) => {
  t = tspl(t, { plan: 5 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent(proxyUrl)

  server.on('request', (req, res) => {
    res.end(JSON.stringify({ hello: 'world' }))
  })

  await t.rejects(
    request(
      serverUrl + '/hello?foo=bar',
      {
        dispatcher: proxyAgent,
        headers: {
          'proxy-authorization': Buffer.from('user:pass').toString('base64')
        }
      }
    ),
    'Proxy-Authorization should be sent in ProxyAgent'
  )

  await t.rejects(
    request(
      serverUrl + '/hello?foo=bar',
      {
        dispatcher: proxyAgent,
        headers: {
          'PROXY-AUTHORIZATION': Buffer.from('user:pass').toString('base64')
        }
      }
    ),
    'Proxy-Authorization should be sent in ProxyAgent'
  )

  await t.rejects(
    request(
      serverUrl + '/hello?foo=bar',
      {
        dispatcher: proxyAgent,
        headers: {
          'Proxy-Authorization': Buffer.from('user:pass').toString('base64')
        }
      }
    ),
    'Proxy-Authorization should be sent in ProxyAgent'
  )

  // Iterable containers (Map/Headers) must not bypass the guard (GHSA-6cv7-626c-qhqw)
  await t.rejects(
    request(
      serverUrl + '/hello?foo=bar',
      {
        dispatcher: proxyAgent,
        headers: new Map([['proxy-authorization', Buffer.from('user:pass').toString('base64')]])
      }
    ),
    'Proxy-Authorization should be sent in ProxyAgent'
  )

  await t.rejects(
    request(
      serverUrl + '/hello?foo=bar',
      {
        dispatcher: proxyAgent,
        headers: new Headers({ 'proxy-authorization': Buffer.from('user:pass').toString('base64') })
      }
    ),
    'Proxy-Authorization should be sent in ProxyAgent'
  )

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with setGlobalDispatcher', async (t) => {
  t = tspl(t, { plan: 5 })
  const defaultDispatcher = getGlobalDispatcher()

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl })
  const parsedOrigin = new URL(serverUrl)
  setGlobalDispatcher(proxyAgent)

  after(() => setGlobalDispatcher(defaultDispatcher))

  proxy.on('connect', () => {
    // proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
    t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
  })
  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar')
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('use proxy-agent with setGlobalDispatcher with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 6 })
  const defaultDispatcher = getGlobalDispatcher()

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const parsedOrigin = new URL(serverUrl)
  setGlobalDispatcher(proxyAgent)

  after(() => setGlobalDispatcher(defaultDispatcher))

  proxy.on('connect', () => {
    t.ok(true, 'should call proxy')
  })
  server.on('request', (req, res) => {
    t.strictEqual(req.url, '/hello?foo=bar')
    t.strictEqual(req.headers.host, parsedOrigin.host, 'should not use proxyUrl as host')
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ hello: 'world' }))
  })

  const {
    statusCode,
    headers,
    body
  } = await request(serverUrl + '/hello?foo=bar')
  const json = await body.json()

  t.strictEqual(statusCode, 200)
  t.deepStrictEqual(json, { hello: 'world' })
  t.strictEqual(headers.connection, 'keep-alive', 'should remain the connection open')

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('ProxyAgent correctly sends headers when using fetch - #1355, #1623', async (t) => {
  t = tspl(t, { plan: 1 })
  const defaultDispatcher = getGlobalDispatcher()

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`

  const proxyAgent = new ProxyAgent({ uri: proxyUrl })
  setGlobalDispatcher(proxyAgent)

  after(() => setGlobalDispatcher(defaultDispatcher))

  const expectedHeaders = {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive',
    'test-header': 'value',
    accept: '*/*',
    'accept-language': '*',
    'sec-fetch-mode': 'cors',
    'user-agent': 'undici',
    'accept-encoding': 'gzip, deflate'
  }

  proxy.on('connect', (req, res) => {
    // proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
    t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
  })

  server.on('request', (req, res) => {
    // The `proxy` package will add a "via" and "x-forwarded-for" header for non-tunneled Proxy requests
    for (const header of ['via', 'x-forwarded-for']) {
      delete req.headers[header]
    }
    t.deepStrictEqual({ ...req.headers }, expectedHeaders)
    res.end('goodbye')
  })

  await fetch(serverUrl, {
    headers: { 'Test-header': 'value' }
  })

  server.close()
  proxy.close()
  proxyAgent.close()
  t.end()
})

test('ProxyAgent correctly sends headers when using fetch - #1355, #1623 (with proxy tunneling enabled)', async (t) => {
  t = tspl(t, { plan: 2 })
  const defaultDispatcher = getGlobalDispatcher()

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`

  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  setGlobalDispatcher(proxyAgent)

  after(() => setGlobalDispatcher(defaultDispatcher))

  const expectedHeaders = {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive',
    'test-header': 'value',
    accept: '*/*',
    'accept-language': '*',
    'sec-fetch-mode': 'cors',
    'user-agent': 'undici',
    'accept-encoding': 'gzip, deflate'
  }

  const expectedProxyHeaders = {
    'proxy-connection': 'keep-alive',
    host: `localhost:${server.address().port}`,
    connection: 'close'
  }

  proxy.on('connect', (req, res) => {
    t.deepStrictEqual({ ...req.headers }, expectedProxyHeaders)
  })

  server.on('request', (req, res) => {
    t.deepStrictEqual({ ...req.headers }, expectedHeaders)
    res.end('goodbye')
  })

  await fetch(serverUrl, {
    headers: { 'Test-header': 'value' }
  })

  server.close()
  proxy.close()
  proxyAgent.close()
  t.end()
})

test('should throw when proxy does not return 200', async (t) => {
  t = tspl(t, { plan: 3 })

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`

  proxy.on('connect', () => {
    // proxyTunnel must be set to true in order to tunnel into the endpoint for HTTP->HTTP proxy connections
    t.fail(true, 'connect to proxy should unreachable by default for HTTP->HTTP proxy connections')
  })

  proxy.authenticate = function (_req) {
    t.ok(true, 'should call authenticate')
    return false
  }

  const proxyAgent = new ProxyAgent({ uri: proxyUrl })
  try {
    await request(serverUrl, { dispatcher: proxyAgent })
    t.fail()
  } catch (e) {
    t.ok(true, 'pass')
    t.ok(e)
  }

  server.close()
  proxy.close()
  proxyAgent.close()
  await t.completed
})

test('should throw when proxy does not return 200 with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 3 })

  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`

  proxy.authenticate = function (_req) {
    t.ok(true, 'should call authenticate')
    return false
  }

  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  try {
    await request(serverUrl, { dispatcher: proxyAgent })
    t.fail()
  } catch (e) {
    t.ok(true, 'pass')
    t.ok(e)
  }

  server.close()
  proxy.close()
  proxyAgent.close()
  await t.completed
})

test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => {
  t = tspl(t, { plan: 2 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`

  proxy.authenticate = function (_req) {
    t.ok(true, 'should call authenticate')
    return false
  }

  const proxyAgent = new ProxyAgent(proxyUrl)
  try {
    await fetch(serverUrl, { dispatcher: proxyAgent })
  } catch (e) {
    t.ok('cause' in e)
  }

  server.close()
  proxy.close()
  proxyAgent.close()

  await t.completed
})

test('Proxy via HTTP to HTTPS endpoint', async (t) => {
  t = tspl(t, { plan: 4 })

  const server = await buildSSLServer()
  const proxy = await buildProxy()

  const serverUrl = `https://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    requestTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'agent1'
    }
  })

  server.on('request', function (req, res) {
    t.ok(req.connection.encrypted)
    res.end(JSON.stringify(req.headers))
  })

  server.on('secureConnection', () => {
    t.ok(true, 'server should be connected secured')
  })

  proxy.on('secureConnection', () => {
    t.fail('proxy over http should not call secureConnection')
  })

  proxy.on('connect', function () {
    t.ok(true, 'proxy should be connected')
  })

  proxy.on('request', function () {
    t.fail('proxy should never receive requests')
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTPS to HTTPS endpoint', async (t) => {
  t = tspl(t, { plan: 5 })
  const server = await buildSSLServer()
  const proxy = await buildSSLProxy()

  const serverUrl = `https://localhost:${server.address().port}`
  const proxyUrl = `https://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'proxy'
    },
    requestTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'agent1'
    }
  })

  server.on('request', function (req, res) {
    t.ok(req.connection.encrypted)
    res.end(JSON.stringify(req.headers))
  })

  server.on('secureConnection', () => {
    t.ok(true, 'server should be connected secured')
  })

  proxy.on('secureConnection', () => {
    t.ok(true, 'proxy over http should call secureConnection')
  })

  proxy.on('connect', function () {
    t.ok(true, 'proxy should be connected')
  })

  proxy.on('request', function () {
    t.fail('proxy should never receive requests')
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTPS to HTTP endpoint forwards without tunneling by default', async (t) => {
  t = tspl(t, { plan: 4 })
  const server = await buildServer()
  const proxy = await buildSSLProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `https://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'proxy'
    }
  })

  server.on('request', function (req, res) {
    t.ok(!req.connection.encrypted)
    const headers = { host: req.headers.host, connection: req.headers.connection }
    res.end(JSON.stringify(headers))
  })

  server.on('secureConnection', () => {
    t.fail('server is http')
  })

  proxy.on('secureConnection', () => {
    t.ok(true, 'TLS handshake to the proxy must still happen')
  })

  proxy.on('connect', () => {
    t.fail('should not tunnel plain HTTP through an HTTPS proxy by default')
  })

  proxy.on('request', function (req) {
    const bits = { method: req.method, url: req.url }
    t.deepStrictEqual(bits, {
      method: 'GET',
      url: `${serverUrl}/`
    })
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTPS to HTTP endpoint uses HTTP/1.1 when proxy also supports HTTP/2', async (t) => {
  t = tspl(t, { plan: 4 })
  const proxy = await buildH2CapableSSLProxy()

  const serverUrl = 'http://example.com/'
  const proxyUrl = `https://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'proxy'
    }
  })

  proxy.on('stream', (stream) => {
    t.fail('non-tunneled HTTP forwarding must not negotiate HTTP/2')
    stream.close()
  })

  proxy.on('request', function (req, res) {
    t.strictEqual(req.httpVersion, '1.1')
    t.strictEqual(req.method, 'GET')
    t.strictEqual(req.url, serverUrl)
    res.end('ok')
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  t.strictEqual(await data.body.text(), 'ok')

  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTPS to HTTP endpoint with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 3 })
  const server = await buildServer()
  const proxy = await buildSSLProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `https://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTls: {
      ca: [
        certs.root.crt
      ],
      servername: 'proxy'
    },
    proxyTunnel: true
  })

  server.on('request', function (req, res) {
    t.ok(!req.connection.encrypted)
    res.end(JSON.stringify(req.headers))
  })

  server.on('secureConnection', () => {
    t.fail('server is http')
  })

  proxy.on('secureConnection', () => {
    t.ok(true, 'proxy over http should call secureConnection')
  })

  proxy.on('request', function () {
    t.fail('proxy should never receive requests')
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTP to HTTP endpoint with tunneling enabled', async (t) => {
  t = tspl(t, { plan: 3 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })

  server.on('request', function (req, res) {
    t.ok(!req.connection.encrypted)
    res.end(JSON.stringify(req.headers))
  })

  server.on('secureConnection', () => {
    t.fail('server is http')
  })

  proxy.on('secureConnection', () => {
    t.fail('proxy is http')
  })

  proxy.on('connect', () => {
    t.ok(true, 'connect to proxy')
  })

  proxy.on('request', function () {
    t.fail('proxy should never receive requests')
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTP to HTTP endpoint with tunneling disabled', async (t) => {
  t = tspl(t, { plan: 3 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: false })

  server.on('request', function (req, res) {
    t.ok(!req.connection.encrypted)
    const headers = { host: req.headers.host, connection: req.headers.connection }
    res.end(JSON.stringify(headers))
  })

  server.on('secureConnection', () => {
    t.fail('server is http')
  })

  proxy.on('secureConnection', () => {
    t.fail('proxy is http')
  })

  proxy.on('connect', () => {
    t.fail(true, 'connect to proxy should unreachable if proxyTunnel is false')
  })

  proxy.on('request', function (req) {
    const bits = { method: req.method, url: req.url }
    t.deepStrictEqual(bits, {
      method: 'GET',
      url: `${serverUrl}/`
    })
  })

  const data = await request(serverUrl, { dispatcher: proxyAgent })
  const json = await data.body.json()
  t.deepStrictEqual(json, {
    host: `localhost:${server.address().port}`,
    connection: 'keep-alive'
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('Proxy via HTTPS to HTTP fails on wrong SNI', async (t) => {
  t = tspl(t, { plan: 3 })
  const server = await buildServer()
  const proxy = await buildSSLProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `https://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({
    uri: proxyUrl,
    proxyTls: {
      ca: [
        certs.root.crt
      ]
    }
  })

  server.on('request', function (req, res) {
    t.ok(!req.connection.encrypted)
    res.end(JSON.stringify(req.headers))
  })

  server.on('secureConnection', () => {
    t.fail('server is http')
  })

  proxy.on('secureConnection', () => {
    t.fail('proxy is http')
  })

  proxy.on('connect', () => {
    t.ok(true, 'connect to proxy')
  })

  proxy.on('request', function () {
    t.fail('proxy should never receive requests')
  })

  try {
    await request(serverUrl, { dispatcher: proxyAgent })
    throw new Error('should fail')
  } catch (e) {
    t.ok(e instanceof SecureProxyConnectionError)
    t.ok(e.cause instanceof Error)
    t.ok(e.cause.code === 'ERR_TLS_CERT_ALTNAME_INVALID')
  }

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('ProxyAgent keeps customized host in request headers - #3019', async (t) => {
  t = tspl(t, { plan: 2 })
  const server = await buildServer()
  const proxy = await buildProxy()

  const serverUrl = `http://localhost:${server.address().port}`
  const proxyUrl = `http://localhost:${proxy.address().port}`
  const proxyAgent = new ProxyAgent({ uri: proxyUrl, proxyTunnel: true })
  const customHost = 'example.com'

  proxy.on('connect', (req) => {
    t.strictEqual(req.headers.host, `localhost:${server.address().port}`)
  })

  server.on('request', (req, res) => {
    t.strictEqual(req.headers.host, customHost)
    res.end()
  })

  await request(serverUrl, {
    headers: { Host: customHost },
    dispatcher: proxyAgent
  })

  server.close()
  proxy.close()
  proxyAgent.close()
})

test('ProxyAgent handles multiple concurrent HTTP requests via HTTP proxy', async (t) => {
  t = tspl(t, { plan: 20 })
  // Start target HTTP server
  const server = createServer((req, res) => {
    setTimeout(() => {
      res.setHeader('content-type', 'application/json')
      res.end(JSON.stringify({ url: req.url }))
    }, 50)
  })
  await new Promise(resolve => server.listen(0, resolve))
  const targetPort = server.address().port

  // Start HTTP proxy server
  const proxy = createProxy(createServer())
  await new Promise(resolve => proxy.listen(0, resolve))
  const proxyPort = proxy.address().port

  // Create ProxyAgent (no tunneling, plain HTTP)
  const proxyAgent = new ProxyAgent(`http://localhost:${proxyPort}`)

  const N = 10
  const requests = []
  for (let i = 0; i < N; i++) {
    requests.push(
      request(`http://localhost:${targetPort}/test${i}`, { dispatcher: proxyAgent })
        .then(async res => {
          t.strictEqual(res.statusCode, 200)
          const json = await res.body.json()
          t.deepStrictEqual(json, { url: `/test${i}` })
        })
    )
  }
  try {
    await Promise.all(requests)
  } catch (err) {
    t.fail(err)
  }
  server.close()
  proxy.close()
  proxyAgent.close()
})

function buildServer () {
  return new Promise((resolve) => {
    const server = createServer({ joinDuplicateHeaders: true })
    server.listen(0, () => resolve(server))
  })
}

function buildSSLServer () {
  const serverOptions = {
    ca: [
      certs.root.crt
    ],
    key: certs.server.key,
    cert: certs.server.crt,
    joinDuplicateHeaders: true
  }
  return new Promise((resolve) => {
    const server = https.createServer(serverOptions)
    server.listen(0, () => resolve(server))
  })
}

function buildProxy (listener) {
  return new Promise((resolve) => {
    const server = listener
      ? createProxy(createServer(listener))
      : createProxy(createServer({ joinDuplicateHeaders: true }))
    server.listen(0, () => resolve(server))
  })
}

function buildSSLProxy () {
  const serverOptions = {
    ca: [
      certs.root.crt
    ],
    key: certs.proxy.key,
    cert: certs.proxy.crt,
    joinDuplicateHeaders: true
  }

  return new Promise((resolve) => {
    const server = createProxy(https.createServer(serverOptions))
    server.listen(0, () => resolve(server))
  })
}

function buildH2CapableSSLProxy () {
  const serverOptions = {
    ca: [
      certs.root.crt
    ],
    key: certs.proxy.key,
    cert: certs.proxy.crt,
    allowHTTP1: true,
    joinDuplicateHeaders: true
  }

  return new Promise((resolve) => {
    const server = http2.createSecureServer(serverOptions)
    server.listen(0, () => resolve(server))
  })
}