fetch.js 6.5 KB

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