| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- 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';
- import { toByteStringHeaderObject } from '../helpers/sanitizeHeaderValue.js';
-
- const DEFAULT_CHUNK_SIZE = 64 * 1024;
-
- const { isFunction } = utils;
-
- /**
- * Encode a UTF-8 string to a Latin-1 byte string for use with btoa().
- * This is a modern replacement for the deprecated unescape(encodeURIComponent(str)) pattern.
- *
- * @param {string} str The string to encode
- *
- * @returns {string} UTF-8 bytes as a Latin-1 string
- */
- const encodeUTF8 = (str) =>
- encodeURIComponent(str).replace(/%([0-9A-F]{2})/gi, (_, hex) =>
- String.fromCharCode(parseInt(hex, 16))
- );
-
- // Node's WHATWG URL parser returns `username` and `password` percent-encoded.
- // Decode before composing the `auth` option so credentials such as
- // `my%40email.com:pass` are sent as `my@email.com:pass`. Falls back to the
- // original value for malformed input so a bad encoding never throws.
- const decodeURIComponentSafe = (value) => {
- if (!utils.isString(value)) {
- return value;
- }
-
- try {
- return decodeURIComponent(value);
- } catch (error) {
- return value;
- }
- };
-
- const test = (fn, ...args) => {
- try {
- return !!fn(...args);
- } catch (e) {
- return false;
- }
- };
-
- const maybeWithAuthCredentials = (url) => {
- const protocolIndex = url.indexOf('://');
- let urlToCheck = url;
- if (protocolIndex !== -1) {
- urlToCheck = urlToCheck.slice(protocolIndex + 3);
- }
- return urlToCheck.includes('@') || urlToCheck.includes(':');
- };
-
- const factory = (env) => {
- const globalObject =
- utils.global !== undefined && utils.global !== null
- ? 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;
- const own = (key) => (utils.hasOwnProp(config, key) ? config[key] : undefined);
-
- 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;
-
- // AxiosError we raise while the request body is being streamed. Captured
- // by identity so the catch block can surface it directly, regardless of
- // how the runtime wraps the resulting fetch rejection (undici exposes it
- // as `err.cause`; some browsers drop the original error entirely).
- let pendingBodyError = null;
-
- const maxBodyLengthError = () =>
- new AxiosError(
- 'Request body larger than maxBodyLength limit',
- AxiosError.ERR_BAD_REQUEST,
- config,
- request
- );
-
- try {
- // HTTP basic authentication
- let auth = undefined;
- const configAuth = own('auth');
-
- if (configAuth) {
- const username = utils.getSafeProp(configAuth, 'username') || '';
- const password = utils.getSafeProp(configAuth, 'password') || '';
- auth = {
- username,
- password
- };
- }
-
- if (maybeWithAuthCredentials(url)) {
- const parsedURL = new URL(url, platform.origin);
-
- if (!auth && (parsedURL.username || parsedURL.password)) {
- const urlUsername = decodeURIComponentSafe(parsedURL.username);
- const urlPassword = decodeURIComponentSafe(parsedURL.password);
- auth = {
- username: urlUsername,
- password: urlPassword
- };
- }
-
- if (parsedURL.username || parsedURL.password) {
- parsedURL.username = '';
- parsedURL.password = '';
- url = parsedURL.href;
- }
- }
-
- if (auth) {
- headers.delete('authorization');
- headers.set(
- 'Authorization',
- 'Basic ' + btoa(encodeUTF8((auth.username || '') + ':' + (auth.password || '')))
- );
- }
-
- // 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 known-size bodies before dispatch using
- // the body's *actual* size — never a caller-declared Content-Length,
- // which could under-report to slip an oversized body past the check.
- // Unknown-size streams return undefined here and are counted per-chunk
- // below as fetch consumes them.
- if (hasMaxBodyLength && method !== 'get' && method !== 'head') {
- const outboundLength = await getBodyLength(data);
- if (typeof outboundLength === 'number' && isFinite(outboundLength)) {
- requestContentLength = outboundLength;
- if (outboundLength > maxBodyLength) {
- throw maxBodyLengthError();
- }
- }
- }
-
- // A streamed body under maxBodyLength must be counted as fetch consumes
- // it; its size is never trusted from a caller-declared Content-Length.
- const mustEnforceStreamBody =
- hasMaxBodyLength && (utils.isReadableStream(data) || utils.isStream(data));
-
- const trackRequestStream = (stream, onProgress, flush) =>
- trackStream(
- stream,
- DEFAULT_CHUNK_SIZE,
- (loadedBytes) => {
- if (hasMaxBodyLength && loadedBytes > maxBodyLength) {
- throw (pendingBodyError = maxBodyLengthError());
- }
- onProgress && onProgress(loadedBytes);
- },
- flush
- );
-
- if (
- supportsRequestStream &&
- method !== 'get' &&
- method !== 'head' &&
- (onUploadProgress || mustEnforceStreamBody)
- ) {
- requestContentLength =
- requestContentLength == null ? await resolveBodyLength(headers, data) : requestContentLength;
-
- // A declared length of 0 is only trusted to skip the wrap when we are
- // not enforcing a stream limit (which must not rely on that header).
- if (requestContentLength !== 0 || mustEnforceStreamBody) {
- 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] =
- (onUploadProgress &&
- progressEventDecorator(
- requestContentLength,
- progressEventReducer(asyncDecorator(onUploadProgress))
- )) ||
- [];
-
- data = trackRequestStream(_request.body, onProgress, flush);
- }
- }
- } else if (
- mustEnforceStreamBody &&
- !isRequestSupported &&
- isReadableStreamSupported &&
- method !== 'get' &&
- method !== 'head'
- ) {
- data = trackRequestStream(data);
- } else if (
- mustEnforceStreamBody &&
- isRequestSupported &&
- !supportsRequestStream &&
- method !== 'get' &&
- method !== 'head'
- ) {
- throw new AxiosError(
- 'Stream request bodies are not supported by the current fetch implementation',
- AxiosError.ERR_NOT_SUPPORT,
- config,
- request
- );
- }
-
- 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: toByteStringHeaderObject(headers.normalize()),
- body: data,
- duplex: 'half',
- credentials: isCredentialsSupported ? withCredentials : undefined,
- };
-
- request = isRequestSupported && new Request(url, resolvedOptions);
-
- let response = await (isRequestSupported
- ? _fetch(request, fetchOptions)
- : _fetch(url, resolvedOptions));
-
- const responseHeaders = AxiosHeaders.from(response.headers);
-
- // 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(responseHeaders.getContentLength());
- 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(responseHeaders.getContentLength());
-
- 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;
- }
-
- // Surface a maxBodyLength violation we raised while the request body was
- // being streamed. Matching by identity (rather than reading
- // `err.cause.isAxiosError`) keeps the error deterministic across runtimes
- // and avoids both prototype-pollution reads and mis-attributing a foreign
- // AxiosError that merely happened to land in `err.cause`.
- if (pendingBodyError) {
- request && !pendingBodyError.request && (pendingBodyError.request = request);
- throw pendingBodyError;
- }
-
- // Re-throw AxiosErrors we raised synchronously (data: URL / content-length
- // pre-checks, response size enforcement) without re-wrapping them.
- if (err instanceof AxiosError) {
- request && !err.request && (err.request = request);
- throw err;
- }
-
- 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;
|