fetch.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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 {
  8. progressEventReducer,
  9. progressEventDecorator,
  10. asyncDecorator,
  11. } from '../helpers/progressEventReducer.js';
  12. import resolveConfig from '../helpers/resolveConfig.js';
  13. import settle from '../core/settle.js';
  14. import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js';
  15. import { VERSION } from '../env/data.js';
  16. const DEFAULT_CHUNK_SIZE = 64 * 1024;
  17. const { isFunction } = utils;
  18. const test = (fn, ...args) => {
  19. try {
  20. return !!fn(...args);
  21. } catch (e) {
  22. return false;
  23. }
  24. };
  25. const factory = (env) => {
  26. const globalObject = utils.global ?? globalThis;
  27. const { ReadableStream, TextEncoder } = globalObject;
  28. env = utils.merge.call(
  29. {
  30. skipUndefined: true,
  31. },
  32. {
  33. Request: globalObject.Request,
  34. Response: globalObject.Response,
  35. },
  36. env
  37. );
  38. const { fetch: envFetch, Request, Response } = env;
  39. const isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function';
  40. const isRequestSupported = isFunction(Request);
  41. const isResponseSupported = isFunction(Response);
  42. if (!isFetchSupported) {
  43. return false;
  44. }
  45. const isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream);
  46. const encodeText =
  47. isFetchSupported &&
  48. (typeof TextEncoder === 'function'
  49. ? (
  50. (encoder) => (str) =>
  51. encoder.encode(str)
  52. )(new TextEncoder())
  53. : async (str) => new Uint8Array(await new Request(str).arrayBuffer()));
  54. const supportsRequestStream =
  55. isRequestSupported &&
  56. isReadableStreamSupported &&
  57. test(() => {
  58. let duplexAccessed = false;
  59. const request = new Request(platform.origin, {
  60. body: new ReadableStream(),
  61. method: 'POST',
  62. get duplex() {
  63. duplexAccessed = true;
  64. return 'half';
  65. },
  66. });
  67. const hasContentType = request.headers.has('Content-Type');
  68. if (request.body != null) {
  69. request.body.cancel();
  70. }
  71. return duplexAccessed && !hasContentType;
  72. });
  73. const supportsResponseStream =
  74. isResponseSupported &&
  75. isReadableStreamSupported &&
  76. test(() => utils.isReadableStream(new Response('').body));
  77. const resolvers = {
  78. stream: supportsResponseStream && ((res) => res.body),
  79. };
  80. isFetchSupported &&
  81. (() => {
  82. ['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach((type) => {
  83. !resolvers[type] &&
  84. (resolvers[type] = (res, config) => {
  85. let method = res && res[type];
  86. if (method) {
  87. return method.call(res);
  88. }
  89. throw new AxiosError(
  90. `Response type '${type}' is not supported`,
  91. AxiosError.ERR_NOT_SUPPORT,
  92. config
  93. );
  94. });
  95. });
  96. })();
  97. const getBodyLength = async (body) => {
  98. if (body == null) {
  99. return 0;
  100. }
  101. if (utils.isBlob(body)) {
  102. return body.size;
  103. }
  104. if (utils.isSpecCompliantForm(body)) {
  105. const _request = new Request(platform.origin, {
  106. method: 'POST',
  107. body,
  108. });
  109. return (await _request.arrayBuffer()).byteLength;
  110. }
  111. if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
  112. return body.byteLength;
  113. }
  114. if (utils.isURLSearchParams(body)) {
  115. body = body + '';
  116. }
  117. if (utils.isString(body)) {
  118. return (await encodeText(body)).byteLength;
  119. }
  120. };
  121. const resolveBodyLength = async (headers, body) => {
  122. const length = utils.toFiniteNumber(headers.getContentLength());
  123. return length == null ? getBodyLength(body) : length;
  124. };
  125. return async (config) => {
  126. let {
  127. url,
  128. method,
  129. data,
  130. signal,
  131. cancelToken,
  132. timeout,
  133. onDownloadProgress,
  134. onUploadProgress,
  135. responseType,
  136. headers,
  137. withCredentials = 'same-origin',
  138. fetchOptions,
  139. maxContentLength,
  140. maxBodyLength,
  141. } = resolveConfig(config);
  142. const hasMaxContentLength = utils.isNumber(maxContentLength) && maxContentLength > -1;
  143. const hasMaxBodyLength = utils.isNumber(maxBodyLength) && maxBodyLength > -1;
  144. let _fetch = envFetch || fetch;
  145. responseType = responseType ? (responseType + '').toLowerCase() : 'text';
  146. let composedSignal = composeSignals(
  147. [signal, cancelToken && cancelToken.toAbortSignal()],
  148. timeout
  149. );
  150. let request = null;
  151. const unsubscribe =
  152. composedSignal &&
  153. composedSignal.unsubscribe &&
  154. (() => {
  155. composedSignal.unsubscribe();
  156. });
  157. let requestContentLength;
  158. try {
  159. // Enforce maxContentLength for data: URLs up-front so we never materialize
  160. // an oversized payload. The HTTP adapter applies the same check (see http.js
  161. // "if (protocol === 'data:')" branch).
  162. if (hasMaxContentLength && typeof url === 'string' && url.startsWith('data:')) {
  163. const estimated = estimateDataURLDecodedBytes(url);
  164. if (estimated > maxContentLength) {
  165. throw new AxiosError(
  166. 'maxContentLength size of ' + maxContentLength + ' exceeded',
  167. AxiosError.ERR_BAD_RESPONSE,
  168. config,
  169. request
  170. );
  171. }
  172. }
  173. // Enforce maxBodyLength against the outbound request body before dispatch.
  174. // Mirrors http.js behavior (ERR_BAD_REQUEST / 'Request body larger than
  175. // maxBodyLength limit'). Skip when the body length cannot be determined
  176. // (e.g. a live ReadableStream supplied by the caller).
  177. if (hasMaxBodyLength && method !== 'get' && method !== 'head') {
  178. const outboundLength = await resolveBodyLength(headers, data);
  179. if (
  180. typeof outboundLength === 'number' &&
  181. isFinite(outboundLength) &&
  182. outboundLength > maxBodyLength
  183. ) {
  184. throw new AxiosError(
  185. 'Request body larger than maxBodyLength limit',
  186. AxiosError.ERR_BAD_REQUEST,
  187. config,
  188. request
  189. );
  190. }
  191. }
  192. if (
  193. onUploadProgress &&
  194. supportsRequestStream &&
  195. method !== 'get' &&
  196. method !== 'head' &&
  197. (requestContentLength = await resolveBodyLength(headers, data)) !== 0
  198. ) {
  199. let _request = new Request(url, {
  200. method: 'POST',
  201. body: data,
  202. duplex: 'half',
  203. });
  204. let contentTypeHeader;
  205. if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
  206. headers.setContentType(contentTypeHeader);
  207. }
  208. if (_request.body) {
  209. const [onProgress, flush] = progressEventDecorator(
  210. requestContentLength,
  211. progressEventReducer(asyncDecorator(onUploadProgress))
  212. );
  213. data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
  214. }
  215. }
  216. if (!utils.isString(withCredentials)) {
  217. withCredentials = withCredentials ? 'include' : 'omit';
  218. }
  219. // Cloudflare Workers throws when credentials are defined
  220. // see https://github.com/cloudflare/workerd/issues/902
  221. const isCredentialsSupported = isRequestSupported && 'credentials' in Request.prototype;
  222. // If data is FormData and Content-Type is multipart/form-data without boundary,
  223. // delete it so fetch can set it correctly with the boundary
  224. if (utils.isFormData(data)) {
  225. const contentType = headers.getContentType();
  226. if (
  227. contentType &&
  228. /^multipart\/form-data/i.test(contentType) &&
  229. !/boundary=/i.test(contentType)
  230. ) {
  231. headers.delete('content-type');
  232. }
  233. }
  234. // Set User-Agent header if not already set (fetch defaults to 'node' in Node.js)
  235. headers.set('User-Agent', 'axios/' + VERSION, false);
  236. const resolvedOptions = {
  237. ...fetchOptions,
  238. signal: composedSignal,
  239. method: method.toUpperCase(),
  240. headers: headers.normalize().toJSON(),
  241. body: data,
  242. duplex: 'half',
  243. credentials: isCredentialsSupported ? withCredentials : undefined,
  244. };
  245. request = isRequestSupported && new Request(url, resolvedOptions);
  246. let response = await (isRequestSupported
  247. ? _fetch(request, fetchOptions)
  248. : _fetch(url, resolvedOptions));
  249. // Cheap pre-check: if the server honestly declares a content-length that
  250. // already exceeds the cap, reject before we start streaming.
  251. if (hasMaxContentLength) {
  252. const declaredLength = utils.toFiniteNumber(response.headers.get('content-length'));
  253. if (declaredLength != null && declaredLength > maxContentLength) {
  254. throw new AxiosError(
  255. 'maxContentLength size of ' + maxContentLength + ' exceeded',
  256. AxiosError.ERR_BAD_RESPONSE,
  257. config,
  258. request
  259. );
  260. }
  261. }
  262. const isStreamResponse =
  263. supportsResponseStream && (responseType === 'stream' || responseType === 'response');
  264. if (
  265. supportsResponseStream &&
  266. response.body &&
  267. (onDownloadProgress || hasMaxContentLength || (isStreamResponse && unsubscribe))
  268. ) {
  269. const options = {};
  270. ['status', 'statusText', 'headers'].forEach((prop) => {
  271. options[prop] = response[prop];
  272. });
  273. const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));
  274. const [onProgress, flush] =
  275. (onDownloadProgress &&
  276. progressEventDecorator(
  277. responseContentLength,
  278. progressEventReducer(asyncDecorator(onDownloadProgress), true)
  279. )) ||
  280. [];
  281. let bytesRead = 0;
  282. const onChunkProgress = (loadedBytes) => {
  283. if (hasMaxContentLength) {
  284. bytesRead = loadedBytes;
  285. if (bytesRead > maxContentLength) {
  286. throw new AxiosError(
  287. 'maxContentLength size of ' + maxContentLength + ' exceeded',
  288. AxiosError.ERR_BAD_RESPONSE,
  289. config,
  290. request
  291. );
  292. }
  293. }
  294. onProgress && onProgress(loadedBytes);
  295. };
  296. response = new Response(
  297. trackStream(response.body, DEFAULT_CHUNK_SIZE, onChunkProgress, () => {
  298. flush && flush();
  299. unsubscribe && unsubscribe();
  300. }),
  301. options
  302. );
  303. }
  304. responseType = responseType || 'text';
  305. let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](
  306. response,
  307. config
  308. );
  309. // Fallback enforcement for environments without ReadableStream support
  310. // (legacy runtimes). Detect materialized size from typed output; skip
  311. // streams/Response passthrough since the user will read those themselves.
  312. if (hasMaxContentLength && !supportsResponseStream && !isStreamResponse) {
  313. let materializedSize;
  314. if (responseData != null) {
  315. if (typeof responseData.byteLength === 'number') {
  316. materializedSize = responseData.byteLength;
  317. } else if (typeof responseData.size === 'number') {
  318. materializedSize = responseData.size;
  319. } else if (typeof responseData === 'string') {
  320. materializedSize =
  321. typeof TextEncoder === 'function'
  322. ? new TextEncoder().encode(responseData).byteLength
  323. : responseData.length;
  324. }
  325. }
  326. if (typeof materializedSize === 'number' && materializedSize > maxContentLength) {
  327. throw new AxiosError(
  328. 'maxContentLength size of ' + maxContentLength + ' exceeded',
  329. AxiosError.ERR_BAD_RESPONSE,
  330. config,
  331. request
  332. );
  333. }
  334. }
  335. !isStreamResponse && unsubscribe && unsubscribe();
  336. return await new Promise((resolve, reject) => {
  337. settle(resolve, reject, {
  338. data: responseData,
  339. headers: AxiosHeaders.from(response.headers),
  340. status: response.status,
  341. statusText: response.statusText,
  342. config,
  343. request,
  344. });
  345. });
  346. } catch (err) {
  347. unsubscribe && unsubscribe();
  348. // Safari can surface fetch aborts as a DOMException-like object whose
  349. // branded getters throw. Prefer our composed signal reason before reading
  350. // the caught error, preserving timeout vs cancellation semantics.
  351. if (composedSignal && composedSignal.aborted && composedSignal.reason instanceof AxiosError) {
  352. const canceledError = composedSignal.reason;
  353. canceledError.config = config;
  354. request && (canceledError.request = request);
  355. err !== canceledError && (canceledError.cause = err);
  356. throw canceledError;
  357. }
  358. if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
  359. throw Object.assign(
  360. new AxiosError(
  361. 'Network Error',
  362. AxiosError.ERR_NETWORK,
  363. config,
  364. request,
  365. err && err.response
  366. ),
  367. {
  368. cause: err.cause || err,
  369. }
  370. );
  371. }
  372. throw AxiosError.from(err, err && err.code, config, request, err && err.response);
  373. }
  374. };
  375. };
  376. const seedCache = new Map();
  377. export const getFetch = (config) => {
  378. let env = (config && config.env) || {};
  379. const { fetch, Request, Response } = env;
  380. const seeds = [Request, Response, fetch];
  381. let len = seeds.length,
  382. i = len,
  383. seed,
  384. target,
  385. map = seedCache;
  386. while (i--) {
  387. seed = seeds[i];
  388. target = map.get(seed);
  389. target === undefined && map.set(seed, (target = i ? new Map() : factory(env)));
  390. map = target;
  391. }
  392. return target;
  393. };
  394. const adapter = getFetch();
  395. export default adapter;