fetch.js 6.1 KB

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