fetch.js 8.5 KB

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