fix(deps): update dependency axios to v1.18.1#5633
Conversation
|
[puLL-Merge] - axios/axios@v1.18.0..v1.18.1 Diffdiff --git CHANGELOG.md CHANGELOG.md
index 23ea1ef5be..24c010a254 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,37 @@
# Changelog
+## v1.18.0 — June 13, 2026
+
+This release hardens redirect and URL handling, improves the validateStatus configuration semantics, and includes updates to documentation, dependencies, and release metadata.
+
+## 🔒 Security Fixes
+
+* **Redirect Header Safety:** Added Node HTTP adapter support for stripping caller-specified sensitive headers on cross-origin redirects, helping prevent custom auth headers such as API keys from leaking to another origin. (__#10892__)
+
+* **URL And Request Hardening:** Rejects malformed `http:` and `https:` URLs that omit `//` with `ERR_INVALID_URL`, while tightening prototype-pollution-safe config reads, stream size limits, FormData depth handling, data URL sizing, and local `NO_PROXY` matching. (__#11000__)
+
+## 🐛 Bug Fixes
+
+* **Status Validation:** Added `transitional.validateStatusUndefinedResolves` so applications can opt in to treating `validateStatus: undefined` like the option was omitted, while `validateStatus: null` remains the explicit way to accept every status. (__#10899__)
+
+## 🔧 Maintenance & Chores
+
+* **Documentation:** Published the v1.17.0 release notes, fixed a changelog typo, clarified the package update PR policy, and marked the `proxy` request config as Node.js-only in the advanced docs. (__#10984__, __#10988__, __#10992__, __#10995__)
+
+* **Dependencies:** Bumped `@babel/core`, `@babel/preset-env`, `@commitlint/cli`, `@commitlint/config-conventional`, `@rollup/plugin-babel`, `@rollup/plugin-commonjs`, `@vitest/browser`, `@vitest/browser-playwright`, `eslint`, `lint-staged`, `rollup`, `vitest`, and `actions/checkout`. (__#10989__, __#10996__, __#10997__)
+
+* **Release Metadata:** Prepared the 1.18.0 release by updating package metadata and the runtime `VERSION` value. (__#11003__)
+
+## 🌟 New Contributors
+
+We are thrilled to welcome our new contributors. Thank you for helping improve axios:
+
+* __@drori12__ (__#10984__)
+* __@eyupcanakman__ (__#10899__)
+* __@Adi-Beker__ (__#10995__)
+
+[Full Changelog](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)
+
## v1.17.0 — June 1, 2026
This release adds Node HTTP zstd decompression, hardens config and release workflows, and fixes authentication, header, proxy, and type-handling regressions.
diff --git PRE_RELEASE_CHANGELOG.md PRE_RELEASE_CHANGELOG.md
index eaaf648ad8..b28909d67f 100644
--- PRE_RELEASE_CHANGELOG.md
+++ PRE_RELEASE_CHANGELOG.md
@@ -2,16 +2,17 @@
## Unreleased
-## Security Fixes
+## Bug Fixes
-- **HTTP Adapter Redirects:** Added a Node.js `sensitiveHeaders` request config option that strips caller-selected custom secret headers from cross-origin redirects. (**#10892**)
+- **Params serialization:** Custom `paramsSerializer.encode` functions now receive the active `AxiosURLSearchParams` instance as `this`, matching the intended `encoder.call(this, value, defaultEncode)` behavior during query string construction. (**#11019**)
+- **Runtime and types hardening:** Guarded several edge-case crashes in cookie decoding, data URI parsing, form serialization, config merging, option validation, XHR cleanup, and Node HTTP URL serialization error handling. Type declarations now expose missing `CanceledError`, `CancelToken`, `AxiosHeaders`, `SerializerOptions`, and Cloudflare 52x status-code members that already exist at runtime. (**#10959**)
+- **HTTP Adapter - native env proxy:** Avoid double-applying environment proxy handling when Node.js native HTTP proxy support is active for the selected agent. Axios still resolves env proxies itself when the selected agent is not using Node's `proxyEnv` support. (**#10942**, closes **#7299**)
+- **HTTP Adapter - socketPath:** Path-only request URLs (e.g. `'/foo'`) now work again with `config.socketPath`, fixing the `TypeError [ERR_INVALID_URL]` regression introduced in 1.7.4 when `new URL()` was added to the dispatch path. A synthetic `http://localhost` base is supplied only when an own `socketPath` is set, so absolute URLs, non-socket requests, and prototype-polluted `socketPath` values are unaffected. (**#6611**)
-## Bug Fixes
+## Documentation
-- **URL Validation:** Reject malformed `http:` and `https:` URLs that omit `//` before adapter URL normalization, returning `ERR_INVALID_URL` instead of silently normalizing invalid input. (**#10900**, closes **#7315**)
-- **Types:** Add the missing readonly `name: 'CanceledError'` declaration to CommonJS `CanceledError` typings to match the ESM declarations. (**#10922**)
-- **Config Merge:** Added `transitional.validateStatusUndefinedResolves` (default `true`) so applications can opt into treating explicit `validateStatus: undefined` like an omitted option by setting it to `false`. `validateStatus: null` still accepts every response status. (**#10899**, closes **#6688**)
+- **Request data defaults:** Clarified that `data` is request-specific and is not inherited or deep-merged from global or instance defaults. Shared body fields should be added with a request interceptor or `transformRequest`, scoped carefully to avoid sending sensitive values to unintended endpoints.
## Release Tracking
-- ESM/CJS typings are updated for `transitional.validateStatusUndefinedResolves`; README/docs updates are tracked in `PRE_RELEASE_DOCS.md` for release preparation.
+- **Proxy Agent Streams:** Guarded Node HTTP adapter TCP keep-alive setup so proxy agents that return generic Duplex streams do not throw when `setKeepAlive` is unavailable. (**#10917**, closes **#10908**)
diff --git PRE_RELEASE_DOCS.md PRE_RELEASE_DOCS.md
index c087de332c..293c5fe25d 100644
--- PRE_RELEASE_DOCS.md
+++ PRE_RELEASE_DOCS.md
@@ -19,51 +19,3 @@ Do not store raw diffs or line-number-only instructions here; prefer stable sect
- **Notes:** Constraints, release-only wording, translation follow-up, etc.
## Unreleased
-
-### malformed HTTP URL rejection
-
-- **Change:** Note that malformed `http:` and `https:` URLs missing `//` are rejected before adapter normalization.
-- **Source:** `PRE_RELEASE_CHANGELOG.md` Bug Fixes, #10900, closes #7315.
-- **Status:** Skipped.
-- **Docs targets:** None beyond release notes.
-- **Required content:** No API documentation update is needed because this changes handling for invalid URL input without adding or changing request config, types, or public APIs. The release note should mention that axios now throws `AxiosError` with `ERR_INVALID_URL` for malformed HTTP(S) URLs such as `https:example.com` or `http:/example.com` instead of allowing platform URL normalization.
-- **Examples:** None.
-- **Notes:** Treat as a bug/security-hardening release note, not a request-config documentation change.
-
-### sensitiveHeaders request config
-
-- **Change:** Document the Node.js `sensitiveHeaders` request config option for stripping custom secret headers from cross-origin redirects.
-- **Source:** `PRE_RELEASE_CHANGELOG.md` Security Fixes, #10892.
-- **Status:** Pending.
-- **Docs targets:** `docs/pages/misc/security.md`; `docs/pages/advanced/request-config.md`; README request config section if it lists all config options; translated docs after English docs are finalized.
-- **Required content:** Explain that `sensitiveHeaders` is an optional array of custom secret-bearing header names. Matching is case-insensitive. The Node.js HTTP adapter removes matching headers only when following a redirect to a different origin. Same-origin redirects keep these headers. If `maxRedirects` is `0`, axios does not follow redirects and `sensitiveHeaders` is not used. Mention common custom authentication headers such as `X-API-Key`.
-- **Examples:** Include this request example.
-
-```js
-axios.get('https://api.example.com/users', {
- headers: { 'X-API-Key': 'secret' },
- sensitiveHeaders: ['X-API-Key']
-});
-```
-
-- **Notes:** Add a security page row linking to the request-config section and add a `sensitiveHeaders` request-config entry marked Node.js only.
-
-### validateStatus undefined transitional option
-
-- **Change:** Document `transitional.validateStatusUndefinedResolves` for the `validateStatus: undefined` merge behavior.
-- **Source:** `PRE_RELEASE_CHANGELOG.md` Bug Fixes, #10899, closes #6688.
-- **Status:** Pending.
-- **Docs targets:** README request config section; `docs/pages/advanced/request-config.md` `validateStatus` section and request config example; translated request-config docs after English docs are finalized.
-- **Required content:** Explain that `validateStatus: undefined` keeps legacy behavior by default and resolves every response status because `transitional.validateStatusUndefinedResolves` defaults to `true`. Explain that setting `transitional.validateStatusUndefinedResolves` to `false` makes explicit `validateStatus: undefined` behave like the option was omitted, so axios uses the configured/default validator and rejects non-2xx responses by default. Mention that `validateStatus: null` still accepts every response status, and users who disable the transitional behavior should use `null` or `() => true` when they intentionally want all statuses to resolve.
-- **Examples:** Include a short opt-in example.
-
-```js
-axios.get('/user/12345', {
- validateStatus: undefined,
- transitional: {
- validateStatusUndefinedResolves: false
- }
-});
-```
-
-- **Notes:** This is release-prep documentation only; do not update README or docs pages in the feature/fix PR.
diff --git README.md README.md
index b0248d921d..7c3fa71a0b 100644
--- README.md
+++ README.md
@@ -298,7 +298,6 @@
</tr>
</table>
-
<!--<div>marker</div>-->
<br><br>
@@ -430,6 +429,12 @@ Using bun:
$ bun add axios
```
+Using Deno:
+
+```bash
+$ deno add axios
+```
+
Once the package is installed, import it with `import` or `require`:
```js
@@ -513,16 +518,16 @@ axios
// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
try {
-// Example: GET request with query parameters
-const response = await axios.get('/user', {
- params: {
- ID: 12345
- }
-});
+ // Example: GET request with query parameters
+ const response = await axios.get('/user', {
+ params: {
+ ID: 12345,
+ },
+ });
-// Using the `params` option improves readability and automatically formats query strings
+ // Using the `params` option improves readability and automatically formats query strings
-console.log(response);
+ console.log(response);
} catch (error) {
console.error(error);
}
@@ -770,6 +775,8 @@ These config options are available for requests. Only `url` is required. Request
// `data` is the data to be sent as the request body
// Only applicable for request methods 'PUT', 'POST', 'DELETE', and 'PATCH'
+ // `data` is request-specific: axios does not inherit or deep-merge it from defaults.
+ // To add shared body fields, use a request interceptor or transformRequest.
// When no `transformRequest` is set, it must be of one of the following types:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - Browser only: FormData, File, Blob
@@ -891,8 +898,11 @@ These config options are available for requests. Only `url` is required. Request
redact: ['authorization', 'password'],
// `validateStatus` defines whether to resolve or reject the promise for a given
- // HTTP response status code. If `validateStatus` returns `true` (or is set to `null`
- // or `undefined`), Axios resolves the promise; otherwise, Axios rejects it.
+ // HTTP response status code. If `validateStatus` returns `true` or is set to
+ // `null`, Axios resolves the promise; otherwise, Axios rejects it.
+ // Explicit `validateStatus: undefined` resolves every status by default for
+ // backward compatibility. Set `transitional.validateStatusUndefinedResolves`
+ // to `false` to make explicit `undefined` behave as if this option was omitted.
validateStatus: function (status) {
return status >= 200 && status < 300; // default
},
@@ -902,9 +912,9 @@ These config options are available for requests. Only `url` is required. Request
maxRedirects: 21, // default
// `sensitiveHeaders` (Node only option) lists custom secret-bearing headers
- // to remove from cross-origin redirects. Matching is case-insensitive.
- // Same-origin redirects keep these headers. If `maxRedirects` is 0, this
- // option is not used.
+ // (such as `X-API-Key`) to remove from cross-origin redirects. Matching is
+ // case-insensitive. Same-origin redirects keep these headers. If
+ // `maxRedirects` is 0, this option is not used.
sensitiveHeaders: ['X-API-Key'],
// `beforeRedirect` defines a function that Axios calls before redirect.
@@ -966,6 +976,12 @@ These config options are available for requests. Only `url` is required. Request
// for your proxy configuration, you can also define a `no_proxy` environment
// variable as a comma-separated list of domains that should not be proxied.
// Use `false` to disable proxies, ignoring environment variables.
+ // On Node.js versions with native environment proxy support, axios defers
+ // environment proxy handling to Node when the selected agent has `proxyEnv`
+ // enabled, including processes started with `NODE_USE_ENV_PROXY=1`,
+ // `--use-env-proxy`, or `NODE_OPTIONS=--use-env-proxy`. Custom agents without
+ // `proxyEnv` continue to use axios environment proxy resolution. Explicit
+ // `proxy` config is still handled by axios.
// `auth` indicates that HTTP Basic auth should be used to connect to the proxy, and
// supplies credentials.
// For `http://` targets, axios sends the request to the proxy in
@@ -1042,6 +1058,11 @@ These config options are available for requests. Only `url` is required. Request
// throw ETIMEDOUT error instead of generic ECONNABORTED on request timeouts
clarifyTimeoutError: false,
+ // keep explicit `validateStatus: undefined` resolving every response status
+ // for backward compatibility. Set to false to make explicit undefined behave
+ // as if validateStatus was omitted.
+ validateStatusUndefinedResolves: true,
+
// advertise `zstd` in the default Accept-Encoding header when the current
// Node.js runtime supports zstd decompression. Axios still decompresses
// zstd responses when support exists and `decompress` is true.
@@ -1072,6 +1093,15 @@ These config options are available for requests. Only `url` is required. Request
}
```
+For custom secret-bearing headers in Node.js, list them in `sensitiveHeaders` so Axios removes them when following a redirect to another origin:
+
+```js
+axios.get('https://api.example.com/users', {
+ headers: { 'X-API-Key': 'secret' },
+ sensitiveHeaders: ['X-API-Key'],
+});
+```
+
### Strict RFC 3986 percent-encoding for query params
By default, axios decodes `%3A`, `%24`, `%2C` and `%20` back to `:`, `$`, `,` and `+` for readability (the `+` follows the `application/x-www-form-urlencoded` convention for spaces in query strings). These characters are valid in a query component under [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.4), so the default output is correct, but some backends require strict percent-encoding and reject the readable form.
@@ -1082,12 +1112,12 @@ Override the default encoder via `paramsSerializer.encode`:
// Per-request: emit strict RFC 3986 percent-encoding for query values
axios.get('/foo', {
params: { filter: JSON.stringify({ startedAt: '2026-01-23' }) },
- paramsSerializer: { encode: encodeURIComponent }
+ paramsSerializer: { encode: encodeURIComponent },
});
// Or set it on the instance defaults
const client = axios.create({
- paramsSerializer: { encode: encodeURIComponent }
+ paramsSerializer: { encode: encodeURIComponent },
});
```
@@ -1176,6 +1206,8 @@ instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
Axios merges config in this order: library defaults from [lib/defaults/index.js](https://github.com/axios/axios/blob/main/lib/defaults/index.js#L49), the instance `defaults` property, and the request `config` argument. Later values take precedence over earlier ones.
+Some options are request-specific and are only taken from the request `config`. `data` is one of those options: axios does not inherit or deep-merge request bodies from global or instance defaults. If every request needs shared body fields, add them with a request interceptor or `transformRequest`, and scope that logic carefully so sensitive values are not sent to the wrong endpoint.
+
```js
// Create an instance using the config defaults provided by the library
// At this point the timeout config value is `0` as is the default for the library
@@ -1369,7 +1401,7 @@ These are the internal Axios error codes:
| ERR_INVALID_URL | Invalid URL provided for axios request. |
| ECONNABORTED | Typically indicates that the request has been timed out (unless `transitional.clarifyTimeoutError` is set) or aborted by the browser or its plugin. |
| ERR_CANCELED | The user explicitly canceled the request with an AbortSignal or CancelToken. |
-| ETIMEDOUT | Request timed out after exceeding the configured Axios timeout. Set `transitional.clarifyTimeoutError` to `true`; otherwise Axios throws a generic `ECONNABORTED` error. |
+| ETIMEDOUT | Request timed out after exceeding the configured Axios timeout. Set `transitional.clarifyTimeoutError` to `true`; otherwise Axios throws a generic `ECONNABORTED` error. |
| ERR_NETWORK | Network-related issue. In the browser, this error can also be caused by a [CORS](https://developer.mozilla.org/ru/docs/Web/HTTP/Guides/CORS) or [Mixed Content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content) policy violation. The browser does not allow the JS code to clarify the real reason for the error caused by security issues, so please check the console. |
| ERR_FR_TOO_MANY_REDIRECTS | Request exceeded the configured maximum number of redirects. |
| ERR_BAD_RESPONSE | Response cannot be parsed properly or is in an unexpected format. Usually related to a response with `5xx` status code. |
@@ -1410,6 +1442,19 @@ axios.get('/user/12345', {
});
```
+By default, explicit `validateStatus: undefined` keeps legacy behavior and resolves every response status because `transitional.validateStatusUndefinedResolves` defaults to `true`. Set it to `false` to make explicit `validateStatus: undefined` behave like the option was omitted, so Axios uses the configured/default validator and rejects non-2xx responses by default.
+
+`validateStatus: null` still accepts every response status. If you disable the transitional behavior and intentionally want all statuses to resolve, use `null` or `() => true`.
+
+```js
+axios.get('/user/12345', {
+ validateStatus: undefined,
+ transitional: {
+ validateStatusUndefinedResolves: false,
+ },
+});
+```
+
Use `toJSON` to get more information about the HTTP error.
```js
@@ -1421,12 +1466,14 @@ axios.get('/user/12345').catch(function (error) {
To avoid logging secrets from `error.config`, pass a `redact` array in the request config. Matching config keys are masked case-insensitively at any depth when `AxiosError#toJSON()` is called.
```js
-axios.get('/user/12345', {
- headers: { Authorization: 'Bearer token' },
- redact: ['authorization']
-}).catch(function (error) {
- console.log(error.toJSON().config.headers.Authorization); // [REDACTED ****]
-});
+axios
+ .get('/user/12345', {
+ headers: { Authorization: 'Bearer token' },
+ redact: ['authorization'],
+ })
+ .catch(function (error) {
+ console.log(error.toJSON().config.headers.Authorization); // [REDACTED ****]
+ });
```
## Handling timeouts
@@ -1536,11 +1583,44 @@ axios.get('/user/12345', {
cancel();
```
+`CancelToken` also exposes low-level helpers for legacy integrations:
+
+```js
+const source = axios.CancelToken.source();
+
+const listener = (cancel) => {
+ console.log(cancel.message);
+};
+
+source.token.subscribe(listener);
+
+const signal = source.token.toAbortSignal();
+// Pass `signal` to APIs that accept AbortSignal.
+
+source.cancel('Operation canceled by the user.');
+source.token.unsubscribe(listener);
+```
+
+Canceled requests reject with `axios.CanceledError`. The legacy `axios.Cancel` export is an alias of `axios.CanceledError`, and cancellation errors include `__CANCEL__` for `axios.isCancel` compatibility.
+
> Note: You can cancel several requests with the same cancel token or abort controller.
> If a cancellation token is already cancelled when an Axios request starts, Axios cancels the request immediately without making a real request.
> During the transition period, you can use both cancellation APIs, even for the same request:
+```js
+const controller = new AbortController();
+const source = axios.CancelToken.source();
+
+axios.get('/user/12345', {
+ cancelToken: source.token,
+ signal: controller.signal,
+});
+
+controller.abort();
+source.cancel('Operation canceled by the user.');
+```
+
## Using `application/x-www-form-urlencoded` format
### URLSearchParams
@@ -1742,6 +1822,9 @@ FormData serializer supports additional options via `config.formSerializer: obje
input object exceeds this depth, an `AxiosError` with `code: 'ERR_FORM_DATA_DEPTH_EXCEEDED'` is
thrown instead of overflowing the call stack. This protects server applications from DoS
attacks via deeply nested payloads. Set to `Infinity` to disable the limit and restore pre-fix behaviour.
+- `Blob: typeof Blob` - Blob constructor used when converting ArrayBuffer-like values for spec-compliant
+ `FormData`. Override it only for runtimes that provide a compatible `Blob` constructor under a
+ different binding.
```js
// Raise the limit for a schema that genuinely nests deeper than 100 levels:
@@ -2068,6 +2151,7 @@ console.log(headers);
set(headerName, value: Axios, rewrite?: boolean);
set(headerName, value, rewrite?: (this: AxiosHeaders, value: string, name: string, headers: RawAxiosHeaders) => boolean);
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean);
+set(headers?: Iterable<[string, AxiosHeaderValue]>, rewrite?: boolean);
```
The `rewrite` argument controls the overwriting behavior:
@@ -2080,6 +2164,19 @@ The option can also accept a user-defined function that determines whether to ov
Empty or whitespace-only header names are ignored.
+Iterable key/value pairs, such as a `Map`, are accepted:
+
+```js
+const headers = new AxiosHeaders();
+
+headers.set(
+ new Map([
+ ['X-Trace-Id', 'abc123'],
+ ['Accept', 'application/json'],
+ ])
+);
+```
+
Returns `this`.
### AxiosHeaders#get(header)
@@ -2200,6 +2297,14 @@ toJSON(asStrings?: false): Record<string, string | string[]>;
Resolves all internal header values into a new null prototype object.
Set `asStrings` to true to resolve arrays as a string containing all elements, separated by commas.
+### AxiosHeaders#toString()
+
+```
+toString(): string;
+```
+
+Returns the headers as a CRLF-free HTTP header block, one `name: value` pair per line.
+
### AxiosHeaders.from(thing?)
```
diff --git index.d.cts index.d.cts
index 2996ce1409..626605c1f8 100644
--- index.d.cts
+++ index.d.cts
@@ -50,6 +50,7 @@ declare class AxiosHeaders {
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
set(headers?: axios.RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
+ set(headers?: Iterable<[string, axios.AxiosHeaderValue]>, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): axios.AxiosHeaderValue;
@@ -119,6 +120,8 @@ declare class AxiosHeaders {
getSetCookie(): string[];
+ toString(): string;
+
[Symbol.iterator](): IterableIterator<[string, axios.AxiosHeaderValue]>;
}
@@ -165,7 +168,9 @@ declare class AxiosError<T = unknown, D = any> extends Error {
}
declare class CanceledError<T> extends AxiosError<T> {
+ constructor(message?: string, config?: axios.InternalAxiosRequestConfig, request?: any);
readonly name: 'CanceledError';
+ __CANCEL__?: boolean;
}
declare class Axios {
@@ -296,6 +301,12 @@ declare enum HttpStatusCode {
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
+ WebServerIsDown = 521,
+ ConnectionTimedOut = 522,
+ OriginIsUnreachable = 523,
+ TimeoutOccurred = 524,
+ SslHandshakeFailed = 525,
+ InvalidSslCertificate = 526,
}
type InternalAxiosError<T = unknown, D = any> = AxiosError<T, D>;
@@ -428,6 +439,8 @@ declare namespace axios {
dots?: boolean;
metaTokens?: boolean;
indexes?: boolean | null;
+ maxDepth?: number;
+ Blob?: { new (...args: any[]): any };
}
// tslint:disable-next-line
@@ -625,6 +638,9 @@ declare namespace axios {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
+ subscribe(listener: (cancel: Cancel | any) => void): void;
+ unsubscribe(listener: (cancel: Cancel | any) => void): void;
+ toAbortSignal(): AbortSignal;
}
interface CancelTokenSource {
@@ -691,7 +707,7 @@ declare namespace axios {
}
interface AxiosStatic extends AxiosInstance {
- Cancel: CancelStatic;
+ Cancel: typeof CanceledError;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
diff --git index.d.ts index.d.ts
index e9392b41e1..d63aa4fe3a 100644
--- index.d.ts
+++ index.d.ts
@@ -31,6 +31,7 @@ export class AxiosHeaders {
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
+ set(headers?: Iterable<[string, AxiosHeaderValue]>, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): AxiosHeaderValue;
@@ -91,6 +92,8 @@ export class AxiosHeaders {
getSetCookie(): string[];
+ toString(): string;
+
[Symbol.iterator](): IterableIterator<[string, AxiosHeaderValue]>;
}
@@ -233,6 +236,12 @@ export enum HttpStatusCode {
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
+ WebServerIsDown = 521,
+ ConnectionTimedOut = 522,
+ OriginIsUnreachable = 523,
+ TimeoutOccurred = 524,
+ SslHandshakeFailed = 525,
+ InvalidSslCertificate = 526,
}
type UppercaseMethod =
@@ -315,6 +324,8 @@ export interface SerializerOptions {
dots?: boolean;
metaTokens?: boolean;
indexes?: boolean | null;
+ maxDepth?: number;
+ Blob?: { new (...args: any[]): any };
}
// tslint:disable-next-line
@@ -538,7 +549,9 @@ export class AxiosError<T = unknown, D = any> extends Error {
}
export class CanceledError<T> extends AxiosError<T> {
+ constructor(message?: string, config?: InternalAxiosRequestConfig, request?: any);
readonly name: 'CanceledError';
+ __CANCEL__?: boolean;
}
export type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
@@ -564,6 +577,9 @@ export interface CancelToken {
promise: Promise<Cancel>;
reason?: Cancel;
throwIfRequested(): void;
+ subscribe(listener: (cancel: Cancel | any) => void): void;
+ unsubscribe(listener: (cancel: Cancel | any) => void): void;
+ toAbortSignal(): AbortSignal;
}
export interface CancelTokenSource {
@@ -716,7 +732,7 @@ export function mergeConfig<D = any>(
export function create(config?: CreateAxiosDefaults): AxiosInstance;
export interface AxiosStatic extends AxiosInstance {
- Cancel: CancelStatic;
+ Cancel: typeof CanceledError;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
diff --git lib/adapters/adapters.js lib/adapters/adapters.js
index 68f675f356..aa5da5e7e5 100644
--- lib/adapters/adapters.js
+++ lib/adapters/adapters.js
@@ -107,7 +107,7 @@ function getAdapter(adapters, config) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request ` + s,
- 'ERR_NOT_SUPPORT'
+ AxiosError.ERR_NOT_SUPPORT
);
}
diff --git lib/adapters/fetch.js lib/adapters/fetch.js
index 9b031015ac..1a2bfd5cba 100644
--- lib/adapters/fetch.js
+++ lib/adapters/fetch.js
@@ -557,7 +557,17 @@ const factory = (env) => {
const canceledError = composedSignal.reason;
canceledError.config = config;
request && (canceledError.request = request);
- err !== canceledError && (canceledError.cause = err);
+ if (err !== canceledError) {
+ // Non-enumerable to match native Error `cause` semantics so loggers
+ // don't recurse into circular fetch internals (see #7205).
+ Object.defineProperty(canceledError, 'cause', {
+ __proto__: null,
+ value: err,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ }
throw canceledError;
}
@@ -579,18 +589,23 @@ const factory = (env) => {
}
if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
- throw Object.assign(
- new AxiosError(
- 'Network Error',
- AxiosError.ERR_NETWORK,
- config,
- request,
- err && err.response
- ),
- {
- cause: err.cause || err,
- }
+ const networkError = new AxiosError(
+ 'Network Error',
+ AxiosError.ERR_NETWORK,
+ config,
+ request,
+ err && err.response
);
+ // Non-enumerable to match native Error `cause` semantics so loggers
+ // don't recurse into circular fetch internals (see #7205).
+ Object.defineProperty(networkError, 'cause', {
+ __proto__: null,
+ value: err.cause || err,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ throw networkError;
}
throw AxiosError.from(err, err && err.code, config, request, err && err.response);
diff --git lib/adapters/http.js lib/adapters/http.js
index b95795b3b9..c016fc0015 100755
--- lib/adapters/http.js
+++ lib/adapters/http.js
@@ -89,6 +89,53 @@ const kAxiosInstalledTunnel = Symbol('axios.http.installedTunnel');
// so unbounded growth is not a concern in practice.
const tunnelingAgentCache = new Map();
const tunnelingAgentCacheUser = new WeakMap();
+// Minimum minor versions where Node's HTTP Agent supports native proxyEnv
+// handling. Checking the selected agent below also covers startup modes such
+// as NODE_OPTIONS=--use-env-proxy and --no-use-env-proxy precedence.
+const NODE_NATIVE_ENV_PROXY_SUPPORT = {
+ 22: 21,
+ 24: 5,
+};
+
+function isNodeNativeEnvProxySupported(nodeVersion = process.versions && process.versions.node) {
+ if (!nodeVersion) {
+ return false;
+ }
+
+ const [major, minor] = nodeVersion.split('.').map((part) => Number(part));
+
+ if (!Number.isInteger(major) || !Number.isInteger(minor)) {
+ return false;
+ }
+
+ if (major > 24) {
+ return true;
+ }
+
+ return (
+ NODE_NATIVE_ENV_PROXY_SUPPORT[major] != null && minor >= NODE_NATIVE_ENV_PROXY_SUPPORT[major]
+ );
+}
+
+function isNodeEnvProxyEnabled(agent, nodeVersion = process.versions && process.versions.node) {
+ if (!isNodeNativeEnvProxySupported(nodeVersion)) {
+ return false;
+ }
+
+ const agentOptions = agent && agent.options;
+
+ return Boolean(
+ agentOptions &&
+ utils.hasOwnProp(agentOptions, 'proxyEnv') &&
+ agentOptions.proxyEnv != null
+ );
+}
+
+function getProxyEnvAgent(options, configHttpAgent, configHttpsAgent) {
+ return isHttps.test(options.protocol)
+ ? (configHttpsAgent || https.globalAgent)
+ : (configHttpAgent || http.globalAgent);
+}
function getTunnelingAgent(agentOptions, userHttpsAgent) {
const key =
@@ -210,9 +257,10 @@ function isSameOriginRedirect(redirectOptions, requestDetails) {
*
* @returns {http.ClientRequestArgs}
*/
-function setProxy(options, configProxy, location, isRedirect, configHttpsAgent) {
+function setProxy(options, configProxy, location, isRedirect, configHttpsAgent, configHttpAgent) {
let proxy = configProxy;
- if (!proxy && proxy !== false) {
+ const proxyEnvAgent = getProxyEnvAgent(options, configHttpAgent, configHttpsAgent);
+ if (!proxy && proxy !== false && !isNodeEnvProxyEnabled(proxyEnvAgent)) {
const proxyUrl = getProxyForUrl(location);
if (proxyUrl) {
if (!shouldBypassProxy(location)) {
@@ -363,7 +411,14 @@ function setProxy(options, configProxy, location, isRedirect, configHttpsAgent)
options.beforeRedirects.proxy = function beforeRedirect(redirectOptions) {
// Configure proxy for redirected request, passing the original config proxy to apply
// the exact same logic as if the redirected request was performed by axios directly.
- setProxy(redirectOptions, configProxy, redirectOptions.href, true, configHttpsAgent);
+ setProxy(
+ redirectOptions,
+ configProxy,
+ redirectOptions.href,
+ true,
+ configHttpsAgent,
+ configHttpAgent
+ );
};
}
@@ -475,10 +530,12 @@ export default isHttpAdapterSupported &&
let httpVersion = own('httpVersion');
if (httpVersion === undefined) httpVersion = 1;
let http2Options = own('http2Options');
- const responseType = own('responseType');
- const responseEncoding = own('responseEncoding');
const httpAgent = own('httpAgent');
const httpsAgent = own('httpsAgent');
+ const configProxy = own('proxy');
+ const responseType = own('responseType');
+ const responseEncoding = own('responseEncoding');
+ const socketPath = own('socketPath');
const method = own('method').toUpperCase();
const maxRedirects = own('maxRedirects');
const maxBodyLength = own('maxBodyLength');
@@ -603,7 +660,14 @@ export default isHttpAdapterSupported &&
// Parse url
const fullPath = buildFullPath(own('baseURL'), own('url'), own('allowAbsoluteUrls'), config);
- const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined);
+ // Unix-socket requests (own socketPath) commonly pass a path-only url
+ // like '/foo'; supply a synthetic base so new URL() can still parse it.
+ // Use the own-property value (not config.socketPath) so a polluted
+ // prototype cannot influence URL base selection.
+ const urlBase = socketPath
+ ? 'http://localhost'
+ : (platform.hasBrowserEnv ? platform.origin : undefined);
+ const parsed = new URL(fullPath, urlBase);
const protocol = parsed.protocol || supportedProtocols[0];
if (protocol === 'data:') {
@@ -810,11 +874,12 @@ export default isHttpAdapterSupported &&
own('paramsSerializer')
).replace(/^\?/, '');
} catch (err) {
- const customErr = new Error(err.message);
- customErr.config = config;
- customErr.url = own('url');
- customErr.exists = true;
- return reject(customErr);
+ return reject(
+ AxiosError.from(err, AxiosError.ERR_BAD_REQUEST, config, null, null, {
+ url: own('url'),
+ exists: true
+ })
+ );
}
headers.set(
@@ -842,7 +907,6 @@ export default isHttpAdapterSupported &&
// cacheable-lookup integration hotfix
!utils.isUndefined(lookup) && (options.lookup = lookup);
- const socketPath = own('socketPath');
if (socketPath) {
if (typeof socketPath !== 'string') {
return reject(
@@ -880,10 +944,11 @@ export default isHttpAdapterSupported &&
options.port = parsed.port;
setProxy(
options,
- own('proxy'),
+ configProxy,
protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path,
false,
- httpsAgent
+ httpsAgent,
+ httpAgent
);
}
let transport;
@@ -979,10 +1044,12 @@ export default isHttpAdapterSupported &&
}
}
+ // Set an explicit maxBodyLength option for transports that inspect it.
+ // When maxBodyLength is -1 (default/unlimited), use Infinity so
+ // follow-redirects does not fall back to its own 10MB default.
if (maxBodyLength > -1) {
options.maxBodyLength = maxBodyLength;
} else {
- // follow-redirects does not skip comparison, so it should always succeed for axios -1 unlimited
options.maxBodyLength = Infinity;
}
@@ -1205,7 +1272,11 @@ export default isHttpAdapterSupported &&
req.on('socket', function handleRequestSocket(socket) {
// default interval of sending ack packet is 1 minute
- socket.setKeepAlive(true, 1000 * 60);
+ // proxy agents (e.g. agent-base) may return a generic Duplex stream
+ // that doesn't have setKeepAlive, so guard before calling
+ if (typeof socket.setKeepAlive === 'function') {
+ socket.setKeepAlive(true, 1000 * 60);
+ }
// Install a single 'error' listener per socket (not per request) to avoid
// accumulating listeners on pooled keep-alive sockets that get reassigned
@@ -1342,4 +1413,5 @@ export default isHttpAdapterSupported &&
};
export const __setProxy = setProxy;
+export const __isNodeEnvProxyEnabled = isNodeEnvProxyEnabled;
export const __isSameOriginRedirect = isSameOriginRedirect;
diff --git lib/adapters/xhr.js lib/adapters/xhr.js
index 256367764f..5432ca97e7 100644
--- lib/adapters/xhr.js
+++ lib/adapters/xhr.js
@@ -218,6 +218,7 @@ export default isXHRAdapterSupported &&
config
)
);
+ done();
return;
}
diff --git lib/core/AxiosError.js lib/core/AxiosError.js
index d492485488..c61eda0e69 100644
--- lib/core/AxiosError.js
+++ lib/core/AxiosError.js
@@ -75,7 +75,19 @@ function redactConfig(config, redactKeys) {
class AxiosError extends Error {
static from(error, code, config, request, response, customProps) {
const axiosError = new AxiosError(error.message, code || error.code, config, request, response);
- axiosError.cause = error;
+ // Match native `Error` `cause` semantics: non-enumerable. The wrapped
+ // error often carries circular internals (sockets, requests, agents), so
+ // an enumerable `cause` makes structured loggers (pino/winston) and any
+ // own-property walk throw "Converting circular structure to JSON".
+ // Regression from #6982; see #7205. `__proto__: null` mirrors the
+ // `message` descriptor below (prototype-pollution-safe descriptor).
+ Object.defineProperty(axiosError, 'cause', {
+ __proto__: null,
+ value: error,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
axiosError.name = error.name;
// Preserve status from the original error if not already set from response
diff --git lib/core/mergeConfig.js lib/core/mergeConfig.js
index a31a9f16fb..8e3a2000fa 100644
--- lib/core/mergeConfig.js
+++ lib/core/mergeConfig.js
@@ -16,6 +16,7 @@ const headersToObject = (thing) => (thing instanceof AxiosHeaders ? { ...thing }
*/
export default function mergeConfig(config1, config2) {
// eslint-disable-next-line no-param-reassign
+ config1 = config1 || {};
config2 = config2 || {};
// Use a null-prototype object so that downstream reads such as `config.auth`
diff --git lib/env/data.js lib/env/data.js
index 6b5a6ceb55..c690166da5 100644
--- lib/env/data.js
+++ lib/env/data.js
@@ -1 +1 @@
-export const VERSION = "1.18.0";
\ No newline at end of file
+export const VERSION = "1.18.1";
\ No newline at end of file
diff --git lib/helpers/AxiosURLSearchParams.js lib/helpers/AxiosURLSearchParams.js
index 57cf16d114..b77b6805d1 100644
--- lib/helpers/AxiosURLSearchParams.js
+++ lib/helpers/AxiosURLSearchParams.js
@@ -46,9 +46,7 @@ prototype.append = function append(name, value) {
prototype.toString = function toString(encoder) {
const _encode = encoder
- ? function (value) {
- return encoder.call(this, value, encode);
- }
+ ? (value) => encoder.call(this, value, encode)
: encode;
return this._pairs
diff --git lib/helpers/buildURL.js lib/helpers/buildURL.js
index c3606f68fc..ba4f5f53d5 100644
--- lib/helpers/buildURL.js
+++ lib/helpers/buildURL.js
@@ -32,6 +32,7 @@ export default function buildURL(url, params, options) {
if (!params) {
return url;
}
+ url = url || '';
const _options = utils.isFunction(options)
? {
diff --git lib/helpers/composeSignals.js lib/helpers/composeSignals.js
index 74e99ed42f..6ea734f3cd 100644
--- lib/helpers/composeSignals.js
+++ lib/helpers/composeSignals.js
@@ -45,7 +45,7 @@ const composeSignals = (signals, timeout) => {
signals = null;
};
- signals.forEach((signal) => signal.addEventListener('abort', onabort));
+ signals.forEach((signal) => signal.addEventListener('abort', onabort, { once: true }));
const { signal } = controller;
diff --git lib/helpers/cookies.js lib/helpers/cookies.js
index 3f0baf2421..c197875f6a 100644
--- lib/helpers/cookies.js
+++ lib/helpers/cookies.js
@@ -40,7 +40,11 @@ export default platform.hasStandardBrowserEnv
const cookie = cookies[i].replace(/^\s+/, '');
const eq = cookie.indexOf('=');
if (eq !== -1 && cookie.slice(0, eq) === name) {
- return decodeURIComponent(cookie.slice(eq + 1));
+ try {
+ return decodeURIComponent(cookie.slice(eq + 1));
+ } catch (e) {
+ return cookie.slice(eq + 1);
+ }
}
}
return null;
diff --git lib/helpers/fromDataURI.js lib/helpers/fromDataURI.js
index 7319588914..81fcc6e005 100644
--- lib/helpers/fromDataURI.js
+++ lib/helpers/fromDataURI.js
@@ -42,14 +42,16 @@ export default function fromDataURI(uri, asBlob, options) {
// RFC 2397 section 3: default mediatype is text/plain;charset=US-ASCII
// Bare `data:,` leaves mime undefined; Blob normalises that to "" per spec.
- let mime;
+ let mime = '';
if (type) {
mime = params ? type + params : type;
} else if (params) {
mime = 'text/plain' + params;
}
- const buffer = Buffer.from(decodeURIComponent(body), encoding);
+ const buffer = encoding === 'base64'
+ ? Buffer.from(body, 'base64')
+ : Buffer.from(decodeURIComponent(body), encoding);
if (asBlob) {
if (!_Blob) {
diff --git lib/helpers/resolveConfig.js lib/helpers/resolveConfig.js
index 8920b632db..77718c2bad 100644
--- lib/helpers/resolveConfig.js
+++ lib/helpers/resolveConfig.js
@@ -1,5 +1,6 @@
import platform from '../platform/index.js';
import utils from '../utils.js';
+import AxiosError from '../core/AxiosError.js';
import isURLSameOrigin from './isURLSameOrigin.js';
import cookies from './cookies.js';
import buildFullPath from '../core/buildFullPath.js';
@@ -15,7 +16,7 @@ function setFormDataHeaders(headers, formHeaders, policy) {
return;
}
- Object.entries(formHeaders).forEach(([key, val]) => {
+ Object.entries(formHeaders || {}).forEach(([key, val]) => {
if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
headers.set(key, val);
}
@@ -65,10 +66,14 @@ function resolveConfig(config) {
const username = utils.getSafeProp(auth, 'username') || '';
const password = utils.getSafeProp(auth, 'password') || '';
- headers.set(
- 'Authorization',
- 'Basic ' + btoa(username + ':' + (password ? encodeUTF8(password) : ''))
- );
+ try {
+ headers.set(
+ 'Authorization',
+ 'Basic ' + btoa(username + ':' + (password ? encodeUTF8(password) : ''))
+ );
+ } catch (e) {
+ throw AxiosError.from(e, AxiosError.ERR_BAD_OPTION_VALUE, config);
+ }
}
if (utils.isFormData(data)) {
diff --git lib/helpers/toFormData.js lib/helpers/toFormData.js
index b63fc2436b..335c1ad87e 100644
--- lib/helpers/toFormData.js
+++ lib/helpers/toFormData.js
@@ -143,7 +143,13 @@ function toFormData(obj, formData, options) {
}
if (utils.isArrayBuffer(value) || utils.isTypedArray(value)) {
- return useBlob && typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value);
+ if (useBlob && typeof _Blob === 'function') {
+ return new _Blob([value]);
+ }
+ if (typeof Buffer !== 'undefined') {
+ return Buffer.from(value);
+ }
+ throw new AxiosError('Blob is not supported. Use a Buffer instead.', AxiosError.ERR_NOT_SUPPORT);
}
return value;
diff --git lib/helpers/validator.js lib/helpers/validator.js
index 077f34d079..a152c8c980 100644
--- lib/helpers/validator.js
+++ lib/helpers/validator.js
@@ -79,7 +79,7 @@ validators.spelling = function spelling(correctSpelling) {
*/
function assertOptions(options, schema, allowUnknown) {
- if (typeof options !== 'object') {
+ if (typeof options !== 'object' || options === null) {
throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE);
}
const keys = Object.keys(options);
diff --git package.json package.json
index ffc848cb8a..27e27846d8 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
{
"name": "axios",
- "version": "1.18.0",
+ "version": "1.18.1",
"description": "Promise based HTTP client for the browser and node.js",
"main": "./dist/node/axios.cjs",
"module": "./index.js",
@@ -87,8 +87,8 @@
"Martti Laine (https://github.com/codeclown)",
"Xianming Zhong (https://github.com/chinesedfan)",
"Shaan Majid (https://github.com/shaanmajid)",
- "Willian Agostini (https://github.com/WillianAgostini)",
"Remco Haszing (https://github.com/remcohaszing)",
+ "Willian Agostini (https://github.com/WillianAgostini)",
"Rikki Gibson (https://github.com/RikkiGibson)"
],
"sideEffects": false,
diff --git tests/browser/cookies.browser.test.js tests/browser/cookies.browser.test.js
index 0baa89c50c..1a3e963b19 100644
--- tests/browser/cookies.browser.test.js
+++ tests/browser/cookies.browser.test.js
@@ -75,6 +75,27 @@ describe('helpers::cookies (vitest browser)', () => {
expect(document.cookie).toBe('foo=bar%20baz%25');
});
+ it('returns raw cookie value when it is not valid URI encoding', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(document, 'cookie');
+
+ Object.defineProperty(document, 'cookie', {
+ configurable: true,
+ get() {
+ return 'foo=bar%';
+ },
+ });
+
+ try {
+ expect(cookies.read('foo')).toBe('bar%');
+ } finally {
+ if (descriptor) {
+ Object.defineProperty(document, 'cookie', descriptor);
+ } else {
+ delete document.cookie;
+ }
+ }
+ });
+
it('matches cookie names exactly even when the name contains regex metacharacters', () => {
// previously cookies.read built a RegExp by interpolating
// the requested name. Metacharacters could match a different cookie or trigger
diff --git tests/browser/requests.browser.test.js tests/browser/requests.browser.test.js
index 06184fcbaf..1b07b338d2 100644
--- tests/browser/requests.browser.test.js
+++ tests/browser/requests.browser.test.js
@@ -533,4 +533,38 @@ describe('requests (vitest browser)', () => {
message: 'Unsupported protocol ftp:',
});
});
+
+ it('should clean up cancellation listeners after unsupported protocol rejection', async () => {
+ const source = axios.CancelToken.source();
+ const controller = new AbortController();
+ let abortListenerCount = 0;
+ const nativeAdd = controller.signal.addEventListener.bind(controller.signal);
+ const nativeRemove = controller.signal.removeEventListener.bind(controller.signal);
+
+ controller.signal.addEventListener = (type, fn, options) => {
+ if (type === 'abort') {
+ abortListenerCount++;
+ }
+ return nativeAdd(type, fn, options);
+ };
+ controller.signal.removeEventListener = (type, fn, options) => {
+ if (type === 'abort') {
+ abortListenerCount--;
+ }
+ return nativeRemove(type, fn, options);
+ };
+
+ await expect(
+ axios.get('ftp:localhost', {
+ adapter: 'xhr',
+ cancelToken: source.token,
+ signal: controller.signal,
+ })
+ ).rejects.toMatchObject({
+ message: 'Unsupported protocol ftp:',
+ });
+
+ expect(source.token._listeners || []).toEqual([]);
+ expect(abortListenerCount).toBe(0);
+ });
});
diff --git a/tests/module/cjs/tests/helpers/cjs-added-types.ts b/tests/module/cjs/tests/helpers/cjs-added-types.ts
new file mode 100644
index 0000000000..644c8dec11
--- /dev/null
+++ tests/module/cjs/tests/helpers/cjs-added-types.ts
@@ -0,0 +1,38 @@
+import axios = require('axios');
+
+const headers = new axios.AxiosHeaders();
+const iterableHeaders: Iterable<[string, axios.AxiosHeaderValue]> = [['x-test', 'ok']];
+headers.set(iterableHeaders);
+const serializedHeaders: string = headers.toString();
+
+const source = axios.CancelToken.source();
+source.token.subscribe((cancel) => {
+ const message: string | undefined = cancel && cancel.message;
+ console.log(message);
+});
+source.token.unsubscribe(() => {});
+const signal: AbortSignal = source.token.toAbortSignal();
+
+const cancel = new axios.CanceledError<{ ok: true }>(
+ 'stop',
+ {} as axios.InternalAxiosRequestConfig,
+ {}
+);
+const cancelFlag: boolean | undefined = cancel.__CANCEL__;
+const cancelCtor: typeof axios.CanceledError = axios.Cancel;
+const cancelFromAlias = new cancelCtor('from alias');
+
+const status = axios.HttpStatusCode.WebServerIsDown;
+
+class CustomBlob {
+ constructor(_parts?: any[]) {}
+}
+
+const serializerOptions: axios.FormSerializerOptions = {
+ maxDepth: 2,
+ Blob: CustomBlob,
+};
+
+axios.toFormData({ file: new Uint8Array([1]) }, undefined, serializerOptions);
+
+console.log(serializedHeaders, signal.aborted, cancelFlag, cancelFromAlias.message, status);
diff --git tests/module/cjs/tests/typings.module.test.cjs tests/module/cjs/tests/typings.module.test.cjs
index d5d360bc0a..f78a3cf501 100644
--- tests/module/cjs/tests/typings.module.test.cjs
+++ tests/module/cjs/tests/typings.module.test.cjs
@@ -36,4 +36,15 @@ describe('module cjs typings compatibility', () => {
cleanupTempFixture(fixturePath);
}
});
+
+ it('type-checks additive commonjs public typings', () => {
+ const sourcePath = path.join(repoRoot, 'tests/module/cjs/tests/helpers/cjs-added-types.ts');
+ const fixturePath = createTempFixture(suiteRoot, 'typings-cjs-added', sourcePath, tsconfig);
+
+ try {
+ runCommand('node', [tscBin, '--noEmit', '-p', 'tsconfig.json'], { cwd: fixturePath });
+ } finally {
+ cleanupTempFixture(fixturePath);
+ }
+ });
});
diff --git a/tests/module/esm/tests/helpers/esm-added-types.ts b/tests/module/esm/tests/helpers/esm-added-types.ts
new file mode 100644
index 0000000000..db1f581bce
--- /dev/null
+++ tests/module/esm/tests/helpers/esm-added-types.ts
@@ -0,0 +1,46 @@
+import axios, {
+ AxiosHeaders,
+ CanceledError,
+ HttpStatusCode,
+ toFormData,
+ type AxiosHeaderValue,
+ type FormSerializerOptions,
+ type InternalAxiosRequestConfig,
+} from 'axios';
+
+const headers = new AxiosHeaders();
+const iterableHeaders: Iterable<[string, AxiosHeaderValue]> = [['x-test', 'ok']];
+headers.set(iterableHeaders);
+const serializedHeaders: string = headers.toString();
+
+const source = axios.CancelToken.source();
+source.token.subscribe((cancel) => {
+ const message: string | undefined = cancel && cancel.message;
+ console.log(message);
+});
+source.token.unsubscribe(() => {});
+const signal: AbortSignal = source.token.toAbortSignal();
+
+const cancel = new CanceledError<{ ok: true }>(
+ 'stop',
+ {} as InternalAxiosRequestConfig,
+ {}
+);
+const cancelFlag: boolean | undefined = cancel.__CANCEL__;
+const cancelCtor: typeof CanceledError = axios.Cancel;
+const cancelFromAlias = new cancelCtor('from alias');
+
+const status: HttpStatusCode = HttpStatusCode.WebServerIsDown;
+
+class CustomBlob {
+ constructor(_parts?: any[]) {}
+}
+
+const serializerOptions: FormSerializerOptions = {
+ maxDepth: 2,
+ Blob: CustomBlob,
+};
+
+toFormData({ file: new Uint8Array([1]) }, undefined, serializerOptions);
+
+console.log(serializedHeaders, signal.aborted, cancelFlag, cancelFromAlias.message, status);
diff --git tests/module/esm/tests/typings.module.test.js tests/module/esm/tests/typings.module.test.js
index 84a2a060e0..7126b45b8b 100644
--- tests/module/esm/tests/typings.module.test.js
+++ tests/module/esm/tests/typings.module.test.js
@@ -28,4 +28,17 @@ describe('module esm typings compatibility', () => {
cleanupTempFixture(fixturePath);
}
});
+
+ it('type-checks additive esm public typings', () => {
+ const sourcePath = path.join(repoRoot, 'tests/module/esm/tests/helpers/esm-added-types.ts');
+ const fixturePath = createTempFixture(suiteRoot, 'typings-esm-added', sourcePath, tsconfig, {
+ type: 'module',
+ });
+
+ try {
+ runCommand('node', [tscBin, '--noEmit', '-p', 'tsconfig.json'], { cwd: fixturePath });
+ } finally {
+ cleanupTempFixture(fixturePath);
+ }
+ });
});
diff --git tests/unit/adapters/adapters.test.js tests/unit/adapters/adapters.test.js
index 32af566dc4..cc761417db 100644
--- tests/unit/adapters/adapters.test.js
+++ tests/unit/adapters/adapters.test.js
@@ -1,6 +1,7 @@
import { beforeEach, describe, it } from 'vitest';
import assert from 'assert';
import adapters from '../../../lib/adapters/adapters.js';
+import AxiosError from '../../../lib/core/AxiosError.js';
describe('adapters', () => {
const store = { ...adapters.adapters };
@@ -31,7 +32,15 @@ describe('adapters', () => {
it('should detect adapter unsupported status', () => {
adapters.adapters.testadapter = false;
- assert.throws(() => adapters.getAdapter('testAdapter'), /is not supported by the environment/);
+ assert.throws(
+ () => adapters.getAdapter('testAdapter'),
+ (err) => {
+ assert.ok(err instanceof AxiosError);
+ assert.strictEqual(err.code, AxiosError.ERR_NOT_SUPPORT);
+ assert.match(err.message, /is not supported by the environment/);
+ return true;
+ }
+ );
});
it('should pick suitable adapter from the list', () => {
diff --git tests/unit/adapters/fetch.test.js tests/unit/adapters/fetch.test.js
index 53a9845fb1..557a7bbb26 100644
--- tests/unit/adapters/fetch.test.js
+++ tests/unit/adapters/fetch.test.js
@@ -844,6 +844,45 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
);
});
+ it('sets a non-enumerable cause on canceled fetch errors so loggers do not throw (#7205)', async () => {
+ const underlying = new Error('abort internals');
+ const socket = { name: 'Socket' };
+ socket.self = socket;
+ underlying.socket = socket;
+
+ const abortingFetch = (url, init) => {
+ const signal = getFetchSignal(url, init);
+
+ return new Promise((_resolve, reject) => {
+ const onAbort = () => {
+ signal.removeEventListener('abort', onAbort);
+ reject(underlying);
+ };
+
+ if (signal.aborted) return onAbort();
+ signal.addEventListener('abort', onAbort);
+ });
+ };
+
+ const controller = new AbortController();
+
+ const request = fetchAxios.get('/', {
+ signal: controller.signal,
+ env: { fetch: abortingFetch },
+ });
+
+ controller.abort();
+
+ const err = await request.catch((e) => e);
+
+ assert.strictEqual(err.name, 'CanceledError');
+ assert.strictEqual(err.code, 'ERR_CANCELED');
+ assert.strictEqual(err.cause, underlying);
+ assert.strictEqual(Object.getOwnPropertyDescriptor(err, 'cause').enumerable, false);
+ assert.ok(!Object.keys(err).includes('cause'));
+ assert.doesNotThrow(() => JSON.stringify(Object.fromEntries(Object.entries(err))));
+ });
+
// Timing-sensitive: a 50ms abort race observed by a fake fetch can flake
// under CI runner load even though the production code is fine. Retry as
// a backstop.
@@ -945,9 +984,32 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
} catch (err) {
assert.strictEqual(String(err), 'AxiosError: Network Error');
assert.strictEqual(err.cause && err.cause.code, 'ENOTFOUND');
+ // `cause` must be non-enumerable so own-property serialization is safe (#7205).
+ assert.strictEqual(Object.getOwnPropertyDescriptor(err, 'cause').enumerable, false);
}
});
+ it('sets a non-enumerable cause on network errors so loggers do not throw (#7205)', async () => {
+ // Underlying error carrying a circular reference, like a Node socket.
+ const underlying = new Error('connect ECONNREFUSED');
+ underlying.code = 'ECONNREFUSED';
+ const socket = { name: 'Socket' };
+ socket.self = socket; // circular
+ underlying.socket = socket;
+
+ const failingFetch = () =>
+ Promise.reject(Object.assign(new TypeError('fetch failed'), { cause: underlying }));
+
+ const err = await fetchAxios.get('/', { env: { fetch: failingFetch } }).catch((e) => e);
+
+ assert.strictEqual(err.code, 'ERR_NETWORK');
+ assert.strictEqual(err.cause, underlying); // still accessible for debugging
+ assert.strictEqual(Object.getOwnPropertyDescriptor(err, 'cause').enumerable, false);
+ assert.ok(!Object.keys(err).includes('cause'));
+ // pino/winston-style own-property walk must not throw on the circular cause.
+ assert.doesNotThrow(() => JSON.stringify(Object.fromEntries(Object.entries(err))));
+ });
+
it('should get response headers', async () => {
const server = await startHTTPServer(
(req, res) => {
diff --git tests/unit/adapters/http.test.js tests/unit/adapters/http.test.js
index 6aee7d1635..3de697880c 100644
--- tests/unit/adapters/http.test.js
+++ tests/unit/adapters/http.test.js
@@ -10,7 +10,11 @@ import {
} from '../../setup/server.js';
import axios from '../../../index.js';
import AxiosError from '../../../lib/core/AxiosError.js';
-import httpAdapter, { __isSameOriginRedirect, __setProxy } from '../../../lib/adapters/http.js';
+import httpAdapter, {
+ __isNodeEnvProxyEnabled,
+ __isSameOriginRedirect,
+ __setProxy,
+} from '../../../lib/adapters/http.js';
import HttpsProxyAgent from 'https-proxy-agent';
import http from 'http';
import https from 'https';
@@ -1898,7 +1902,10 @@ describe('supports http with nodejs', () => {
},
}),
(error) => {
- assert.deepStrictEqual(error.exists, true);
+ assert.ok(error instanceof AxiosError, 'error should be an AxiosError');
+ assert.strictEqual(error.code, AxiosError.ERR_BAD_REQUEST);
+ assert.strictEqual(error.exists, true);
+ assert.strictEqual(error.url, `http://localhost:${server.address().port}/`);
return true;
}
);
@@ -2565,6 +2572,169 @@ describe('supports http with nodejs', () => {
}
});
+ it('should detect Node native env proxy support from the selected agent', () => {
+ const nativeProxyAgent = { options: { proxyEnv: { HTTP_PROXY: 'http://proxy.local:9000' } } };
+ const plainAgent = { options: {} };
+
+ assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '22.20.0'), false);
+ assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '22.21.0'), true);
+ assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '24.4.0'), false);
+ assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '24.5.0'), true);
+ assert.strictEqual(__isNodeEnvProxyEnabled(nativeProxyAgent, '25.0.0'), true);
+ assert.strictEqual(__isNodeEnvProxyEnabled(plainAgent, '24.5.0'), false);
+ assert.strictEqual(__isNodeEnvProxyEnabled(undefined, '24.5.0'), false);
+ });
+
+ it('should leave env proxy handling to supported Node versions when the selected agent uses proxyEnv', () => {
+ 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 originalNodeUseEnvProxy = process.env.NODE_USE_ENV_PROXY;
+
+ process.env.NODE_USE_ENV_PROXY = '1';
+ process.env.http_proxy = 'http://proxy.local:9000/';
+ process.env.HTTP_PROXY = 'http://proxy.local:9000/';
+ process.env.no_proxy = '';
+ process.env.NO_PROXY = '';
+
+ try {
+ const options = {
+ headers: {},
+ beforeRedirects: {},
+ hostname: 'target.example',
+ host: 'target.example',
+ port: '4000',
+ protocol: 'http:',
+ path: '/resource',
+ };
+ const nativeProxyAgent = { options: { proxyEnv: process.env } };
+
+ __setProxy(
+ options,
+ undefined,
+ 'http://target.example:4000/resource',
+ false,
+ undefined,
+ nativeProxyAgent
+ );
+
+ if (__isNodeEnvProxyEnabled(nativeProxyAgent, process.versions.node)) {
+ assert.strictEqual(options.hostname, 'target.example');
+ assert.strictEqual(options.port, '4000');
+ assert.strictEqual(options.path, '/resource');
+ assert.strictEqual(options.headers.host, undefined);
+ } else {
+ assert.strictEqual(options.hostname, 'proxy.local');
+ assert.strictEqual(options.port, '9000');
+ assert.strictEqual(options.path, 'http://target.example:4000/resource');
+ }
+
+ assert.strictEqual(typeof options.beforeRedirects.proxy, 'function');
+ } finally {
+ 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;
+ }
+
+ if (originalNodeUseEnvProxy === undefined) {
+ delete process.env.NODE_USE_ENV_PROXY;
+ } else {
+ process.env.NODE_USE_ENV_PROXY = originalNodeUseEnvProxy;
+ }
+ }
+ });
+
+ it('should keep axios env proxy handling when the selected agent has no proxyEnv', () => {
+ 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 originalNodeUseEnvProxy = process.env.NODE_USE_ENV_PROXY;
+
+ process.env.NODE_USE_ENV_PROXY = '1';
+ process.env.http_proxy = 'http://proxy.local:9000/';
+ process.env.HTTP_PROXY = 'http://proxy.local:9000/';
+ process.env.no_proxy = '';
+ process.env.NO_PROXY = '';
+
+ try {
+ const options = {
+ headers: {},
+ beforeRedirects: {},
+ hostname: 'target.example',
+ host: 'target.example',
+ port: '4000',
+ protocol: 'http:',
+ path: '/resource',
+ };
+ const plainAgent = { options: {} };
+
+ __setProxy(
+ options,
+ undefined,
+ 'http://target.example:4000/resource',
+ false,
+ undefined,
+ plainAgent
+ );
+
+ assert.strictEqual(options.hostname, 'proxy.local');
+ assert.strictEqual(options.port, '9000');
+ assert.strictEqual(options.path, 'http://target.example:4000/resource');
+ assert.strictEqual(typeof options.beforeRedirects.proxy, 'function');
+ } finally {
+ 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;
+ }
+
+ if (originalNodeUseEnvProxy === undefined) {
+ delete process.env.NODE_USE_ENV_PROXY;
+ } else {
+ process.env.NODE_USE_ENV_PROXY = originalNodeUseEnvProxy;
+ }
+ }
+ });
+
it('should support HTTPS proxy set via env var', async () => {
const originalHttpsProxy = process.env.https_proxy;
const originalHTTPSProxy = process.env.HTTPS_PROXY;
@@ -6169,6 +6339,58 @@ describe('supports http with nodejs', () => {
'second request should be destroyed by its own active socket error'
);
});
+
+ it('should not throw TypeError when a proxy agent stream does not define setKeepAlive (regression #10908)', async () => {
+ // proxy agents (e.g. agent-base) may provide a generic Duplex stream as
+ // the socket; that stream does not define setKeepAlive.
+ const socket = new stream.Duplex({
+ read() {},
+ write(_chunk, _encoding, callback) {
+ callback();
+ },
+ });
+ assert.strictEqual(typeof socket.setKeepAlive, 'undefined');
+
+ 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');
+ }
+ })();
+ },
+ };
+
+ const result = await axios.get('http://example.com/', {
+ transport,
+ maxRedirects: 0,
+ });
+
+ assert.strictEqual(result.status, 200);
+ });
});
describe('redirect listener accumulation', () => {
@@ -6316,9 +6538,10 @@ describe('supports http with nodejs', () => {
: path.join(os.tmpdir(), `${pipe}.sock`);
}
- function startUnixServer(socketPath) {
+ function startUnixServer(socketPath, onRequest) {
return new Promise((resolveStart, rejectStart) => {
const server = http.createServer((req, res) => {
+ onRequest && onRequest(req);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, url: req.url }));
});
@@ -6357,6 +6580,61 @@ describe('supports http with nodejs', () => {
}
});
+ it('accepts a path-only url when socketPath is set (regression #6611)', async () => {
+ const socketPath = makeSocketPath();
+ const server = await startUnixServer(socketPath);
+ try {
+ const res = await axios.get('/echo?q=1', { socketPath });
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.data.ok, true);
+ assert.strictEqual(res.data.url, '/echo?q=1');
+ } finally {
+ await stopUnixServer(server, socketPath);
+ }
+ });
+
+ it('accepts a path-only url when socketPath matches allowedSocketPaths', async () => {
+ const socketPath = makeSocketPath();
+ const server = await startUnixServer(socketPath);
+ try {
+ const res = await axios.get('/echo?q=1', {
+ socketPath,
+ allowedSocketPaths: [socketPath],
+ });
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.data.ok, true);
+ assert.strictEqual(res.data.url, '/echo?q=1');
+ } finally {
+ await stopUnixServer(server, socketPath);
+ }
+ });
+
+ it('ignores a prototype-polluted socketPath (security, regression #6611)', async () => {
+ const socketPath = makeSocketPath();
+ let requestCount = 0;
+ const server = await startUnixServer(socketPath, () => {
+ requestCount += 1;
+ });
+ // Pollute the prototype so `socketPath` is visible via the chain but is
+ // NOT an own property of the request config.
+ Object.prototype.socketPath = socketPath;
+ try {
+ // With no own socketPath, the polluted prototype value must not be
+ // honored: the path-only url gets no synthetic base and the request is
+ // never routed to the (attacker-controlled) socket, so it rejects
+ // instead of silently connecting.
+ await assert.rejects(axios.get('/echo?q=1'), (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, AxiosError.ERR_INVALID_URL);
+ return true;
+ });
+ assert.strictEqual(requestCount, 0);
+ } finally {
+ delete Object.prototype.socketPath;
+ await stopUnixServer(server, socketPath);
+ }
+ });
+
it('allows socketPath when it matches an allowedSocketPaths string', async () => {
const socketPath = makeSocketPath();
const server = await startUnixServer(socketPath);
diff --git tests/unit/core/AxiosError.test.js tests/unit/core/AxiosError.test.js
index 63de026d66..53380dbf69 100644
--- tests/unit/core/AxiosError.test.js
+++ tests/unit/core/AxiosError.test.js
@@ -76,6 +76,55 @@ describe('core::AxiosError', () => {
});
});
+ describe('cause serialization (regression #7205)', () => {
+ // A wrapped low-level error carrying a circular reference, like a Node
+ // socket/request held by network errors.
+ const makeCircularCause = () => {
+ const cause = new Error('socket hang up');
+ cause.code = 'ECONNRESET';
+ const socket = { name: 'Socket' };
+ socket.self = socket; // circular
+ cause.socket = socket;
+ return cause;
+ };
+
+ it('sets `cause` as a non-enumerable own property (native Error parity)', () => {
+ const axiosError = AxiosError.from(new Error('boom'), 'ERR_NETWORK', { url: '/x' });
+ const descriptor = Object.getOwnPropertyDescriptor(axiosError, 'cause');
+
+ expect(descriptor).toBeDefined();
+ expect(descriptor.enumerable).toBe(false);
+ expect(Object.keys(axiosError)).not.toContain('cause');
+ expect('cause' in axiosError).toBe(true); // still discoverable via `in`
+ });
+
+ it('keeps `cause` fully accessible for debugging', () => {
+ const original = makeCircularCause();
+ const axiosError = AxiosError.from(original, 'ERR_NETWORK', { url: '/x' });
+
+ expect(axiosError.cause).toBe(original);
+ expect(axiosError.cause.code).toBe('ECONNRESET');
+ });
+
+ it('does not break structured loggers / own-property serialization', () => {
+ const axiosError = AxiosError.from(makeCircularCause(), 'ERR_NETWORK', { url: '/x' });
+
+ // pino/winston-style: enumerate own enumerable props and serialize.
+ const loggerWalk = () =>
+ JSON.stringify(Object.fromEntries(Object.entries(axiosError)));
+
+ expect(loggerWalk).not.toThrow();
+ expect(() => JSON.stringify(axiosError)).not.toThrow();
+ expect(() => JSON.stringify({ wrapped: axiosError })).not.toThrow();
+ });
+
+ it('omits `cause` from toJSON() output', () => {
+ const axiosError = AxiosError.from(makeCircularCause(), 'ERR_NETWORK', { url: '/x' });
+
+ expect(axiosError.toJSON()).not.toHaveProperty('cause');
+ });
+ });
+
it('is recognized as a native error by Node util/types', () => {
expect(isNativeError(new AxiosError('My Axios Error'))).toBe(true);
});
diff --git tests/unit/core/mergeConfig.test.js tests/unit/core/mergeConfig.test.js
index 8b377929ec..f5750809c3 100644
--- tests/unit/core/mergeConfig.test.js
+++ tests/unit/core/mergeConfig.test.js
@@ -4,6 +4,10 @@ import mergeConfig from '../../../lib/core/mergeConfig.js';
import { AxiosHeaders } from '../../../index.js';
describe('core::mergeConfig', () => {
+ it('accepts null for first argument', () => {
+ expect(mergeConfig(null, { url: '/foo' })).toEqual({ url: '/foo' });
+ });
+
it('accepts undefined for second argument', () => {
expect(mergeConfig(defaults, undefined)).toEqual(defaults);
});
diff --git tests/unit/fromDataURI.test.js tests/unit/fromDataURI.test.js
index c55172c10f..957d69837c 100644
--- tests/unit/fromDataURI.test.js
+++ tests/unit/fromDataURI.test.js
@@ -11,6 +11,22 @@ describe('helpers::fromDataURI', () => {
assert.deepStrictEqual(fromDataURI(dataURI, false), buffer);
});
+ it('should not call decodeURIComponent for base64 data', () => {
+ const buffer = Buffer.from('123');
+ const originalDecodeURIComponent = globalThis.decodeURIComponent;
+ globalThis.decodeURIComponent = () => {
+ throw new Error('base64 body should not be URL decoded');
+ };
+
+ try {
+ const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');
+
+ assert.deepStrictEqual(fromDataURI(dataURI, false), buffer);
+ } finally {
+ globalThis.decodeURIComponent = originalDecodeURIComponent;
+ }
+ });
+
it('should parse data URI with no mediatype and base64', () => {
const buffer = Buffer.from('123');
const dataURI = 'data:;base64,' + buffer.toString('base64');
diff --git a/tests/unit/helpers/AxiosURLSearchParams.test.js b/tests/unit/helpers/AxiosURLSearchParams.test.js
new file mode 100644
index 0000000000..6298d602f5
--- /dev/null
+++ tests/unit/helpers/AxiosURLSearchParams.test.js
@@ -0,0 +1,17 @@
+import { describe, it, expect } from 'vitest';
+import AxiosURLSearchParams from '../../../lib/helpers/AxiosURLSearchParams.js';
+
+describe('AxiosURLSearchParams::toString', () => {
+ it('should pass the AxiosURLSearchParams instance as `this` to a custom encoder', () => {
+ const params = new AxiosURLSearchParams({ foo: 'bar', baz: 'qux' });
+ const capturedThis = [];
+
+ const serialized = params.toString(function customEncoder(value, defaultEncode) {
+ capturedThis.push(this);
+ return defaultEncode(value);
+ });
+
+ expect(serialized).toBe('foo=bar&baz=qux');
+ expect(capturedThis).toEqual([params, params, params, params]);
+ });
+});
diff --git tests/unit/helpers/buildURL.test.js tests/unit/helpers/buildURL.test.js
index 5cc0f2dd71..7cb68b7258 100644
--- tests/unit/helpers/buildURL.test.js
+++ tests/unit/helpers/buildURL.test.js
@@ -16,6 +16,10 @@ describe('helpers::buildURL', () => {
).toEqual('/foo?foo=bar');
});
+ it('should support params with undefined url', () => {
+ expect(buildURL(undefined, { foo: 'bar' })).toEqual('?foo=bar');
+ });
+
it('should support sending raw params to custom serializer func', () => {
const serializer = vi.fn().mockReturnValue('foo=bar');
const params = { foo: 'bar' };
@@ -63,6 +67,21 @@ describe('helpers::buildURL', () => {
).toEqual('/foo?foo%5B%5D=bar&foo%5B%5D=baz');
});
+ it('should pass the params serializer instance as `this` to custom encode', () => {
+ const capturedThis = [];
+
+ expect(
+ buildURL('/foo', { foo: 'bar', baz: 'qux' }, {
+ encode(value, defaultEncode) {
+ capturedThis.push(this);
+ return defaultEncode(value);
+ },
+ })
+ ).toEqual('/foo?foo=bar&baz=qux');
+ expect(capturedThis).toHaveLength(4);
+ expect(new Set(capturedThis).size).toBe(1);
+ });
+
it('should support special char params', () => {
expect(
buildURL('/foo', {
diff --git a/tests/unit/helpers/composeSignals.test.js b/tests/unit/helpers/composeSignals.test.js
new file mode 100644
index 0000000000..4f8c3ed940
--- /dev/null
+++ tests/unit/helpers/composeSignals.test.js
@@ -0,0 +1,24 @@
+import { describe, it, expect } from 'vitest';
+import composeSignals from '../../../lib/helpers/composeSignals.js';
+
+describe('helpers::composeSignals', () => {
+ it('registers abort listeners as once-only listeners', () => {
+ const controller = new AbortController();
+ const calls = [];
+ const nativeAdd = controller.signal.addEventListener.bind(controller.signal);
+
+ controller.signal.addEventListener = (type, fn, options) => {
+ calls.push({ type, options });
+ return nativeAdd(type, fn, options);
+ };
+
+ const signal = composeSignals([controller.signal]);
+
+ try {
+ expect(calls).toHaveLength(1);
+ expect(calls[0]).toEqual({ type: 'abort', options: { once: true } });
+ } finally {
+ signal.unsubscribe();
+ }
+ });
+});
diff --git tests/unit/helpers/resolveConfig.test.js tests/unit/helpers/resolveConfig.test.js
index bda659124e..e4a1ed2949 100644
--- tests/unit/helpers/resolveConfig.test.js
+++ tests/unit/helpers/resolveConfig.test.js
@@ -1,6 +1,8 @@
import { describe, it } from 'vitest';
import assert from 'assert';
+import FormData from 'form-data';
import resolveConfig from '../../../lib/helpers/resolveConfig.js';
+import AxiosError from '../../../lib/core/AxiosError.js';
class ReactNativeFormData {
append() {}
@@ -56,6 +58,40 @@ describe('helpers::resolveConfig', () => {
}
});
+ it('should wrap invalid auth encoding as AxiosError', () => {
+ assert.throws(
+ () =>
+ resolveConfig({
+ url: '/foo',
+ auth: {
+ username: 'user',
+ password: '\uD800',
+ },
+ }),
+ (err) => {
+ assert.ok(err instanceof AxiosError);
+ assert.strictEqual(err.code, AxiosError.ERR_BAD_OPTION_VALUE);
+ return true;
+ }
+ );
+ });
+
+ it('should ignore null form-data headers with content-only policy', () => {
+ const data = new FormData();
+ data.getHeaders = () => null;
+
+ const config = resolveConfig({
+ url: '/upload',
+ data,
+ formDataHeaderPolicy: 'content-only',
+ headers: {
+ 'X-Test': 'ok',
+ },
+ });
+
+ assert.strictEqual(config.headers.get('X-Test'), 'ok');
+ });
+
it('should ignore inherited nested serializer fields', () => {
let serializeInvoked = false;
let encodeInvoked = false;
diff --git tests/unit/helpers/validator.test.js tests/unit/helpers/validator.test.js
index 27dcd259b6..6f2a636ad8 100644
--- tests/unit/helpers/validator.test.js
+++ tests/unit/helpers/validator.test.js
@@ -64,4 +64,17 @@ describe('validator::assertOptions', () => {
);
}).not.toThrow();
});
+
+ it('should reject null options', () => {
+ let error;
+ try {
+ validator.assertOptions(null, {});
+ } catch (err) {
+ error = err;
+ }
+
+ expect(error).toBeInstanceOf(AxiosError);
+ expect(error.message).toBe('options must be an object');
+ expect(error.code).toBe(AxiosError.ERR_BAD_OPTION_VALUE);
+ });
});
diff --git tests/unit/toFormData.test.js tests/unit/toFormData.test.js
index 63d8b7f64b..f6949600f4 100644
--- tests/unit/toFormData.test.js
+++ tests/unit/toFormData.test.js
@@ -66,6 +66,53 @@ describe('helpers::toFormData', () => {
assert.ok(formData instanceof FormData);
});
+ it('should use custom Blob constructor for typed array values', () => {
+ class CustomBlob {
+ constructor(parts) {
+ this.parts = parts;
+ }
+ }
+
+ const formData = {
+ calls: [],
+ append(key, value) {
+ this.calls.push([key, value]);
+ },
+ get [Symbol.toStringTag]() {
+ return 'FormData';
+ },
+ *[Symbol.iterator]() {}
+ };
+
+ const value = new Uint8Array([1, 2, 3]);
+ toFormData({ file: value }, formData, { Blob: CustomBlob });
+
+ assert.strictEqual(formData.calls.length, 1);
+ assert.strictEqual(formData.calls[0][0], 'file');
+ assert.ok(formData.calls[0][1] instanceof CustomBlob);
+ assert.deepStrictEqual(formData.calls[0][1].parts, [value]);
+ });
+
+ it('should throw AxiosError when typed array values require Buffer and Buffer is unavailable', () => {
+ const originalBuffer = globalThis.Buffer;
+ const formData = createRNFormDataSpy();
+
+ try {
+ globalThis.Buffer = undefined;
+
+ assert.throws(
+ () => toFormData({ file: new Uint8Array([1]) }, formData),
+ (err) => {
+ assert.ok(err instanceof AxiosError);
+ assert.strictEqual(err.code, AxiosError.ERR_NOT_SUPPORT);
+ return true;
+ }
+ );
+ } finally {
+ globalThis.Buffer = originalBuffer;
+ }
+ });
+
it('should append root-level React Native blob without recursion', () => {
const formData = createRNFormDataSpy();
PR ReviewDescriptionPatch release bundling 1.18.1 fixes: hardening runtime edge cases (cookie decode, data URI base64, form serialization, config merge, validator null checks, XHR cleanup), fixing Possible Issues
Security Hotspots
ChangesChangeslib/adapters/http.js
lib/adapters/fetch.js
lib/core/AxiosError.js
lib/core/mergeConfig.js
lib/helpers/
lib/adapters/xhr.js
lib/adapters/adapters.js
index.d.ts / index.d.cts
version bumps
docs
tests
sequenceDiagram
participant App
participant Axios
participant HttpAdapter
participant Node
App->>Axios: get('/foo', { socketPath })
Axios->>HttpAdapter: dispatch(config)
HttpAdapter->>HttpAdapter: own('socketPath')?
alt own socketPath set
HttpAdapter->>HttpAdapter: new URL(fullPath, 'http://localhost')
else no own socketPath
HttpAdapter->>HttpAdapter: new URL(fullPath, origin/undefined)
end
HttpAdapter->>HttpAdapter: getProxyEnvAgent(options, httpAgent, httpsAgent)
alt Node native proxyEnv enabled
HttpAdapter->>Node: defer env proxy to Node
else
HttpAdapter->>HttpAdapter: setProxy() resolve env proxy
end
HttpAdapter->>Node: request()
Node-->>HttpAdapter: socket
HttpAdapter->>HttpAdapter: typeof socket.setKeepAlive === 'function'?
alt has setKeepAlive
HttpAdapter->>Node: socket.setKeepAlive(true, 60000)
end
Node-->>HttpAdapter: response / error
alt error
HttpAdapter->>HttpAdapter: AxiosError.from (non-enumerable cause)
end
HttpAdapter-->>Axios: response / reject
Axios-->>App: result
|
This PR contains the following updates:
1.18.0→1.18.1Release Notes
axios/axios (axios)
v1.18.1Compare Source
v1.18.1 — June 21, 2026
This release focuses on Node HTTP adapter fixes, safer AxiosError serialisation, runtime/type correctness fixes, documentation updates, and dependency maintenance.
🐛 Bug Fixes
encoder.call(this)receives theAxiosURLSearchParamsinstance correctly. (#11019)🔧 Maintenance & Chores
Documentation: Documented sensitive headers and status transition behaviour, prepared cleaned-up docs, added Deno install instructions, and clarified that request data is request-specific (#11007, #11010, #11023, #11025)
Dependencies: Bumped vite, rollup, form-data, js-yaml, and multer across the root project, docs, smoke tests, and module test workspaces. (#11011, #11012, #11013, #11014, #11015, #11016, #11017, #11026)
🌟 New Contributors
We are thrilled to welcome our new contributors. Thank you for helping improve axios:
Full Changelog
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.