Skip to content

Commit a3ff99b

Browse files
authoredApr 28, 2024··
feat(adapter): add fetch adapter; (#6371)
1 parent 751133e commit a3ff99b

21 files changed

+1015
-127
lines changed
 

‎.github/workflows/ci.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ jobs:
1515

1616
strategy:
1717
matrix:
18-
node-version: [12.x, 14.x, 16.x, 18.x, 20.x]
18+
node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 21.x]
19+
fail-fast: false
1920

2021
steps:
2122
- uses: actions/checkout@v3

‎index.d.cts

+5-2
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ declare namespace axios {
268268
| 'document'
269269
| 'json'
270270
| 'text'
271-
| 'stream';
271+
| 'stream'
272+
| 'formdata';
272273

273274
type responseEncoding =
274275
| 'ascii' | 'ASCII'
@@ -353,11 +354,12 @@ declare namespace axios {
353354
upload?: boolean;
354355
download?: boolean;
355356
event?: BrowserProgressEvent;
357+
lengthComputable: boolean;
356358
}
357359

358360
type Milliseconds = number;
359361

360-
type AxiosAdapterName = 'xhr' | 'http' | string;
362+
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | string;
361363

362364
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
363365

@@ -415,6 +417,7 @@ declare namespace axios {
415417
lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) |
416418
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
417419
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
420+
fetchOptions?: Record<string, any>;
418421
}
419422

420423
// Alias

‎index.d.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ export type ResponseType =
209209
| 'document'
210210
| 'json'
211211
| 'text'
212-
| 'stream';
212+
| 'stream'
213+
| 'formdata';
213214

214215
export type responseEncoding =
215216
| 'ascii' | 'ASCII'
@@ -294,11 +295,12 @@ export interface AxiosProgressEvent {
294295
upload?: boolean;
295296
download?: boolean;
296297
event?: BrowserProgressEvent;
298+
lengthComputable: boolean;
297299
}
298300

299301
type Milliseconds = number;
300302

301-
type AxiosAdapterName = 'xhr' | 'http' | string;
303+
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | string;
302304

303305
type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;
304306

@@ -356,6 +358,7 @@ export interface AxiosRequestConfig<D = any> {
356358
lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: LookupAddress | LookupAddress[], family?: AddressFamily) => void) => void) |
357359
((hostname: string, options: object) => Promise<[address: LookupAddressEntry | LookupAddressEntry[], family?: AddressFamily] | LookupAddress>);
358360
withXSRFToken?: boolean | ((config: InternalAxiosRequestConfig) => boolean | undefined);
361+
fetchOptions?: Record<string, any>;
359362
}
360363

361364
// Alias

‎lib/adapters/adapters.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import utils from '../utils.js';
22
import httpAdapter from './http.js';
33
import xhrAdapter from './xhr.js';
4+
import fetchAdapter from './fetch.js';
45
import AxiosError from "../core/AxiosError.js";
56

67
const knownAdapters = {
78
http: httpAdapter,
8-
xhr: xhrAdapter
9+
xhr: xhrAdapter,
10+
fetch: fetchAdapter
911
}
1012

