Compare commits

..

10 Commits

Author SHA1 Message Date
Jay
c7a76ddbf2
fix(types): make response headers case insensative (#10677) 2026-04-09 18:49:52 +02:00
Eve
914bc2605a
fix(progress): clamp loaded to total for computable upload/download events (#7458)
* fix(progress): clamp loaded value to total in progress reducer

* test(progress): cover out-of-order events in reducer clamp case

* fix(progress): keep bytesNotified monotonic for out-of-order events

---------

Co-authored-by: Jay <jasonsaayman@gmail.com>
2026-04-09 17:40:50 +02:00
Jay
0912bde937
fix: support multiselect in form data (#10676) 2026-04-09 17:31:39 +02:00
Raashish Aggarwal
8b68491d04
fix(http): handle socket-only request errors without leaking keep-alive listeners (#10576)
* fix(http): handle request socket-only errors

* test(http): cover socket-only error handling cleanup

---------

Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
2026-04-08 22:03:30 +02:00
Darwin ❤️❤️❤️
62f6281660
fix(fetch): remove Content-Type without boundary for FormData (#7314)
* fix(fetch): remove Content-Type without boundary for FormData

When using the fetch adapter with FormData and manually setting
Content-Type to 'multipart/form-data' (without boundary), the
request would fail because the boundary is required.

This fix detects when Content-Type is set to multipart/form-data
without a boundary and removes it, allowing fetch to automatically
set the correct Content-Type header with the proper boundary.

Fixes #7054

* chore: update fixes

---------

Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
2026-04-08 21:07:51 +02:00
Ilya
c44f4d728b
refactor(types): use TypeScript utils to transform duplicated literals (#7520)
* refactor(types): use TypeScript utils to transform Method literals

* refactor(types): use TypeScript utils to transform responseEncoding literals

* refactor: revert added export for responseEncoding

---------

Co-authored-by: Jay <jasonsaayman@gmail.com>
2026-04-08 19:42:34 +02:00
Jay
34d137cbae
docs: update hopper secuirty (#10672) 2026-04-08 10:14:57 +02:00
techcodie
9f992a8eb9
fix(types): align runWhen type with runtime behavior in InterceptorManager (#7529)
The `runWhen` property in `AxiosInterceptorOptions` and
`AxiosInterceptorHandler` had type inconsistencies that caused
TypeScript build failures when upgrading from axios 1.13.2 to 1.13.5+.

Issues fixed:

1. `AxiosInterceptorOptions.runWhen` did not allow `null`, but
   `InterceptorManager.use()` explicitly sets it to `null` when no
   options are provided (`runWhen: options ? options.runWhen : null`).

2. `AxiosInterceptorHandler.runWhen` used `AxiosRequestConfig` instead
   of `InternalAxiosRequestConfig`, creating a type mismatch with
   `AxiosInterceptorOptions` which uses `InternalAxiosRequestConfig`.

3. In `index.d.ts`, the `AxiosInterceptorHandler.runWhen` type was
   `(config: AxiosRequestConfig) => boolean | null` — the `| null`
   was incorrectly placed as part of the return type rather than making
   the entire function type nullable.

4. `AxiosInterceptorHandler.runWhen` is now optional (`?`) to also
   allow `undefined`, matching the runtime case where options are
   passed without a `runWhen` property (i.e. `options.runWhen` is
   `undefined`).

Fixes #7404

Co-authored-by: Jay <jasonsaayman@gmail.com>
2026-04-07 20:58:30 +02:00
Gabriel Quaresma
9df2cd3df7
chore: add Location to CommonRequestHeadersList types (#7528)
Co-authored-by: Jay <jasonsaayman@gmail.com>
2026-04-07 19:55:58 +02:00
Nitya Jain
f53ebf2198
fix(core): use strict equality in buildFullPath check (#7252)
Replace loose equality (==) with strict equality (===)

Co-authored-by: Jay <jasonsaayman@gmail.com>
2026-04-07 19:48:21 +02:00
12 changed files with 398 additions and 261 deletions

View File

@ -1,5 +1,5 @@
<h3 align="center"> 💎 Platinum sponsors <br> </h3> <table align="center"><tr><td align="center" width="50%"> <a href="https://thanks.dev/?utm_source&#x3D;axios&amp;utm_medium&#x3D;sponsorlist&amp;utm_campaign&#x3D;sponsorship" style="padding: 10px; display: inline-block" target="_blank"> <img width="90px" height="90px" src="https://axios-http.com/assets/sponsors/opencollective/ed51c2ee8f1b70aa3484d6dd678652134079a036.png" alt="THANKS.DEV"/> </a> <p align="center" title="We&#x27;re passionate about making open source sustainable. Scan your dependancy tree to better understand which open source projects need funding the most. Maintainers can also register their projects to become eligible for funding.">We&#x27;re passionate about making open source sustainable. Scan your dependancy tree to better understand which open source projects need funding the...</p> <p align="center"> <a href="https://thanks.dev/?utm_source&#x3D;axios&amp;utm_medium&#x3D;readme_sponsorlist&amp;utm_campaign&#x3D;sponsorship" target="_blank"><b>thanks.dev</b></a> </p>
</td><td align="center" width="50%"> <a href="https://opencollective.com/hopper-security?utm_source&#x3D;axios&amp;utm_medium&#x3D;sponsorlist&amp;utm_campaign&#x3D;sponsorship" style="padding: 10px; display: inline-block" target="_blank"> <img width="90px" height="90px" src="https://axios-http.com/assets/sponsors/opencollective/180d02a83ee99448f850e39eed6dbb95f56000ba.png" alt="Hopper Security"/> </a> <p align="center"> </p>
</td><td align="center" width="50%"> <a href="https://opencollective.com/hopper-security?utm_source&#x3D;axios&amp;utm_medium&#x3D;sponsorlist&amp;utm_campaign&#x3D;sponsorship" style="padding: 10px; display: inline-block" target="_blank"> <img width="90px" height="90px" src="https://axios-http.com/assets/sponsors/opencollective/180d02a83ee99448f850e39eed6dbb95f56000ba.png" alt="Hopper Security"/> </a> <p align="center">Hopper provides a secure open-source registry where every component is verified against malware and continuously remediated for vulnerabilities across any version. In simple terms, Hopper removes the need to manage software supply chain risk altogether.</p><p align="center"> <a href="https://hopper.security/?utm_source&#x3D;axios&amp;utm_medium&#x3D;readme_sponsorlist&amp;utm_campaign&#x3D;sponsorship" target="_blank"><b>Hopper.Security</b></a> </p>
</td></tr></table><table align="center"><tr><td align="center" width="50%"> <a href="https://opencollective.com/axios/contribute" target="_blank" >💜 Become a sponsor</a>
</td><td align="center" width="50%"> <a href="https://opencollective.com/axios/contribute" target="_blank" >💜 Become a sponsor</a>
</td></tr></table>

View File

@ -20,7 +20,8 @@ type CommonRequestHeadersList =
| 'Content-Length'
| 'User-Agent'
| 'Content-Encoding'
| 'Authorization';
| 'Authorization'
| 'Location';
type ContentType =
| axios.AxiosHeaderValue
@ -38,6 +39,8 @@ type CommonResponseHeadersList =
| 'Cache-Control'
| 'Content-Encoding';
type CommonResponseHeaderKey = CommonResponseHeadersList | Lowercase<CommonResponseHeadersList>;
type BrowserProgressEvent = any;
declare class AxiosHeaders {
@ -306,7 +309,7 @@ declare namespace axios {
type AxiosHeaderValue = AxiosHeaders | string | string[] | number | boolean | null;
type RawCommonResponseHeaders = {
[Key in CommonResponseHeadersList]: AxiosHeaderValue;
[Key in CommonResponseHeaderKey]: AxiosHeaderValue;
} & {
'set-cookie': string[];
};
@ -344,56 +347,38 @@ declare namespace axios {
protocol?: string;
}
type Method =
| 'get'
type UppercaseMethod =
| 'GET'
| 'delete'
| 'DELETE'
| 'head'
| 'HEAD'
| 'options'
| 'OPTIONS'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'patch'
| 'PATCH'
| 'purge'
| 'PURGE'
| 'link'
| 'LINK'
| 'unlink'
| 'UNLINK';
type Method = (UppercaseMethod | Lowercase<UppercaseMethod>) & {};
type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata';
type responseEncoding =
| 'ascii'
type UppercaseResponseEncoding =
| 'ASCII'
| 'ansi'
| 'ANSI'
| 'binary'
| 'BINARY'
| 'base64'
| 'BASE64'
| 'base64url'
| 'BASE64URL'
| 'hex'
| 'HEX'
| 'latin1'
| 'LATIN1'
| 'ucs-2'
| 'UCS-2'
| 'ucs2'
| 'UCS2'
| 'utf-8'
| 'UTF-8'
| 'utf8'
| 'UTF8'
| 'utf16le'
| 'UTF16LE';
type responseEncoding = (UppercaseResponseEncoding | Lowercase<UppercaseResponseEncoding>) & {};
interface TransitionalOptions {
silentJSONParsing?: boolean;
forcedJSONParsing?: boolean;
@ -628,7 +613,7 @@ declare namespace axios {
interface AxiosInterceptorOptions {
synchronous?: boolean;
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
runWhen?: ((config: InternalAxiosRequestConfig) => boolean) | null;
}
type AxiosInterceptorFulfilled<T> = (value: T) => T | Promise<T>;
@ -649,7 +634,7 @@ declare namespace axios {
fulfilled: AxiosInterceptorFulfilled<T>;
rejected?: AxiosInterceptorRejected;
synchronous: boolean;
runWhen?: (config: AxiosRequestConfig) => boolean;
runWhen?: ((config: InternalAxiosRequestConfig) => boolean) | null;
}
interface AxiosInterceptorManager<V> {

355
index.d.ts vendored
View File

@ -1,13 +1,7 @@
// TypeScript Version: 4.7
type StringLiteralsOrString<Literals extends string> = Literals | (string & {});
export type AxiosHeaderValue =
| AxiosHeaders
| string
| string[]
| number
| boolean
| null;
export type AxiosHeaderValue = AxiosHeaders | string | string[] | number | boolean | null;
interface RawAxiosHeaders {
[key: string]: AxiosHeaderValue;
@ -24,11 +18,7 @@ type AxiosHeaderMatcher =
| RegExp
| ((this: AxiosHeaders, value: string, name: string) => boolean);
type AxiosHeaderParser = (
this: AxiosHeaders,
value: AxiosHeaderValue,
header: string,
) => any;
type AxiosHeaderParser = (this: AxiosHeaders, value: AxiosHeaderValue, header: string) => any;
export class AxiosHeaders {
constructor(headers?: RawAxiosHeaders | AxiosHeaders | string);
@ -38,12 +28,9 @@ export class AxiosHeaders {
set(
headerName?: string,
value?: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
set(
headers?: RawAxiosHeaders | AxiosHeaders | string,
rewrite?: boolean,
rewrite?: boolean | AxiosHeaderMatcher
): AxiosHeaders;
set(headers?: RawAxiosHeaders | AxiosHeaders | string, rewrite?: boolean): AxiosHeaders;
get(headerName: string, parser: RegExp): RegExpExecArray | null;
get(headerName: string, matcher?: true | AxiosHeaderParser): AxiosHeaderValue;
@ -57,9 +44,7 @@ export class AxiosHeaders {
normalize(format: boolean): AxiosHeaders;
concat(
...targets: Array<
AxiosHeaders | RawAxiosHeaders | string | undefined | null
>
...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>
): AxiosHeaders;
toJSON(asStrings?: boolean): RawAxiosHeaders;
@ -69,55 +54,35 @@ export class AxiosHeaders {
static accessor(header: string | string[]): AxiosHeaders;
static concat(
...targets: Array<
AxiosHeaders | RawAxiosHeaders | string | undefined | null
>
...targets: Array<AxiosHeaders | RawAxiosHeaders | string | undefined | null>
): AxiosHeaders;
setContentType(
value: ContentType,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setContentType(value: ContentType, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentType(parser?: RegExp): RegExpExecArray | null;
getContentType(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentType(matcher?: AxiosHeaderMatcher): boolean;
setContentLength(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setContentLength(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentLength(parser?: RegExp): RegExpExecArray | null;
getContentLength(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentLength(matcher?: AxiosHeaderMatcher): boolean;
setAccept(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setAccept(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAccept(parser?: RegExp): RegExpExecArray | null;
getAccept(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAccept(matcher?: AxiosHeaderMatcher): boolean;
setUserAgent(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setUserAgent(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getUserAgent(parser?: RegExp): RegExpExecArray | null;
getUserAgent(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasUserAgent(matcher?: AxiosHeaderMatcher): boolean;
setContentEncoding(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setContentEncoding(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getContentEncoding(parser?: RegExp): RegExpExecArray | null;
getContentEncoding(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasContentEncoding(matcher?: AxiosHeaderMatcher): boolean;
setAuthorization(
value: AxiosHeaderValue,
rewrite?: boolean | AxiosHeaderMatcher,
): AxiosHeaders;
setAuthorization(value: AxiosHeaderValue, rewrite?: boolean | AxiosHeaderMatcher): AxiosHeaders;
getAuthorization(parser?: RegExp): RegExpExecArray | null;
getAuthorization(matcher?: AxiosHeaderMatcher): AxiosHeaderValue;
hasAuthorization(matcher?: AxiosHeaderMatcher): boolean;
@ -128,56 +93,53 @@ export class AxiosHeaders {
}
type CommonRequestHeadersList =
| "Accept"
| "Content-Length"
| "User-Agent"
| "Content-Encoding"
| "Authorization";
| 'Accept'
| 'Content-Length'
| 'User-Agent'
| 'Content-Encoding'
| 'Authorization'
| 'Location';
type ContentType =
| AxiosHeaderValue
| "text/html"
| "text/plain"
| "multipart/form-data"
| "application/json"
| "application/x-www-form-urlencoded"
| "application/octet-stream";
| 'text/html'
| 'text/plain'
| 'multipart/form-data'
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export type RawAxiosRequestHeaders = Partial<
RawAxiosHeaders & {
[Key in CommonRequestHeadersList]: AxiosHeaderValue;
} & {
"Content-Type": ContentType;
'Content-Type': ContentType;
}
>;
export type AxiosRequestHeaders = RawAxiosRequestHeaders & AxiosHeaders;
type CommonResponseHeadersList =
| "Server"
| "Content-Type"
| "Content-Length"
| "Cache-Control"
| "Content-Encoding";
| 'Server'
| 'Content-Type'
| 'Content-Length'
| 'Cache-Control'
| 'Content-Encoding';
type CommonResponseHeaderKey = CommonResponseHeadersList | Lowercase<CommonResponseHeadersList>;
type RawCommonResponseHeaders = {
[Key in CommonResponseHeadersList]: AxiosHeaderValue;
[Key in CommonResponseHeaderKey]: AxiosHeaderValue;
} & {
"set-cookie": string[];
'set-cookie': string[];
};
export type RawAxiosResponseHeaders = Partial<
RawAxiosHeaders & RawCommonResponseHeaders
>;
export type RawAxiosResponseHeaders = Partial<RawAxiosHeaders & RawCommonResponseHeaders>;
export type AxiosResponseHeaders = RawAxiosResponseHeaders & AxiosHeaders;
export interface AxiosRequestTransformer {
(
this: InternalAxiosRequestConfig,
data: any,
headers: AxiosRequestHeaders,
): any;
(this: InternalAxiosRequestConfig, data: any, headers: AxiosRequestHeaders): any;
}
export interface AxiosResponseTransformer {
@ -185,7 +147,7 @@ export interface AxiosResponseTransformer {
this: InternalAxiosRequestConfig,
data: any,
headers: AxiosResponseHeaders,
status?: number,
status?: number
): any;
}
@ -271,62 +233,47 @@ export enum HttpStatusCode {
NetworkAuthenticationRequired = 511,
}
export type Method =
| "get"
| "GET"
| "delete"
| "DELETE"
| "head"
| "HEAD"
| "options"
| "OPTIONS"
| "post"
| "POST"
| "put"
| "PUT"
| "patch"
| "PATCH"
| "purge"
| "PURGE"
| "link"
| "LINK"
| "unlink"
| "UNLINK";
type UppercaseMethod =
| 'GET'
| 'DELETE'
| 'HEAD'
| 'OPTIONS'
| 'POST'
| 'PUT'
| 'PATCH'
| 'PURGE'
| 'LINK'
| 'UNLINK';
export type Method = (UppercaseMethod | Lowercase<UppercaseMethod>) & {};
export type ResponseType =
| "arraybuffer"
| "blob"
| "document"
| "json"
| "text"
| "stream"
| "formdata";
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream'
| 'formdata';
export type responseEncoding =
| "ascii"
| "ASCII"
| "ansi"
| "ANSI"
| "binary"
| "BINARY"
| "base64"
| "BASE64"
| "base64url"
| "BASE64URL"
| "hex"
| "HEX"
| "latin1"
| "LATIN1"
| "ucs-2"
| "UCS-2"
| "ucs2"
| "UCS2"
| "utf-8"
| "UTF-8"
| "utf8"
| "UTF8"
| "utf16le"
| "UTF16LE";
type UppercaseResponseEncoding =
| 'ASCII'
| 'ANSI'
| 'BINARY'
| 'BASE64'
| 'BASE64URL'
| 'HEX'
| 'LATIN1'
| 'UCS-2'
| 'UCS2'
| 'UTF-8'
| 'UTF8'
| 'UTF16LE';
export type responseEncoding = (
| UppercaseResponseEncoding
| Lowercase<UppercaseResponseEncoding>
) & {};
export interface TransitionalOptions {
silentJSONParsing?: boolean;
@ -354,7 +301,7 @@ export interface SerializerVisitor {
value: any,
key: string | number,
path: null | Array<string | number>,
helpers: FormDataVisitorHelpers,
helpers: FormDataVisitorHelpers
): boolean;
}
@ -402,7 +349,7 @@ export interface AxiosProgressEvent {
type Milliseconds = number;
type AxiosAdapterName = StringLiteralsOrString<"xhr" | "http" | "fetch">;
type AxiosAdapterName = StringLiteralsOrString<'xhr' | 'http' | 'fetch'>;
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
@ -447,7 +394,7 @@ export interface AxiosRequestConfig<D = any> {
responseDetails: {
headers: Record<string, string>;
statusCode: HttpStatusCode;
},
}
) => void;
socketPath?: string | null;
transport?: any;
@ -461,24 +408,11 @@ export interface AxiosRequestConfig<D = any> {
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
fetch?: (
input: URL | Request | string,
init?: RequestInit,
) => Promise<Response>;
Request?: new (
input: URL | Request | string,
init?: RequestInit,
) => Request;
fetch?: (input: URL | Request | string, init?: RequestInit) => Promise<Response>;
Request?: new (input: URL | Request | string, init?: RequestInit) => Request;
Response?: new (
body?:
| ArrayBuffer
| ArrayBufferView
| Blob
| FormData
| URLSearchParams
| string
| null,
init?: ResponseInit,
body?: ArrayBuffer | ArrayBufferView | Blob | FormData | URLSearchParams | string | null,
init?: ResponseInit
) => Response;
};
formSerializer?: FormSerializerOptions;
@ -490,26 +424,18 @@ export interface AxiosRequestConfig<D = any> {
cb: (
err: Error | null,
address: LookupAddress | LookupAddress[],
family?: AddressFamily,
) => void,
family?: AddressFamily
) => void
) => void)
| ((
hostname: string,
options: object,
options: object
) => Promise<
| [
address: LookupAddressEntry | LookupAddressEntry[],
family?: AddressFamily,
]
| LookupAddress
[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress
>);
withXSRFToken?:
| boolean
| ((config: InternalAxiosRequestConfig) => boolean | undefined);
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
parseReviver?: (this: any, key: string, value: any) => any;
fetchOptions?:
| Omit<RequestInit, "body" | "headers" | "method" | "signal">
| Record<string, any>;
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'> | Record<string, any>;
httpVersion?: 1 | 2;
http2Options?: Record<string, any> & {
sessionTimeout?: number;
@ -519,9 +445,7 @@ export interface AxiosRequestConfig<D = any> {
// Alias
export type RawAxiosRequestConfig<D = any> = AxiosRequestConfig<D>;
export interface InternalAxiosRequestConfig<
D = any,
> extends AxiosRequestConfig<D> {
export interface InternalAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
headers: AxiosRequestHeaders;
}
@ -539,17 +463,11 @@ export interface HeadersDefaults {
unlink?: RawAxiosRequestHeaders;
}
export interface AxiosDefaults<D = any> extends Omit<
AxiosRequestConfig<D>,
"headers"
> {
export interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers: HeadersDefaults;
}
export interface CreateAxiosDefaults<D = any> extends Omit<
AxiosRequestConfig<D>,
"headers"
> {
export interface CreateAxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
headers?: RawAxiosRequestHeaders | AxiosHeaders | Partial<HeadersDefaults>;
}
@ -568,7 +486,7 @@ export class AxiosError<T = unknown, D = any> extends Error {
code?: string,
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>,
response?: AxiosResponse<T, D>
);
config?: InternalAxiosRequestConfig<D>;
@ -586,24 +504,24 @@ export class AxiosError<T = unknown, D = any> extends Error {
config?: InternalAxiosRequestConfig<D>,
request?: any,
response?: AxiosResponse<T, D>,
customProps?: object,
customProps?: object
): AxiosError<T, D>;
static readonly ERR_FR_TOO_MANY_REDIRECTS = "ERR_FR_TOO_MANY_REDIRECTS";
static readonly ERR_BAD_OPTION_VALUE = "ERR_BAD_OPTION_VALUE";
static readonly ERR_BAD_OPTION = "ERR_BAD_OPTION";
static readonly ERR_NETWORK = "ERR_NETWORK";
static readonly ERR_DEPRECATED = "ERR_DEPRECATED";
static readonly ERR_BAD_RESPONSE = "ERR_BAD_RESPONSE";
static readonly ERR_BAD_REQUEST = "ERR_BAD_REQUEST";
static readonly ERR_NOT_SUPPORT = "ERR_NOT_SUPPORT";
static readonly ERR_INVALID_URL = "ERR_INVALID_URL";
static readonly ERR_CANCELED = "ERR_CANCELED";
static readonly ECONNABORTED = "ECONNABORTED";
static readonly ETIMEDOUT = "ETIMEDOUT";
static readonly ERR_FR_TOO_MANY_REDIRECTS = 'ERR_FR_TOO_MANY_REDIRECTS';
static readonly ERR_BAD_OPTION_VALUE = 'ERR_BAD_OPTION_VALUE';
static readonly ERR_BAD_OPTION = 'ERR_BAD_OPTION';
static readonly ERR_NETWORK = 'ERR_NETWORK';
static readonly ERR_DEPRECATED = 'ERR_DEPRECATED';
static readonly ERR_BAD_RESPONSE = 'ERR_BAD_RESPONSE';
static readonly ERR_BAD_REQUEST = 'ERR_BAD_REQUEST';
static readonly ERR_NOT_SUPPORT = 'ERR_NOT_SUPPORT';
static readonly ERR_INVALID_URL = 'ERR_INVALID_URL';
static readonly ERR_CANCELED = 'ERR_CANCELED';
static readonly ECONNABORTED = 'ECONNABORTED';
static readonly ETIMEDOUT = 'ETIMEDOUT';
}
export class CanceledError<T> extends AxiosError<T> {
readonly name: "CanceledError";
readonly name: 'CanceledError';
}
export type AxiosPromise<T = any> = Promise<AxiosResponse<T>>;
@ -638,7 +556,7 @@ export interface CancelTokenSource {
export interface AxiosInterceptorOptions {
synchronous?: boolean;
runWhen?: (config: InternalAxiosRequestConfig) => boolean;
runWhen?: ((config: InternalAxiosRequestConfig) => boolean) | null;
}
type AxiosInterceptorFulfilled<T> = (value: T) => T | Promise<T>;
@ -647,25 +565,23 @@ type AxiosInterceptorRejected = (error: any) => any;
type AxiosRequestInterceptorUse<T> = (
onFulfilled?: AxiosInterceptorFulfilled<T> | null,
onRejected?: AxiosInterceptorRejected | null,
options?: AxiosInterceptorOptions,
options?: AxiosInterceptorOptions
) => number;
type AxiosResponseInterceptorUse<T> = (
onFulfilled?: AxiosInterceptorFulfilled<T> | null,
onRejected?: AxiosInterceptorRejected | null,
onRejected?: AxiosInterceptorRejected | null
) => number;
interface AxiosInterceptorHandler<T> {
fulfilled: AxiosInterceptorFulfilled<T>;
rejected?: AxiosInterceptorRejected;
synchronous: boolean;
runWhen: (config: AxiosRequestConfig) => boolean | null;
runWhen?: ((config: InternalAxiosRequestConfig) => boolean) | null;
}
export interface AxiosInterceptorManager<V> {
use: V extends AxiosResponse
? AxiosResponseInterceptorUse<V>
: AxiosRequestInterceptorUse<V>;
use: V extends AxiosResponse ? AxiosResponseInterceptorUse<V> : AxiosRequestInterceptorUse<V>;
eject(id: number): void;
clear(): void;
handlers?: Array<AxiosInterceptorHandler<V>>;
@ -679,68 +595,61 @@ export class Axios {
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>, D = any>(
config: AxiosRequestConfig<D>,
): Promise<R>;
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
get<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
delete<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
head<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
options<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
post<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
put<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
patch<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
postForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
putForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
patchForm<T = any, R = AxiosResponse<T>, D = any>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>,
config?: AxiosRequestConfig<D>
): Promise<R>;
}
export interface AxiosInstance extends Axios {
<T = any, R = AxiosResponse<T>, D = any>(
config: AxiosRequestConfig<D>,
): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(
url: string,
config?: AxiosRequestConfig<D>,
): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
create(config?: CreateAxiosDefaults): AxiosInstance;
defaults: Omit<AxiosDefaults, "headers"> & {
defaults: Omit<AxiosDefaults, 'headers'> & {
headers: HeadersDefaults & {
[key: string]: AxiosHeaderValue;
};
@ -758,22 +667,18 @@ export interface GenericHTMLFormElement {
}
export function getAdapter(
adapters: AxiosAdapterConfig | AxiosAdapterConfig[] | undefined,
adapters: AxiosAdapterConfig | AxiosAdapterConfig[] | undefined
): AxiosAdapter;
export function toFormData(
sourceObj: object,
targetFormData?: GenericFormData,
options?: FormSerializerOptions,
options?: FormSerializerOptions
): GenericFormData;
export function formToJSON(
form: GenericFormData | GenericHTMLFormElement,
): object;
export function formToJSON(form: GenericFormData | GenericHTMLFormElement): object;
export function isAxiosError<T = any, D = any>(
payload: any,
): payload is AxiosError<T, D>;
export function isAxiosError<T = any, D = any>(payload: any): payload is AxiosError<T, D>;
export function spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
@ -783,7 +688,7 @@ export function all<T>(values: Array<T | Promise<T>>): Promise<T[]>;
export function mergeConfig<D = any>(
config1: AxiosRequestConfig<D>,
config2: AxiosRequestConfig<D>,
config2: AxiosRequestConfig<D>
): AxiosRequestConfig<D>;
export interface AxiosStatic extends AxiosInstance {

View File

@ -221,6 +221,19 @@ const factory = (env) => {
// see https://github.com/cloudflare/workerd/issues/902
const isCredentialsSupported = isRequestSupported && 'credentials' in Request.prototype;
// If data is FormData and Content-Type is multipart/form-data without boundary,
// delete it so fetch can set it correctly with the boundary
if (utils.isFormData(data)) {
const contentType = headers.getContentType();
if (
contentType &&
/^multipart\/form-data/i.test(contentType) &&
!/boundary=/i.test(contentType)
) {
headers.delete('content-type');
}
}
const resolvedOptions = {
...fetchOptions,
signal: composedSignal,

View File

@ -875,6 +875,21 @@ export default isHttpAdapterSupported &&
req.on('socket', function handleRequestSocket(socket) {
// default interval of sending ack packet is 1 minute
socket.setKeepAlive(true, 1000 * 60);
const removeSocketErrorListener = () => {
socket.removeListener('error', handleRequestSocketError);
};
function handleRequestSocketError(err) {
removeSocketErrorListener();
if (!req.destroyed) {
req.destroy(err);
}
}
socket.on('error', handleRequestSocketError);
req.once('close', removeSocketErrorListener);
});
// Handle request timeout

View File

@ -15,7 +15,7 @@ import combineURLs from '../helpers/combineURLs.js';
*/
export default function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
let isRelativeUrl = !isAbsoluteURL(requestedURL);
if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) {
if (baseURL && (isRelativeUrl || allowAbsoluteUrls === false)) {
return combineURLs(baseURL, requestedURL);
}
return requestedURL;

View File

@ -58,7 +58,9 @@ function formDataToJSON(formData) {
if (isLast) {
if (utils.hasOwnProp(target, name)) {
target[name] = [target[name], value];
target[name] = utils.isArray(target[name])
? target[name].concat(value)
: [target[name], value];
} else {
target[name] = value;
}

View File

@ -7,13 +7,13 @@ export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
const _speedometer = speedometer(50, 250);
return throttle((e) => {
const loaded = e.loaded;
const rawLoaded = e.loaded;
const total = e.lengthComputable ? e.total : undefined;
const progressBytes = loaded - bytesNotified;
const loaded = total != null ? Math.min(rawLoaded, total) : rawLoaded;
const progressBytes = Math.max(0, loaded - bytesNotified);
const rate = _speedometer(progressBytes);
const inRange = loaded <= total;
bytesNotified = loaded;
bytesNotified = Math.max(bytesNotified, loaded);
const data = {
loaded,
@ -21,7 +21,7 @@ export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
progress: total ? loaded / total : undefined,
bytes: progressBytes,
rate: rate ? rate : undefined,
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
estimated: rate && total ? (total - loaded) / rate : undefined,
event: e,
lengthComputable: total != null,
[isDownloadStream ? 'download' : 'upload']: true,

View File

@ -0,0 +1,27 @@
import assert from 'assert';
import { progressEventReducer } from '../../../lib/helpers/progressEventReducer.js';
describe('helpers::progressEventReducer', () => {
it('should clamp loaded/progress and avoid negative bytes for out-of-order events', () => {
const events = [];
const [onProgress, flush] = progressEventReducer((data) => {
events.push(data);
}, false, Number.POSITIVE_INFINITY);
onProgress({ lengthComputable: true, loaded: 80, total: 100 });
onProgress({ lengthComputable: true, loaded: 60, total: 100 });
onProgress({ lengthComputable: true, loaded: 180, total: 100 });
flush();
assert.strictEqual(events.length, 3);
assert.strictEqual(events[0].bytes, 80);
assert.strictEqual(events[1].bytes, 0);
const last = events[events.length - 1];
assert.strictEqual(last.loaded, 100);
assert.strictEqual(last.total, 100);
assert.strictEqual(last.progress, 1);
assert.strictEqual(last.upload, true);
assert.strictEqual(last.bytes, 20);
});
});

View File

@ -550,6 +550,54 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
await stopHTTPServer(server);
}
});
it('should remove manually set Content-Type without boundary for FormData', async () => {
const form = new FormData();
form.append('foo', 'bar');
const server = await startHTTPServer(
(req, res) => {
const contentType = req.headers['content-type'];
assert.match(contentType, /^multipart\/form-data; boundary=/i);
res.end('OK');
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
} finally {
await stopHTTPServer(server);
}
});
it('should preserve Content-Type if it already has boundary', async () => {
const form = new FormData();
form.append('foo', 'bar');
const customBoundary = '----CustomBoundary123';
const server = await startHTTPServer(
(req, res) => {
const contentType = req.headers['content-type'];
assert.ok(contentType.includes(customBoundary));
res.end('OK');
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, {
headers: {
'Content-Type': `multipart/form-data; boundary=${customBoundary}`,
},
});
} finally {
await stopHTTPServer(server);
}
});
});
describe('env config', () => {

View File

@ -29,6 +29,7 @@ import getStream from 'get-stream';
import bodyParser from 'body-parser';
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import { lookup } from 'dns';
import { EventEmitter } from 'events';
const OPEN_WEB_PORT = 80;
const SERVER_PORT = 8020;
@ -3817,6 +3818,60 @@ describe('supports http with nodejs', () => {
}
});
it('should reject when only the request socket emits an error', async () => {
const noop = () => {};
const socket = new EventEmitter();
socket.setKeepAlive = noop;
socket.on('error', noop);
const transport = {
request() {
return new (class MockRequest extends EventEmitter {
constructor() {
super();
this.destroyed = false;
}
setTimeout() {}
write() {}
end() {
this.emit('socket', socket);
setImmediate(() => {
socket.emit('error', Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }));
});
}
destroy(err) {
if (this.destroyed) {
return;
}
this.destroyed = true;
err && this.emit('error', err);
this.emit('close');
}
})();
},
};
const error = await Promise.race([
axios.post('http://example.com/', 'test', {
transport,
maxRedirects: 0,
}),
setTimeoutAsync(200).then(() => {
throw new Error('socket error did not reject the request');
}),
]).catch((err) => err);
assert.ok(error instanceof AxiosError);
assert.strictEqual(error.code, 'EPIPE');
assert.strictEqual(error.message, 'write EPIPE');
});
describe('keep-alive', () => {
it('should not fail with "socket hang up" when using timeouts', async () => {
const server = await startHTTPServer(
@ -3838,5 +3893,66 @@ describe('supports http with nodejs', () => {
await stopHTTPServer(server);
}
}, 15000);
it('should remove request socket error listeners after keep-alive requests close', async () => {
const noop = () => {};
const socket = new EventEmitter();
socket.setKeepAlive = noop;
socket.on('error', noop);
const baseErrorListenerCount = socket.listenerCount('error');
const transport = {
request(_, cb) {
return new (class MockRequest extends EventEmitter {
constructor() {
super();
this.destroyed = false;
}
setTimeout() {}
write() {}
end() {
this.emit('socket', socket);
setImmediate(() => {
const response = stream.Readable.from(['ok']);
response.statusCode = 200;
response.headers = {};
cb(response);
this.emit('close');
});
}
destroy(err) {
if (this.destroyed) {
return;
}
this.destroyed = true;
err && this.emit('error', err);
this.emit('close');
}
})();
},
};
await axios.get('http://example.com/first', {
transport,
maxRedirects: 0,
});
await setTimeoutAsync(0);
assert.strictEqual(socket.listenerCount('error'), baseErrorListenerCount);
await axios.get('http://example.com/second', {
transport,
maxRedirects: 0,
});
await setTimeoutAsync(0);
assert.strictEqual(socket.listenerCount('error'), baseErrorListenerCount);
});
});
});

View File

@ -27,6 +27,32 @@ describe('formDataToJSON', () => {
});
});
it('should keep repeatable values flat for 3+ entries', () => {
const formData = new FormData();
formData.append('select3', '301');
formData.append('select3', '302');
formData.append('select3', '303');
expect(formDataToJSON(formData)).toEqual({
select3: ['301', '302', '303'],
});
});
it('should keep nested repeatable values flat for 3+ entries', () => {
const formData = new FormData();
formData.append('foo[bar]', '1');
formData.append('foo[bar]', '2');
formData.append('foo[bar]', '3');
expect(formDataToJSON(formData)).toEqual({
foo: {
bar: ['1', '2', '3'],
},
});
});
it('should convert props with empty brackets to arrays', () => {
const formData = new FormData();