import { describe, it } from 'vitest';
import assert from 'assert';
import {
  startHTTPServer,
  stopHTTPServer,
  SERVER_HANDLER_STREAM_ECHO,
  handleFormData,
  setTimeoutAsync,
  generateReadable,
} from '../../setup/server.js';
import axios from '../../../index.js';
import AxiosError from '../../../lib/core/AxiosError.js';
import { __setProxy } from '../../../lib/adapters/http.js';
import http from 'http';
import https from 'https';
import net from 'net';
import stream from 'stream';
import zlib from 'zlib';
import fs from 'fs';
import os from 'os';
import path from 'path';
import devNull from 'dev-null';
import FormDataLegacy from 'form-data';
import { IncomingForm } from 'formidable';
import { FormData as FormDataPolyfill, Blob as BlobPolyfill } from 'formdata-node';
import express from 'express';
import multer from 'multer';
import getStream from 'get-stream';
import bodyParser from 'body-parser';
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import { lookup } from 'dns';
import { EventEmitter } from 'events';

const OPEN_WEB_PORT = 80;
const SERVER_PORT = 8020;
const PROXY_PORT = 8030;
const ALTERNATE_SERVER_PORT = 8040;

describe('supports http with nodejs', () => {
  const adaptersTestsDir = path.join(process.cwd(), 'tests/unit/adapters');
  const thisTestFilePath = path.join(adaptersTestsDir, 'http.test.js');
  const FormDataSpecCompliant = typeof FormData !== 'undefined' ? FormData : FormDataPolyfill;
  const BlobSpecCompliant = typeof Blob !== 'undefined' ? Blob : BlobPolyfill;
  const isBlobSupported = typeof Blob !== 'undefined';

  function toleranceRange(positive, negative) {
    const p = 1 + positive / 100;
    const n = 1 - negative / 100;

    return (actualValue, value) => {
      return actualValue > value ? actualValue <= value * p : actualValue >= value * n;
    };
  }

  class HangingConnectSocket extends stream.Duplex {
    constructor() {
      super();
      this.connecting = true;
    }

    _read() {}

    _write(_chunk, _encoding, callback) {
      callback();
    }

    setKeepAlive() {
      return this;
    }

    setNoDelay() {
      return this;
    }

    setTimeout() {
      return this;
    }
  }

  class HangingConnectAgent extends http.Agent {
    createConnection() {
      return new HangingConnectSocket();
    }
  }

  it('should support IPv4 literal strings', async () => {
    const data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com',
    };

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(data));
      },
      { port: SERVER_PORT }
    );

    try {
      const { data: responseData } = await axios.get(`http://127.0.0.1:${server.address().port}`);
      assert.deepStrictEqual(responseData, data);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support IPv6 literal strings', async () => {
    var data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com',
    };

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(data));
      },
      { port: SERVER_PORT }
    );

    try {
      const { data: responseData } = await axios.get(`http://[::1]:${server.address().port}`, {
        proxy: false,
      });
      assert.deepStrictEqual(responseData, data);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should throw an error if the timeout property is not parsable as a number', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        setTimeout(() => {
          res.end();
        }, 1000);
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}`, {
          timeout: { strangeTimeout: 250 },
        }),
        (error) => {
          assert.strictEqual(error.code, AxiosError.ERR_BAD_OPTION_VALUE);
          assert.strictEqual(error.message, 'error trying to parse `config.timeout` to int');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should sanitize request headers containing CRLF characters', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(
          JSON.stringify({
            xTest: req.headers['x-test'],
            injected: req.headers.injected ?? null,
          })
        );
      },
      { port: SERVER_PORT }
    );

    try {
      const { data } = await axios.get(`http://localhost:${server.address().port}/`, {
        headers: {
          'x-test': '\tok\r\nInjected: yes ',
        },
      });

      assert.strictEqual(data.xTest, 'okInjected: yes');
      assert.strictEqual(data.injected, null);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should parse the timeout property', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        setTimeout(() => {
          res.end();
        }, 1000);
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}`, {
          timeout: '250',
        }),
        (error) => {
          assert.strictEqual(error.code, 'ECONNABORTED');
          assert.strictEqual(error.message, 'timeout of 250ms exceeded');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should respect the timeout property', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        setTimeout(() => {
          res.end();
        }, 1000);
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}`, {
          timeout: 250,
        }),
        (error) => {
          assert.strictEqual(error.code, 'ECONNABORTED');
          assert.strictEqual(error.message, 'timeout of 250ms exceeded');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should respect the timeout property during TCP connect with maxRedirects set to 0', async () => {
    const timeout = 100;
    const guardTimeout = 1000;
    const started = Date.now();
    const controller = new AbortController();
    const agent = new HangingConnectAgent();
    let guardTimer;
    const request = axios.get('http://connect-timeout.test/', {
      httpAgent: agent,
      maxRedirects: 0,
      proxy: false,
      signal: controller.signal,
      timeout,
    });
    const guard = new Promise((_resolve, reject) => {
      guardTimer = setTimeout(() => {
        controller.abort();
        reject(new Error('request did not honor timeout during connect'));
      }, guardTimeout);
    });

    try {
      await assert.rejects(Promise.race([request, guard]), (error) => {
        const elapsed = Date.now() - started;
        assert.strictEqual(error.code, 'ECONNABORTED');
        assert.strictEqual(error.message, `timeout of ${timeout}ms exceeded`);
        assert.ok(elapsed < guardTimeout, `request timed out after ${elapsed}ms`);
        return true;
      });
    } finally {
      clearTimeout(guardTimer);
      controller.abort();
      agent.destroy();
    }
  });

  it('should not time out immediately for timeout set to zero during TCP connect', async () => {
    const controller = new AbortController();
    const agent = new HangingConnectAgent();
    const request = axios
      .get('http://connect-timeout.test/', {
        httpAgent: agent,
        maxRedirects: 0,
        proxy: false,
        signal: controller.signal,
        timeout: '0',
      })
      .then(
        () => null,
        (error) => error
      );

    try {
      await setTimeoutAsync(50);
      controller.abort();
      const error = await request;
      assert.strictEqual(error.code, AxiosError.ERR_CANCELED);
    } finally {
      controller.abort();
      agent.destroy();
    }
  });

  it('should respect the timeoutErrorMessage property', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        setTimeout(() => {
          res.end();
        }, 1000);
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}`, {
          timeout: 250,
          timeoutErrorMessage: 'oops, timeout',
        }),
        (error) => {
          assert.strictEqual(error.code, 'ECONNABORTED');
          assert.strictEqual(error.message, 'oops, timeout');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow passing JSON', async () => {
    const data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com',
    };

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(data));
      },
      { port: SERVER_PORT }
    );

    try {
      const { data: responseData } = await axios.get(`http://localhost:${server.address().port}`);
      assert.deepStrictEqual(responseData, data);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow passing JSON with BOM', async () => {
    const data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com',
    };

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/json');
        const bomBuffer = Buffer.from([0xef, 0xbb, 0xbf]);
        const jsonBuffer = Buffer.from(JSON.stringify(data));
        res.end(Buffer.concat([bomBuffer, jsonBuffer]));
      },
      { port: SERVER_PORT }
    );

    try {
      const { data: responseData } = await axios.get(`http://localhost:${server.address().port}`);
      assert.deepStrictEqual(responseData, data);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should redirect', async () => {
    const expectedResponse = 'test response';
    const server = await startHTTPServer(
      (req, res) => {
        if (req.url === '/one') {
          res.setHeader('Location', '/two');
          res.statusCode = 302;
          res.end();
          return;
        }

        res.end(expectedResponse);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/one`, {
        maxRedirects: 1,
      });

      assert.strictEqual(response.data, expectedResponse);
      assert.strictEqual(response.request.path, '/two');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should not redirect', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Location', '/foo');
        res.statusCode = 302;
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/one`, {
        maxRedirects: 0,
      });

      assert.strictEqual(response.status, 302);
      assert.strictEqual(response.headers.location, '/foo');
    } catch (error) {
      assert.strictEqual(error.message, 'Request failed with status code 302');
      assert.strictEqual(error.response.status, 302);
      assert.strictEqual(error.response.headers.location, '/foo');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support max redirects', async () => {
    var i = 1;
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Location', `/${i}`);
        res.statusCode = 302;
        res.end();
        i++;
      },
      { port: SERVER_PORT }
    );

    try {
      await axios.get(`http://localhost:${server.address().port}`, {
        maxRedirects: 3,
      });
    } catch (error) {
      assert.strictEqual(error.code, AxiosError.ERR_FR_TOO_MANY_REDIRECTS);
      assert.strictEqual(error.message, 'Maximum number of redirects exceeded');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support beforeRedirect', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Location', '/foo');
        res.statusCode = 302;
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await axios.get(`http://localhost:${server.address().port}/one`, {
        maxRedirects: 3,
        beforeRedirect: (options, responseDetails) => {
          if (options.path === '/foo' && responseDetails.headers.location === '/foo') {
            throw new Error('Provided path is not allowed');
          }
        },
      });
    } catch (error) {
      assert.strictEqual(error.message, 'Redirected request failed: Provided path is not allowed');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should pass requestDetails to beforeRedirect with the original URL', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Location', '/foo');
        res.statusCode = 302;
        res.end();
      },
      { port: SERVER_PORT }
    );

    const originalUrl = `http://localhost:${server.address().port}/bar`;
    let capturedUrl;

    try {
      await axios.get(originalUrl, {
        maxRedirects: 3,
        beforeRedirect: (options, responseDetails, requestDetails) => {
          if (options.path === '/foo' && responseDetails.headers.location === '/foo') {
            capturedUrl = requestDetails.url;
            throw new Error('Provided path is not allowed');
          }
        },
      });
    } catch (error) {
      assert.strictEqual(error.message, 'Redirected request failed: Provided path is not allowed');
      assert.strictEqual(capturedUrl, originalUrl);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support beforeRedirect and proxy with redirect', async () => {
    let requestCount = 0;
    let proxyUseCount = 0;
    let totalRedirectCount = 5;
    let configBeforeRedirectCount = 0;

    const server = await startHTTPServer(
      (req, res) => {
        requestCount += 1;
        if (requestCount <= totalRedirectCount) {
          res.setHeader('Location', `http://localhost:${SERVER_PORT}`);
          res.writeHead(302);
        }
        res.end();
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (req, res) => {
        proxyUseCount += 1;
        const targetUrl = new URL(req.url, `http://localhost:${server.address().port}`);
        const opts = {
          host: targetUrl.hostname,
          port: targetUrl.port,
          path: targetUrl.path,
          method: req.method,
        };

        const request = http.get(opts, (response) => {
          res.writeHead(response.statusCode, response.headers);
          stream.pipeline(response, res, () => {});
        });

        request.on('error', (err) => {
          console.warn('request error', err);
          res.statusCode = 500;
          res.end();
        });
      },
      { port: PROXY_PORT }
    );

    await axios.get(`http://localhost:${server.address().port}/`, {
      proxy: {
        host: 'localhost',
        port: PROXY_PORT,
      },
      maxRedirects: totalRedirectCount,
      beforeRedirect: (options) => {
        configBeforeRedirectCount += 1;
      },
    });

    assert.strictEqual(totalRedirectCount, configBeforeRedirectCount);
    assert.strictEqual(totalRedirectCount + 1, proxyUseCount);

    await stopHTTPServer(server);
    await stopHTTPServer(proxy);
  });

  it('should wrap HTTP errors and keep stack', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.statusCode = 400;
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        async function stackTraceTest() {
          await axios.get(`http://localhost:${server.address().port}/`);
        },
        (error) => {
          const matches = [...error.stack.matchAll(/stackTraceTest/g)];

          assert.strictEqual(error.name, 'AxiosError');
          assert.strictEqual(error.isAxiosError, true);
          assert.strictEqual(error.code, AxiosError.ERR_BAD_REQUEST);
          assert.strictEqual(error.message, 'Request failed with status code 400');
          assert.strictEqual(matches.length, 1, error.stack);

          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should wrap interceptor errors and keep stack', async () => {
    const axiosInstance = axios.create();

    axiosInstance.interceptors.request.use((res) => {
      throw new Error('from request interceptor');
    });

    const server = await startHTTPServer(
      (req, res) => {
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        async function stackTraceTest() {
          await axiosInstance.get(`http://localhost:${server.address().port}/one`);
        },
        (error) => {
          const matches = [...error.stack.matchAll(/stackTraceTest/g)];

          assert.strictEqual(error.name, 'Error');
          assert.strictEqual(error.message, 'from request interceptor');
          assert.strictEqual(matches.length, 1, error.stack);

          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should preserve the HTTP verb on redirect', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        if (req.method.toLowerCase() !== 'head') {
          res.statusCode = 400;
          res.end();
          return;
        }

        var parsed = new URL(req.url, 'http://localhost');
        if (parsed.pathname === '/one') {
          res.setHeader('Location', '/two');
          res.statusCode = 302;
          res.end();
        } else {
          res.end();
        }
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.head(`http://localhost:${server.address().port}/one`);
      assert.strictEqual(response.status, 200);
    } finally {
      await stopHTTPServer(server);
    }
  });

  describe('compression', async () => {
    it('should support transparent gunzip', async () => {
      const data = {
        firstName: 'Fred',
        lastName: 'Flintstone',
        emailAddr: 'fred@example.com',
      };

      const zipped = await new Promise((resolve, reject) => {
        zlib.gzip(JSON.stringify(data), (error, compressed) => {
          if (error) {
            reject(error);
            return;
          }

          resolve(compressed);
        });
      });

      const server = await startHTTPServer(
        (req, res) => {
          res.setHeader('Content-Type', 'application/json');
          res.setHeader('Content-Encoding', 'gzip');
          res.end(zipped);
        },
        { port: SERVER_PORT }
      );

      try {
        const { data: responseData } = await axios.get(
          `http://localhost:${server.address().port}/`
        );
        assert.deepStrictEqual(responseData, data);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support gunzip error handling', async () => {
      const server = await startHTTPServer(
        (req, res) => {
          res.statusCode = 206;
          res.setHeader('Content-Type', 'application/json');
          res.setHeader('Content-Encoding', 'gzip');
          res.setHeader('X-Stream-Error', 'yes');
          res.end('invalid response');
        },
        { port: SERVER_PORT }
      );

      try {
        await assert.rejects(
          async () => {
            await axios.get(`http://localhost:${server.address().port}/`);
          },
          (error) => {
            assert.strictEqual(error.response.status, 206);
            assert.strictEqual(error.response.headers.get('x-stream-error'), 'yes');
            assert.strictEqual(error.status, 206);

            return true;
          }
        );
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support disabling automatic decompression of response data', async () => {
      const data = 'Test data';

      const zipped = await new Promise((resolve, reject) => {
        zlib.gzip(data, (error, compressed) => {
          if (error) {
            reject(error);
            return;
          }

          resolve(compressed);
        });
      });

      const server = await startHTTPServer(
        (req, res) => {
          res.setHeader('Content-Type', 'text/html;charset=utf-8');
          res.setHeader('Content-Encoding', 'gzip');
          res.end(zipped);
        },
        { port: SERVER_PORT }
      );

      try {
        const response = await axios.get(`http://localhost:${server.address().port}/`, {
          decompress: false,
          responseType: 'arraybuffer',
        });
        assert.strictEqual(response.data.toString('base64'), zipped.toString('base64'));
      } finally {
        await stopHTTPServer(server);
      }
    });

    describe('algorithms', () => {
      const responseBody = 'str';

      const gzip = (value) =>
        new Promise((resolve, reject) => {
          zlib.gzip(value, (error, compressed) => {
            if (error) {
              reject(error);
              return;
            }

            resolve(compressed);
          });
        });

      const deflate = (value) =>
        new Promise((resolve, reject) => {
          zlib.deflate(value, (error, compressed) => {
            if (error) {
              reject(error);
              return;
            }

            resolve(compressed);
          });
        });

      const deflateRaw = (value) =>
        new Promise((resolve, reject) => {
          zlib.deflateRaw(value, (error, compressed) => {
            if (error) {
              reject(error);
              return;
            }

            resolve(compressed);
          });
        });

      const brotliCompress = (value) =>
        new Promise((resolve, reject) => {
          zlib.brotliCompress(value, (error, compressed) => {
            if (error) {
              reject(error);
              return;
            }

            resolve(compressed);
          });
        });

      for (const [typeName, zipped] of Object.entries({
        gzip: gzip(responseBody),
        GZIP: gzip(responseBody),
        compress: gzip(responseBody),
        deflate: deflate(responseBody),
        'deflate-raw': deflateRaw(responseBody),
        br: brotliCompress(responseBody),
      })) {
        const type = typeName.split('-')[0];

        describe(`${typeName} decompression`, () => {
          it('should support decompression', async () => {
            const server = await startHTTPServer(
              async (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.end(await zipped);
              },
              { port: SERVER_PORT }
            );

            try {
              const { data } = await axios.get(`http://localhost:${server.address().port}`);
              assert.strictEqual(data, responseBody);
            } finally {
              await stopHTTPServer(server);
            }
          });

          it(`should not fail if response content-length header is missing (${type})`, async () => {
            const server = await startHTTPServer(
              async (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.removeHeader('Content-Length');
                res.end(await zipped);
              },
              { port: SERVER_PORT }
            );

            try {
              const { data } = await axios.get(`http://localhost:${server.address().port}`);
              assert.strictEqual(data, responseBody);
            } finally {
              await stopHTTPServer(server);
            }
          });

          it('should not fail with chunked responses (without Content-Length header)', async () => {
            const server = await startHTTPServer(
              async (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.setHeader('Transfer-Encoding', 'chunked');
                res.removeHeader('Content-Length');
                res.write(await zipped);
                res.end();
              },
              { port: SERVER_PORT }
            );

            try {
              const { data } = await axios.get(`http://localhost:${server.address().port}`);
              assert.strictEqual(data, responseBody);
            } finally {
              await stopHTTPServer(server);
            }
          });

          it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async () => {
            const server = await startHTTPServer(
              (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.removeHeader('Content-Length');
                res.end();
              },
              { port: SERVER_PORT }
            );

            try {
              const { data } = await axios.get(`http://localhost:${server.address().port}`);
              assert.strictEqual(data, '');
            } finally {
              await stopHTTPServer(server);
            }
          });

          it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async () => {
            const server = await startHTTPServer(
              (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.end();
              },
              { port: SERVER_PORT }
            );

            try {
              await axios.get(`http://localhost:${server.address().port}`);
            } finally {
              await stopHTTPServer(server);
            }
          });

          it('should reject when the server aborts mid-stream and maxRedirects is 0', async () => {
            const server = await startHTTPServer(
              async (req, res) => {
                res.setHeader('Content-Encoding', type);
                res.setHeader('Transfer-Encoding', 'chunked');
                res.removeHeader('Content-Length');
                res.write(await zipped);
                setTimeout(() => res.socket.destroy(), 10);
              },
              { port: SERVER_PORT }
            );

            try {
              await assert.rejects(
                axios.get(`http://localhost:${server.address().port}`, { maxRedirects: 0 }),
                (err) => err && err.code === 'ECONNRESET'
              );
            } finally {
              await stopHTTPServer(server);
            }
          });
        });
      }
    });
  });

  it('should support UTF8', async () => {
    const str = Array(100000).join('ж');

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end(str);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      assert.strictEqual(response.data, str);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support basic auth', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers.authorization);
      },
      { port: SERVER_PORT }
    );

    try {
      const user = 'foo';
      const headers = { Authorization: 'Bearer 1234' };
      const response = await axios.get(`http://${user}@localhost:${server.address().port}/`, {
        headers,
      });
      const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64');
      assert.strictEqual(response.data, `Basic ${base64}`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should decode basic auth credentials from the request URL', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers.authorization);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(
        `http://my%40email.com:pa%24ss@localhost:${server.address().port}/`
      );
      const base64 = Buffer.from('my@email.com:pa$ss', 'utf8').toString('base64');
      assert.strictEqual(response.data, `Basic ${base64}`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('keeps malformed URL credentials percent-encoding and does not throw', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers.authorization);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://user%:foo%zz@localhost:${server.address().port}/`);
      const base64 = Buffer.from('user%:foo%zz', 'utf8').toString('base64');
      assert.strictEqual(response.data, `Basic ${base64}`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support basic auth with a header', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers.authorization);
      },
      { port: SERVER_PORT }
    );

    try {
      const auth = { username: 'foo', password: 'bar' };
      const headers = { AuThOrIzAtIoN: 'Bearer 1234' }; // wonky casing to ensure caseless comparison
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        auth,
        headers,
      });
      const base64 = Buffer.from('foo:bar', 'utf8').toString('base64');
      assert.strictEqual(response.data, `Basic ${base64}`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should provides a default User-Agent header', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers['user-agent']);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      assert.ok(
        /^axios\/[\d.]+[-]?[a-z]*[.]?[\d]+$/.test(response.data),
        `User-Agent header does not match: ${response.data}`
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow the User-Agent header to be overridden', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end(req.headers['user-agent']);
      },
      { port: SERVER_PORT }
    );

    try {
      const headers = { 'UsEr-AgEnT': 'foo bar' }; // wonky casing to ensure caseless comparison
      const response = await axios.get(`http://localhost:${server.address().port}/`, { headers });
      assert.strictEqual(response.data, 'foo bar');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow the Content-Length header to be overridden', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        assert.strictEqual(req.headers['content-length'], '42');
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      const headers = { 'CoNtEnT-lEnGtH': '42' }; // wonky casing to ensure caseless comparison
      await axios.post(`http://localhost:${server.address().port}/`, 'foo', { headers });
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support max content length', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end(Array(5000).join('#'));
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}/`, {
          maxContentLength: 2000,
          maxRedirects: 0,
        }),
        /maxContentLength size of 2000 exceeded/
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support max content length for redirected', async () => {
    const str = Array(100000).join('ж');
    const server = await startHTTPServer(
      (req, res) => {
        const parsed = new URL(req.url, 'http://localhost');

        if (parsed.pathname === '/two') {
          res.setHeader('Content-Type', 'text/html; charset=UTF-8');
          res.end(str);
          return;
        }

        res.setHeader('Location', '/two');
        res.statusCode = 302;
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}/one`, {
          maxContentLength: 2000,
        }),
        (error) => {
          assert.strictEqual(error.message, 'maxContentLength size of 2000 exceeded');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support max body length', async () => {
    const data = Array(100000).join('ж');
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.post(
          `http://localhost:${server.address().port}/`,
          {
            data,
          },
          {
            maxBodyLength: 2000,
          }
        ),
        (error) => {
          assert.strictEqual(error.message, 'Request body larger than maxBodyLength limit');
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should enforce maxContentLength for streamed responses', async () => {
    const size = 2 * 1024 * 1024;
    const body = Buffer.alloc(size, 0x63);
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/octet-stream');
        res.end(body);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        responseType: 'stream',
        maxContentLength: 1024,
      });

      let bytesRead = 0;
      const err = await new Promise((resolve) => {
        response.data.on('data', (chunk) => {
          bytesRead += chunk.length;
        });
        response.data.on('error', resolve);
        response.data.on('end', () => resolve(null));
      });

      assert.ok(err, 'stream should emit an error');
      assert.strictEqual(err.message, 'maxContentLength size of 1024 exceeded');
      assert.ok(bytesRead <= 1024 * 64, `stream should not deliver full payload; got ${bytesRead}`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow streamed responses under maxContentLength', async () => {
    const body = Buffer.alloc(512, 0x64);
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'application/octet-stream');
        res.end(body);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        responseType: 'stream',
        maxContentLength: 1024,
      });

      const chunks = [];
      await new Promise((resolve, reject) => {
        response.data.on('data', (chunk) => chunks.push(chunk));
        response.data.on('error', reject);
        response.data.on('end', resolve);
      });

      assert.strictEqual(Buffer.concat(chunks).length, body.length);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should enforce maxBodyLength for streamed uploads with maxRedirects: 0', async () => {
    let bytesReceived = 0;
    const server = await startHTTPServer(
      (req, res) => {
        req.on('data', (chunk) => {
          bytesReceived += chunk.length;
        });
        req.on('end', () => {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ received: bytesReceived }));
        });
      },
      { port: SERVER_PORT }
    );

    try {
      const size = 2 * 1024 * 1024;
      const buf = Buffer.alloc(size, 0x61);
      const source = stream.Readable.from([buf]);

      await assert.rejects(
        axios.post(`http://localhost:${server.address().port}/`, source, {
          maxBodyLength: 1024,
          maxRedirects: 0,
          headers: { 'Content-Type': 'application/octet-stream' },
        }),
        (error) => {
          assert.strictEqual(error.message, 'Request body larger than maxBodyLength limit');
          return true;
        }
      );

      assert.ok(
        bytesReceived <= 1024 * 4,
        `server should not receive full payload; got ${bytesReceived}`
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should allow streamed uploads under maxBodyLength with maxRedirects: 0', async () => {
    let bytesReceived = 0;
    const server = await startHTTPServer(
      (req, res) => {
        req.on('data', (chunk) => {
          bytesReceived += chunk.length;
        });
        req.on('end', () => {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ received: bytesReceived }));
        });
      },
      { port: SERVER_PORT }
    );

    try {
      const payload = Buffer.alloc(512, 0x62);
      const source = stream.Readable.from([payload]);

      const response = await axios.post(`http://localhost:${server.address().port}/`, source, {
        maxBodyLength: 1024,
        maxRedirects: 0,
        headers: { 'Content-Type': 'application/octet-stream' },
      });

      assert.strictEqual(response.data.received, payload.length);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should properly support default max body length (follow-redirects as well)', async () => {
    // Taken from follow-redirects defaults.
    const followRedirectsMaxBodyDefaults = 10 * 1024 * 1024;
    const data = Array(2 * followRedirectsMaxBodyDefaults).join('ж');

    const server = await startHTTPServer(
      (req, res) => {
        // Consume the req stream before responding to avoid ECONNRESET.
        req.on('data', () => {});
        req.on('end', () => {
          res.end('OK');
        });
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.post(`http://localhost:${server.address().port}/`, {
        data,
      });
      assert.strictEqual(response.data, 'OK', 'should handle response');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should display error while parsing params', async () => {
    const server = await startHTTPServer(() => {}, { port: SERVER_PORT });

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}/`, {
          params: {
            errorParam: new Date(undefined),
          },
        }),
        (error) => {
          assert.deepStrictEqual(error.exists, true);
          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support sockets', async () => {
    let socketName = path.join(
      os.tmpdir(),
      `axios-test-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.sock`
    );

    if (process.platform === 'win32') {
      socketName = '\\\\.\\pipe\\libuv-test';
    }

    let server;
    try {
      server = await new Promise((resolve, reject) => {
        const socketServer = net
          .createServer((socket) => {
            socket.on('data', () => {
              socket.end('HTTP/1.1 200 OK\r\n\r\n');
            });
          })
          .listen(socketName, () => resolve(socketServer));

        socketServer.on('error', reject);
      });
    } catch (error) {
      if (error && error.code === 'EPERM') {
        return;
      }

      throw error;
    }

    try {
      const response = await axios({
        socketPath: socketName,
        url: 'http://localhost:4444/socket',
      });
      assert.strictEqual(response.status, 200);
      assert.strictEqual(response.statusText, 'OK');
    } finally {
      await new Promise((resolve, reject) => {
        server.close((error) => {
          if (error) {
            reject(error);
            return;
          }

          resolve();
        });
      });
    }
  });

  describe('streams', () => {
    it('should support streams', async () => {
      const server = await startHTTPServer(
        (req, res) => {
          req.pipe(res);
        },
        { port: SERVER_PORT }
      );

      try {
        const response = await axios.post(
          `http://localhost:${server.address().port}/`,
          fs.createReadStream(thisTestFilePath),
          {
            responseType: 'stream',
          }
        );

        const responseText = await new Promise((resolve, reject) => {
          const chunks = [];

          response.data.on('data', (chunk) => {
            chunks.push(chunk);
          });

          response.data.on('end', () => {
            resolve(Buffer.concat(chunks).toString('utf8'));
          });

          response.data.on('error', reject);
        });

        assert.strictEqual(responseText, fs.readFileSync(thisTestFilePath, 'utf8'));
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should pass errors for a failed stream', async () => {
      const server = await startHTTPServer(() => {}, { port: SERVER_PORT });
      const notExistPath = path.join(adaptersTestsDir, 'does_not_exist');

      try {
        await assert.rejects(
          axios.post(
            `http://localhost:${server.address().port}/`,
            fs.createReadStream(notExistPath)
          ),
          (error) => {
            assert.strictEqual(
              error.message,
              `ENOENT: no such file or directory, open '${notExistPath}'`
            );
            return true;
          }
        );
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should destroy the response stream with an error on request stream destroying', async () => {
      const server = await startHTTPServer();
      const requestStream = generateReadable();

      setTimeout(() => {
        requestStream.destroy();
      }, 1000);

      const { data } = await axios.post(
        `http://localhost:${server.address().port}/`,
        requestStream,
        {
          responseType: 'stream',
        }
      );

      let streamError;
      data.on('error', (error) => {
        streamError = error;
      });

      try {
        await new Promise((resolve, reject) => {
          stream.pipeline(data, devNull(), (error) => {
            if (error) {
              reject(error);
              return;
            }

            resolve();
          });
        });
        assert.fail('stream was not aborted');
      } catch (error) {
        // Expected: the request stream is destroyed before completion.
      } finally {
        assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
        await stopHTTPServer(server);
      }
    });
  });

  it('should support buffers', async () => {
    const buf = Buffer.alloc(1024, 'x'); // Unsafe buffer < Buffer.poolSize (8192 bytes)
    const server = await startHTTPServer(
      (req, res) => {
        assert.strictEqual(req.headers['content-length'], buf.length.toString());
        req.pipe(res);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.post(`http://localhost:${server.address().port}/`, buf, {
        responseType: 'stream',
      });

      const responseText = await new Promise((resolve, reject) => {
        const chunks = [];

        response.data.on('data', (chunk) => {
          chunks.push(chunk);
        });

        response.data.on('end', () => {
          resolve(Buffer.concat(chunks).toString('utf8'));
        });

        response.data.on('error', reject);
      });

      assert.strictEqual(responseText, buf.toString());
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support HTTP proxies', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end('12345');
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };

        http.get(opts, (res) => {
          let body = '';

          res.on('data', (data) => {
            body += data;
          });

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '6789');
          });
        });
      },
      { port: PROXY_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        proxy: {
          host: 'localhost',
          port: proxy.address().port,
        },
      });

      assert.strictEqual(Number(response.data), 123456789, 'should pass through proxy');
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);
    }
  });

  it('should support HTTPS proxies', async () => {
    const tlsOptions = {
      key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')),
      cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')),
    };

    const closeServer = (server) =>
      new Promise((resolve, reject) => {
        server.close((error) => {
          if (error) {
            reject(error);
            return;
          }

          resolve();
        });
      });

    const server = await new Promise((resolve, reject) => {
      const httpsServer = https
        .createServer(
          tlsOptions,
          (req, res) => {
            res.setHeader('Content-Type', 'text/html; charset=UTF-8');
            res.end('12345');
          },
          { port: SERVER_PORT }
        )
        .listen(SERVER_PORT, () => resolve(httpsServer));

      httpsServer.on('error', reject);
    });

    const proxy = await new Promise((resolve, reject) => {
      const httpsProxy = https
        .createServer(
          tlsOptions,
          (request, response) => {
            const targetUrl = new URL(request.url);
            const opts = {
              host: targetUrl.hostname,
              port: targetUrl.port,
              path: `${targetUrl.pathname}${targetUrl.search}`,
              protocol: targetUrl.protocol,
              rejectUnauthorized: false,
            };

            const proxyRequest = https.get(opts, (res) => {
              let body = '';

              res.on('data', (data) => {
                body += data;
              });

              res.on('end', () => {
                response.setHeader('Content-Type', 'text/html; charset=UTF-8');
                response.end(body + '6789');
              });
            });

            proxyRequest.on('error', () => {
              response.statusCode = 502;
              response.end();
            });
          },
          { port: PROXY_PORT }
        )
        .listen(PROXY_PORT, () => resolve(httpsProxy));

      httpsProxy.on('error', reject);
    });

    try {
      const response = await axios.get(`https://localhost:${server.address().port}/`, {
        proxy: {
          host: 'localhost',
          port: proxy.address().port,
          protocol: 'https:',
        },
        httpsAgent: new https.Agent({
          rejectUnauthorized: false,
        }),
      });

      assert.strictEqual(Number(response.data), 123456789, 'should pass through proxy');
    } finally {
      await Promise.all([closeServer(server), closeServer(proxy)]);
    }
  });

  it('should not pass through disabled proxy', async () => {
    const originalHttpProxy = process.env.http_proxy;
    process.env.http_proxy = 'http://does-not-exists.example.com:4242/';

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end('123456789');
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        proxy: false,
      });

      assert.strictEqual(Number(response.data), 123456789, 'should not pass through proxy');
    } finally {
      await stopHTTPServer(server);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }
    }
  });

  it('should support proxy set via env var', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end('4567');
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };

        http.get(opts, (res) => {
          let body = '';

          res.on('data', (data) => {
            body += data;
          });

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });
      },
      { port: PROXY_PORT }
    );

    const proxyUrl = `http://localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = '';
    process.env.NO_PROXY = '';

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);

      assert.strictEqual(
        String(response.data),
        '45671234',
        'should use proxy set by process.env.http_proxy'
      );
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should support HTTPS proxy set via env var', async () => {
    const originalHttpsProxy = process.env.https_proxy;
    const originalHTTPSProxy = process.env.HTTPS_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    const tlsOptions = {
      key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')),
      cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')),
    };

    const closeServer = (server) =>
      new Promise((resolve, reject) => {
        server.close((error) => {
          if (error) {
            reject(error);
            return;
          }

          resolve();
        });
      });

    const server = await new Promise((resolve, reject) => {
      const httpsServer = https
        .createServer(
          tlsOptions,
          (req, res) => {
            res.setHeader('Content-Type', 'text/html; charset=UTF-8');
            res.end('12345');
          },
          { port: SERVER_PORT }
        )
        .listen(SERVER_PORT, () => resolve(httpsServer));

      httpsServer.on('error', reject);
    });

    const proxy = await new Promise((resolve, reject) => {
      const httpsProxy = https
        .createServer(
          tlsOptions,
          (request, response) => {
            const targetUrl = new URL(request.url);
            const opts = {
              host: targetUrl.hostname,
              port: targetUrl.port,
              path: `${targetUrl.pathname}${targetUrl.search}`,
              protocol: targetUrl.protocol,
              rejectUnauthorized: false,
            };

            const proxyRequest = https.get(opts, (res) => {
              let body = '';

              res.on('data', (data) => {
                body += data;
              });

              res.on('end', () => {
                response.setHeader('Content-Type', 'text/html; charset=UTF-8');
                response.end(body + '6789');
              });
            });

            proxyRequest.on('error', () => {
              response.statusCode = 502;
              response.end();
            });
          },
          { port: PROXY_PORT }
        )
        .listen(PROXY_PORT, () => resolve(httpsProxy));

      httpsProxy.on('error', reject);
    });

    const proxyUrl = `https://localhost:${proxy.address().port}/`;
    process.env.https_proxy = proxyUrl;
    process.env.HTTPS_PROXY = proxyUrl;
    process.env.no_proxy = '';
    process.env.NO_PROXY = '';

    try {
      const response = await axios.get(`https://localhost:${server.address().port}/`, {
        httpsAgent: new https.Agent({
          rejectUnauthorized: false,
        }),
      });

      assert.equal(response.data, '123456789', 'should pass through proxy');
    } finally {
      await Promise.all([closeServer(server), closeServer(proxy)]);

      if (originalHttpsProxy === undefined) {
        delete process.env.https_proxy;
      } else {
        process.env.https_proxy = originalHttpsProxy;
      }

      if (originalHTTPSProxy === undefined) {
        delete process.env.HTTPS_PROXY;
      } else {
        process.env.HTTPS_PROXY = originalHTTPSProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should re-evaluate proxy on redirect when proxy set via env var', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    let proxyUseCount = 0;

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Location', `http://localhost:${proxy.address().port}/redirected`);
        res.statusCode = 302;
        res.end();
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url, 'http://localhost');

        if (parsed.pathname === '/redirected') {
          response.statusCode = 200;
          response.end();
          return;
        }

        proxyUseCount += 1;

        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
          protocol: parsed.protocol,
        };

        http.get(opts, (res) => {
          let body = '';

          res.on('data', (data) => {
            body += data;
          });

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.setHeader('Location', res.headers.location);
            response.end(body);
          });
        });
      },
      { port: PROXY_PORT }
    );

    const proxyUrl = `http://localhost:${proxy.address().port}`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = `localhost:${proxy.address().port}`;
    process.env.NO_PROXY = `localhost:${proxy.address().port}`;

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      assert.equal(response.status, 200);
      assert.equal(proxyUseCount, 1);
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should not use proxy for domains in no_proxy', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end('4567');
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };

        http.get(opts, (res) => {
          let body = '';

          res.on('data', (data) => {
            body += data;
          });

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });
      },
      { port: PROXY_PORT }
    );

    const noProxyValue = 'foo.com, localhost,bar.net , , quix.co';
    const proxyUrl = `http://localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = noProxyValue;
    process.env.NO_PROXY = noProxyValue;

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      assert.equal(response.data, '4567', 'should not use proxy for domains in no_proxy');
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should not use proxy for localhost with trailing dot when listed in no_proxy', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    let proxyRequests = 0;
    const proxy = await startHTTPServer(
      (_, response) => {
        proxyRequests += 1;
        response.end('proxied');
      },
      { port: PROXY_PORT }
    );

    const noProxyValue = 'localhost,127.0.0.1,::1';
    const proxyUrl = `http://localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = noProxyValue;
    process.env.NO_PROXY = noProxyValue;

    try {
      await assert.rejects(axios.get('http://localhost.:1/', { timeout: 100 }));
      assert.equal(proxyRequests, 0, 'should not use proxy for localhost with trailing dot');
    } finally {
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should not use proxy for bracketed IPv6 loopback when listed in no_proxy', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    let proxyRequests = 0;
    const proxy = await startHTTPServer(
      (_, response) => {
        proxyRequests += 1;
        response.end('proxied');
      },
      { port: PROXY_PORT }
    );

    const noProxyValue = 'localhost,127.0.0.1,::1';
    const proxyUrl = `http://localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = noProxyValue;
    process.env.NO_PROXY = noProxyValue;

    try {
      await assert.rejects(axios.get('http://[::1]:1/', { timeout: 100 }));
      assert.equal(proxyRequests, 0, 'should not use proxy for IPv6 loopback');
    } finally {
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should use proxy for domains not in no_proxy', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    const server = await startHTTPServer(
      (req, res) => {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end('4567');
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };

        http.get(opts, (res) => {
          let body = '';

          res.on('data', (data) => {
            body += data;
          });

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });
      },
      { port: PROXY_PORT }
    );

    const noProxyValue = 'foo.com, ,bar.net , quix.co';
    const proxyUrl = `http://localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = noProxyValue;
    process.env.NO_PROXY = noProxyValue;

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      assert.equal(response.data, '45671234', 'should use proxy for domains not in no_proxy');
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  it('should support HTTP proxy auth', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end();
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };
        const proxyAuth = request.headers['proxy-authorization'];

        http.get(opts, (res) => {
          res.on('data', () => {});

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(proxyAuth);
          });
        });
      },
      { port: PROXY_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`, {
        proxy: {
          host: 'localhost',
          port: proxy.address().port,
          auth: {
            username: 'user',
            password: 'pass',
          },
        },
      });

      const base64 = Buffer.from('user:pass', 'utf8').toString('base64');
      assert.equal(response.data, `Basic ${base64}`, 'should authenticate to the proxy');
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);
    }
  });

  it('should support proxy auth from env', async () => {
    const originalHttpProxy = process.env.http_proxy;
    const originalHTTPProxy = process.env.HTTP_PROXY;
    const originalNoProxy = process.env.no_proxy;
    const originalNOProxy = process.env.NO_PROXY;

    const server = await startHTTPServer(
      (req, res) => {
        res.end();
      },
      { port: SERVER_PORT }
    );

    const proxy = await startHTTPServer(
      (request, response) => {
        const parsed = new URL(request.url);
        const opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: `${parsed.pathname}${parsed.search}`,
        };
        const proxyAuth = request.headers['proxy-authorization'];

        http.get(opts, (res) => {
          res.on('data', () => {});

          res.on('end', () => {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(proxyAuth);
          });
        });
      },
      { port: PROXY_PORT }
    );

    const proxyUrl = `http://user:pass@localhost:${proxy.address().port}/`;
    process.env.http_proxy = proxyUrl;
    process.env.HTTP_PROXY = proxyUrl;
    process.env.no_proxy = '';
    process.env.NO_PROXY = '';

    try {
      const response = await axios.get(`http://localhost:${server.address().port}/`);
      const base64 = Buffer.from('user:pass', 'utf8').toString('base64');
      assert.equal(
        response.data,
        `Basic ${base64}`,
        'should authenticate to the proxy set by process.env.http_proxy'
      );
    } finally {
      await stopHTTPServer(server);
      await stopHTTPServer(proxy);

      if (originalHttpProxy === undefined) {
        delete process.env.http_proxy;
      } else {
        process.env.http_proxy = originalHttpProxy;
      }

      if (originalHTTPProxy === undefined) {
        delete process.env.HTTP_PROXY;
      } else {
        process.env.HTTP_PROXY = originalHTTPProxy;
      }

      if (originalNoProxy === undefined) {
        delete process.env.no_proxy;
      } else {
        process.env.no_proxy = originalNoProxy;
      }

      if (originalNOProxy === undefined) {
        delete process.env.NO_PROXY;
      } else {
        process.env.NO_PROXY = originalNOProxy;
      }
    }
  });

  describe('when invalid proxy options are provided', () => {
    it('should throw error', async () => {
      const proxy = {
        protocol: 'http:',
        host: 'hostname.abc.xyz',
        port: PROXY_PORT,
        auth: {
          username: '',
          password: '',
        },
      };

      await assert.rejects(axios.get('https://test-domain.abc', { proxy }), (error) => {
        assert.strictEqual(error.message, 'Invalid proxy authorization');
        assert.strictEqual(error.code, 'ERR_BAD_OPTION');
        assert.deepStrictEqual(error.config.proxy, proxy);
        return true;
      });
    });
  });

  describe('different options for direct proxy configuration (without env variables)', () => {
    const destination = 'www.example.com';

    const testCases = [
      {
        description: 'hostname and trailing colon in protocol',
        proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: OPEN_WEB_PORT },
        expectedOptions: {
          host: '127.0.0.1',
          protocol: 'http:',
          port: OPEN_WEB_PORT,
          path: destination,
        },
      },
      {
        description: 'hostname and no trailing colon in protocol',
        proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: OPEN_WEB_PORT },
        expectedOptions: {
          host: '127.0.0.1',
          protocol: 'http:',
          port: OPEN_WEB_PORT,
          path: destination,
        },
      },
      {
        description: 'both hostname and host -> hostname takes precedence',
        proxyConfig: {
          hostname: '127.0.0.1',
          host: '0.0.0.0',
          protocol: 'http',
          port: OPEN_WEB_PORT,
        },
        expectedOptions: {
          host: '127.0.0.1',
          protocol: 'http:',
          port: OPEN_WEB_PORT,
          path: destination,
        },
      },
      {
        description: 'only host and https protocol',
        proxyConfig: { host: '0.0.0.0', protocol: 'https', port: OPEN_WEB_PORT },
        expectedOptions: {
          host: '0.0.0.0',
          protocol: 'https:',
          port: OPEN_WEB_PORT,
          path: destination,
        },
      },
    ];

    for (const test of testCases) {
      it(test.description, () => {
        const options = { headers: {}, beforeRedirects: {} };
        __setProxy(options, test.proxyConfig, destination);

        for (const [key, expected] of Object.entries(test.expectedOptions)) {
          assert.strictEqual(options[key], expected);
        }
      });
    }
  });

  describe('Host header preservation when forwarding through a proxy (#10805)', () => {
    const proxyConfig = { hostname: '127.0.0.1', protocol: 'http:', port: 8888 };

    it('defaults the Host header to the request target when the user does not set one', () => {
      const options = {
        headers: {},
        beforeRedirects: {},
        hostname: '127.0.0.1',
        port: 4000,
      };

      __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/');

      assert.strictEqual(options.headers.host, '127.0.0.1:4000');
    });

    it('preserves a user-supplied lowercase host header', () => {
      const options = {
        headers: { host: 'example.com' },
        beforeRedirects: {},
        hostname: '127.0.0.1',
        port: 4000,
      };

      __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/');

      assert.strictEqual(options.headers.host, 'example.com');
    });

    it('preserves a user-supplied Host header regardless of casing', () => {
      const options = {
        headers: { Host: 'example.com' },
        beforeRedirects: {},
        hostname: '127.0.0.1',
        port: 4000,
      };

      __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/');

      assert.strictEqual(options.headers.Host, 'example.com');
      assert.strictEqual(options.headers.host, undefined);
    });

    it('preserves a user-supplied Host header across a redirect re-invocation', () => {
      const options = {
        headers: { Host: 'example.com' },
        beforeRedirects: {},
        hostname: '127.0.0.1',
        port: 4000,
      };

      __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/', true);

      assert.strictEqual(options.headers.Host, 'example.com');
      assert.strictEqual(options.headers.host, undefined);
    });

    it('ignores polluted prototype Host fields when detecting user-supplied headers', () => {
      Object.prototype.host = 'polluted.example.com';

      const options = {
        headers: {},
        beforeRedirects: {},
        hostname: '127.0.0.1',
        port: 4000,
      };

      try {
        __setProxy(options, proxyConfig, 'http://127.0.0.1:4000/');

        assert.strictEqual(options.headers.host, '127.0.0.1:4000');
      } finally {
        delete Object.prototype.host;
      }
    });
  });

  describe('Proxy-Authorization header leak on redirect', () => {
    it('clears a stale Proxy-Authorization header when redirected request resolves to no proxy (configProxy=false)', () => {
      const options = {
        headers: {},
        beforeRedirects: {},
        hostname: 'initial.example.com',
        host: 'initial.example.com',
        port: 80,
      };

      __setProxy(
        options,
        { host: '127.0.0.1', port: 8030, auth: { username: 'user', password: 'pass' } },
        'http://initial.example.com/start'
      );
      assert.strictEqual(
        options.headers['Proxy-Authorization'],
        'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'),
        'initial request should carry Proxy-Authorization'
      );

      // Simulate redirect re-invocation where the redirected request is resolved to no proxy.
      // This mirrors the beforeRedirects.proxy hook being called with configProxy=false.
      const redirectOptions = {
        headers: { ...options.headers },
        beforeRedirects: {},
        hostname: 'attacker.example.com',
        host: 'attacker.example.com',
        port: 443,
      };
      __setProxy(redirectOptions, false, 'https://attacker.example.com/final', true);

      assert.strictEqual(
        redirectOptions.headers['Proxy-Authorization'],
        undefined,
        'stale Proxy-Authorization must be stripped when redirected request no longer uses a proxy'
      );
    });

    it('clears a stale Proxy-Authorization header when environment-derived proxy is bypassed on redirect (NO_PROXY)', () => {
      const originalHttpProxy = process.env.http_proxy;
      const originalHttpsProxy = process.env.https_proxy;
      const originalNoProxy = process.env.no_proxy;

      process.env.http_proxy = 'http://user:pass@127.0.0.1:8030';
      process.env.https_proxy = 'http://user:pass@127.0.0.1:8030';
      process.env.no_proxy = 'attacker.example.com';

      try {
        const options = {
          headers: {},
          beforeRedirects: {},
          hostname: 'initial.example.com',
          host: 'initial.example.com',
          port: 80,
        };

        __setProxy(options, undefined, 'http://initial.example.com/start');
        assert.strictEqual(
          options.headers['Proxy-Authorization'],
          'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'),
          'initial request should pick up proxy credentials from env'
        );

        const redirectOptions = {
          headers: { ...options.headers },
          beforeRedirects: {},
          hostname: 'attacker.example.com',
          host: 'attacker.example.com',
          port: 443,
          protocol: 'https:',
        };
        __setProxy(redirectOptions, undefined, 'https://attacker.example.com/final', true);

        assert.strictEqual(
          redirectOptions.headers['Proxy-Authorization'],
          undefined,
          'stale Proxy-Authorization must be stripped when redirect target is covered by NO_PROXY'
        );
      } finally {
        if (originalHttpProxy === undefined) delete process.env.http_proxy;
        else process.env.http_proxy = originalHttpProxy;
        if (originalHttpsProxy === undefined) delete process.env.https_proxy;
        else process.env.https_proxy = originalHttpsProxy;
        if (originalNoProxy === undefined) delete process.env.no_proxy;
        else process.env.no_proxy = originalNoProxy;
      }
    });

    it('replaces Proxy-Authorization when redirect target resolves to a different proxy without credentials', () => {
      const options = {
        headers: {},
        beforeRedirects: {},
        hostname: 'initial.example.com',
        host: 'initial.example.com',
        port: 80,
      };

      __setProxy(
        options,
        { host: '127.0.0.1', port: 8030, auth: { username: 'user', password: 'pass' } },
        'http://initial.example.com/start'
      );
      assert.ok(
        options.headers['Proxy-Authorization'],
        'precondition: initial proxy auth header set'
      );

      const redirectOptions = {
        headers: { ...options.headers },
        beforeRedirects: {},
        hostname: 'second.example.com',
        host: 'second.example.com',
        port: 80,
      };
      __setProxy(
        redirectOptions,
        { host: '127.0.0.2', port: 8031 },
        'http://second.example.com/final',
        true
      );

      assert.strictEqual(
        redirectOptions.headers['Proxy-Authorization'],
        undefined,
        'stale credentials from previous proxy must not leak to a new proxy without credentials'
      );
    });

    it('strips stale Proxy-Authorization when the beforeRedirects.proxy hook is invoked with configProxy=false', () => {
      const options = {
        headers: {
          'Proxy-Authorization': 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'),
        },
        beforeRedirects: {},
        hostname: 'initial.example.com',
        host: 'initial.example.com',
        port: 80,
      };

      __setProxy(options, false, 'http://initial.example.com/start');
      assert.strictEqual(
        typeof options.beforeRedirects.proxy,
        'function',
        'initial setProxy must install redirect hook'
      );

      const redirectOptions = {
        headers: {
          'Proxy-Authorization': 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64'),
        },
        beforeRedirects: {},
        hostname: 'attacker.example.com',
        host: 'attacker.example.com',
        port: 443,
        href: 'https://attacker.example.com/final',
      };

      options.beforeRedirects.proxy(redirectOptions);

      assert.strictEqual(
        redirectOptions.headers['Proxy-Authorization'],
        undefined,
        'beforeRedirects.proxy hook must strip stale Proxy-Authorization when redirect target has no proxy'
      );
    });

    it('preserves a user-supplied Proxy-Authorization header on the initial request when no proxy is configured', () => {
      const userValue = 'Basic ' + Buffer.from('alice:secret', 'utf8').toString('base64');
      const options = {
        headers: { 'Proxy-Authorization': userValue },
        beforeRedirects: {},
        hostname: 'example.com',
        host: 'example.com',
        port: 80,
      };

      __setProxy(options, false, 'http://example.com/start');

      assert.strictEqual(
        options.headers['Proxy-Authorization'],
        userValue,
        'user-supplied Proxy-Authorization must not be stripped on the initial request'
      );
    });

    it('strips stale Proxy-Authorization regardless of header key casing', () => {
      const staleValue = 'Basic ' + Buffer.from('user:pass', 'utf8').toString('base64');
      const casings = [
        'proxy-authorization',
        'PROXY-AUTHORIZATION',
        'Proxy-authorization',
        'pRoXy-AuThOrIzAtIoN',
      ];

      for (const casing of casings) {
        const redirectOptions = {
          headers: { [casing]: staleValue },
          beforeRedirects: {},
          hostname: 'attacker.example.com',
          host: 'attacker.example.com',
          port: 443,
        };

        __setProxy(redirectOptions, false, 'https://attacker.example.com/final', true);

        const leaked = Object.keys(redirectOptions.headers).filter(
          (name) => name.toLowerCase() === 'proxy-authorization'
        );
        assert.deepStrictEqual(
          leaked,
          [],
          `stale Proxy-Authorization with key "${casing}" must be stripped regardless of casing`
        );
      }
    });

    // End-to-end exercise of the redirect leak. An
    // authenticated env-supplied proxy sees the initial request, 302s the
    // client to a target that NO_PROXY excludes, and the redirected request
    // must not carry the stale Proxy-Authorization to the direct target.
    it('does not forward Proxy-Authorization to a redirect target that resolves to no-proxy', async () => {
      const startServer = (handler) =>
        new Promise((resolve) => {
          const s = http.createServer(handler);
          s.listen(0, '127.0.0.1', () => resolve(s));
        });
      const stop = (s) => new Promise((r) => s.close(r));

      let attackerPort;
      const proxySaw = [];
      const attackerSaw = [];

      // The proxy receives the absolute-form URL (`GET http://target/path`) on
      // the initial request, then forwards to the destination. We short-circuit
      // by responding directly with the redirect.
      const corpProxy = await startServer((req, res) => {
        proxySaw.push({ url: req.url, proxyAuth: req.headers['proxy-authorization'] });
        res.writeHead(302, { Location: `http://127.0.0.1:${attackerPort}/final` });
        res.end();
      });

      const attacker = await startServer((req, res) => {
        attackerSaw.push({
          url: req.url,
          proxyAuth: req.headers['proxy-authorization'],
          authorization: req.headers.authorization,
        });
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end('{"final":true}');
      });
      attackerPort = attacker.address().port;

      const corpProxyPort = corpProxy.address().port;
      const originalHttpProxy = process.env.http_proxy;
      const originalNoProxy = process.env.no_proxy;
      process.env.http_proxy = `http://user:pass@127.0.0.1:${corpProxyPort}`;
      // NO_PROXY entry covers only the attacker target (port-specific), so the
      // initial request still uses the proxy but the redirect resolves direct.
      process.env.no_proxy = `127.0.0.1:${attackerPort}`;

      try {
        await axios.get('http://example.com/start');

        assert.ok(
          proxySaw.some((h) => h.proxyAuth),
          'precondition: corp proxy must see Proxy-Authorization on the initial request'
        );
        assert.strictEqual(
          attackerSaw.length,
          1,
          'attacker target must receive exactly the redirected request'
        );
        assert.strictEqual(
          attackerSaw[0].proxyAuth,
          undefined,
          'stale Proxy-Authorization must not leak to the redirect target'
        );
      } finally {
        if (originalHttpProxy === undefined) delete process.env.http_proxy;
        else process.env.http_proxy = originalHttpProxy;
        if (originalNoProxy === undefined) delete process.env.no_proxy;
        else process.env.no_proxy = originalNoProxy;
        await stop(corpProxy);
        await stop(attacker);
      }
    }, 10000);
  });

  it('should support cancel', async () => {
    const source = axios.CancelToken.source();

    const server = await startHTTPServer(
      (req, res) => {
        // Call cancel() when the request has been sent but no response received.
        source.cancel('Operation has been canceled.');
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        async function stackTraceTest() {
          await axios.get(`http://localhost:${server.address().port}/`, {
            cancelToken: source.token,
          });
        },
        (thrown) => {
          assert.ok(
            thrown instanceof axios.Cancel,
            'Promise must be rejected with a CanceledError object'
          );
          assert.equal(thrown.message, 'Operation has been canceled.');

          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should combine baseURL and url', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get('/foo', {
        baseURL: `http://localhost:${server.address().port}/`,
      });

      assert.equal(response.config.baseURL, `http://localhost:${server.address().port}/`);
      assert.equal(response.config.url, '/foo');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support HTTP protocol', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        setTimeout(() => {
          res.end();
        }, 1000);
      },
      { port: SERVER_PORT }
    );

    try {
      const response = await axios.get(`http://localhost:${server.address().port}`);
      assert.equal(response.request.agent.protocol, 'http:');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should support HTTPS protocol', async () => {
    const tlsOptions = {
      key: fs.readFileSync(path.join(adaptersTestsDir, 'key.pem')),
      cert: fs.readFileSync(path.join(adaptersTestsDir, 'cert.pem')),
    };

    const server = await new Promise((resolve, reject) => {
      const httpsServer = https
        .createServer(
          tlsOptions,
          (req, res) => {
            setTimeout(() => {
              res.end();
            }, 1000);
          },
          { port: SERVER_PORT }
        )
        .listen(SERVER_PORT, () => resolve(httpsServer));

      httpsServer.on('error', reject);
    });

    try {
      const response = await axios.get(`https://localhost:${server.address().port}`, {
        httpsAgent: new https.Agent({
          rejectUnauthorized: false,
        }),
      });
      assert.equal(response.request.agent.protocol, 'https:');
    } finally {
      await new Promise((resolve, reject) => {
        server.close((error) => {
          if (error) {
            reject(error);
            return;
          }

          resolve();
        });
      });
    }
  });

  it('should return malformed URL', async () => {
    await assert.rejects(axios.get('tel:484-695-3408'), (error) => {
      assert.equal(error.message, 'Unsupported protocol tel:');
      return true;
    });
  });

  it('should return unsupported protocol', async () => {
    await assert.rejects(axios.get('ftp:google.com'), (error) => {
      assert.equal(error.message, 'Unsupported protocol ftp:');
      return true;
    });
  });

  it('should supply a user-agent if one is not specified', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        assert.equal(req.headers['user-agent'], `axios/${axios.VERSION}`);
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await axios.get(`http://localhost:${server.address().port}/`);
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should omit a user-agent if one is explicitly disclaimed', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        assert.equal('user-agent' in req.headers, false);
        assert.equal('User-Agent' in req.headers, false);
        res.end();
      },
      { port: SERVER_PORT }
    );

    try {
      await axios.get(`http://localhost:${server.address().port}/`, {
        headers: {
          'User-Agent': null,
        },
      });
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should throw an error if http server that aborts a chunked request', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.writeHead(200, { 'Content-Type': 'text/plain', 'X-Stream-Aborted': 'yes' });
        res.write('chunk 1');

        setTimeout(() => {
          res.write('chunk 2');
        }, 100);

        setTimeout(() => {
          res.destroy();
        }, 200);
      },
      { port: SERVER_PORT }
    );

    try {
      await assert.rejects(
        axios.get(`http://localhost:${server.address().port}/aborted`, {
          timeout: 500,
        }),
        (error) => {
          assert.strictEqual(error.code, 'ERR_BAD_RESPONSE');
          assert.strictEqual(error.message, 'stream has been aborted');
          assert.strictEqual(error.response.status, 200);
          assert.strictEqual(error.response.headers.get('x-stream-aborted'), 'yes');
          assert.strictEqual(error.status, 200);

          return true;
        }
      );
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should able to cancel multiple requests with CancelToken', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.end('ok');
      },
      { port: SERVER_PORT }
    );

    try {
      const source = axios.CancelToken.source();
      const canceledStack = [];

      const requests = [1, 2, 3, 4, 5].map(async (id) => {
        try {
          await axios.get('/foo/bar', {
            baseURL: `http://localhost:${server.address().port}`,
            cancelToken: source.token,
          });
        } catch (error) {
          if (!axios.isCancel(error)) {
            throw error;
          }

          canceledStack.push(id);
        }
      });

      source.cancel('Aborted by user');

      await Promise.all(requests);
      assert.deepStrictEqual(canceledStack.sort(), [1, 2, 3, 4, 5]);
    } finally {
      await stopHTTPServer(server);
    }
  });

  describe('FormData', () => {
    describe('form-data instance (https://www.npmjs.com/package/form-data)', () => {
      it('should allow passing FormData', async () => {
        const form = new FormDataLegacy();
        const file1 = Buffer.from('foo', 'utf8');
        const image = path.resolve(adaptersTestsDir, './axios.png');
        const fileStream = fs.createReadStream(image);
        const stat = fs.statSync(image);

        form.append('foo', 'bar');
        form.append('file1', file1, {
          filename: 'bar.jpg',
          filepath: 'temp/bar.jpg',
          contentType: 'image/jpeg',
        });
        form.append('fileStream', fileStream);

        const server = await startHTTPServer(
          (req, res) => {
            const receivedForm = new IncomingForm();

            assert.ok(req.rawHeaders.some((header) => header.toLowerCase() === 'content-length'));

            receivedForm.parse(req, (error, fields, files) => {
              if (error) {
                res.statusCode = 500;
                res.end(error.message);
                return;
              }

              res.end(
                JSON.stringify({
                  fields,
                  files,
                })
              );
            });
          },
          { port: SERVER_PORT }
        );

        try {
          const response = await axios.post(`http://localhost:${server.address().port}/`, form, {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          });

          assert.deepStrictEqual(response.data.fields, { foo: ['bar'] });

          assert.strictEqual(response.data.files.file1[0].mimetype, 'image/jpeg');
          assert.strictEqual(response.data.files.file1[0].originalFilename, 'temp/bar.jpg');
          assert.strictEqual(response.data.files.file1[0].size, 3);

          assert.strictEqual(response.data.files.fileStream[0].mimetype, 'image/png');
          assert.strictEqual(response.data.files.fileStream[0].originalFilename, 'axios.png');
          assert.strictEqual(response.data.files.fileStream[0].size, stat.size);
        } finally {
          await stopHTTPServer(server);
        }
      });
    });

    describe('SpecCompliant FormData', () => {
      it('should allow passing FormData', { retry: 2 }, async () => {
        // Use an ephemeral port and a non-keep-alive agent. Sharing the fixed
        // SERVER_PORT across tests can leave keep-alive sockets in the global
        // pool that a follow-up test picks up just as the server FINs them,
        // which surfaces here as EPIPE on the multipart write.
        const server = await startHTTPServer(
          async (req, res) => {
            const { fields, files } = await handleFormData(req);

            res.end(
              JSON.stringify({
                fields,
                files,
              })
            );
          },
          { port: 0 }
        );

        const oneShotAgent = new http.Agent({ keepAlive: false });

        try {
          const form = new FormDataSpecCompliant();
          const blobContent = 'blob-content';
          const blob = new BlobSpecCompliant([blobContent], { type: 'image/jpeg' });

          form.append('foo1', 'bar1');
          form.append('foo2', 'bar2');
          form.append('file1', blob);

          const { data } = await axios.post(`http://localhost:${server.address().port}`, form, {
            maxRedirects: 0,
            httpAgent: oneShotAgent,
            headers: { Connection: 'close' },
          });

          assert.deepStrictEqual(data.fields, { foo1: ['bar1'], foo2: ['bar2'] });
          assert.deepStrictEqual(typeof data.files.file1[0], 'object');

          const { size, mimetype, originalFilename } = data.files.file1[0];

          assert.deepStrictEqual(
            { size, mimetype, originalFilename },
            {
              mimetype: 'image/jpeg',
              originalFilename: 'blob',
              size: Buffer.from(blobContent).byteLength,
            }
          );
        } finally {
          oneShotAgent.destroy();
          await stopHTTPServer(server);
        }
      });
    });

    describe('prototype pollution', () => {
      const pollutedKeys = ['getHeaders', 'append', 'pipe', 'on', 'once'];
      const toStringTagSym = Symbol.toStringTag;

      function pollute() {
        Object.prototype[toStringTagSym] = 'FormData';
        Object.prototype.append = () => {};
        Object.prototype.getHeaders = () => ({
          'x-injected': 'attacker',
          authorization: 'Bearer ATTACKER_TOKEN',
        });
        Object.prototype.pipe = function (d) {
          if (d && d.end) d.end();
          return d;
        };
        Object.prototype.on = function () {
          return this;
        };
        Object.prototype.once = function () {
          return this;
        };
      }

      function cleanup() {
        for (const k of pollutedKeys) delete Object.prototype[k];
        delete Object.prototype[toStringTagSym];
      }

      it('should not merge prototype-polluted getHeaders into outgoing request', async () => {
        // Use a stub transport rather than a real HTTP server: polluting
        // Object.prototype in-process can destabilise Node's HTTP server
        // internals and cause spurious ECONNRESET. The stub captures the final
        // outgoing headers axios constructs, which is what this test asserts on.
        let capturedHeaders;
        const stubTransport = {
          request(options, handleResponse) {
            capturedHeaders = { ...options.headers };
            const req = new EventEmitter();
            req.write = () => true;
            req.setTimeout = () => {};
            req.destroy = () => {};
            req.end = () => {
              const res = new stream.Readable({ read() {} });
              res.statusCode = 200;
              res.statusMessage = 'OK';
              res.headers = {};
              res.rawHeaders = [];
              res.req = req;
              process.nextTick(() => {
                handleResponse(res);
                res.push(null);
              });
            };
            return req;
          },
        };

        try {
          pollute();
          await axios.post(
            'http://stub.invalid/',
            { userId: 42 },
            {
              headers: { Authorization: 'Bearer VALID_USER_TOKEN' },
              transport: stubTransport,
              maxRedirects: 0,
            }
          );
        } finally {
          cleanup();
        }

        assert.ok(capturedHeaders, 'transport was not invoked');
        assert.strictEqual(capturedHeaders['x-injected'], undefined);
        assert.notStrictEqual(capturedHeaders['Authorization'], 'Bearer ATTACKER_TOKEN');
        assert.notStrictEqual(capturedHeaders['authorization'], 'Bearer ATTACKER_TOKEN');
      });
    });

    describe('formDataHeaderPolicy', () => {
      function createStubTransport(captureHeaders) {
        return {
          request(options, handleResponse) {
            captureHeaders({ ...options.headers });
            const req = new EventEmitter();
            req.write = () => true;
            req.setTimeout = () => {};
            req.destroy = () => {};
            req.end = () => {
              const res = new stream.Readable({ read() {} });
              res.statusCode = 200;
              res.statusMessage = 'OK';
              res.headers = {};
              res.rawHeaders = [];
              res.req = req;
              process.nextTick(() => {
                handleResponse(res);
                res.push(null);
              });
            };
            return req;
          },
        };
      }

      class CustomFormData extends stream.Readable {
        _read() {
          this.push(null);
        }
        append() {}
        getHeaders() {
          return {
            'content-type': 'multipart/form-data; boundary=----fake',
            'x-injected': 'custom',
            'x-forwarded-for': '10.0.0.1',
            authorization: 'Bearer CUSTOM_TOKEN',
            host: 'custom.example.com',
          };
        }
        get [Symbol.toStringTag]() {
          return 'FormData';
        }
      }

      it('preserves legacy getHeaders() propagation by default', async () => {
        let capturedHeaders;

        await axios.post('http://stub.invalid/', new CustomFormData(), {
          transport: createStubTransport((headers) => {
            capturedHeaders = headers;
          }),
          maxRedirects: 0,
        });

        assert.ok(capturedHeaders, 'transport was not invoked');
        const ct = capturedHeaders['Content-Type'] || capturedHeaders['content-type'];
        assert.match(ct, /multipart\/form-data/);
        assert.strictEqual(capturedHeaders['x-injected'], 'custom');
        assert.strictEqual(capturedHeaders['x-forwarded-for'], '10.0.0.1');
        assert.strictEqual(
          capturedHeaders.Authorization || capturedHeaders.authorization,
          'Bearer CUSTOM_TOKEN'
        );
        assert.strictEqual(capturedHeaders.Host || capturedHeaders.host, 'custom.example.com');
      });

      it('only copies content headers when formDataHeaderPolicy is content-only', async () => {
        let capturedHeaders;

        await axios.post('http://stub.invalid/', new CustomFormData(), {
          transport: createStubTransport((headers) => {
            capturedHeaders = headers;
          }),
          maxRedirects: 0,
          formDataHeaderPolicy: 'content-only',
        });

        assert.ok(capturedHeaders, 'transport was not invoked');
        const ct = capturedHeaders['Content-Type'] || capturedHeaders['content-type'];
        assert.match(ct, /multipart\/form-data/);
        assert.strictEqual(capturedHeaders['x-injected'], undefined);
        assert.strictEqual(capturedHeaders['x-forwarded-for'], undefined);
        assert.strictEqual(
          capturedHeaders.Authorization || capturedHeaders.authorization,
          undefined
        );
        assert.strictEqual(capturedHeaders.Host || capturedHeaders.host, undefined);
      });
    });
  });

  describe('toFormData helper', () => {
    it('should properly serialize nested objects for parsing with multer.js (express.js)', async () => {
      const app = express();
      const obj = {
        arr1: ['1', '2', '3'],
        arr2: ['1', ['2'], '3'],
        obj: { x: '1', y: { z: '1' } },
        users: [
          { name: 'Peter', surname: 'griffin' },
          { name: 'Thomas', surname: 'Anderson' },
        ],
      };

      app.post('/', multer().none(), (req, res) => {
        res.send(JSON.stringify(req.body));
      });

      const server = await new Promise(
        (resolve, reject) => {
          const expressServer = app.listen(0, () => resolve(expressServer));
          expressServer.on('error', reject);
        },
        { port: SERVER_PORT }
      );

      try {
        await Promise.all(
          [null, false, true].map((mode) =>
            axios
              .postForm(`http://localhost:${server.address().port}/`, obj, {
                formSerializer: { indexes: mode },
              })
              .then((response) => {
                assert.deepStrictEqual(response.data, obj, `Index mode ${mode}`);
              })
          )
        );
      } finally {
        await new Promise((resolve, reject) => {
          server.close((error) => {
            if (error) {
              reject(error);
              return;
            }

            resolve();
          });
        });
      }
    });

    it('should only match explicit routes for express 5 form handlers', async () => {
      const app = express();

      app.post('/', multer().none(), (req, res) => {
        res.status(200).send(JSON.stringify({ route: 'root', body: req.body }));
      });

      app.post('/unexpected', multer().none(), (req, res) => {
        res.status(418).send('wrong-route');
      });

      const server = await new Promise(
        (resolve, reject) => {
          const expressServer = app.listen(0, () => resolve(expressServer));
          expressServer.on('error', reject);
        },
        { port: SERVER_PORT }
      );

      const rootUrl = `http://localhost:${server.address().port}`;

      try {
        const rootResponse = await axios.postForm(rootUrl, { foo: 'bar' });
        assert.strictEqual(rootResponse.status, 200);
        assert.deepStrictEqual(rootResponse.data, { route: 'root', body: { foo: 'bar' } });

        await assert.rejects(
          () => axios.postForm(`${rootUrl}/unexpected`, { foo: 'bar' }),
          (error) => {
            assert.strictEqual(error.response.status, 418);
            assert.strictEqual(error.response.data, 'wrong-route');
            return true;
          }
        );
      } finally {
        await new Promise((resolve, reject) => {
          server.close((error) => {
            if (error) {
              reject(error);
              return;
            }

            resolve();
          });
        });
      }
    });
  });

  describe('Blob', () => {
    it('should support Blob', async () => {
      const server = await startHTTPServer(
        async (req, res) => {
          res.end(await getStream(req));
        },
        { port: SERVER_PORT }
      );

      try {
        const blobContent = 'blob-content';
        const blob = new BlobSpecCompliant([blobContent], { type: 'image/jpeg' });

        const { data } = await axios.post(`http://localhost:${server.address().port}`, blob, {
          maxRedirects: 0,
        });

        assert.deepStrictEqual(data, blobContent);
      } finally {
        await stopHTTPServer(server);
      }
    });
  });

  describe('URLEncoded Form', () => {
    it('should post object data as url-encoded form regardless of content-type header casing', async () => {
      const app = express();
      const obj = {
        arr1: ['1', '2', '3'],
        arr2: ['1', ['2'], '3'],
        obj: { x: '1', y: { z: '1' } },
        users: [
          { name: 'Peter', surname: 'griffin' },
          { name: 'Thomas', surname: 'Anderson' },
        ],
      };

      app.use(bodyParser.urlencoded({ extended: true }));

      app.post('/', (req, res) => {
        res.send(JSON.stringify(req.body));
      });

      const server = await new Promise(
        (resolve, reject) => {
          const expressServer = app.listen(0, () => resolve(expressServer));
          expressServer.on('error', reject);
        },
        { port: SERVER_PORT }
      );

      try {
        for (const headerName of ['content-type', 'Content-Type']) {
          const response = await axios.post(`http://localhost:${server.address().port}/`, obj, {
            headers: {
              [headerName]: 'application/x-www-form-urlencoded',
            },
          });

          assert.deepStrictEqual(response.data, obj);
        }
      } finally {
        await new Promise((resolve, reject) => {
          server.close((error) => {
            if (error) {
              reject(error);
              return;
            }

            resolve();
          });
        });
      }
    });

    it('should respect formSerializer config', async () => {
      const obj = {
        arr1: ['1', '2', '3'],
        arr2: ['1', ['2'], '3'],
      };

      const form = new URLSearchParams();
      form.append('arr1[0]', '1');
      form.append('arr1[1]', '2');
      form.append('arr1[2]', '3');
      form.append('arr2[0]', '1');
      form.append('arr2[1][0]', '2');
      form.append('arr2[2]', '3');

      const server = await startHTTPServer(
        (req, res) => {
          req.pipe(res);
        },
        { port: SERVER_PORT }
      );

      try {
        const response = await axios.post(`http://localhost:${server.address().port}/`, obj, {
          headers: {
            'content-type': 'application/x-www-form-urlencoded',
          },
          formSerializer: {
            indexes: true,
          },
        });

        assert.strictEqual(response.data, form.toString());
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should parse nested urlencoded payloads and ignore mismatched content-type', async () => {
      const app = express();

      app.use(bodyParser.urlencoded({ extended: true }));

      app.post('/', (req, res) => {
        const parserRanBeforeHandler = Boolean(req.body && Object.keys(req.body).length);

        res.send(
          JSON.stringify({
            parserRanBeforeHandler,
            body: req.body,
          })
        );
      });

      const server = await new Promise(
        (resolve, reject) => {
          const expressServer = app.listen(0, () => resolve(expressServer));
          expressServer.on('error', reject);
        },
        { port: SERVER_PORT }
      );

      const rootUrl = `http://localhost:${server.address().port}/`;
      const payload = 'user[name]=Peter&tags[]=a&tags[]=b';

      try {
        const parsedResponse = await axios.post(rootUrl, payload, {
          headers: {
            'content-type': 'application/x-www-form-urlencoded',
          },
        });

        assert.deepStrictEqual(parsedResponse.data, {
          parserRanBeforeHandler: true,
          body: {
            user: { name: 'Peter' },
            tags: ['a', 'b'],
          },
        });

        const ignoredResponse = await axios.post(rootUrl, payload, {
          headers: {
            'content-type': 'text/plain',
          },
        });

        assert.strictEqual(ignoredResponse.data.parserRanBeforeHandler, false);
        assert.notDeepStrictEqual(ignoredResponse.data.body, {
          user: { name: 'Peter' },
          tags: ['a', 'b'],
        });
      } finally {
        await new Promise((resolve, reject) => {
          server.close((error) => {
            if (error) {
              reject(error);
              return;
            }

            resolve();
          });
        });
      }
    });
  });

  describe('Data URL', () => {
    it('should support requesting data URL as a Buffer', async () => {
      const buffer = Buffer.from('123');
      const dataURI = `data:application/octet-stream;base64,${buffer.toString('base64')}`;

      const { data } = await axios.get(dataURI);
      assert.deepStrictEqual(data, buffer);
    });

    it('should support requesting data URL as a Blob (if supported by the environment)', async () => {
      if (!isBlobSupported) {
        return;
      }

      const buffer = Buffer.from('123');
      const dataURI = `data:application/octet-stream;base64,${buffer.toString('base64')}`;

      const { data } = await axios.get(dataURI, { responseType: 'blob' });
      assert.strictEqual(data.type, 'application/octet-stream');
      assert.deepStrictEqual(await data.text(), '123');
    });

    it('should support requesting data URL as a String (text)', async () => {
      const buffer = Buffer.from('123', 'utf-8');
      const dataURI = `data:application/octet-stream;base64,${buffer.toString('base64')}`;

      const { data } = await axios.get(dataURI, { responseType: 'text' });
      assert.deepStrictEqual(data, '123');
    });

    it('should support requesting data URL as a Stream', async () => {
      const buffer = Buffer.from('123', 'utf-8');
      const dataURI = `data:application/octet-stream;base64,${buffer.toString('base64')}`;

      const { data } = await axios.get(dataURI, { responseType: 'stream' });
      assert.strictEqual(await getStream(data), '123');
    });
  });

  describe('progress', () => {
    describe('upload', () => {
      it('should support upload progress capturing', async () => {
        const server = await startHTTPServer(
          {
            rate: 100 * 1024,
          },
          { port: SERVER_PORT }
        );

        try {
          let content = '';
          const count = 10;
          const chunk = 'test';
          const chunkLength = Buffer.byteLength(chunk);
          const contentLength = count * chunkLength;

          const readable = stream.Readable.from(
            (async function* () {
              let i = count;

              while (i-- > 0) {
                await setTimeoutAsync(1100);
                content += chunk;
                yield chunk;
              }
            })()
          );

          const samples = [];

          const { data } = await axios.post(`http://localhost:${server.address().port}`, readable, {
            onUploadProgress: ({ loaded, total, progress, bytes, upload }) => {
              samples.push({
                loaded,
                total,
                progress,
                bytes,
                upload,
              });
            },
            headers: {
              'Content-Length': contentLength,
            },
            responseType: 'text',
          });

          assert.strictEqual(data, content);
          assert.deepStrictEqual(
            samples,
            Array.from(
              (function* () {
                for (let i = 1; i <= 10; i++) {
                  yield {
                    loaded: chunkLength * i,
                    total: contentLength,
                    progress: (chunkLength * i) / contentLength,
                    bytes: 4,
                    upload: true,
                  };
                }
              })()
            )
          );
        } finally {
          await stopHTTPServer(server);
        }
      }, 15000);
    });

    describe('download', () => {
      it('should support download progress capturing', async () => {
        const server = await startHTTPServer(
          {
            rate: 100 * 1024,
          },
          { port: SERVER_PORT }
        );

        try {
          let content = '';
          const count = 10;
          const chunk = 'test';
          const chunkLength = Buffer.byteLength(chunk);
          const contentLength = count * chunkLength;

          const readable = stream.Readable.from(
            (async function* () {
              let i = count;

              while (i-- > 0) {
                await setTimeoutAsync(1100);
                content += chunk;
                yield chunk;
              }
            })()
          );

          const samples = [];

          const { data } = await axios.post(`http://localhost:${server.address().port}`, readable, {
            onDownloadProgress: ({ loaded, total, progress, bytes, download }) => {
              samples.push({
                loaded,
                total,
                progress,
                bytes,
                download,
              });
            },
            headers: {
              'Content-Length': contentLength,
            },
            responseType: 'text',
            maxRedirects: 0,
          });

          assert.strictEqual(data, content);
          assert.deepStrictEqual(
            samples,
            Array.from(
              (function* () {
                for (let i = 1; i <= 10; i++) {
                  yield {
                    loaded: chunkLength * i,
                    total: contentLength,
                    progress: (chunkLength * i) / contentLength,
                    bytes: 4,
                    download: true,
                  };
                }
              })()
            )
          );
        } finally {
          await stopHTTPServer(server);
        }
      }, 15000);
    });
  });

  describe('Rate limit', () => {
    it('should support upload rate limit', async () => {
      const secs = 10;
      const configRate = 100000;
      const chunkLength = configRate * secs;
      const server = await startHTTPServer();

      try {
        const buf = Buffer.alloc(chunkLength).fill('s');
        const samples = [];
        const skip = 4;
        const compareValues = toleranceRange(50, 50);

        const { data } = await axios.post(`http://localhost:${server.address().port}`, buf, {
          onUploadProgress: ({ loaded, total, progress, bytes, rate }) => {
            samples.push({
              loaded,
              total,
              progress,
              bytes,
              rate,
            });
          },
          maxRate: [configRate],
          responseType: 'text',
          maxRedirects: 0,
        });

        samples.slice(skip).forEach(({ rate, progress }, i, _samples) => {
          assert.ok(
            compareValues(rate, configRate),
            `Rate sample at index ${i} is out of the expected range (${rate} / ${configRate}) [${_samples
              .map((sample) => sample.rate)
              .join(', ')}]`
          );

          const progressTicksRate = 2;
          const expectedProgress = (i + skip) / secs / progressTicksRate;

          assert.ok(
            Math.abs(expectedProgress - progress) < 0.25,
            `Progress sample at index ${i} is out of the expected range (${progress} / ${expectedProgress}) [${_samples
              .map((sample) => sample.progress)
              .join(', ')}]`
          );
        });

        assert.strictEqual(data, buf.toString(), 'content corrupted');
      } finally {
        await stopHTTPServer(server);
      }
    }, 30000);

    it('should support download rate limit', async () => {
      const secs = 10;
      const configRate = 100000;
      const chunkLength = configRate * secs;
      const server = await startHTTPServer();

      try {
        const buf = Buffer.alloc(chunkLength).fill('s');
        const samples = [];
        const skip = 4;
        const compareValues = toleranceRange(50, 50);

        const { data } = await axios.post(`http://localhost:${server.address().port}`, buf, {
          onDownloadProgress: ({ loaded, total, progress, bytes, rate }) => {
            samples.push({
              loaded,
              total,
              progress,
              bytes,
              rate,
            });
          },
          maxRate: [0, configRate],
          responseType: 'text',
          maxRedirects: 0,
        });

        samples.slice(skip).forEach(({ rate, progress }, i, _samples) => {
          assert.ok(
            compareValues(rate, configRate),
            `Rate sample at index ${i} is out of the expected range (${rate} / ${configRate}) [${_samples
              .map((sample) => sample.rate)
              .join(', ')}]`
          );

          const progressTicksRate = 3;
          const expectedProgress = (i + skip) / secs / progressTicksRate;

          assert.ok(
            Math.abs(expectedProgress - progress) < 0.25,
            `Progress sample at index ${i} is out of the expected range (${progress} / ${expectedProgress}) [${_samples
              .map((sample) => sample.progress)
              .join(', ')}]`
          );
        });

        assert.strictEqual(data, buf.toString(), 'content corrupted');
      } finally {
        await stopHTTPServer(server);
      }
    }, 30000);
  });

  describe('request aborting', () => {
    it('should be able to abort the response stream', async () => {
      const server = await startHTTPServer(
        {
          rate: 100000,
          useBuffering: true,
        },
        { port: SERVER_PORT }
      );

      try {
        const buf = Buffer.alloc(1024 * 1024);
        const controller = new AbortController();

        const { data } = await axios.post(`http://localhost:${server.address().port}`, buf, {
          responseType: 'stream',
          signal: controller.signal,
          maxRedirects: 0,
        });

        setTimeout(() => {
          controller.abort();
        }, 500);

        let streamError;
        data.on('error', (error) => {
          streamError = error;
        });

        await assert.rejects(
          new Promise((resolve, reject) => {
            stream.pipeline(data, devNull(), (error) => {
              if (error) {
                reject(error);
                return;
              }

              resolve();
            });
          })
        );

        assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
      } finally {
        await stopHTTPServer(server);
      }
    });
  });

  it('should properly handle synchronous errors inside the adapter', async () => {
    await assert.rejects(() => axios.get('http://192.168.0.285'), /Invalid URL/);
  });

  it('should support function as paramsSerializer value', async () => {
    const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT });

    try {
      const { data } = await axios.post(`http://localhost:${server.address().port}`, 'test', {
        params: {
          x: 1,
        },
        paramsSerializer: () => 'foo',
        maxRedirects: 0,
      });

      assert.strictEqual(data, '/?foo');
    } finally {
      await stopHTTPServer(server);
    }
  });

  describe('DNS', () => {
    it('should support a custom DNS lookup function', async () => {
      const server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);
      const payload = 'test';
      let isCalled = false;

      try {
        const { data } = await axios.post(
          `http://fake-name.axios:${server.address().port}`,
          payload,
          {
            lookup: (hostname, opt, cb) => {
              isCalled = true;
              cb(null, '127.0.0.1', 4);
            },
          }
        );

        assert.ok(isCalled);
        assert.strictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support a custom DNS lookup function with address entry passing', async () => {
      const server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);
      const payload = 'test';
      let isCalled = false;

      try {
        const { data } = await axios.post(
          `http://fake-name.axios:${server.address().port}`,
          payload,
          {
            lookup: (hostname, opt, cb) => {
              isCalled = true;
              cb(null, { address: '127.0.0.1', family: 4 });
            },
          }
        );

        assert.ok(isCalled);
        assert.strictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support a custom DNS lookup function (async)', async () => {
      const server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);
      const payload = 'test';
      let isCalled = false;

      try {
        const { data } = await axios.post(
          `http://fake-name.axios:${server.address().port}`,
          payload,
          {
            lookup: async (hostname, opt) => {
              isCalled = true;
              return ['127.0.0.1', 4];
            },
          }
        );

        assert.ok(isCalled);
        assert.strictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support a custom DNS lookup function with address entry (async)', async () => {
      const server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);
      const payload = 'test';
      let isCalled = false;

      try {
        const { data } = await axios.post(
          `http://fake-name.axios:${server.address().port}`,
          payload,
          {
            lookup: async (hostname, opt) => {
              isCalled = true;
              return { address: '127.0.0.1', family: 4 };
            },
          }
        );

        assert.ok(isCalled);
        assert.strictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support a custom DNS lookup function that returns only IP address (async)', async () => {
      const server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);
      const payload = 'test';
      let isCalled = false;

      try {
        const { data } = await axios.post(
          `http://fake-name.axios:${server.address().port}`,
          payload,
          {
            lookup: async (hostname, opt) => {
              isCalled = true;
              return '127.0.0.1';
            },
          }
        );

        assert.ok(isCalled);
        assert.strictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should handle errors', async () => {
      await assert.rejects(async () => {
        await axios.get('https://no-such-domain-987654.com', {
          lookup,
        });
      }, /ENOTFOUND/);
    });
  });

  describe('JSON', () => {
    it('should support reviver on JSON.parse', async () => {
      const server = await startHTTPServer(
        async (_, res) => {
          res.end(
            JSON.stringify({
              foo: 'bar',
            })
          );
        },
        { port: SERVER_PORT }
      );

      try {
        const { data } = await axios.get(`http://localhost:${server.address().port}`, {
          parseReviver: (key, value) => {
            return key === 'foo' ? 'success' : value;
          },
        });

        assert.deepStrictEqual(data, { foo: 'success' });
      } finally {
        await stopHTTPServer(server);
      }
    });
  });

  describe('HTTP2', () => {
    const createHttp2Axios = (baseURL) =>
      axios.create({
        baseURL,
        httpVersion: 2,
        http2Options: {
          rejectUnauthorized: false,
        },
      });

    it('should merge request http2Options with its instance config', async () => {
      const http2Axios = createHttp2Axios('https://localhost:8080');

      const { data } = await http2Axios.get('/', {
        http2Options: {
          foo: 'test',
        },
        adapter: async (config) => {
          return {
            data: config.http2Options,
          };
        },
      });

      assert.deepStrictEqual(data, {
        rejectUnauthorized: false,
        foo: 'test',
      });
    });

    it('should support http2 transport', async () => {
      const server = await startHTTPServer(
        (req, res) => {
          res.end('OK');
        },
        {
          useHTTP2: true,
          port: SERVER_PORT,
        }
      );

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);
        const { data } = await http2Axios.get(localServerURL);
        assert.deepStrictEqual(data, 'OK');
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support request payload', async () => {
      const server = await startHTTPServer(null, {
        useHTTP2: true,
        port: SERVER_PORT,
      });

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);
        const payload = 'DATA';
        const { data } = await http2Axios.post(localServerURL, payload);
        assert.deepStrictEqual(data, payload);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support FormData as a payload', async () => {
      if (typeof FormData !== 'function') {
        return;
      }

      const server = await startHTTPServer(
        async (req, res) => {
          const { fields, files } = await handleFormData(req);

          res.end(
            JSON.stringify({
              fields,
              files,
            })
          );
        },
        {
          useHTTP2: true,
          port: SERVER_PORT,
        }
      );

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);
        const form = new FormData();
        form.append('x', 'foo');
        form.append('y', 'bar');

        const { data } = await http2Axios.post(localServerURL, form);

        assert.deepStrictEqual(data, {
          fields: {
            x: ['foo'],
            y: ['bar'],
          },
          files: {},
        });
      } finally {
        await stopHTTPServer(server);
      }
    });

    describe('response types', () => {
      const originalData = '{"test": "OK"}';
      const fixtures = {
        text: (value) => assert.strictEqual(value, originalData),
        arraybuffer: (value) => assert.deepStrictEqual(value, Buffer.from(originalData)),
        stream: async (value) => assert.deepStrictEqual(await getStream(value), originalData),
        json: async (value) => assert.deepStrictEqual(value, JSON.parse(originalData)),
      };

      for (const [responseType, assertValue] of Object.entries(fixtures)) {
        it(`should support ${responseType} response type`, async () => {
          const server = await startHTTPServer(
            (req, res) => {
              res.end(originalData);
            },
            {
              useHTTP2: true,
              port: SERVER_PORT,
            }
          );

          try {
            const localServerURL = `https://localhost:${server.address().port}`;
            const http2Axios = createHttp2Axios(localServerURL);
            const { data } = await http2Axios.get(localServerURL, {
              responseType,
            });
            await assertValue(data);
          } finally {
            await stopHTTPServer(server);
          }
        });
      }
    });

    it('should support request timeout', async () => {
      let isAborted = false;
      let aborted;
      const promise = new Promise((resolve) => (aborted = resolve));

      const server = await startHTTPServer(
        (req, res) => {
          setTimeout(() => {
            res.end('OK');
          }, 15000);
        },
        {
          useHTTP2: true,
          port: SERVER_PORT,
        }
      );

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);

        server.on('stream', (http2Stream) => {
          http2Stream.once('aborted', () => {
            isAborted = true;
            aborted();
          });
        });

        await assert.rejects(async () => {
          await http2Axios.get(localServerURL, {
            timeout: 500,
          });
        }, /timeout/);

        await promise;
        assert.ok(isAborted);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support request cancellation', async () => {
      if (typeof AbortSignal !== 'function' || !AbortSignal.timeout) {
        return;
      }

      let isAborted = false;
      let aborted;
      const promise = new Promise((resolve) => (aborted = resolve));

      const server = await startHTTPServer(
        (req, res) => {
          setTimeout(() => {
            res.end('OK');
          }, 15000);
        },
        {
          useHTTP2: true,
          port: SERVER_PORT,
        }
      );

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);

        server.on('stream', (http2Stream) => {
          http2Stream.once('aborted', () => {
            isAborted = true;
            aborted();
          });
        });

        await assert.rejects(async () => {
          await http2Axios.get(localServerURL, {
            signal: AbortSignal.timeout(500),
          });
        }, /CanceledError: canceled/);

        await promise;
        assert.ok(isAborted);
      } finally {
        await stopHTTPServer(server);
      }
    });

    it('should support stream response cancellation', async () => {
      let isAborted = false;
      const source = axios.CancelToken.source();

      let aborted;
      const promise = new Promise((resolve) => (aborted = resolve));

      const server = await startHTTPServer(
        (req, res) => {
          generateReadable(10000, 100, 100).pipe(res);
        },
        {
          useHTTP2: true,
          port: SERVER_PORT,
        }
      );

      try {
        const localServerURL = `https://localhost:${server.address().port}`;
        const http2Axios = createHttp2Axios(localServerURL);

        server.on('stream', (http2Stream) => {
          http2Stream.once('aborted', () => {
            isAborted = true;
            aborted();
          });
        });

        const { data } = await http2Axios.get(localServerURL, {
          cancelToken: source.token,
          responseType: 'stream',
        });

        setTimeout(() => source.cancel());

        await assert.rejects(
          new Promise((resolve, reject) => {
            stream.pipeline(data, devNull(), (error) => {
              if (error) {
                reject(error);
                return;
              }

              resolve();
            });
          }),
          /CanceledError: canceled/
        );

        await promise;
        assert.ok(isAborted);
      } finally {
        await stopHTTPServer(server);
      }
    });

    describe('session', () => {
      // HTTP2 session tests are sensitive to cross-test port reuse: when one
      // test's server is torn down (closeAllSessions destroys h2 sessions),
      // a follow-up test binding the same port can observe a "Premature
      // close" on its own stream. Use ephemeral ports (port: 0, the default
      // from startHTTPServer) and a small retry budget as a backstop.
      it('should reuse session for the target authority', { retry: 2 }, async () => {
        const server = await startHTTPServer(
          (req, res) => {
            setTimeout(() => res.end('OK'), 1000);
          },
          {
            useHTTP2: true,
          }
        );

        try {
          const localServerURL = `https://localhost:${server.address().port}`;
          const http2Axios = createHttp2Axios(localServerURL);

          const [response1, response2] = await Promise.all([
            http2Axios.get(localServerURL, {
              responseType: 'stream',
            }),
            http2Axios.get(localServerURL, {
              responseType: 'stream',
            }),
          ]);

          assert.strictEqual(response1.data.session, response2.data.session);

          assert.deepStrictEqual(
            await Promise.all([getStream(response1.data), getStream(response2.data)]),
            ['OK', 'OK']
          );
        } finally {
          await stopHTTPServer(server);
        }
      });

      it('should use different sessions for different authorities', { retry: 2 }, async () => {
        const server = await startHTTPServer(
          (req, res) => {
            setTimeout(() => {
              res.end('OK');
            }, 2000);
          },
          {
            useHTTP2: true,
          }
        );

        const server2 = await startHTTPServer(
          (req, res) => {
            setTimeout(() => {
              res.end('OK');
            }, 2000);
          },
          {
            useHTTP2: true,
          }
        );

        try {
          const localServerURL = `https://localhost:${server.address().port}`;
          const localServerURL2 = `https://localhost:${server2.address().port}`;
          const http2Axios = createHttp2Axios(localServerURL);

          const [response1, response2] = await Promise.all([
            http2Axios.get(localServerURL, {
              responseType: 'stream',
            }),
            http2Axios.get(localServerURL2, {
              responseType: 'stream',
            }),
          ]);

          assert.notStrictEqual(response1.data.session, response2.data.session);

          assert.deepStrictEqual(
            await Promise.all([getStream(response1.data), getStream(response2.data)]),
            ['OK', 'OK']
          );
        } finally {
          await Promise.all([stopHTTPServer(server), stopHTTPServer(server2)]);
        }
      });

      it(
        'should use different sessions for requests with different http2Options set',
        { retry: 2 },
        async () => {
          const server = await startHTTPServer(
            (req, res) => {
              setTimeout(() => {
                res.end('OK');
              }, 1000);
            },
            {
              useHTTP2: true,
            }
          );

          try {
            const localServerURL = `https://localhost:${server.address().port}`;
            const http2Axios = createHttp2Axios(localServerURL);

            const [response1, response2] = await Promise.all([
              http2Axios.get(localServerURL, {
                http2Options: {
                  sessionTimeout: 2000,
                },
              }),
              http2Axios.get(localServerURL, {
                http2Options: {
                  sessionTimeout: 4000,
                },
              }),
            ]);

            assert.notStrictEqual(response1.request.session, response2.request.session);
            assert.deepStrictEqual([response1.data, response2.data], ['OK', 'OK']);
          } finally {
            await stopHTTPServer(server);
          }
        }
      );

      it(
        'should use the same session for request with the same resolved http2Options set',
        { retry: 2 },
        async () => {
          const server = await startHTTPServer(
            (req, res) => {
              setTimeout(() => res.end('OK'), 1000);
            },
            {
              useHTTP2: true,
            }
          );

          try {
            const localServerURL = `https://localhost:${server.address().port}`;
            const http2Axios = createHttp2Axios(localServerURL);

            const responses = await Promise.all([
              http2Axios.get(localServerURL, {
                responseType: 'stream',
              }),
              http2Axios.get(localServerURL, {
                responseType: 'stream',
                http2Options: undefined,
              }),
              http2Axios.get(localServerURL, {
                responseType: 'stream',
                http2Options: {},
              }),
            ]);

            assert.strictEqual(responses[1].data.session, responses[0].data.session);
            assert.strictEqual(responses[2].data.session, responses[0].data.session);

            assert.deepStrictEqual(
              await Promise.all(responses.map(({ data }) => getStream(data))),
              ['OK', 'OK', 'OK']
            );
          } finally {
            await stopHTTPServer(server);
          }
        }
      );

      it(
        'should use different sessions after previous session timeout',
        { retry: 2, timeout: 15000 },
        async () => {
          const server = await startHTTPServer(
            (req, res) => {
              setTimeout(() => res.end('OK'), 100);
            },
            {
              useHTTP2: true,
            }
          );

          try {
            const localServerURL = `https://localhost:${server.address().port}`;
            const http2Axios = createHttp2Axios(localServerURL);

            const response1 = await http2Axios.get(localServerURL, {
              responseType: 'stream',
              http2Options: {
                sessionTimeout: 1000,
              },
            });

            const session1 = response1.data.session;
            const data1 = await getStream(response1.data);

            await setTimeoutAsync(5000);

            const response2 = await http2Axios.get(localServerURL, {
              responseType: 'stream',
              http2Options: {
                sessionTimeout: 1000,
              },
            });

            const session2 = response2.data.session;
            const data2 = await getStream(response2.data);

            assert.notStrictEqual(session1, session2);
            assert.strictEqual(data1, 'OK');
            assert.strictEqual(data2, 'OK');
          } finally {
            await stopHTTPServer(server);
          }
        }
      );
    });
  });

  it('should not abort stream on settle rejection', async () => {
    const server = await startHTTPServer(
      (req, res) => {
        res.statusCode = 404;
        res.end('OK');
      },
      { port: SERVER_PORT }
    );

    try {
      let error;

      try {
        await axios.get(`http://localhost:${server.address().port}`, {
          responseType: 'stream',
        });
      } catch (err) {
        error = err;
      }

      assert.ok(error, 'request should be rejected');
      assert.strictEqual(await getStream(error.response.data), 'OK');
    } finally {
      await stopHTTPServer(server);
    }
  });

  it('should reject when only the request socket emits an error', async () => {
    const noop = () => {};
    const socket = new EventEmitter();
    socket.setKeepAlive = noop;
    socket.on('error', noop);

    const transport = {
      request() {
        return new (class MockRequest extends EventEmitter {
          constructor() {
            super();
            this.destroyed = false;
          }

          setTimeout() {}

          write() {}

          end() {
            this.emit('socket', socket);

            setImmediate(() => {
              socket.emit('error', Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }));
            });
          }

          destroy(err) {
            if (this.destroyed) {
              return;
            }

            this.destroyed = true;
            err && this.emit('error', err);
            this.emit('close');
          }
        })();
      },
    };

    const error = await Promise.race([
      axios.post('http://example.com/', 'test', {
        transport,
        maxRedirects: 0,
      }),
      setTimeoutAsync(200).then(() => {
        throw new Error('socket error did not reject the request');
      }),
    ]).catch((err) => err);

    assert.ok(error instanceof AxiosError);
    assert.strictEqual(error.code, 'EPIPE');
    assert.strictEqual(error.message, 'write EPIPE');
  });

  describe('keep-alive', () => {
    it('should not emit MaxListenersExceededWarning under concurrent requests through a pooled keep-alive agent (regression #10780)', async () => {
      const server = await startHTTPServer(
        (req, res) => {
          // Small delay forces concurrent requests to queue on the single pooled socket.
          setTimeout(() => {
            res.writeHead(200, { 'Content-Type': 'text/plain' });
            res.end('ok');
          }, 5);
        },
        { port: SERVER_PORT }
      );

      const warnings = [];
      const warningHandler = (warning) => {
        if (warning && warning.name === 'MaxListenersExceededWarning') {
          warnings.push(warning);
        }
      };
      process.on('warning', warningHandler);

      const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });

      try {
        const baseURL = `http://localhost:${server.address().port}`;
        const CONCURRENCY = 30;

        const results = await Promise.all(
          Array.from({ length: CONCURRENCY }, (_, i) =>
            axios.get(`/req-${i}`, { baseURL, httpAgent: agent })
          )
        );

        assert.strictEqual(results.length, CONCURRENCY);
        for (const r of results) {
          assert.strictEqual(r.status, 200);
          assert.strictEqual(r.data, 'ok');
        }

        // Allow any deferred process 'warning' emissions to flush.
        await setTimeoutAsync(50);

        assert.strictEqual(
          warnings.length,
          0,
          `expected no MaxListenersExceededWarning, got ${warnings.length}: ${warnings.map((w) => w.message).join('; ')}`
        );

        // Inspect live sockets on the agent: none should have more than one
        // axios-installed error listener, regardless of how many requests ran.
        const allSockets = []
          .concat(...Object.values(agent.sockets || {}))
          .concat(...Object.values(agent.freeSockets || {}));
        for (const sock of allSockets) {
          assert.ok(
            sock.listenerCount('error') <= 2,
            `socket should have at most a couple of error listeners (agent + axios), got ${sock.listenerCount('error')}`
          );
        }
      } finally {
        process.removeListener('warning', warningHandler);
        agent.destroy();
        await stopHTTPServer(server);
      }
    }, 30000);

    it('should not leak memory via retained request closures under a long burst of keep-alive requests (regression #10780)', async () => {
      // This guards against stage88's report of OOM at ~480k sequential requests:
      // if the per-request closure leaked, heap would grow linearly. We simulate
      // a shorter burst and verify retained closures are released (via WeakRef
      // reachability check after GC, if exposed).
      if (typeof global.gc !== 'function') {
        // Skip when GC is not exposed (run with `node --expose-gc`).
        return;
      }

      const server = await startHTTPServer(
        (req, res) => {
          res.writeHead(200);
          res.end('ok');
        },
        { port: SERVER_PORT }
      );

      const agent = new http.Agent({ keepAlive: true, maxSockets: 4 });

      try {
        const baseURL = `http://localhost:${server.address().port}`;

        const refs = [];
        for (let i = 0; i < 200; i += 1) {
          // eslint-disable-next-line no-await-in-loop
          const response = await axios.get('/', { baseURL, httpAgent: agent });
          refs.push(new WeakRef(response.request));
        }

        // Drop strong refs and force GC.
        global.gc();
        await setTimeoutAsync(10);
        global.gc();

        const retained = refs.filter((r) => r.deref() !== undefined).length;
        // Some trailing requests may still be referenced in internal buffers.
        // The fix's correctness: retained count scales with agent socket count,
        // NOT with request count. A pre-fix leak would keep >>socket count.
        assert.ok(
          retained <= 20,
          `expected most request objects to be collectible after GC; ${retained}/200 retained suggests a closure leak`
        );
      } finally {
        agent.destroy();
        await stopHTTPServer(server);
      }
    }, 30000);

    it('should not fail with "socket hang up" when using timeouts', async () => {
      const server = await startHTTPServer(
        async (req, res) => {
          if (req.url === '/wait') {
            await new Promise((resolve) => setTimeout(resolve, 5000));
          }

          res.end('ok');
        },
        { port: SERVER_PORT }
      );

      try {
        const baseURL = `http://localhost:${server.address().port}`;
        await axios.get('/1', { baseURL, timeout: 1000 });
        await axios.get('/wait', { baseURL, timeout: 0 });
      } finally {
        await stopHTTPServer(server);
      }
    }, 15000);

    it('should install at most one socket error listener across reused keep-alive sockets', async () => {
      const noop = () => {};
      const socket = new EventEmitter();
      socket.setKeepAlive = noop;
      socket.on('error', noop);

      const baseErrorListenerCount = socket.listenerCount('error');

      const transport = {
        request(_, cb) {
          return new (class MockRequest extends EventEmitter {
            constructor() {
              super();
              this.destroyed = false;
            }

            setTimeout() {}

            write() {}

            end() {
              this.emit('socket', socket);

              setImmediate(() => {
                const response = stream.Readable.from(['ok']);
                response.statusCode = 200;
                response.headers = {};

                cb(response);
                this.emit('close');
              });
            }

            destroy(err) {
              if (this.destroyed) {
                return;
              }

              this.destroyed = true;
              err && this.emit('error', err);
              this.emit('close');
            }
          })();
        },
      };

      // First request: axios installs its single per-socket listener.
      await axios.get('http://example.com/first', {
        transport,
        maxRedirects: 0,
      });
      await setTimeoutAsync(0);
      assert.strictEqual(
        socket.listenerCount('error'),
        baseErrorListenerCount + 1,
        'axios should install exactly one socket error listener'
      );

      // Many subsequent requests reusing the same socket must not add more listeners.
      for (let i = 0; i < 20; i += 1) {
        // eslint-disable-next-line no-await-in-loop
        await axios.get(`http://example.com/next-${i}`, {
          transport,
          maxRedirects: 0,
        });
        // eslint-disable-next-line no-await-in-loop
        await setTimeoutAsync(0);
        assert.strictEqual(
          socket.listenerCount('error'),
          baseErrorListenerCount + 1,
          'listener count must stay constant across keep-alive reuse'
        );
      }
    });

    it('should not accumulate socket error listeners when a pooled socket is reassigned before the previous request closes (regression #10780)', async () => {
      const noop = () => {};
      const socket = new EventEmitter();
      socket.setKeepAlive = noop;
      socket.on('error', noop);

      const baseErrorListenerCount = socket.listenerCount('error');

      // Each request defers its 'close' emission so that the socket is
      // reassigned to the next request before the previous one closes.
      // This reproduces the race condition described in #10780.
      const pendingRequests = [];

      const transport = {
        request(_, cb) {
          const req = new (class MockRequest extends EventEmitter {
            constructor() {
              super();
              this.destroyed = false;
            }

            setTimeout() {}
            write() {}

            end() {
              // Share the single pooled socket across every request.
              this.emit('socket', socket);

              setImmediate(() => {
                const response = stream.Readable.from(['ok']);
                response.statusCode = 200;
                response.headers = {};
                cb(response);
                // Intentionally do NOT emit 'close' yet. Collect the req
                // so close can be emitted later, after other reqs have
                // already claimed the socket.
                pendingRequests.push(this);
              });
            }

            destroy(err) {
              if (this.destroyed) return;
              this.destroyed = true;
              err && this.emit('error', err);
              this.emit('close');
            }
          })();

          return req;
        },
      };

      const results = await Promise.all(
        Array.from({ length: 20 }, (_, i) =>
          axios.get(`http://example.com/concurrent-${i}`, {
            transport,
            maxRedirects: 0,
          })
        )
      );

      assert.strictEqual(results.length, 20);

      // Critical assertion: despite 20 concurrent requests all claiming the
      // same pooled socket before any emitted 'close', only ONE axios listener
      // must be attached. This is the difference between the pre-fix
      // behaviour (20 listeners, MaxListenersExceededWarning) and the fix.
      assert.strictEqual(
        socket.listenerCount('error'),
        baseErrorListenerCount + 1,
        `expected a single axios socket error listener under concurrent reuse, got ${socket.listenerCount('error') - baseErrorListenerCount}`
      );

      // Now drain the queued close events. Listener count must still be 1.
      for (const req of pendingRequests) {
        req.emit('close');
      }
      await setTimeoutAsync(0);

      assert.strictEqual(
        socket.listenerCount('error'),
        baseErrorListenerCount + 1,
        'listener must persist on the socket after requests close (cleanup is per-request ownership, not per-listener removal)'
      );
    });

    it('should route a socket error to the currently-active request after the socket has been reassigned', async () => {
      const noop = () => {};
      const socket = new EventEmitter();
      socket.setKeepAlive = noop;
      socket.on('error', noop);

      const createdReqs = [];

      // First transport: completes cleanly (emits response then close).
      const cleanTransport = {
        request(_, cb) {
          const emitter = new (class MockRequest extends EventEmitter {
            constructor() {
              super();
              this.destroyed = false;
              createdReqs.push(this);
            }
            setTimeout() {}
            write() {}
            end() {
              this.emit('socket', socket);
              setImmediate(() => {
                const response = stream.Readable.from(['ok']);
                response.statusCode = 200;
                response.headers = {};
                cb(response);
                this.emit('close');
              });
            }
            destroy(err) {
              if (this.destroyed) return;
              this.destroyed = true;
              err && this.emit('error', err);
              this.emit('close');
            }
          })();
          return emitter;
        },
      };

      // Second transport: emits socket error instead of a response.
      const errorTransport = {
        request() {
          const emitter = new (class MockRequest extends EventEmitter {
            constructor() {
              super();
              this.destroyed = false;
              createdReqs.push(this);
            }
            setTimeout() {}
            write() {}
            end() {
              this.emit('socket', socket);
              setImmediate(() => {
                socket.emit('error', Object.assign(new Error('boom'), { code: 'EPIPE' }));
              });
            }
            destroy(err) {
              if (this.destroyed) return;
              this.destroyed = true;
              err && this.emit('error', err);
              this.emit('close');
            }
          })();
          return emitter;
        },
      };

      // First request completes successfully; socket is released.
      await axios.get('http://example.com/first', { transport: cleanTransport, maxRedirects: 0 });
      await setTimeoutAsync(0);

      const firstReq = createdReqs[0];
      assert.ok(
        firstReq && firstReq.destroyed === false,
        'first request must not have been destroyed by a socket error'
      );

      // Stray socket error after first req has closed: must not destroy firstReq.
      socket.emit('error', new Error('stray error after close'));
      assert.strictEqual(
        firstReq.destroyed,
        false,
        'socket error after close must not destroy the old request'
      );

      // Second request claims the socket, then its socket errors. It should reject.
      const err = await axios
        .get('http://example.com/second', { transport: errorTransport, maxRedirects: 0 })
        .catch((e) => e);

      assert.ok(err instanceof AxiosError, 'second request should reject with an AxiosError');
      assert.strictEqual(err.code, 'EPIPE');

      const secondReq = createdReqs[1];
      assert.strictEqual(
        secondReq.destroyed,
        true,
        'second request should be destroyed by its own active socket error'
      );
    });
  });

  describe('redirect listener accumulation', () => {
    it('should not emit MaxListenersExceededWarning when a single request follows >= 11 redirects', async () => {
      const REDIRECT_COUNT = 11;

      const server = await startHTTPServer(
        (req, res) => {
          const match = req.url.match(/^\/redirect\/(\d+)$/);
          if (match) {
            const n = Number(match[1]);
            if (n < REDIRECT_COUNT) {
              res.writeHead(302, { Location: `/redirect/${n + 1}` });
            } else {
              res.writeHead(200, { 'Content-Type': 'application/json' });
              res.end(JSON.stringify({ redirects: n }));
              return;
            }
            res.end();
            return;
          }
          res.writeHead(302, { Location: '/redirect/1' });
          res.end();
        },
        { port: SERVER_PORT }
      );

      const warnings = [];
      const warningHandler = (warning) => {
        if (warning && warning.name === 'MaxListenersExceededWarning') {
          warnings.push(warning);
        }
      };
      process.on('warning', warningHandler);

      try {
        const baseURL = `http://localhost:${server.address().port}`;
        const response = await axios.get('/start', {
          baseURL,
          maxRedirects: REDIRECT_COUNT + 5,
        });

        assert.strictEqual(response.status, 200);
        assert.deepStrictEqual(response.data, { redirects: REDIRECT_COUNT });

        // Allow any deferred process 'warning' emissions to flush.
        await setTimeoutAsync(50);

        assert.strictEqual(
          warnings.length,
          0,
          `expected no MaxListenersExceededWarning across ${REDIRECT_COUNT} redirects, got ${warnings.length}: ${warnings.map((w) => w.message).join('; ')}`
        );
      } finally {
        process.removeListener('warning', warningHandler);
        await stopHTTPServer(server);
      }
    }, 30000);

    it('should attach at most one close listener to the outer request across a long redirect chain', async () => {
      const REDIRECT_COUNT = 20;
      const maxObservedCloseListeners = { value: 0 };

      const server = await startHTTPServer(
        (req, res) => {
          const match = req.url.match(/^\/r\/(\d+)$/);
          if (match) {
            const n = Number(match[1]);
            if (n < REDIRECT_COUNT) {
              res.writeHead(302, { Location: `/r/${n + 1}` });
            } else {
              res.writeHead(200);
              res.end('done');
              return;
            }
            res.end();
            return;
          }
          res.writeHead(302, { Location: '/r/1' });
          res.end();
        },
        { port: SERVER_PORT }
      );

      try {
        const baseURL = `http://localhost:${server.address().port}`;

        // Patch EventEmitter.prototype.on briefly to observe the peak close-listener
        // count on any emitter. The outer RedirectableRequest is the only target
        // that would accumulate listeners under the bug. Other emitters in the
        // process (server sockets, etc.) will also be observed but are irrelevant
        // as long as the peak stays within a small bound.
        const originalOn = EventEmitter.prototype.on;
        const originalOnce = EventEmitter.prototype.once;
        function record(eventName) {
          if (eventName === 'close') {
            const count = this.listenerCount('close');
            if (count > maxObservedCloseListeners.value) {
              maxObservedCloseListeners.value = count;
            }
          }
        }
        EventEmitter.prototype.on = function patchedOn(eventName, listener) {
          const res = originalOn.call(this, eventName, listener);
          record.call(this, eventName);
          return res;
        };
        EventEmitter.prototype.once = function patchedOnce(eventName, listener) {
          const res = originalOnce.call(this, eventName, listener);
          record.call(this, eventName);
          return res;
        };

        try {
          const response = await axios.get('/start', {
            baseURL,
            maxRedirects: REDIRECT_COUNT + 5,
          });
          assert.strictEqual(response.status, 200);
        } finally {
          EventEmitter.prototype.on = originalOn;
          EventEmitter.prototype.once = originalOnce;
        }

        // Pre-fix: peak would be >= REDIRECT_COUNT (one axios close listener per hop
        // on the outer RedirectableRequest). Post-fix: axios attaches exactly one
        // close listener to the outer request; framework internals typically add
        // a couple more. A generous bound of 10 distinguishes the behaviours.
        assert.ok(
          maxObservedCloseListeners.value < 10,
          `close listener count should stay below 10 across ${REDIRECT_COUNT} redirects, peak was ${maxObservedCloseListeners.value}`
        );
      } finally {
        await stopHTTPServer(server);
      }
    }, 30000);
  });

  describe('socketPath security', () => {
    function makeSocketPath() {
      const pipe = `axios-socketpath-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;

      return os.platform() === 'win32'
        ? `\\\\.\\pipe\\${pipe}`
        : path.join(os.tmpdir(), `${pipe}.sock`);
    }

    function startUnixServer(socketPath) {
      return new Promise((resolveStart, rejectStart) => {
        const server = http.createServer((req, res) => {
          res.writeHead(200, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ ok: true, url: req.url }));
        });
        try {
          fs.unlinkSync(socketPath);
        } catch (_) {
          /* noop */
        }
        server.once('error', rejectStart);
        server.listen(socketPath, () => resolveStart(server));
      });
    }

    function stopUnixServer(server, socketPath) {
      return new Promise((done) => {
        server.close(() => {
          try {
            fs.unlinkSync(socketPath);
          } catch (_) {
            /* noop */
          }
          done();
        });
      });
    }

    it('allows socketPath when no allowedSocketPaths is set (backwards compatible)', async () => {
      const socketPath = makeSocketPath();
      const server = await startUnixServer(socketPath);
      try {
        const res = await axios.get('http://localhost/echo', { socketPath });
        assert.strictEqual(res.status, 200);
        assert.strictEqual(res.data.ok, true);
      } finally {
        await stopUnixServer(server, socketPath);
      }
    });

    it('allows socketPath when it matches an allowedSocketPaths string', async () => {
      const socketPath = makeSocketPath();
      const server = await startUnixServer(socketPath);
      try {
        const res = await axios.get('http://localhost/echo', {
          socketPath,
          allowedSocketPaths: socketPath,
        });
        assert.strictEqual(res.status, 200);
      } finally {
        await stopUnixServer(server, socketPath);
      }
    });

    it('allows socketPath when it matches an entry in allowedSocketPaths array', async () => {
      const socketPath = makeSocketPath();
      const server = await startUnixServer(socketPath);
      try {
        const res = await axios.get('http://localhost/echo', {
          socketPath,
          allowedSocketPaths: ['/var/run/does-not-exist.sock', socketPath],
        });
        assert.strictEqual(res.status, 200);
      } finally {
        await stopUnixServer(server, socketPath);
      }
    });

    it('rejects socketPath not in allowedSocketPaths', async () => {
      await assert.rejects(
        axios.get('http://localhost/echo', {
          socketPath: '/var/run/docker.sock',
          allowedSocketPaths: ['/tmp/allowed.sock'],
        }),
        (err) => {
          assert.ok(err instanceof AxiosError);
          assert.strictEqual(err.code, AxiosError.ERR_BAD_OPTION_VALUE);
          assert.match(err.message, /allowedSocketPaths/);
          return true;
        }
      );
    });

    it('rejects socketPath attempting path traversal that escapes allowlist', async () => {
      const allowedDir = path.join(os.tmpdir(), 'axios-allowed');
      const allowed = path.join(allowedDir, 'app.sock');
      await assert.rejects(
        axios.get('http://localhost/echo', {
          socketPath: path.join(allowedDir, '..', 'other.sock'),
          allowedSocketPaths: [allowed],
        }),
        (err) => {
          assert.strictEqual(err.code, AxiosError.ERR_BAD_OPTION_VALUE);
          return true;
        }
      );
    });

    it('treats relative and absolute allowedSocketPaths entries equivalently', async () => {
      const socketPath = makeSocketPath();
      const server = await startUnixServer(socketPath);
      try {
        const relative = path.relative(process.cwd(), socketPath);
        const res = await axios.get('http://localhost/echo', {
          socketPath,
          allowedSocketPaths: [relative],
        });
        assert.strictEqual(res.status, 200);
      } finally {
        await stopUnixServer(server, socketPath);
      }
    });

    it('rejects non-string socketPath', async () => {
      await assert.rejects(axios.get('http://localhost/echo', { socketPath: 12345 }), (err) => {
        assert.ok(err instanceof AxiosError);
        assert.strictEqual(err.code, AxiosError.ERR_BAD_OPTION_VALUE);
        assert.match(err.message, /socketPath must be a string/);
        return true;
      });
    });

    it('empty allowedSocketPaths array blocks all socketPath values', async () => {
      await assert.rejects(
        axios.get('http://localhost/echo', {
          socketPath: '/tmp/anything.sock',
          allowedSocketPaths: [],
        }),
        (err) => {
          assert.strictEqual(err.code, AxiosError.ERR_BAD_OPTION_VALUE);
          return true;
        }
      );
    });
  });
});