1113
utils.forEach(knownAdapters, (fn, value) => {

‎lib/adapters/fetch.js

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import platform from "../platform/index.js";
2+
import utils from "../utils.js";
3+
import AxiosError from "../core/AxiosError.js";
4+
import composeSignals from "../helpers/composeSignals.js";
5+
import {trackStream} from "../helpers/trackStream.js";
6+
import AxiosHeaders from "../core/AxiosHeaders.js";
7+
import progressEventReducer from "../helpers/progressEventReducer.js";
8+
import resolveConfig from "../helpers/resolveConfig.js";
9+
import settle from "../core/settle.js";
10+
11+
const fetchProgressDecorator = (total, fn) => {
12+
const lengthComputable = total != null;
13+
return (loaded) => setTimeout(() => fn({
14+
lengthComputable,
15+
total,
16+
loaded
17+
}));
18+
}
19+
20+
const isFetchSupported = typeof fetch !== 'undefined';
21+
22+
const supportsRequestStreams = isFetchSupported && (() => {
23+
let duplexAccessed = false;
24+
25+
const hasContentType = new Request(platform.origin, {
26+
body: new ReadableStream(),
27+
method: 'POST',
28+
get duplex() {
29+
duplexAccessed = true;
30+
return 'half';
31+
},
32+
}).headers.has('Content-Type');
33+
34+
return duplexAccessed && !hasContentType;
35+
})();
36+
37+
const DEFAULT_CHUNK_SIZE = 64 * 1024;
38+
39+
const resolvers = {
40+
stream: (res) => res.body
41+
};
42+
43+
isFetchSupported && ['text', 'arrayBuffer', 'blob', 'formData'].forEach(type => [
44+
resolvers[type] = utils.isFunction(Response.prototype[type]) ? (res) => res[type]() : (_, config) => {
45+
throw new AxiosError(`Response type ${type} is not supported`, AxiosError.ERR_NOT_SUPPORT, config);
46+
}
47+
])
48+
49+
const getBodyLength = async (body) => {
50+
if(utils.isBlob(body)) {
51+
return body.size;
52+
}
53+
54+
if(utils.isSpecCompliantForm(body)) {
55+
return (await new Request(body).arrayBuffer()).byteLength;
56+
}
57+
58+
if(utils.isArrayBufferView(body)) {
59+
return body.byteLength;
60+
}
61+
62+
if(utils.isURLSearchParams(body)) {
63+
body = body + '';
64+
}
65+
66+
if(utils.isString(body)) {
67+
return (await new TextEncoder().encode(body)).byteLength;
68+
}
69+
}
70+
71+
const resolveBodyLength = async (headers, body) => {
72+
const length = utils.toFiniteNumber(headers.getContentLength());
73+
74+
return length == null ? getBodyLength(body) : length;
75+
}
76+
77+
export default async (config) => {
78+
let {
79+
url,
80+
method,
81+
data,
82+
signal,
83+
cancelToken,
84+
timeout,
85+
onDownloadProgress,
86+
onUploadProgress,
87+
responseType,
88+
headers,
89+
withCredentials = 'same-origin',
90+
fetchOptions
91+
} = resolveConfig(config);
92+
93+
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
94+
95+
let [composedSignal, stopTimeout] = (signal || cancelToken || timeout) ?
96+
composeSignals([signal, cancelToken], timeout) : [];
97+
98+
let finished, request;
99+
100+
const onFinish = () => {
101+
!finished && setTimeout(() => {
102+
composedSignal && composedSignal.unsubscribe();
103+
});
104+
105+
finished = true;
106+
}
107+
108+
try {
109+
if (onUploadProgress && supportsRequestStreams && method !== 'get' && method !== 'head') {
110+
let requestContentLength = await resolveBodyLength(headers, data);
111+
112+
let _request = new Request(url, {
113+
method,
114+
body: data,
115+
duplex: "half"
116+
});
117+
118+
let contentTypeHeader;
119+
120+
if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
121+
headers.setContentType(contentTypeHeader)
122+
}
123+
124+
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, fetchProgressDecorator(
125+
requestContentLength,
126+
progressEventReducer(onUploadProgress)
127+
));
128+
}
129+
130+
if (!utils.isString(withCredentials)) {
131+
withCredentials = withCredentials ? 'cors' : 'omit';
132+
}
133+
134+
request = new Request(url, {
135+
...fetchOptions,
136+
signal: composedSignal,
137+
method,
138+
headers: headers.normalize().toJSON(),
139+
body: data,
140+
duplex: "half",
141+
withCredentials
142+
});
143+
144+
let response = await fetch(request);
145+
146+
const isStreamResponse = responseType === 'stream' || responseType === 'response';
147+
148+
if (onDownloadProgress || isStreamResponse) {
149+
const options = {};
150+
151+
Object.getOwnPropertyNames(response).forEach(prop => {
152+
options[prop] = response[prop];
153+
});
154+
155+
const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));
156+
157+
response = new Response(
158+
trackStream(response.body, DEFAULT_CHUNK_SIZE, onDownloadProgress && fetchProgressDecorator(
159+
responseContentLength,
160+
progressEventReducer(onDownloadProgress, true)
161+
), isStreamResponse && onFinish),
162+
options
163+
);
164+
}
165+
166+
responseType = responseType || 'text';
167+
168+
let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](response, config);
169+
170+
!isStreamResponse && onFinish();
171+
172+
stopTimeout && stopTimeout();
173+
174+
return await new Promise((resolve, reject) => {
175+
settle(resolve, reject, {
176+
data: responseData,
177+
headers: AxiosHeaders.from(response.headers),
178+
status: response.status,
179+
statusText: response.statusText,
180+
config,
181+
request
182+
})
183+
})
184+
} catch (err) {
185+
onFinish();
186+
187+
let {code} = err;
188+
189+
if (err.name === 'NetworkError') {
190+
code = AxiosError.ERR_NETWORK;
191+
}
192+
193+
throw AxiosError.from(err, code, config, request);
194+
}
195+
}
196+
197+

‎lib/adapters/xhr.js

+31-101
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,39 @@
1-
'use strict';
2-
31
import utils from './../utils.js';
42
import settle from './../core/settle.js';
5-
import cookies from './../helpers/cookies.js';
6-
import buildURL from './../helpers/buildURL.js';
7-
import buildFullPath from '../core/buildFullPath.js';
8-
import isURLSameOrigin from './../helpers/isURLSameOrigin.js';
93
import transitionalDefaults from '../defaults/transitional.js';
104
import AxiosError from '../core/AxiosError.js';
115
import CanceledError from '../cancel/CanceledError.js';
126
import parseProtocol from '../helpers/parseProtocol.js';
137
import platform from '../platform/index.js';
148
import AxiosHeaders from '../core/AxiosHeaders.js';
15-
import speedometer from '../helpers/speedometer.js';
16-
17-
function progressEventReducer(listener, isDownloadStream) {
18-
let bytesNotified = 0;
19-
const _speedometer = speedometer(50, 250);
20-
21-
return e => {
22-
const loaded = e.loaded;
23-
const total = e.lengthComputable ? e.total : undefined;
24-
const progressBytes = loaded - bytesNotified;
25-
const rate = _speedometer(progressBytes);
26-
const inRange = loaded <= total;
27-
28-
bytesNotified = loaded;
29-
30-
const data = {
31-
loaded,
32-
total,
33-
progress: total ? (loaded / total) : undefined,
34-
bytes: progressBytes,
35-
rate: rate ? rate : undefined,
36-
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
37-
event: e
38-
};
39-
40-
data[isDownloadStream ? 'download' : 'upload'] = true;
41-
42-
listener(data);
43-
};
44-
}
9+
import progressEventReducer from '../helpers/progressEventReducer.js';
10+
import resolveConfig from "../helpers/resolveConfig.js";
4511

4612
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
4713

