axios/axios
github.com/axios/axios ↗ · 457 files · 2.7 MB
💎 Platinum sponsors We're passionate about making open source sustainable.
Overview
axios is the promise-based HTTP client library for browsers and Node.js, shipped as ESM source in `lib/` and Rollup-built UMD/ESM/CJS bundles in `dist/`. Its distinguishing shape is a single request pipeline (`Axios._request` → interceptors → `dispatchRequest` → adapter) that fans out to three interchangeable adapters — XHR, Node HTTP/HTTP2, and Fetch — selected by capability probe rather than environment name. This particular fork/branch (v1.x at 1.20.0) is unusually security-hardened: config reads go through own-property guards (`utils.getSafeProp`, `utils.toSafeFlatObject`), merges produce null-prototype objects, and there is a 94KB `THREATMODEL.md` plus a 1600-line prototype-pollution test suite. It is consumed by application developers as a dependency, and its contributor surface is governed by `AGENTS.md` plus a pre-release changelog/docs staging workflow.
System map
The public ESM entry is `index.js`, which unwraps the default instance built by `lib/axios.js` and re-exports its statics as named exports; CJS consumers get the Rollup-built `dist/node/axios.cjs`. Domain logic lives in `lib/core/` (Axios class, dispatch, merge, headers, errors, interceptors), I/O in `lib/adapters/`, environment selection in `lib/platform/` (Node by default, browser via package.json `browser` aliasing), and generic utilities in `lib/helpers/`. There is no storage layer — the only persistent state is browser cookies read for XSRF (`lib/helpers/cookies.js`) and pooled HTTP/2 sessions (`lib/helpers/Http2Sessions.js`). Build is `gulp clear && rollup -c` producing five bundles; tests are Vitest with three projects (unit/browser/browser-headless) plus packed-tarball smoke and module suites. Deployment shape is an npm package published on `v1.*.*` tags with provenance.
- `index.js` is the ESM public entry: it imports the default instance from `lib/axios.js` and destructures 18 statics (`Axios`, `AxiosError`, `CanceledError`, `CancelToken`, `VERSION`, `toFormData`, `AxiosHeaders`, `HttpStatusCode`, `formToJSON`, `getAdapter`, `mergeConfig`, `create`, …) so ESM and CJS named-import shapes stay identical.
- `createInstance(defaultConfig)` in `lib/axios.js` binds `Axios.prototype.request` to a fresh `Axios` context via `helpers/bind.js`, then copies both prototype and context onto the callable with `utils.extend(..., {allOwnKeys: true})`, which is why `axios(...)` is callable *and* carries `.get`/`.interceptors`/`.defaults`.
- Runtime boundary between Node and browser is resolved by package.json `browser`/`react-native` maps that swap `lib/adapters/http.js` → `lib/helpers/null.js`, `lib/platform/node/index.js` → `lib/platform/browser/index.js`, and Node `Buffer`/`FormData` classes to null — `lib/platform/index.js` itself always imports the Node platform.
- Adapter registry `knownAdapters` maps `http` → Node adapter, `xhr` → XHR adapter, and `fetch` → an object with a `get(config)` factory (`fetchAdapter.getFetch`), so fetch is resolved lazily per-`config.env` while the other two are plain functions or `false`.
- Default adapter preference is the ordered list `['xhr', 'http', 'fetch']` set in `lib/defaults/index.js`; `getAdapter` walks it, treating `false` as 'not supported by the environment' and `undefined` as 'not available in the build' when composing the `ERR_NOT_SUPPORT` message.
- The Node adapter is the heaviest module (1490 lines) and pulls in `http`, `https`, `http2`, `zlib`, `stream`, `path`, plus the four runtime deps `follow-redirects`, `form-data` (indirectly), `https-proxy-agent`, and `proxy-from-env`.
- Build emits five artifacts from two inputs: `dist/esm/axios.js` (ESM, from `index.js`), `dist/axios.js` (UMD, Babel-transpiled to ES5, from `lib/axios.js`), `dist/browser/axios.cjs`, and `dist/node/axios.cjs` (Babel-targeted at Node 12, with `proxy-from-env` deliberately inlined by `nodeCjsExternal`).
- `gulp version` (run via the `preversion` npm script) regenerates `lib/env/data.js` from package.json and rewrites the `contributors` array by calling the GitHub API through an axios instance built in `scripts/axios-build-instance.js` using `GITHUB_TOKEN`.
- Test commands are Vitest-project-scoped: `npm run test:vitest:unit` (node env, `tests/unit/**/*.test.js`), `test:vitest:browser` (chromium headed), and `test:vitest:browser:headless` (chromium+firefox+webkit via Playwright), all declared in `vitest.config.js` with a 10s default `testTimeout`.
- CI order in `run-ci.yml` is: checkout → `npm ci --ignore-scripts` → lint → build → `npx playwright install --with-deps` → unit → browser headless → `npm pack` → dependency-review → upload tarball; every downstream job (`cjs-smoke`, `esm-smoke`, `cjs-module`, `esm-module`, `bun-smoke`, `deno-smoke`) `needs` that job and installs the packed tarball rather than the source tree.
- Publishing is tag-triggered (`v1.*.*`) into the `npm-publish` GitHub environment with `id-token: write` and `npm stage publish --provenance --access public`; release branches are cut by a manual `workflow_dispatch` in `release-branch.yml` with patch/minor/major + prerelease inputs.
- Supply-chain guards are separate workflows: `lockfile-lint.yml` validates HTTPS registry URLs and integrity hashes on any package.json/package-lock.json change (pinned by name, deliberately not a devDependency), `zizmor.yml` scans the Actions themselves with `permissions: {}`, and `verify-build-reproducibility.yml` does a two-pass build diff (currently `continue-on-error: true`).
Feature catalog
The user-facing surface is the axios instance API: a callable default export, per-method aliases generated onto `Axios.prototype`, interceptor managers, cancellation via both `CancelToken` and `AbortSignal`, and a large `AxiosRequestConfig` surface typed in `index.d.ts`/`index.d.cts`. Beyond the classic v1 surface this branch carries newer options: `httpVersion: 2`, `allowedSocketPaths`, `sensitiveHeaders`, `redact`, `formDataHeaderPolicy`, `parseReviver`, and `query` as a first-class HTTP method. Transport features include transparent gzip/deflate/br/zstd decompression, upload/download progress with rate throttling (`maxRate`), proxy handling with CIDR-aware `NO_PROXY` bypass, and `maxContentLength`/`maxBodyLength` enforcement on buffered *and* streamed bodies in all three adapters. Form-data handling is bidirectional: `toFormData`/`formToJSON` plus `formDataToStream` for Node uploads.
- Method aliases are generated in two loops on `Axios.prototype`: `delete/get/head/options` (no body, but forward `config.data` via an own-property check) and `post/put/patch/query` (with body); each of the latter except `query` also gets a `<method>Form` variant that forces `Content-Type: multipart/form-data`.
- `query` is a supported HTTP method throughout: it appears in the frozen `methodList` alongside `purge`/`link`/`unlink`, gets a default header bucket, and is deliberately excluded from the `Form` shorthand generation because multipart bodies don't fit its safe/idempotent semantics.
- Interceptors are registered through `InterceptorManager.use(fulfilled, rejected, {synchronous, runWhen})` and support `eject(id)` and `clear()`; ejection writes a `null` tombstone and trims trailing tombstones once no `forEach` iteration is in flight, so repeated register/eject cycles don't grow `handlers`.
- Cancellation is dual-path: legacy `CancelToken` (with `CancelToken.source()`, `throwIfRequested()`, and a `toAbortSignal()` bridge that exposes `signal.unsubscribe`) and modern `config.signal`; `composeSignals` merges any number of signals plus a timeout into one `AbortController`.
- Node transparent decompression covers gzip/x-gzip/compress/x-compress, deflate (through `ZlibHeaderTransformStream`), brotli, and zstd, each gated on runtime support and each deleting `content-encoding` after piping so downstream consumers aren't confused; `decompress: false` disables the whole path.
- `Accept-Encoding` advertising is transitional-gated: `zstd` is only appended when `transitional.advertiseZstdAcceptEncoding === true` (default `false`), so servers aren't offered an encoding by default.
- HTTP/2 is opt-in via `httpVersion: 2` with `http2Options`; sessions are pooled per-authority by `Http2Sessions.getSession()` using `util.isDeepStrictEqual` on options, and combining HTTP/2 with an applied proxy rejects with `ERR_NOT_SUPPORT`.
- Progress reporting is available on both directions (`onUploadProgress`/`onDownloadProgress`) and is rate-limited plus decorated with `loaded/total/rate/estimated` by `progressEventReducer`/`progressEventDecorator`; `config.maxRate` (scalar or `[up, down]` tuple) throttles bytes via `AxiosTransformStream`.
- `data:` URLs are a supported protocol in both Node and fetch adapters: Node returns a synthetic 200 (or 405 for non-GET) built from `fromDataURI`, and both adapters pre-check the decoded size with `estimateDataURLDecodedBytes` against `maxContentLength` before materializing the payload.
- XSRF support reads a cookie (`xsrfCookieName`, default `XSRF-TOKEN`) and sets `xsrfHeaderName` (default `X-XSRF-TOKEN`) only in a standard browser env; `withXSRFToken` may be a boolean or a `(config) => boolean` function, and only strict `=== true` forces attachment cross-origin.
- `AxiosError.toJSON()` supports opt-in redaction: when `config.redact` is a non-empty array, `redactConfig` walks the config (including arrays and `AxiosHeaders`, short-circuiting cycles) and replaces matching keys case-insensitively with `'[REDACTED ****]'`.
- `sensitiveHeaders: string[]` lets callers name headers that must be stripped on cross-origin redirects; it is validated to be an array of strings (else `ERR_BAD_OPTION_VALUE`) and installed as `options.beforeRedirects.sensitiveHeaders`, layered on top of follow-redirects' own stripping.
Data flow
Input boundaries are the user config object (or URL string), instance defaults, and environment state (env proxy vars, `document.cookie`, platform globals). `mergeConfig` is the single state-materialization point: it emits a **null-prototype** object with `hasOwnProperty` restored as a non-enumerable own slot, skipping `__proto__`/`constructor`/`prototype` keys entirely. `dispatchRequest` re-flattens whatever interceptors handed back via `utils.toSafeFlatObject`, normalizes headers into `AxiosHeaders`, runs `transformRequest`, then hands off to the adapter. Adapters own the outbound byte boundary (header sanitization to byte strings, body size accounting, stream piping) and produce a raw `{data, status, statusText, headers, config, request}`, which flows back through `transformResponse`, `AxiosHeaders.from`, `settle`, and finally the response interceptor chain.
- `mergeConfig` builds its result on `Object.create(null)` and re-attaches `hasOwnProperty` as a non-enumerable own data property with a `__proto__: null` descriptor, so `config.baseURL`/`config.auth` reads can never inherit from a polluted `Object.prototype` while user code calling `config.hasOwnProperty(...)` still works.
- Per-key merge strategy is table-driven: `url`/`method`/`data` take config2 only, ~25 keys (`baseURL`, `timeout`, `httpAgent`, `socketPath`, `allowedSocketPaths`, …) use `defaultToConfig2`, `validateStatus` uses `mergeDirectKeys`, `headers` deep-merges caselessly after `headersToObject`, and everything unlisted falls through to `mergeDeepProperties`.
- The merge loop explicitly `return`s on `__proto__`, `constructor`, and `prototype` before dispatching to a merge function, so those keys can never be materialized onto the merged config.
- Headers enter via three-way flattening in `Axios._request`: `headers.common` merged with `headers[config.method]`, the per-method buckets then deleted from the raw object, and the remainder concatenated through `AxiosHeaders.concat(contextHeaders, headers)`.
- `dispatchRequest` calls `utils.toSafeFlatObject(_config)` first, which flattens the config and its application-defined prototype chain into a null-prototype copy while stopping at the realm's `Object.prototype` boundary — this is the defense against an interceptor returning an ordinary (pollutable) object.
- `transformData` is invoked with `config` as `this`, wraps `context.headers` in `AxiosHeaders`, calls each fn as `fn.call(config, data, headers.normalize(), status)`, and normalizes headers again afterwards — which is how `transformRequest` can mutate content-type in place.
- Default `transformRequest` is a type-dispatch ladder: HTMLForm→FormData, FormData→(JSON if JSON content-type else passthrough), ArrayBuffer/Buffer/Stream/File/Blob/ReadableStream passthrough, ArrayBufferView→`.buffer`, URLSearchParams→string with urlencoded content-type, FileList→`toFormData({'files[]': …})`, plain object→`stringifySafely` with `application/json`.
- Outbound header values are sanitized twice: `AxiosHeaders.normalizeValue` strips C0/DEL via `sanitizeHeaderValue`, and both the Node and XHR adapters additionally pass the whole header set through `toByteStringHeaderObject`, which rewrites into a null-prototype object with non-byte-string chars removed.
- URL construction is a two-stage pipeline: `buildFullPath(baseURL, url, allowAbsoluteUrls, config)` combines/validates (rejecting `http:foo` style missing-`//` URLs with `ERR_INVALID_URL`), then `buildURL(fullPath, params, paramsSerializer)` appends serialized params using a custom encoder that un-escapes `:`/`$`/`,` and maps space to `+`.
- URLs embedded in `ERR_INVALID_URL` messages are redacted before stringification — userinfo, query-parameter values, and fragment contents are replaced with `[REDACTED ****]` while scheme/host/path/param names survive — because `AxiosError.toJSON()` serializes `message` verbatim and `config.redact` cannot reach it.
- Basic-auth credentials flow from either `config.auth` (read with `utils.getSafeProp`) or URL userinfo; in the Node adapter they become the `auth` option and any pre-existing `authorization` header is deleted, while in fetch/XHR they're base64-encoded into an `Authorization: Basic` header after UTF-8→Latin-1 transcoding.
- Response data flows back through `settle(resolve, reject, response)`, which resolves when `validateStatus` is absent/passing and otherwise rejects with `ERR_BAD_REQUEST` for 4xx and `ERR_BAD_RESPONSE` for everything else, carrying `config`, `request`, and `response`.
Behavior traces
Three flows dominate: module load/instance creation, a Node GET through the HTTP adapter, and a browser POST through XHR with XSRF. A fourth worth tracing is cancellation, which has to unwind listeners at every stage. Interceptor ordering is the subtle part — request interceptors currently default to LIFO via the `legacyInterceptorReqResOrdering: true` transitional flag, and the whole chain has a synchronous fast path that bypasses Promise wrapping when every request interceptor declares `synchronous: true`.
- Startup trace: `index.js` → `lib/axios.js` module body → `createInstance(defaults)` → `new Axios(defaults)` (constructs two `InterceptorManager`s) → `bind(Axios.prototype.request, context)` → `utils.extend` twice → statics (`Axios`, `AxiosError`, `CancelToken`, `all`, `spread`, `mergeConfig`, `AxiosHeaders`, `formToJSON`, `getAdapter`, `HttpStatusCode`) attached, `axios.default = axios`.
- Request entry trace: `Axios.request` is an `async` wrapper that awaits `_request` and, on error, decorates `err.stack` with a freshly captured stack (via `Error.captureStackTrace` into a dummy, slicing off the `Error:` line) — all inside a nested try/catch so a throwing custom stack hook can never replace the original failure.
- `_request` order is: normalize string URL → `mergeConfig(this.defaults, config)` → assert `transitional` options → normalize function `paramsSerializer` into `{serialize}` → resolve `allowAbsoluteUrls` (config → instance default → `true`) → spelling checks (`baseUrl`→`baseURL`, `withXsrfToken`→`withXSRFToken`) → lowercase method via `utils.getSafeProp` → flatten headers.
- Interceptor chain assembly: request interceptors with a `runWhen(config) === false` are skipped; `synchronousRequestInterceptors` is AND-ed across the remainder; with `transitional.legacyInterceptorReqResOrdering` truthy (the default) each handler pair is `unshift`ed producing LIFO, otherwise `push`ed producing FIFO. Response interceptors are always pushed (FIFO).
- Async path builds `[...requestChain, dispatchRequest.bind(this), undefined, ...responseChain]` and folds it with `promise.then(chain[i++], chain[i++])`; the synchronous path instead calls each `onFulfilled` inline, and if an `onRejected` returns a thenable it resumes with `dispatchRequest.call(this, newConfig)` after it settles.
- Node GET trace: `dispatchRequest` → `adapters.getAdapter(['xhr','http','fetch'])` picks `httpAdapter` (xhr is `false` in Node) → `wrapAsync(dispatchHttpRequest)` → `own()` reads ~20 config keys → `buildFullPath` → `new URL(fullPath, urlBase)` → protocol allow-list check against `platform.protocols` → `AxiosHeaders.from(...).normalize()` → default `User-Agent: axios/<VERSION>` → body encoding → `options` assembled on `Object.create(null)`.
- Transport selection inside the Node adapter is a four-way branch: `httpVersion === 2` → `http2Transport`; else `config.transport` if set; else `maxRedirects === 0` → native `http`/`https` (`isNativeTransport = true`); else `follow-redirects`' `httpFollow`/`httpsFollow` (the only transport that enforces `options.maxBodyLength` itself).
- Response handling trace in Node: `handleResponse(res)` → optional `AxiosTransformStream` for download progress/rate → decompression stream pushed per `content-encoding` → `stream.pipeline(streams)` → for `responseType: 'stream'` an async generator enforces `maxContentLength` lazily; otherwise chunks buffer with a running byte count, `end` concatenates + `stripBOM`, then `settle`.
- Timeout trace in Node: `createTimeoutError()` picks `ETIMEDOUT` when `transitional.clarifyTimeoutError` is true and `ECONNABORTED` otherwise; for the native-transport path an extra wall-clock `connectPhaseTimer` is armed because `ClientRequest#setTimeout` may not fire during TCP connect.
- Cancellation trace: `dispatchRequest` pre-checks `cancelToken.throwIfRequested()` and `signal.aborted`; the Node adapter then subscribes `abort` to both, emits on an internal `EventEmitter`, and `onFinished()` unsubscribes the token, removes the `abort` listener, clears the connect timer, and calls `abortEmitter.removeAllListeners()` — deferred until a streamed response body finishes via `stream.finished`.
- Browser POST trace: `xhrAdapter` → `resolveConfig(config)` (merge, build URL, basic auth, FormData content-type handling, XSRF cookie read) → `new XMLHttpRequest()` → `request.open(method, url, true)` → header set from `toByteStringHeaderObject` → `onloadend` builds the response and calls `settle` with `done()` wrappers that flush progress and unsubscribe cancellation.
- XHR `onloadend` has a status-0 guard added for Firefox 152: if status is 0 and the scheme is not `file:` (checked against the request URL, the page origin, and `responseURL`), it rejects with `ECONNABORTED` instead of letting `settle` resolve an empty success.
Testing signals
Vitest is the only runner for source tests, split into three projects; unit tests run in node, browser tests run under Playwright against chromium/firefox/webkit. Coverage of security behavior is unusually deep — `tests/unit/prototypePollution.test.js` is 1619 lines and `tests/unit/adapters/http.test.js` is 7252 lines, together ~39% of the 22.8k unit-test lines. Beyond the source tree there are four packed-tarball suites (CJS/ESM smoke via mocha/vitest, Bun via `bun test`, Deno via `deno task`) plus two TypeScript declaration suites pinned to different TS versions. The gaps are mostly at the edges: no coverage numbers or coverage gate is configured, the reproducibility workflow is explicitly non-blocking, and `sandbox/`, `examples/`, and `gulpfile.js` have no tests at all.
- Vitest defines three projects — `unit` (node env, `tests/unit/**/*.test.js`, no setup files), `browser` (chromium, headed), and `browser-headless` (chromium+firefox+webkit) — the latter two both using `tests/setup/browser.setup.js`.
- Unit tests are organised by module under `tests/unit/{adapters,cancel,core,helpers,utils}` plus 15 top-level files; `tests/unit/core/` mirrors `lib/core/` almost 1:1 (Axios, AxiosError, buildFullPath, dispatchRequest, InterceptorManager, mergeConfig, settle, transformData).
- `tests/unit/prototypePollution.test.js` (1619 lines) is a dedicated security suite that imports `utils`, `mergeConfig`, `defaults`, `AxiosError`, `AxiosHeaders`, `resolveConfig`, and the public `index.js`, uses `node:vm` for cross-realm cases, and cleans up ~25 `Object.prototype` keys in `afterEach`.
- `tests/setup/server.js` is the shared HTTP(S)/HTTP2 fixture: it generates a self-signed cert once with `selfsigned`, tracks live servers in a `Set` for cleanup, defaults to port 0 (OS-assigned) to avoid TIME_WAIT races in CI, and supports buffering, throttling (`stream-throttle`), and `formidable` parsing.
- Browser suite (21 files) covers adapter selection, basic auth, both cancellation APIs, cookies, fetch cache and fetch response errors, formdata, headers, interceptors, same-origin detection, progress, settle, transforms, and XSRF.
- Smoke suites test the *packed tarball*, not the source: CI runs `npm pack`, uploads the tgz as an artifact, and each downstream job installs it with `npm install --no-save ../../../artifacts/axios-*.tgz` before running that runtime's suite.
- Runtime matrix is explicit: CJS smoke/module on Node 12/14/16/18, ESM smoke/module on Node 20/22/24/26, plus one Bun job and one Deno job — the Node-12 floor matches the `@babel/preset-env` target used for `dist/node/axios.cjs`.
- CJS and ESM smoke suites are kept in lockstep — both have the same 16 test names (auth, basic, cancel, error, fetch, files, formData, headers, http2, import, instance, interceptors, progress, rateLimit, timeout, urlencode) with `.cjs`/`.js` extensions.
- Bun and Deno smoke coverage is deliberately narrower — Deno tests only cancel/error/fetch/headers/import, Bun adds formData/http/interceptors/progress/timeout — so runtime-specific regressions outside those areas would not be caught.
- Type declarations are exercised by two separate workspaces: `tests/module/cjs` (`ts-require.module.test.cjs`, `ts-require-default.module.test.cjs`, `typings.module.test.cjs`) and `tests/module/esm` (`ts.module.test.js`, `typings.module.test.js`), each with its own lockfile and, per AGENTS.md, pinned to TypeScript 4.9 vs 5.x.
- Lint scope is source-only (`eslint lib/**/*.js`); tests, `gulpfile.js`, `rollup.config.js`, and `scripts/` are not linted, and `lib/adapters/http.js` / `lib/adapters/xhr.js` get environment-specific globals blocks rather than the shared browser+node block.
- There is no coverage provider, threshold, or reporter configured in `vitest.config.js`, and no coverage step in `run-ci.yml` — so coverage is unmeasured and ungated despite the large suite.
Risk map
This is a library whose entire job is attacker-adjacent I/O, and the code reflects that: nearly every security control is a hand-rolled invariant with a comment explaining the CVE or issue behind it, which means the blast radius of an innocuous-looking refactor is large. The highest-risk surfaces are `lib/adapters/http.js` (proxy/redirect/socketPath/decompression, 1490 lines, one function), `lib/core/mergeConfig.js` + `utils.toSafeFlatObject` (the prototype-pollution boundary), and `lib/helpers/resolveConfig.js` (XSRF cross-origin decision). Fragile coupling exists between the package.json `browser` alias map and any new Node-only import; adding one to a browser-reachable module silently ships `http2`/`zlib` into browser bundles. Legacy surface (`CancelToken`, `axios.Cancel`, `legacyInterceptorReqResOrdering`, `tslint.json`, `webpack.config.js`) is retained for compatibility and is easy to break inadvertently.
- The prototype-pollution boundary is enforced in at least five independent places — `mergeConfig`'s null-proto result and key filter, `dispatchRequest`'s `toSafeFlatObject`, `own()`/`getSafeProp` reads in the http and fetch adapters, `resolveConfig`'s `own()`, and `AxiosHeaders` rejecting `__proto__`/`constructor`/`prototype` parameter names — so a regression in any one of them reopens the class.
- Even `Object.defineProperty` calls use `__proto__: null` descriptors (in `AxiosError`, `mergeConfig`, `adapters.js`, `AxiosHeaders` accessors, and the fetch/http `cause` assignments) specifically so a polluted `Object.prototype.get` cannot convert a data descriptor into an accessor on the way in — an easy detail to drop when copying this pattern.
- Credential-leak-on-redirect is guarded by hand: HTTP Basic `auth` is restored after a redirect only when `new URL(redirectOptions.href).origin === parsed.origin`, with a malformed-URL catch that leaves auth stripped (fail-safe). Removing the origin comparison would leak credentials cross-origin.
- `Proxy-Authorization` is deleted from headers on every redirect hop (skipped only on the initial request) so a target change that drops or swaps the proxy doesn't carry proxy credentials forward.
- Proxy fields are read with `utils.hasOwnProp` unless the proxy is a real `URL` instance, explicitly to stop a prototype-polluted `proxy.host` from redirecting traffic to an attacker-controlled host.
- `socketPath` is an SSRF/local-IPC surface mitigated by `allowedSocketPaths`: when set, the requested path must `path.resolve()`-equal an allow-list entry or the request rejects with `ERR_BAD_OPTION_VALUE`. When `allowedSocketPaths` is unset (the default) any socket path is permitted.
- `options.insecureHTTPParser` is always written as an explicit own boolean (`Boolean(own('insecureHTTPParser'))`) rather than left absent, precisely so a polluted `Object.prototype.insecureHTTPParser` cannot enable Node's lenient parser through its internal options copy — a request-smuggling vector.
- `maxContentLength`/`maxBodyLength` enforcement is duplicated across five code paths (Node buffered, Node streamed via async generator, Node streamed upload Transform, fetch chunk counting, fetch non-ReadableStream fallback) — decompression bombs are only bounded where the check exists, and `-1` (the default) means unlimited everywhere.
- Cookie reading avoids building a `RegExp` from the cookie name — it splits on `;` and compares the literal name — because interpolating an attacker-influenced name would allow ReDoS or wrong-cookie matching.
- The XSRF cross-origin decision hinges on a strict `withXSRFToken === true` comparison; any loosening (e.g. truthiness) would let a polluted `Object.prototype.withXSRFToken = 1` or a string `"false"` leak the token to third-party origins.
- Memory-retention hazards are handled with symbols and bookkeeping that are easy to break: a module-scoped `handleSocketError` plus `kAxiosSocketListener`/`kAxiosCurrentReq` symbols and a `boundSockets` Set exist because per-request socket listeners leaked adapter context across keep-alive reuse and triggered MaxListenersExceededWarning at ≥11 redirects.
- `tunnelingAgentCache` is a module-level unbounded `Map` keyed by proxy config; the code comments argue the keyspace is bounded in practice, which is an assumption rather than an enforced limit.
Extension points
The intended extension seams are well-marked. Adapters plug in through `knownAdapters` or by passing a function/array to `config.adapter`; `config.transport` overrides the Node transport wholesale; `config.env.{fetch,Request,Response,FormData,Blob}` lets a runtime inject its own primitives. Data shaping extends through `transformRequest`/`transformResponse` arrays, `paramsSerializer`, `formSerializer`, and `parseReviver`. Cross-cutting behavior extends through interceptors (with `synchronous` and `runWhen` modifiers) and through `AxiosHeaders.accessor()` for new header shorthands. New error codes are added as UPPER_SNAKE constants on `AxiosError`, new behavior flags as `transitional` keys validated in `Axios._request`.
- `config.adapter` accepts a name, a function, or an array of either; `getAdapter` resolves names against `knownAdapters` and accepts any function unchanged — that is the supported way to ship a custom adapter without touching the registry.
- Adding an adapter to `knownAdapters` either as a function or as a `{get(config)}` capability-probe object is the registry-level seam; the `utils.forEach` loop then stamps `name`/`adapterName` onto it automatically for diagnostics.
- `config.transport` short-circuits the Node transport selection before the `maxRedirects`/follow-redirects branches, letting callers supply any object with a `.request(options, cb)` — but such transports must enforce `maxBodyLength` themselves, since `transportEnforcesMaxBodyLength` stays false.
- `config.env` is a first-class injection point: `getFetch(config)` memoizes a fetch adapter per `(Request, Response, fetch)` triple in a nested `Map` seed cache, and `defaults.env` carries `FormData`/`Blob` from the platform for `transformRequest` and `fromDataURI`.
- `AxiosHeaders.accessor(header)` defines camel-cased `get*/set*/has*` shorthand methods on the prototype (with `__proto__: null` descriptors), and is how header conveniences like `getContentType`/`setContentLength` are added.
- `methodList` is a frozen array that drives both the per-method default header buckets in `lib/defaults/index.js` and the header-bucket cleanup in `Axios._request` — adding a method means editing this one list.
- New behavior flags plug into `lib/defaults/transitional.js` and must be added to the `validator.assertOptions(transitional, {...})` map in `Axios._request` — otherwise the option silently bypasses type validation.
- `validator.validators` provides composable validators (`object`/`boolean`/`number`/`function`/`string`/`symbol`, plus `transitional(validator, version, message)` for deprecation warnings that print once and `spelling(correctSpelling)` used for `baseUrl`→`baseURL`), and is the sanctioned place to add config validation.
- New error codes are added as UPPER_SNAKE static assignments at the bottom of `lib/core/AxiosError.js` (14 currently, from `ERR_BAD_OPTION_VALUE` to `ERR_FORM_DATA_DEPTH_EXCEEDED`); `AxiosError.from(error, code, config, request, response, customProps)` is the wrapper for third-party errors.
- Serialization extends at three levels: `paramsSerializer` (function or `{encode, serialize}`), `config.formSerializer` (passed to `toFormData` with `visitor`/`dots`/`metaTokens`/`indexes`/`maxDepth`), and `parseReviver` on the response side.
- The Node adapter exports three internals for testing/extension — `__setProxy`, `__isNodeEnvProxyEnabled`, `__isSameOriginRedirect` — a deliberate seam for unit-testing proxy and redirect logic without a live socket.
- Platform swap points are `lib/platform/{node,browser}/classes/` (URLSearchParams, FormData, Blob, Buffer) and the `protocols` array — Node allows `['http','https','file','data']` while browser allows `['http','https','file','blob','url','data']`, and the Node adapter checks `supportedProtocols` derived from it.
Contributor handoff
Read `AGENTS.md` first — it is the canonical contributor contract and encodes rules that are not derivable from the code (pre-release changelog/docs staging, `ignore-scripts` safety, dependency policy, error-handling conventions). Then read `lib/core/Axios.js`, `lib/core/dispatchRequest.js`, `lib/core/mergeConfig.js`, and whichever adapter you're touching. The cheapest verification loop is `npm run test:vitest:unit -- tests/unit/<file>.test.js` plus `npx eslint lib/<file>.js`; a full build is only needed for packaging/type changes. The two traps that catch most newcomers are (1) editing `README.md`/`CHANGELOG.md` instead of `PRE_RELEASE_DOCS.md`/`PRE_RELEASE_CHANGELOG.md`, and (2) using ordinary property reads on config where the file uses `own()`/`getSafeProp`, which silently reopens a prototype-pollution hole with no test failure unless you add one.
- Read-first order: `AGENTS.md` (canonical rules, mirrored by the `.github/copilot-instructions.md` stub and `CLAUDE.md`'s one-line `@AGENTS.md` include) → `lib/axios.js` → `lib/core/Axios.js` → `lib/core/dispatchRequest.js` → `lib/core/mergeConfig.js` → the target adapter.
- Unreleased user-visible changes go in `PRE_RELEASE_CHANGELOG.md` under `## Unreleased`, and deferred README/docs-site/migration-guide work goes in `PRE_RELEASE_DOCS.md`; `CHANGELOG.md` (141KB) is release-owned and must not be touched during feature work.
- Install safety: `.npmrc` sets `ignore-scripts=true` and CI uses `npm ci --ignore-scripts`; if husky hooks are needed after a fresh install, run `npm rebuild husky && npx husky` once rather than removing the setting.
- Focused verification commands: `npm run test:vitest:unit -- tests/unit/path.test.js` for one unit file, `npx eslint lib/path/to/file.js` for one source file, `npm run test:vitest:browser:headless` for CI-parity browser runs (requires `npx playwright install` first).
- Never hand-edit `dist/` (generated by Rollup and wiped by `gulp clear`) or `lib/env/data.js` (generated by `gulp version` from package.json) — both are regenerated by the build.
- Any API change must be mirrored in three places: the runtime export list in `lib/axios.js`/`index.js`, `index.d.ts` (ESM types), and `index.d.cts` (CJS `export =` types) — and then exercised by `tests/module/esm` (TS 5.x) and `tests/module/cjs` (TS 4.9).
- Convention trap: config reads that affect behavior must use own-property guards. `lib/adapters/http.js` defines a local `own = (key) => utils.getSafeProp(config, key)` and routes ~20 reads through it; `resolveConfig` and `lib/defaults/index.js` each define their own `own()` helper for the same reason.
- Convention trap: do not use `Function.prototype.bind` directly — the library relies on `lib/helpers/bind.js`, which forwards `arguments` via `apply`, and `createInstance` depends on that behavior for the callable-instance trick.
- Convention trap: never throw raw `Error` from library code — throw `AxiosError(message, code, config, request, response)` or wrap with `AxiosError.from(...)` using a code from the list at the bottom of `lib/core/AxiosError.js`.
- Test-hygiene trap: HTTP fixtures come from `tests/setup/server.js` and must be cleaned up in `try/finally`; leaking a server hangs Vitest. Browser tests replace globals such as XHR and must restore them in cleanup hooks.
- Changes touching URL building, redirects, proxy/env handling, XSRF, socket paths, decompression limits, or adapters require consulting `THREATMODEL.md` (94KB, sections: scope/methodology, runtime threat model, supply-chain threat model) and adding a focused regression test.
- Style boundary for new `lib/**/*.js` files: ESM imports with explicit `.js` extensions, `'use strict';` where neighboring files have it, `Symbol`-keyed internal slots (e.g. `const $internals = Symbol('internals')` in `AxiosHeaders` and `InterceptorManager`) rather than underscore-prefixed properties.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 216 | 88% |
| Markdown | 160 | docs/config |
| TypeScript | 29 | 12% |
| JSON | 12 | docs/config |
| YAML | 10 | docs/config |
| HTML | 9 | docs/config |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: index.js — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (73 files)
Repository structure
| Top-level path | Files |
|---|---|
tests/ | 162 |
docs/ | 155 |
lib/ | 73 |
examples/ | 17 |
.github/ | 14 |
sandbox/ | 3 |
.husky/ | 1 |
scripts/ | 1 |
Docs found: AGENTS.mdCLAUDE.mdCONTRIBUTING.mdREADME.mddocs/es/index.mddocs/es/pages/advanced/adapters.mddocs/es/pages/advanced/api-reference.mddocs/es/pages/advanced/authentication.mddocs/es/pages/advanced/cancellation.mddocs/es/pages/advanced/config-defaults.md
Package
package.json — axios v1.20.0, 43 declared dependencies.
@babel/core@babel/preset-env@commitlint/cli@commitlint/config-conventional@eslint/js@rollup/plugin-alias@rollup/plugin-babel@rollup/plugin-commonjs@rollup/plugin-json@rollup/plugin-node-resolve@rollup/plugin-terser@vitest/browser
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/bundle-size.yml— GitHub Actions workflow.github/workflows/verify-build-reproducibility.yml— GitHub Actions workflow.github/workflows/lockfile-lint.yml— GitHub Actions workflow.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/release-branch.yml— GitHub Actions workflow.github/workflows/moderator.yml— GitHub Actions workflow.github/workflows/zizmor.yml— GitHub Actions workflow
Structural health signals
Average score 9.2/10 across 92 analyzed source files; 3 hotspots surfaced.
lib/adapters/http.js— 1407 lines, score 4.7 (Large file, Deep nesting)lib/adapters/fetch.js— 715 lines, score 5.2 (Large file, Deep nesting)lib/core/Axios.js— 306 lines, score 5.9 (Deep nesting, Long function/block)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
56a5f1acdc412026-09-17 — fix(fetch): preserve response size errors across runtime wrappers (#11179)fbe2e8260c862026-09-17 — fix(types): expose the response default marker type (#11125)ba0f3a0c568e2026-09-16 — chore(deps-dev): bump the development_dependencies group with 4 updates (#11234)ed95c083f5742026-09-13 — chore(deps): bump nanoid from 3.3.17 to 3.3.19 in /docs (#11230)dfa30a76dcae2026-09-14 — docs: fix interceptor test links (#11228)