fetch.js 8.5 KB

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