UNPKG

axios

Version:

Promise based HTTP client for the browser and node.js

470 lines (395 loc) 14.1 kB
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, progressEventDecorator, asyncDecorator, } from '../helpers/progressEventReducer.js'; import resolveConfig from '../helpers/resolveConfig.js'; import settle from '../core/settle.js'; import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js'; import { VERSION } from '../env/data.js'; const DEFAULT_CHUNK_SIZE = 64 * 1024; const { isFunction } = utils; const test = (fn, ...args) => { try { return !!fn(...args); } catch (e) { return false; } }; const factory = (env) => { const globalObject = utils.global ?? globalThis; const { ReadableStream, TextEncoder } = globalObject; env = utils.merge.call( { skipUndefined: true, }, { Request: globalObject.Request, Response: globalObject.Response, }, env ); const { fetch: envFetch, Request, Response } = env; const isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function'; const isRequestSupported = isFunction(Request); const isResponseSupported = isFunction(Response); if (!isFetchSupported) { return false; } const isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream); const encodeText = isFetchSupported && (typeof TextEncoder === 'function' ? ( (encoder) => (str) => encoder.encode(str) )(new TextEncoder()) : async (str) => new Uint8Array(await new Request(str).arrayBuffer())); const supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(() => { let duplexAccessed = false; const request = new Request(platform.origin, { body: new ReadableStream(), method: 'POST', get duplex() { duplexAccessed = true; return 'half'; }, }); const hasContentType = request.headers.has('Content-Type'); if (request.body != null) { request.body.cancel(); } return duplexAccessed && !hasContentType; }); const supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(() => utils.isReadableStream(new Response('').body)); const resolvers = { stream: supportsResponseStream && ((res) => res.body), }; isFetchSupported && (() => { ['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach((type) => { !resolvers[type] && (resolvers[type] = (res, config) => { let method = res && res[type]; if (method) { return method.call(res); } throw new AxiosError( `Response type '${type}' is not supported`, AxiosError.ERR_NOT_SUPPORT, config ); }); }); })(); const getBodyLength = async (body) => { if (body == null) { return 0; } if (utils.isBlob(body)) { return body.size; } if (utils.isSpecCompliantForm(body)) { const _request = new Request(platform.origin, { method: 'POST', body, }); return (await _request.arrayBuffer()).byteLength; } if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) { return body.byteLength; } if (utils.isURLSearchParams(body)) { body = body + ''; } if (utils.isString(body)) { return (await encodeText(body)).byteLength; } }; const resolveBodyLength = async (headers, body) => { const length = utils.toFiniteNumber(headers.getContentLength()); return length == null ? getBodyLength(body) : length; }; return async (config) => { let { url, method, data, signal, cancelToken, timeout, onDownloadProgress, onUploadProgress, responseType, headers, withCredentials = 'same-origin', fetchOptions, maxContentLength, maxBodyLength, } = resolveConfig(config); const hasMaxContentLength = utils.isNumber(maxContentLength) && maxContentLength > -1; const hasMaxBodyLength = utils.isNumber(maxBodyLength) && maxBodyLength > -1; let _fetch = envFetch || fetch; responseType = responseType ? (responseType + '').toLowerCase() : 'text'; let composedSignal = composeSignals( [signal, cancelToken && cancelToken.toAbortSignal()], timeout ); let request = null; const unsubscribe = composedSignal && composedSignal.unsubscribe && (() => { composedSignal.unsubscribe(); }); let requestContentLength; try { // Enforce maxContentLength for data: URLs up-front so we never materialize // an oversized payload. The HTTP adapter applies the same check (see http.js // "if (protocol === 'data:')" branch). if (hasMaxContentLength && typeof url === 'string' && url.startsWith('data:')) { const estimated = estimateDataURLDecodedBytes(url); if (estimated > maxContentLength) { throw new AxiosError( 'maxContentLength size of ' + maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, request ); } } // Enforce maxBodyLength against the outbound request body before dispatch. // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than // maxBodyLength limit'). Skip when the body length cannot be determined // (e.g. a live ReadableStream supplied by the caller). if (hasMaxBodyLength && method !== 'get' && method !== 'head') { const outboundLength = await resolveBodyLength(headers, data); if ( typeof outboundLength === 'number' && isFinite(outboundLength) && outboundLength > maxBodyLength ) { throw new AxiosError( 'Request body larger than maxBodyLength limit', AxiosError.ERR_BAD_REQUEST, config, request ); } } if ( onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head' && (requestContentLength = await resolveBodyLength(headers, data)) !== 0 ) { let _request = new Request(url, { method: 'POST', body: data, duplex: 'half', }); let contentTypeHeader; if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) { headers.setContentType(contentTypeHeader); } if (_request.body) { const [onProgress, flush] = progressEventDecorator( requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress)) ); data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush); } } if (!utils.isString(withCredentials)) { withCredentials = withCredentials ? 'include' : 'omit'; } // Cloudflare Workers throws when credentials are defined // 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'); } } // Set User-Agent header if not already set (fetch defaults to 'node' in Node.js) headers.set('User-Agent', 'axios/' + VERSION, false); const resolvedOptions = { ...fetchOptions, signal: composedSignal, method: method.toUpperCase(), headers: headers.normalize().toJSON(), body: data, duplex: 'half', credentials: isCredentialsSupported ? withCredentials : undefined, }; request = isRequestSupported && new Request(url, resolvedOptions); let response = await (isRequestSupported ? _fetch(request, fetchOptions) : _fetch(url, resolvedOptions)); // Cheap pre-check: if the server honestly declares a content-length that // already exceeds the cap, reject before we start streaming. if (hasMaxContentLength) { const declaredLength = utils.toFiniteNumber(response.headers.get('content-length')); if (declaredLength != null && declaredLength > maxContentLength) { throw new AxiosError( 'maxContentLength size of ' + maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, request ); } } const isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response'); if ( supportsResponseStream && response.body && (onDownloadProgress || hasMaxContentLength || (isStreamResponse && unsubscribe)) ) { const options = {}; ['status', 'statusText', 'headers'].forEach((prop) => { options[prop] = response[prop]; }); const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length')); const [onProgress, flush] = (onDownloadProgress && progressEventDecorator( responseContentLength, progressEventReducer(asyncDecorator(onDownloadProgress), true) )) || []; let bytesRead = 0; const onChunkProgress = (loadedBytes) => { if (hasMaxContentLength) { bytesRead = loadedBytes; if (bytesRead > maxContentLength) { throw new AxiosError( 'maxContentLength size of ' + maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, request ); } } onProgress && onProgress(loadedBytes); }; response = new Response( trackStream(response.body, DEFAULT_CHUNK_SIZE, onChunkProgress, () => { flush && flush(); unsubscribe && unsubscribe(); }), options ); } responseType = responseType || 'text'; let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text']( response, config ); // Fallback enforcement for environments without ReadableStream support // (legacy runtimes). Detect materialized size from typed output; skip // streams/Response passthrough since the user will read those themselves. if (hasMaxContentLength && !supportsResponseStream && !isStreamResponse) { let materializedSize; if (responseData != null) { if (typeof responseData.byteLength === 'number') { materializedSize = responseData.byteLength; } else if (typeof responseData.size === 'number') { materializedSize = responseData.size; } else if (typeof responseData === 'string') { materializedSize = typeof TextEncoder === 'function' ? new TextEncoder().encode(responseData).byteLength : responseData.length; } } if (typeof materializedSize === 'number' && materializedSize > maxContentLength) { throw new AxiosError( 'maxContentLength size of ' + maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, request ); } } !isStreamResponse && unsubscribe && unsubscribe(); 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) { unsubscribe && unsubscribe(); // Safari can surface fetch aborts as a DOMException-like object whose // branded getters throw. Prefer our composed signal reason before reading // the caught error, preserving timeout vs cancellation semantics. if (composedSignal && composedSignal.aborted && composedSignal.reason instanceof AxiosError) { const canceledError = composedSignal.reason; canceledError.config = config; request && (canceledError.request = request); err !== canceledError && (canceledError.cause = err); throw canceledError; } if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) { throw Object.assign( new AxiosError( 'Network Error', AxiosError.ERR_NETWORK, config, request, err && err.response ), { cause: err.cause || err, } ); } throw AxiosError.from(err, err && err.code, config, request, err && err.response); } }; }; const seedCache = new Map(); export const getFetch = (config) => { let env = (config && config.env) || {}; const { fetch, Request, Response } = env; const seeds = [Request, Response, fetch]; let len = seeds.length, i = len, seed, target, map = seedCache; while (i--) { seed = seeds[i]; target = map.get(seed); target === undefined && map.set(seed, (target = i ? new Map() : factory(env))); map = target; } return target; }; const adapter = getFetch(); export default adapter;