fetch.js 8.0 KB

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