fetch.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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. const DEFAULT_CHUNK_SIZE = 64 * 1024;
  15. const { isFunction } = utils;
  16. const globalFetchAPI = (({ Request, Response }) => ({
  17. Request,
  18. Response,
  19. }))(utils.global);
  20. const { ReadableStream, TextEncoder } = utils.global;
  21. const test = (fn, ...args) => {
  22. try {
  23. return !!fn(...args);
  24. } catch (e) {
  25. return false;
  26. }
  27. };
  28. const factory = (env) => {
  29. env = utils.merge.call(
  30. {
  31. skipUndefined: true,
  32. },
  33. globalFetchAPI,
  34. env
  35. );
  36. const { fetch: envFetch, Request, Response } = env;
  37. const isFetchSupported = envFetch ? isFunction(envFetch) : typeof fetch === 'function';
  38. const isRequestSupported = isFunction(Request);
  39. const isResponseSupported = isFunction(Response);
  40. if (!isFetchSupported) {
  41. return false;
  42. }
  43. const isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream);
  44. const encodeText =
  45. isFetchSupported &&
  46. (typeof TextEncoder === 'function'
  47. ? (
  48. (encoder) => (str) =>
  49. encoder.encode(str)
  50. )(new TextEncoder())
  51. : async (str) => new Uint8Array(await new Request(str).arrayBuffer()));
  52. const supportsRequestStream =
  53. isRequestSupported &&
  54. isReadableStreamSupported &&
  55. test(() => {
  56. let duplexAccessed = false;
  57. const request = new Request(platform.origin, {
  58. body: new ReadableStream(),
  59. method: 'POST',
  60. get duplex() {
  61. duplexAccessed = true;
  62. return 'half';
  63. },
  64. });
  65. const hasContentType = request.headers.has('Content-Type');
  66. if (request.body != null) {
  67. request.body.cancel();
  68. }
  69. return duplexAccessed && !hasContentType;
  70. });
  71. const supportsResponseStream =
  72. isResponseSupported &&
  73. isReadableStreamSupported &&
  74. test(() => utils.isReadableStream(new Response('').body));
  75. const resolvers = {
  76. stream: supportsResponseStream && ((res) => res.body),
  77. };
  78. isFetchSupported &&
  79. (() => {
  80. ['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach((type) => {
  81. !resolvers[type] &&
  82. (resolvers[type] = (res, config) => {
  83. let method = res && res[type];
  84. if (method) {
  85. return method.call(res);
  86. }
  87. throw new AxiosError(
  88. `Response type '${type}' is not supported`,
  89. AxiosError.ERR_NOT_SUPPORT,
  90. config
  91. );
  92. });
  93. });
  94. })();
  95. const getBodyLength = async (body) => {
  96. if (body == null) {
  97. return 0;
  98. }
  99. if (utils.isBlob(body)) {
  100. return body.size;
  101. }
  102. if (utils.isSpecCompliantForm(body)) {
  103. const _request = new Request(platform.origin, {
  104. method: 'POST',
  105. body,
  106. });
  107. return (await _request.arrayBuffer()).byteLength;
  108. }
  109. if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
  110. return body.byteLength;
  111. }
  112. if (utils.isURLSearchParams(body)) {
  113. body = body + '';
  114. }
  115. if (utils.isString(body)) {
  116. return (await encodeText(body)).byteLength;
  117. }
  118. };
  119. const resolveBodyLength = async (headers, body) => {
  120. const length = utils.toFiniteNumber(headers.getContentLength());
  121. return length == null ? getBodyLength(body) : length;
  122. };
  123. return async (config) => {
  124. let {
  125. url,
  126. method,
  127. data,
  128. signal,
  129. cancelToken,
  130. timeout,
  131. onDownloadProgress,
  132. onUploadProgress,
  133. responseType,
  134. headers,
  135. withCredentials = 'same-origin',
  136. fetchOptions,
  137. } = resolveConfig(config);
  138. let _fetch = envFetch || fetch;
  139. responseType = responseType ? (responseType + '').toLowerCase() : 'text';
  140. let composedSignal = composeSignals(
  141. [signal, cancelToken && cancelToken.toAbortSignal()],
  142. timeout
  143. );
  144. let request = null;
  145. const unsubscribe =
  146. composedSignal &&
  147. composedSignal.unsubscribe &&
  148. (() => {
  149. composedSignal.unsubscribe();
  150. });
  151. let requestContentLength;
  152. try {
  153. if (
  154. onUploadProgress &&
  155. supportsRequestStream &&
  156. method !== 'get' &&
  157. method !== 'head' &&
  158. (requestContentLength = await resolveBodyLength(headers, data)) !== 0
  159. ) {
  160. let _request = new Request(url, {
  161. method: 'POST',
  162. body: data,
  163. duplex: 'half',
  164. });
  165. let contentTypeHeader;
  166. if (utils.isFormData(data) && (contentTypeHeader = _request.headers.get('content-type'))) {
  167. headers.setContentType(contentTypeHeader);
  168. }
  169. if (_request.body) {
  170. const [onProgress, flush] = progressEventDecorator(
  171. requestContentLength,
  172. progressEventReducer(asyncDecorator(onUploadProgress))
  173. );
  174. data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
  175. }
  176. }
  177. if (!utils.isString(withCredentials)) {
  178. withCredentials = withCredentials ? 'include' : 'omit';
  179. }
  180. // Cloudflare Workers throws when credentials are defined
  181. // see https://github.com/cloudflare/workerd/issues/902
  182. const isCredentialsSupported = isRequestSupported && 'credentials' in Request.prototype;
  183. // If data is FormData and Content-Type is multipart/form-data without boundary,
  184. // delete it so fetch can set it correctly with the boundary
  185. if (utils.isFormData(data)) {
  186. const contentType = headers.getContentType();
  187. if (
  188. contentType &&
  189. /^multipart\/form-data/i.test(contentType) &&
  190. !/boundary=/i.test(contentType)
  191. ) {
  192. headers.delete('content-type');
  193. }
  194. }
  195. const resolvedOptions = {
  196. ...fetchOptions,
  197. signal: composedSignal,
  198. method: method.toUpperCase(),
  199. headers: headers.normalize().toJSON(),
  200. body: data,
  201. duplex: 'half',
  202. credentials: isCredentialsSupported ? withCredentials : undefined,
  203. };
  204. request = isRequestSupported && new Request(url, resolvedOptions);
  205. let response = await (isRequestSupported
  206. ? _fetch(request, fetchOptions)
  207. : _fetch(url, resolvedOptions));
  208. const isStreamResponse =
  209. supportsResponseStream && (responseType === 'stream' || responseType === 'response');
  210. if (supportsResponseStream && (onDownloadProgress || (isStreamResponse && unsubscribe))) {
  211. const options = {};
  212. ['status', 'statusText', 'headers'].forEach((prop) => {
  213. options[prop] = response[prop];
  214. });
  215. const responseContentLength = utils.toFiniteNumber(response.headers.get('content-length'));
  216. const [onProgress, flush] =
  217. (onDownloadProgress &&
  218. progressEventDecorator(
  219. responseContentLength,
  220. progressEventReducer(asyncDecorator(onDownloadProgress), true)
  221. )) ||
  222. [];
  223. response = new Response(
  224. trackStream(response.body, DEFAULT_CHUNK_SIZE, onProgress, () => {
  225. flush && flush();
  226. unsubscribe && unsubscribe();
  227. }),
  228. options
  229. );
  230. }
  231. responseType = responseType || 'text';
  232. let responseData = await resolvers[utils.findKey(resolvers, responseType) || 'text'](
  233. response,
  234. config
  235. );
  236. !isStreamResponse && unsubscribe && unsubscribe();
  237. return await new Promise((resolve, reject) => {
  238. settle(resolve, reject, {
  239. data: responseData,
  240. headers: AxiosHeaders.from(response.headers),
  241. status: response.status,
  242. statusText: response.statusText,
  243. config,
  244. request,
  245. });
  246. });
  247. } catch (err) {
  248. unsubscribe && unsubscribe();
  249. if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
  250. throw Object.assign(
  251. new AxiosError(
  252. 'Network Error',
  253. AxiosError.ERR_NETWORK,
  254. config,
  255. request,
  256. err && err.response
  257. ),
  258. {
  259. cause: err.cause || err,
  260. }
  261. );
  262. }
  263. throw AxiosError.from(err, err && err.code, config, request, err && err.response);
  264. }
  265. };
  266. };
  267. const seedCache = new Map();
  268. export const getFetch = (config) => {
  269. let env = (config && config.env) || {};
  270. const { fetch, Request, Response } = env;
  271. const seeds = [Request, Response, fetch];
  272. let len = seeds.length,
  273. i = len,
  274. seed,
  275. target,
  276. map = seedCache;
  277. while (i--) {
  278. seed = seeds[i];
  279. target = map.get(seed);
  280. target === undefined && map.set(seed, (target = i ? new Map() : factory(env)));
  281. map = target;
  282. }
  283. return target;
  284. };
  285. const adapter = getFetch();
  286. export default adapter;