Skip to content

Commit a3ff99b

Browse files
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+

0 commit comments

Comments
 (0)