4814
export default isXHRAdapterSupported && function (config) {
4915
return new Promise(function dispatchXhrRequest(resolve, reject) {
50-
let requestData = config.data;
51-
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
52-
let {responseType, withXSRFToken} = config;
16+
const _config = resolveConfig(config);
17+
let requestData = _config.data;
18+
const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
19+
let {responseType} = _config;
5320
let onCanceled;
5421
function done() {
55-
if (config.cancelToken) {
56-
config.cancelToken.unsubscribe(onCanceled);
22+
if (_config.cancelToken) {
23+
_config.cancelToken.unsubscribe(onCanceled);
5724
}
5825

59-
if (config.signal) {
60-
config.signal.removeEventListener('abort', onCanceled);
61-
}
62-
}
63-
64-
let contentType;
65-
66-
if (utils.isFormData(requestData)) {
67-
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
68-
requestHeaders.setContentType(false); // Let the browser set it
69-
} else if ((contentType = requestHeaders.getContentType()) !== false) {
70-
// fix semicolon duplication issue for ReactNative FormData implementation
71-
const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
72-
requestHeaders.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
26+
if (_config.signal) {
27+
_config.signal.removeEventListener('abort', onCanceled);
7328
}
7429
}
7530

7631
let request = new XMLHttpRequest();
7732

78-
// HTTP basic authentication
79-
if (config.auth) {
80-
const username = config.auth.username || '';
81-
const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
82-
requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
83-
}
84-
85-
const fullPath = buildFullPath(config.baseURL, config.url);
86-
87-
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
33+
request.open(_config.method.toUpperCase(), _config.url, true);
8834

8935
// Set the request timeout in MS
90-
request.timeout = config.timeout;
36+
request.timeout = _config.timeout;
9137

9238
function onloadend() {
9339
if (!request) {
@@ -149,7 +95,7 @@ export default isXHRAdapterSupported && function (config) {
14995
return;
15096
}
15197

152-
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
98+
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, _config, request));
15399

154100
// Clean up request
155101
request = null;
@@ -159,45 +105,29 @@ export default isXHRAdapterSupported && function (config) {
159105
request.onerror = function handleError() {
160106
// Real errors are hidden from us by the browser
161107
// onerror should only fire if it's a network error
162-
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
108+
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, _config, request));
163109

164110
// Clean up request
165111
request = null;
166112
};
167113

168114
// Handle timeout
169115
request.ontimeout = function handleTimeout() {
170-
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
171-
const transitional = config.transitional || transitionalDefaults;
172-
if (config.timeoutErrorMessage) {
173-
timeoutErrorMessage = config.timeoutErrorMessage;
116+
let timeoutErrorMessage = _config.timeout ? 'timeout of ' + _config.timeout + 'ms exceeded' : 'timeout exceeded';
117+
const transitional = _config.transitional || transitionalDefaults;
118+
if (_config.timeoutErrorMessage) {
119+
timeoutErrorMessage = _config.timeoutErrorMessage;
174120
}
175121
reject(new AxiosError(
176122
timeoutErrorMessage,
177123
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
178-
config,
124+
_config,
179125
request));
180126

181127
// Clean up request
182128
request = null;
183129
};
184130

185-
// Add xsrf header
186-
// This is only done if running in a standard browser environment.
187-
// Specifically not if we're in a web worker, or react-native.
188-
if(platform.hasStandardBrowserEnv) {
189-
withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config));
190-
191-
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
192-
// Add xsrf header
193-
const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
194-
195-
if (xsrfValue) {
196-
requestHeaders.set(config.xsrfHeaderName, xsrfValue);
197-
}
198-
}
199-
}
200-
201131
// Remove Content-Type if data is undefined
202132
requestData === undefined && requestHeaders.setContentType(null);
203133

@@ -209,26 +139,26 @@ export default isXHRAdapterSupported && function (config) {
209139
}
210140

211141
// Add withCredentials to request if needed
212-
if (!utils.isUndefined(config.withCredentials)) {
213-
request.withCredentials = !!config.withCredentials;
142+
if (!utils.isUndefined(_config.withCredentials)) {
143+
request.withCredentials = !!_config.withCredentials;
214144
}
215145

216146
// Add responseType to request if needed
217147
if (responseType && responseType !== 'json') {
218-
request.responseType = config.responseType;
148+
request.responseType = _config.responseType;
219149
}
220150

221151
// Handle progress if needed
222-
if (typeof config.onDownloadProgress === 'function') {
223-
request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
152+
if (typeof _config.onDownloadProgress === 'function') {
153+
request.addEventListener('progress', progressEventReducer(_config.onDownloadProgress, true));
224154
}
225155

226156
// Not all browsers support upload events
227-
if (typeof config.onUploadProgress === 'function' && request.upload) {
228-
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
157+
if (typeof _config.onUploadProgress === 'function' && request.upload) {
158+
request.upload.addEventListener('progress', progressEventReducer(_config.onUploadProgress));
229159
}
230160

231-
if (config.cancelToken || config.signal) {
161+
if (_config.cancelToken || _config.signal) {
232162
// Handle cancellation
233163
// eslint-disable-next-line func-names
234164
onCanceled = cancel => {
@@ -240,13 +170,13 @@ export default isXHRAdapterSupported && function (config) {
240170
request = null;
241171
};
242172

243-
config.cancelToken && config.cancelToken.subscribe(onCanceled);
244-
if (config.signal) {
245-
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
173+
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
174+
if (_config.signal) {
175+
_config.signal.aborted ? onCanceled() : _config.signal.addEventListener('abort', onCanceled);
246176
}
247177
}
248178

249-
const protocol = parseProtocol(fullPath);
179+
const protocol = parseProtocol(_config.url);
250180

251181
if (protocol && platform.protocols.indexOf(protocol) === -1) {
252182
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));

‎lib/core/AxiosHeaders.js

+4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ class AxiosHeaders {
100100
setHeaders(header, valueOrRewrite)
101101
} else if(utils.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
102102
setHeaders(parseHeaders(header), valueOrRewrite);
103+
} else if (utils.isHeaders(header)) {
104+
for (const [key, value] of header.entries()) {
105+
setHeader(value, key, rewrite);
106+
}
103107
} else {
104108
header != null && setHeader(valueOrRewrite, header, rewrite);
105109
}

‎lib/defaults/index.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const defaults = {
3737

3838
transitional: transitionalDefaults,
3939

40-
adapter: ['xhr', 'http'],
40+
adapter: ['xhr', 'http', 'fetch'],
4141

4242
transformRequest: [function transformRequest(data, headers) {
4343
const contentType = headers.getContentType() || '';
@@ -58,7 +58,8 @@ const defaults = {
5858
utils.isBuffer(data) ||
5959
utils.isStream(data) ||
6060
utils.isFile(data) ||
61-
utils.isBlob(data)
61+
utils.isBlob(data) ||
62+
utils.isReadableStream(data)
6263
) {
6364
return data;
6465
}
@@ -101,6 +102,10 @@ const defaults = {
101102
const forcedJSONParsing = transitional && transitional.forcedJSONParsing;
102103
const JSONRequested = this.responseType === 'json';
103104

105+
if (utils.isResponse(data) || utils.isReadableStream(data)) {
106+
return data;
107+
}
108+
104109
if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
105110
const silentJSONParsing = transitional && transitional.silentJSONParsing;
106111
const strictJSONParsing = !silentJSONParsing && JSONRequested;

‎lib/helpers/AxiosTransformStream.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ class AxiosTransformStream extends stream.Transform{
6565

6666
process.nextTick(() => {
6767
self.emit('progress', {
68-
'loaded': bytesTransferred,
69-
'total': totalBytes,
70-
'progress': totalBytes ? (bytesTransferred / totalBytes) : undefined,
71-
'bytes': progressBytes,
72-
'rate': rate ? rate : undefined,
73-
'estimated': rate && totalBytes && bytesTransferred <= totalBytes ?
74-
(totalBytes - bytesTransferred) / rate : undefined
68+
loaded: bytesTransferred,
69+
total: totalBytes,
70+
progress: totalBytes ? (bytesTransferred / totalBytes) : undefined,
71+
bytes: progressBytes,
72+
rate: rate ? rate : undefined,
73+
estimated: rate && totalBytes && bytesTransferred <= totalBytes ?
74+
(totalBytes - bytesTransferred) / rate : undefined,
75+
lengthComputable: totalBytes != null
7576
});
7677
});
7778
}, internals.ticksRate);
7879

7980
const onFinish = () => {
80-
internals.updateProgress(true);
81+
internals.updateProgress.call(true);
8182
};
8283

8384
this.once('end', onFinish);

‎lib/helpers/composeSignals.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import CanceledError from "../cancel/CanceledError.js";
2+
import AxiosError from "../core/AxiosError.js";
3+
4+
const composeSignals = (signals, timeout) => {
5+
let controller = new AbortController();
6+
7+
let aborted;
8+
9+
const onabort = function (cancel) {
10+
if (!aborted) {
11+
aborted = true;
12+
unsubscribe();
13+
const err = cancel instanceof Error ? cancel : this.reason;
14+
controller.abort(err instanceof AxiosError ? err : new CanceledError(err instanceof Error ? err.message : err));
15+
}
16+
}
17+
18+
let timer = timeout && setTimeout(() => {
19+
onabort(new AxiosError(`timeout ${timeout} of ms exceeded`, AxiosError.ETIMEDOUT))
20+
}, timeout)
21+
22+
const unsubscribe = () => {
23+
if (signals) {
24+
timer && clearTimeout(timer);
25+
timer = null;
26+
signals.forEach(signal => {
27+
signal &&
28+
(signal.removeEventListener ? signal.removeEventListener('abort', onabort) : signal.unsubscribe(onabort));
29+
});
30+
signals = null;
31+
}
32+
}
33+
34+
signals.forEach((signal) => signal && signal.addEventListener && signal.addEventListener('abort', onabort));
35+
36+
const {signal} = controller;
37+
38+
signal.unsubscribe = unsubscribe;
39+
40+
return [signal, () => {
41+
timer && clearTimeout(timer);
42+
timer = null;
43+
}];
44+
}
45+
46+
export default composeSignals;

‎lib/helpers/progressEventReducer.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import speedometer from "./speedometer.js";
2+
import throttle from "./throttle.js";
3+
4+
export default (listener, isDownloadStream, freq = 3) => {
5+
let bytesNotified = 0;
6+
const _speedometer = speedometer(50, 250);
7+
8+
return throttle(e => {
9+
const loaded = e.loaded;
10+
const total = e.lengthComputable ? e.total : undefined;
11+
const progressBytes = loaded - bytesNotified;
12+
const rate = _speedometer(progressBytes);
13+
const inRange = loaded <= total;
14+
15+
bytesNotified = loaded;
16+
17+
const data = {
18+
loaded,
19+
total,
20+
progress: total ? (loaded / total) : undefined,
21+
bytes: progressBytes,
22+
rate: rate ? rate : undefined,
23+
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
24+
event: e,
25+
lengthComputable: total != null
26+
};
27+
28+
data[isDownloadStream ? 'download' : 'upload'] = true;
29+
30+
listener(data);
31+
}, freq);
32+
}

‎lib/helpers/resolveConfig.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import platform from "../platform/index.js";
2+
import utils from "../utils.js";
3+
import isURLSameOrigin from "./isURLSameOrigin.js";
4+
import cookies from "./cookies.js";
5+
import buildFullPath from "../core/buildFullPath.js";
6+
import mergeConfig from "../core/mergeConfig.js";
7+
import AxiosHeaders from "../core/AxiosHeaders.js";
8+
import buildURL from "./buildURL.js";
9+
10+
export default (config) => {
11+
const newConfig = mergeConfig({}, config);
12+
13+
let {data, withXSRFToken, xsrfHeaderName, xsrfCookieName, headers, auth} = newConfig;
14+
15+
newConfig.headers = headers = AxiosHeaders.from(headers);
16+
17+
newConfig.url = buildURL(buildFullPath(newConfig.baseURL, newConfig.url), config.params, config.paramsSerializer);
18+
19+
// HTTP basic authentication
20+
if (auth) {
21+
headers.set('Authorization', 'Basic ' +
22+
btoa((auth.username || '') + ':' + (auth.password ? unescape(encodeURIComponent(auth.password)) : ''))
23+
);
24+
}
25+
26+
let contentType;
27+
28+
if (utils.isFormData(data)) {
29+
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
30+
headers.setContentType(undefined); // Let the browser set it
31+
} else if ((contentType = headers.getContentType()) !== false) {
32+
// fix semicolon duplication issue for ReactNative FormData implementation
33+
const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
34+
headers.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
35+
}
36+
}
37+
38+
// Add xsrf header
39+
// This is only done if running in a standard browser environment.
40+
// Specifically not if we're in a web worker, or react-native.
41+
42+
if (platform.hasStandardBrowserEnv) {
43+
withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig));
44+
45+
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(newConfig.url))) {
46+
// Add xsrf header
47+
const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
48+
49+
if (xsrfValue) {
50+
headers.set(xsrfHeaderName, xsrfValue);
51+
}
52+
}
53+
}
54+
55+
return newConfig;
56+
}
57+

‎lib/helpers/throttle.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,23 @@ function throttle(fn, freq) {
1010
let timestamp = 0;
1111
const threshold = 1000 / freq;
1212
let timer = null;
13-
return function throttled(force, args) {
13+
return function throttled() {
14+
const force = this === true;
15+
1416
const now = Date.now();
1517
if (force || now - timestamp > threshold) {
1618
if (timer) {
1719
clearTimeout(timer);
1820
timer = null;
1921
}
2022
timestamp = now;
21-
return fn.apply(null, args);
23+
return fn.apply(null, arguments);
2224
}
2325
if (!timer) {
2426
timer = setTimeout(() => {
2527
timer = null;
2628
timestamp = Date.now();
27-
return fn.apply(null, args);
29+
return fn.apply(null, arguments);
2830
}, threshold - (now - timestamp));
2931
}
3032
};

‎lib/helpers/trackStream.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
export const streamChunk = function* (chunk, chunkSize) {
3+
let len = chunk.byteLength;
4+
5+
if (!chunkSize || len < chunkSize) {
6+
yield chunk;
7+
return;
8+
}
9+
10+
let pos = 0;
11+
let end;
12+
13+
while (pos < len) {
14+
end = pos + chunkSize;
15+
yield chunk.slice(pos, end);
16+
pos = end;
17+
}
18+
}
19+
20+
const encoder = new TextEncoder();
21+
22+
export const readBytes = async function* (iterable, chunkSize) {
23+
for await (const chunk of iterable) {
24+
yield* streamChunk(ArrayBuffer.isView(chunk) ? chunk : (await encoder.encode(String(chunk))), chunkSize);
25+
}
26+
}
27+
28+
export const trackStream = (stream, chunkSize, onProgress, onFinish) => {
29+
const iterator = readBytes(stream, chunkSize);
30+
31+
let bytes = 0;
32+
33+
return new ReadableStream({
34+
type: 'bytes',
35+
36+
async pull(controller) {
37+
const {done, value} = await iterator.next();
38+
39+
if (done) {
40+
controller.close();
41+
onFinish();
42+
return;
43+
}
44+
45+
let len = value.byteLength;
46+
onProgress && onProgress(bytes += len);
47+
controller.enqueue(new Uint8Array(value));
48+
},
49+
cancel(reason) {
50+
onFinish(reason);
51+
return iterator.return();
52+
}
53+
}, {
54+
highWaterMark: 2
55+
})
56+
}

‎lib/platform/common/utils.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ const hasStandardBrowserWebWorkerEnv = (() => {
4040
);
4141
})();
4242

43+
const origin = hasBrowserEnv && window.location.href || 'http://localhost';
44+
4345
export {
4446
hasBrowserEnv,
4547
hasStandardBrowserWebWorkerEnv,
46-
hasStandardBrowserEnv
48+
hasStandardBrowserEnv,
49+
origin
4750
}

‎lib/utils.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ const isFormData = (thing) => {
209209
*/
210210
const isURLSearchParams = kindOfTest('URLSearchParams');
211211

212+
const [isReadableStream, isRequest, isResponse, isHeaders] = ['ReadableStream', 'Request', 'Response', 'Headers'].map(kindOfTest);
213+
212214
/**
213215
* Trim excess whitespace off the beginning and end of a string
214216
*
@@ -597,8 +599,7 @@ const toObjectSet = (arrayOrString, delimiter) => {
597599
const noop = () => {}
598600

599601
const toFiniteNumber = (value, defaultValue) => {
600-
value = +value;
601-
return Number.isFinite(value) ? value : defaultValue;
602+
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
602603
}
603604

604605
const ALPHA = 'abcdefghijklmnopqrstuvwxyz'
@@ -679,6 +680,10 @@ export default {
679680
isBoolean,
680681
isObject,
681682
isPlainObject,
683+
isReadableStream,
684+
isRequest,
685+
isResponse,
686+
isHeaders,
682687
isUndefined,
683688
isDate,
684689
isFile,

‎package-lock.json

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@
135135
"stream-throttle": "^0.1.3",
136136
"string-replace-async": "^3.0.2",
137137
"terser-webpack-plugin": "^4.2.3",
138-
"typescript": "^4.9.5"
138+
"typescript": "^4.9.5",
139+
"@rollup/plugin-alias": "^5.1.0"
139140
},
140141
"browser": {
141142
"./lib/adapters/http.js": "./lib/helpers/null.js",
@@ -215,4 +216,4 @@
215216
"@commitlint/config-conventional"
216217
]
217218
}
218-
}
219+
}

‎rollup.config.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {terser} from "rollup-plugin-terser";
44
import json from '@rollup/plugin-json';
55
import { babel } from '@rollup/plugin-babel';
66
import autoExternal from 'rollup-plugin-auto-external';
7-
import bundleSize from 'rollup-plugin-bundle-size'
7+
import bundleSize from 'rollup-plugin-bundle-size';
8+
import aliasPlugin from '@rollup/plugin-alias';
89
import path from 'path';
910

1011
const lib = require("./package.json");
@@ -13,7 +14,7 @@ const name = "axios";
1314
const namedInput = './index.js';
1415
const defaultInput = './lib/axios.js';
1516

16-
const buildConfig = ({es5, browser = true, minifiedVersion = true, ...config}) => {
17+
const buildConfig = ({es5, browser = true, minifiedVersion = true, alias, ...config}) => {
1718
const {file} = config.output;
1819
const ext = path.extname(file);
1920
const basename = path.basename(file, ext);
@@ -29,9 +30,13 @@ const buildConfig = ({es5, browser = true, minifiedVersion = true, ...config}) =
2930
file: `${path.dirname(file)}/${basename}.${(minified ? ['min', ...extArr] : extArr).join('.')}`
3031
},
3132
plugins: [
33+
aliasPlugin({
34+
entries: alias || []
35+
}),
3236
json(),
3337
resolve({browser}),
3438
commonjs(),
39+
3540
minified && terser(),
3641
minified && bundleSize(),
3742
...(es5 ? [babel({
@@ -69,6 +74,21 @@ export default async () => {
6974
banner
7075
}
7176
}),
77+
// browser ESM bundle for CDN with fetch adapter only
78+
// Downsizing from 12.97 kB (gzip) to 12.23 kB (gzip)
79+
/* ...buildConfig({
80+
input: namedInput,
81+
output: {
82+
file: `dist/esm/${outputFileName}-fetch.js`,
83+
format: "esm",
84+
preferConst: true,
85+
exports: "named",
86+
banner
87+
},
88+
alias: [
89+
{ find: './xhr.js', replacement: '../helpers/null.js' }
90+
]
91+
}),*/
7292

7393
// Browser UMD bundle for CDN
7494
...buildConfig({

‎test/helpers/server.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import http from "http";
2+
import stream from "stream";
3+
import getStream from "get-stream";
4+
import {Throttle} from "stream-throttle";
5+
import formidable from "formidable";
6+
7+
export const LOCAL_SERVER_URL = 'http://localhost:4444';
8+
9+
export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
10+
11+
export const setTimeoutAsync = (ms) => new Promise(resolve=> setTimeout(resolve, ms));
12+
13+
export const startHTTPServer = (handlerOrOptions, options) => {
14+
15+
const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} =
16+
Object.assign(typeof handlerOrOptions === 'function' ? {
17+
handler: handlerOrOptions
18+
} : handlerOrOptions || {}, options);
19+
20+
return new Promise((resolve, reject) => {
21+
const server = http.createServer(handler || async function (req, res) {
22+
try {
23+
req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);
24+
25+
let dataStream = req;
26+
27+
if (useBuffering) {
28+
dataStream = stream.Readable.from(await getStream(req));
29+
}
30+
31+
let streams = [dataStream];
32+
33+
if (rate) {
34+
streams.push(new Throttle({rate}))
35+
}
36+
37+
streams.push(res);
38+
39+
stream.pipeline(streams, (err) => {
40+
err && console.log('Server warning: ' + err.message)
41+
});
42+
} catch (err){
43+
console.warn('HTTP server error:', err);
44+
}
45+
46+
}).listen(port, function (err) {
47+
err ? reject(err) : resolve(this);
48+
});
49+
50+
server.keepAliveTimeout = keepAlive;
51+
});
52+
}
53+
54+
export const stopHTTPServer = async (server, timeout = 10000) => {
55+
if (server) {
56+
if (typeof server.closeAllConnections === 'function') {
57+
server.closeAllConnections();
58+
}
59+
60+
await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
61+
}
62+
}
63+
64+
export const handleFormData = (req) => {
65+
return new Promise((resolve, reject) => {
66+
const form = new formidable.IncomingForm();
67+
68+
form.parse(req, (err, fields, files) => {
69+
if (err) {
70+
return reject(err);
71+
}
72+
73+
resolve({fields, files});
74+
});
75+
});
76+
}
77+
78+
export const nodeVersion = process.versions.node.split('.').map(v => parseInt(v, 10));
79+
80+
export const generateReadable = (length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) => {
81+
return stream.Readable.from(async function* (){
82+
let dataLength = 0;
83+
84+
while(dataLength < length) {
85+
const leftBytes = length - dataLength;
86+
87+
const chunk = Buffer.alloc(leftBytes > chunkSize? chunkSize : leftBytes);
88+
89+
dataLength += chunk.length;
90+
91+
yield chunk;
92+
93+
if (sleep) {
94+
await setTimeoutAsync(sleep);
95+
}
96+
}
97+
}());
98+
}
99+
100+
export const makeReadableStream = (chunk = 'chunk', n = 10, timeout = 100) => {
101+
return new ReadableStream({
102+
async pull(controller) {
103+
await setTimeoutAsync(timeout);
104+
n-- ? controller.enqueue(chunk) : controller.close();
105+
}
106+
},
107+
{
108+
highWaterMark: 1
109+
}
110+
)
111+
}
112+
113+
export const makeEchoStream = (echo) => new WritableStream({
114+
write(chunk) {
115+
echo && console.log(`Echo chunk`, chunk);
116+
}
117+
})

‎test/unit/adapters/fetch.js

+373
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
import assert from 'assert';
2+
import {
3+
startHTTPServer,
4+
stopHTTPServer,
5+
LOCAL_SERVER_URL,
6+
setTimeoutAsync,
7+
makeReadableStream,
8+
generateReadable,
9+
makeEchoStream
10+
} from '../../helpers/server.js';
11+
import axios from '../../../index.js';
12+
import stream from "stream";
13+
import {AbortController} from "abortcontroller-polyfill/dist/cjs-ponyfill.js";
14+
import util from "util";
15+
16+
const pipelineAsync = util.promisify(stream.pipeline);
17+
18+
const fetchAxios = axios.create({
19+
baseURL: LOCAL_SERVER_URL,
20+
adapter: 'fetch'
21+
});
22+
23+
let server;
24+
25+
describe('supports fetch with nodejs', function () {
26+
before(function () {
27+
if (typeof fetch !== 'function') {
28+
this.skip();
29+
}
30+
})
31+
32+
afterEach(async function () {
33+
await stopHTTPServer(server);
34+
35+
server = null;
36+
});
37+
38+
describe('responses', async () => {
39+
it(`should support text response type`, async () => {
40+
const originalData = 'my data';
41+
42+
server = await startHTTPServer((req, res) => res.end(originalData));
43+
44+
const {data} = await fetchAxios.get('/', {
45+
responseType: 'text'
46+
});
47+
48+
assert.deepStrictEqual(data, originalData);
49+
});
50+
51+
it(`should support arraybuffer response type`, async () => {
52+
const originalData = 'my data';
53+
54+
server = await startHTTPServer((req, res) => res.end(originalData));
55+
56+
const {data} = await fetchAxios.get('/', {
57+
responseType: 'arraybuffer'
58+
});
59+
60+
assert.deepStrictEqual(data, Uint8Array.from(await new TextEncoder().encode(originalData)).buffer);
61+
});
62+
63+
it(`should support blob response type`, async () => {
64+
const originalData = 'my data';
65+
66+
server = await startHTTPServer((req, res) => res.end(originalData));
67+
68+
const {data} = await fetchAxios.get('/', {
69+
responseType: 'blob'
70+
});
71+
72+
assert.deepStrictEqual(data, new Blob([originalData]));
73+
});
74+
75+
it(`should support stream response type`, async () => {
76+
const originalData = 'my data';
77+
78+
server = await startHTTPServer((req, res) => res.end(originalData));
79+
80+
const {data} = await fetchAxios.get('/', {
81+
responseType: 'stream'
82+
});
83+
84+
assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream');
85+
86+
let response = new Response(data);
87+
88+
assert.deepStrictEqual(await response.text(), originalData);
89+
});
90+
91+
it(`should support formData response type`, async function () {
92+
this.timeout(5000);
93+
94+
const originalData = new FormData();
95+
96+
originalData.append('x', '123');
97+
98+
server = await startHTTPServer(async (req, res) => {
99+
100+
const response = await new Response(originalData);
101+
102+
res.setHeader('Content-Type', response.headers.get('Content-Type'));
103+
104+
res.end(await response.text());
105+
});
106+
107+
const {data} = await fetchAxios.get('/', {
108+
responseType: 'formdata'
109+
});
110+
111+
assert.ok(data instanceof FormData, 'data is not instanceof FormData');
112+
113+
assert.deepStrictEqual(Object.fromEntries(data.entries()), Object.fromEntries(originalData.entries()));
114+
});
115+
116+
it(`should support json response type`, async () => {
117+
const originalData = {x: 'my data'};
118+
119+
server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)));
120+
121+
const {data} = await fetchAxios.get('/', {
122+
responseType: 'json'
123+
});
124+
125+
assert.deepStrictEqual(data, originalData);
126+
});
127+
});
128+
129+
describe("progress", () => {
130+
describe('upload', function () {
131+
it('should support upload progress capturing', async function () {
132+
this.timeout(15000);
133+
134+
server = await startHTTPServer({
135+
rate: 100 * 1024
136+
});
137+
138+
let content = '';
139+
const count = 10;
140+
const chunk = "test";
141+
const chunkLength = Buffer.byteLength(chunk);
142+
const contentLength = count * chunkLength;
143+
144+
const readable = stream.Readable.from(async function* () {
145+
let i = count;
146+
147+
while (i-- > 0) {
148+
await setTimeoutAsync(1100);
149+
content += chunk;
150+
yield chunk;
151+
}
152+
}());
153+
154+
const samples = [];
155+
156+
const {data} = await fetchAxios.post('/', readable, {
157+
onUploadProgress: ({loaded, total, progress, bytes, upload}) => {
158+
console.log(`Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
159+
160+
samples.push({
161+
loaded,
162+
total,
163+
progress,
164+
bytes,
165+
upload
166+
});
167+
},
168+
headers: {
169+
'Content-Length': contentLength
170+
},
171+
responseType: 'text'
172+
});
173+
174+
await setTimeoutAsync(500);
175+
176+
assert.strictEqual(data, content);
177+
178+
assert.deepStrictEqual(samples, Array.from(function* () {
179+
for (let i = 1; i <= 10; i++) {
180+
yield ({
181+
loaded: chunkLength * i,
182+
total: contentLength,
183+
progress: (chunkLength * i) / contentLength,
184+
bytes: 4,
185+
upload: true
186+
});
187+
}
188+
}()));
189+
});
190+
191+
it('should not fail with get method', async() => {
192+
server = await startHTTPServer((req, res) => res.end('OK'));
193+
194+
const {data} = await fetchAxios.get('/', {
195+
onUploadProgress() {
196+
197+
}
198+
});
199+
200+
assert.strictEqual(data, 'OK');
201+
});
202+
});
203+
204+
describe('download', function () {
205+
it('should support download progress capturing', async function () {
206+
this.timeout(15000);
207+
208+
server = await startHTTPServer({
209+
rate: 100 * 1024
210+
});
211+
212+
let content = '';
213+
const count = 10;
214+
const chunk = "test";
215+
const chunkLength = Buffer.byteLength(chunk);
216+
const contentLength = count * chunkLength;
217+
218+
const readable = stream.Readable.from(async function* () {
219+
let i = count;
220+
221+
while (i-- > 0) {
222+
await setTimeoutAsync(1100);
223+
content += chunk;
224+
yield chunk;
225+
}
226+
}());
227+
228+
const samples = [];
229+
230+
const {data} = await fetchAxios.post('/', readable, {
231+
onDownloadProgress: ({loaded, total, progress, bytes, download}) => {
232+
console.log(`Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
233+
234+
samples.push({
235+
loaded,
236+
total,
237+
progress,
238+
bytes,
239+
download
240+
});
241+
},
242+
headers: {
243+
'Content-Length': contentLength
244+
},
245+
responseType: 'text',
246+
maxRedirects: 0
247+
});
248+
249+
await setTimeoutAsync(500);
250+
251+
assert.strictEqual(data, content);
252+
253+
assert.deepStrictEqual(samples, Array.from(function* () {
254+
for (let i = 1; i <= 10; i++) {
255+
yield ({
256+
loaded: chunkLength * i,
257+
total: contentLength,
258+
progress: (chunkLength * i) / contentLength,
259+
bytes: 4,
260+
download: true
261+
});
262+
}
263+
}()));
264+
});
265+
});
266+
});
267+
268+
it('should support basic auth', async () => {
269+
server = await startHTTPServer((req, res) => res.end(req.headers.authorization));
270+
271+
const user = 'foo';
272+
const headers = {Authorization: 'Bearer 1234'};
273+
const res = await axios.get('http://' + user + '@localhost:4444/', {headers: headers});
274+
275+
const base64 = Buffer.from(user + ':', 'utf8').toString('base64');
276+
assert.equal(res.data, 'Basic ' + base64);
277+
});
278+
279+
it("should support stream.Readable as a payload", async () => {
280+
server = await startHTTPServer();
281+
282+
const {data} = await fetchAxios.post('/', stream.Readable.from('OK'));
283+
284+
assert.strictEqual(data, 'OK');
285+
});
286+
287+
describe('request aborting', function() {
288+
it('should be able to abort the request stream', async function () {
289+
server = await startHTTPServer({
290+
rate: 100000,
291+
useBuffering: true
292+
});
293+
294+
const controller = new AbortController();
295+
296+
setTimeout(() => {
297+
controller.abort();
298+
}, 500);
299+
300+
await assert.rejects(async () => {
301+
await fetchAxios.post('/', makeReadableStream(), {
302+
responseType: 'stream',
303+
signal: controller.signal
304+
});
305+
}, /CanceledError/);
306+
});
307+
308+
it('should be able to abort the response stream', async function () {
309+
server = await startHTTPServer((req, res) => {
310+
pipelineAsync(generateReadable(10000, 10), res);
311+
});
312+
313+
const controller = new AbortController();
314+
315+
setTimeout(() => {
316+
controller.abort(new Error('test'));
317+
}, 800);
318+
319+
const {data} = await fetchAxios.get('/', {
320+
responseType: 'stream',
321+
signal: controller.signal
322+
});
323+
324+
await assert.rejects(async () => {
325+
await data.pipeTo(makeEchoStream());
326+
}, /^(AbortError|CanceledError):/);
327+
});
328+
});
329+
330+
it('should support a timeout', async () => {
331+
server = await startHTTPServer(async(req, res) => {
332+
await setTimeoutAsync(1000);
333+
res.end('OK');
334+
});
335+
336+
const timeout = 500;
337+
338+
const ts = Date.now();
339+
340+
await assert.rejects(async() => {
341+
await fetchAxios('/', {
342+
timeout
343+
})
344+
}, /timeout/);
345+
346+
const passed = Date.now() - ts;
347+
348+
assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`);
349+
});
350+
351+
352+
it('should combine baseURL and url', async () => {
353+
server = await startHTTPServer();
354+
355+
const res = await fetchAxios('/foo');
356+
357+
assert.equal(res.config.baseURL, LOCAL_SERVER_URL);
358+
assert.equal(res.config.url, '/foo');
359+
});
360+
361+
it('should support params', async() => {
362+
server = await startHTTPServer((req, res) => res.end(req.url));
363+
364+
const {data} = await fetchAxios.get('/?test=1', {
365+
params: {
366+
foo: 1,
367+
bar: 2
368+
}
369+
});
370+
371+
assert.strictEqual(data, '/?test=1&foo=1&bar=2');
372+
});
373+
});

0 commit comments

Comments
 (0)
Please sign in to comment.