Skip to content

Commit

Permalink
feat(adapter): add fetch adapter; (#6371)
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitalBrainJS authored Apr 28, 2024
1 parent 751133e commit a3ff99b
Show file tree
Hide file tree
Showing 21 changed files with 1,015 additions and 127 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ jobs:

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

steps:
- uses: actions/checkout@v3
Expand Down
7 changes: 5 additions & 2 deletions index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ declare namespace axios {
| 'document'
| 'json'
| 'text'
| 'stream';
| 'stream'
| 'formdata';

type responseEncoding =
| 'ascii' | 'ASCII'
Expand Down Expand Up @@ -353,11 +354,12 @@ declare namespace axios {
upload?: boolean;
download?: boolean;
event?: BrowserProgressEvent;
lengthComputable: boolean;
}

type Milliseconds = number;

type AxiosAdapterName = 'xhr' | 'http' | string;
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | string;

type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;

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

// Alias
Expand Down
7 changes: 5 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ export type ResponseType =
| 'document'
| 'json'
| 'text'
| 'stream';
| 'stream'
| 'formdata';

export type responseEncoding =
| 'ascii' | 'ASCII'
Expand Down Expand Up @@ -294,11 +295,12 @@ export interface AxiosProgressEvent {
upload?: boolean;
download?: boolean;
event?: BrowserProgressEvent;
lengthComputable: boolean;
}

type Milliseconds = number;

type AxiosAdapterName = 'xhr' | 'http' | string;
type AxiosAdapterName = 'fetch' | 'xhr' | 'http' | string;

type AxiosAdapterConfig = AxiosAdapter | AxiosAdapterName;

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

// Alias
Expand Down
4 changes: 3 additions & 1 deletion lib/adapters/adapters.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import utils from '../utils.js';
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
import fetchAdapter from './fetch.js';
import AxiosError from "../core/AxiosError.js";

const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
xhr: xhrAdapter,
fetch: fetchAdapter
}

utils.forEach(knownAdapters, (fn, value) => {
Expand Down
197 changes: 197 additions & 0 deletions lib/adapters/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import platform from "../platform/index.js";
import utils from "../utils.js";
import AxiosError from "../core/AxiosError.js";
import composeSignals from "../helpers/composeSignals.js";
import {trackStream} from "../helpers/trackStream.js";
import AxiosHeaders from "../core/AxiosHeaders.js";
import progressEventReducer from "../helpers/progressEventReducer.js";
import resolveConfig from "../helpers/resolveConfig.js";
import settle from "../core/settle.js";

const fetchProgressDecorator = (total, fn) => {
const lengthComputable = total != null;
return (loaded) => setTimeout(() => fn({
lengthComputable,
total,
loaded
}));
}

const isFetchSupported = typeof fetch !== 'undefined';

const supportsRequestStreams = isFetchSupported && (() => {
let duplexAccessed = false;

const hasContentType = new Request(platform.origin, {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');

return duplexAccessed && !hasContentType;
})();

const DEFAULT_CHUNK_SIZE = 64 * 1024;

const resolvers = {
stream: (res) => res.body
};

isFetchSupported && ['text', 'arrayBuffer', 'blob', 'formData'].forEach(type => [
resolvers[type] = utils.isFunction(Response.prototype[type]) ? (res) => res[type]() : (_, config) => {
throw new AxiosError(`Response type ${type} is not supported`, AxiosError.ERR_NOT_SUPPORT, config);
}
])

const getBodyLength = async (body) => {
if(utils.isBlob(body)) {
return body.size;
}

if(utils.isSpecCompliantForm(body)) {
return (await new Request(body).arrayBuffer()).byteLength;
}

if(utils.isArrayBufferView(body)) {
return body.byteLength;
}

if(utils.isURLSearchParams(body)) {
body = body + '';
}

if(utils.isString(body)) {
return (await new TextEncoder().encode(body)).byteLength;
}
}

const resolveBodyLength = async (headers, body) => {
const length = utils.toFiniteNumber(headers.getContentLength());

return length == null ? getBodyLength(body) : length;
}

export default async (config) => {
let {
url,
method,
data,
signal,
cancelToken,
timeout,
onDownloadProgress,
onUploadProgress,
responseType,
headers,
withCredentials = 'same-origin',
fetchOptions
} = resolveConfig(config);

responseType = responseType ? (responseType + '').toLowerCase() : 'text';

let [composedSignal, stopTimeout] = (signal || cancelToken || timeout) ?
composeSignals([signal, cancelToken], timeout) : [];

let finished, request;

const onFinish = () => {
!finished && setTimeout(() => {
composedSignal && composedSignal.unsubscribe();
});

finished = true;
}

try {
if (onUploadProgress && supportsRequestStreams && method !== 'get' && method !== 'head') {
let requestContentLength = await resolveBodyLength(headers, data);

let _request = new Request(url, {
method,
body: data,
duplex: "half"
});

let contentTypeHeader;

if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
headers.setContentType(contentTypeHeader)
}

data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, fetchProgressDecorator(
requestContentLength,
progressEventReducer(onUploadProgress)
));
}

if (!utils.isString(withCredentials)) {
withCredentials = withCredentials ? 'cors' : 'omit';
}

request = new Request(url, {
...fetchOptions,
signal: composedSignal,
method,
headers: headers.normalize().toJSON(),
body: data,
duplex: "half",
withCredentials
});

let response = await fetch(request);

const isStreamResponse = responseType === 'stream' || responseType === 'response';

if (onDownloadProgress || isStreamResponse) {
const options = {};

Object.getOwnPropertyNames(response).forEach(prop => {
options[prop] = response[prop];
});

const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));

response = new Response(
trackStream(response.body, DEFAULT_CHUNK_SIZE, onDownloadProgress && fetchProgressDecorator(
responseContentLength,
progressEventReducer(onDownloadProgress, true)
), isStreamResponse && onFinish),
options
);
}

responseType = responseType || 'text';

let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](response, config);

!isStreamResponse && onFinish();

stopTimeout && stopTimeout();

return await new Promise((resolve, reject) => {
settle(resolve, reject, {
data: responseData,
headers: AxiosHeaders.from(response.headers),
status: response.status,
statusText: response.statusText,
config,
request
})
})
} catch (err) {
onFinish();

let {code} = err;

if (err.name === 'NetworkError') {
code = AxiosError.ERR_NETWORK;
}

throw AxiosError.from(err, code, config, request);
}
}


Loading

0 comments on commit a3ff99b

Please sign in to comment.