xhr.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. 'use strict';
  2. import utils from './../utils.js';
  3. import settle from './../core/settle.js';
  4. import cookies from './../helpers/cookies.js';
  5. import buildURL from './../helpers/buildURL.js';
  6. import buildFullPath from '../core/buildFullPath.js';
  7. import isURLSameOrigin from './../helpers/isURLSameOrigin.js';
  8. import transitionalDefaults from '../defaults/transitional.js';
  9. import AxiosError from '../core/AxiosError.js';
  10. import CanceledError from '../cancel/CanceledError.js';
  11. import parseProtocol from '../helpers/parseProtocol.js';
  12. import platform from '../platform/index.js';
  13. import AxiosHeaders from '../core/AxiosHeaders.js';
  14. import speedometer from '../helpers/speedometer.js';
  15. function progressEventReducer(listener, isDownloadStream) {
  16. let bytesNotified = 0;
  17. const _speedometer = speedometer(50, 250);
  18. return e => {
  19. const loaded = e.loaded;
  20. const total = e.lengthComputable ? e.total : undefined;
  21. const progressBytes = loaded - bytesNotified;
  22. const rate = _speedometer(progressBytes);
  23. const inRange = loaded <= total;
  24. bytesNotified = loaded;
  25. const data = {
  26. loaded,
  27. total,
  28. progress: total ? (loaded / total) : undefined,
  29. bytes: progressBytes,
  30. rate: rate ? rate : undefined,
  31. estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
  32. event: e
  33. };
  34. data[isDownloadStream ? 'download' : 'upload'] = true;
  35. listener(data);
  36. };
  37. }
  38. const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
  39. export default isXHRAdapterSupported && function (config) {
  40. return new Promise(function dispatchXhrRequest(resolve, reject) {
  41. let requestData = config.data;
  42. const requestHeaders = AxiosHeaders.from(config.headers).normalize();
  43. let {responseType, withXSRFToken} = config;
  44. let onCanceled;
  45. function done() {
  46. if (config.cancelToken) {
  47. config.cancelToken.unsubscribe(onCanceled);
  48. }
  49. if (config.signal) {
  50. config.signal.removeEventListener('abort', onCanceled);
  51. }
  52. }
  53. let contentType;
  54. if (utils.isFormData(requestData)) {
  55. if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
  56. requestHeaders.setContentType(false); // Let the browser set it
  57. } else if ((contentType = requestHeaders.getContentType()) !== false) {
  58. // fix semicolon duplication issue for ReactNative FormData implementation
  59. const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
  60. requestHeaders.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
  61. }
  62. }
  63. let request = new XMLHttpRequest();
  64. // HTTP basic authentication
  65. if (config.auth) {
  66. const username = config.auth.username || '';
  67. const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
  68. requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
  69. }
  70. const fullPath = buildFullPath(config.baseURL, config.url);
  71. request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
  72. // Set the request timeout in MS
  73. request.timeout = config.timeout;
  74. function onloadend() {
  75. if (!request) {
  76. return;
  77. }
  78. // Prepare the response
  79. const responseHeaders = AxiosHeaders.from(
  80. 'getAllResponseHeaders' in request && request.getAllResponseHeaders()
  81. );
  82. const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
  83. request.responseText : request.response;
  84. const response = {
  85. data: responseData,
  86. status: request.status,
  87. statusText: request.statusText,
  88. headers: responseHeaders,
  89. config,
  90. request
  91. };
  92. settle(function _resolve(value) {
  93. resolve(value);
  94. done();
  95. }, function _reject(err) {
  96. reject(err);
  97. done();
  98. }, response);
  99. // Clean up request
  100. request = null;
  101. }
  102. if ('onloadend' in request) {
  103. // Use onloadend if available
  104. request.onloadend = onloadend;
  105. } else {
  106. // Listen for ready state to emulate onloadend
  107. request.onreadystatechange = function handleLoad() {
  108. if (!request || request.readyState !== 4) {
  109. return;
  110. }
  111. // The request errored out and we didn't get a response, this will be
  112. // handled by onerror instead
  113. // With one exception: request that using file: protocol, most browsers
  114. // will return status as 0 even though it's a successful request
  115. if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
  116. return;
  117. }
  118. // readystate handler is calling before onerror or ontimeout handlers,
  119. // so we should call onloadend on the next 'tick'
  120. setTimeout(onloadend);
  121. };
  122. }
  123. // Handle browser request cancellation (as opposed to a manual cancellation)
  124. request.onabort = function handleAbort() {
  125. if (!request) {
  126. return;
  127. }
  128. reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
  129. // Clean up request
  130. request = null;
  131. };
  132. // Handle low level network errors
  133. request.onerror = function handleError() {
  134. // Real errors are hidden from us by the browser
  135. // onerror should only fire if it's a network error
  136. reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
  137. // Clean up request
  138. request = null;
  139. };
  140. // Handle timeout
  141. request.ontimeout = function handleTimeout() {
  142. let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
  143. const transitional = config.transitional || transitionalDefaults;
  144. if (config.timeoutErrorMessage) {
  145. timeoutErrorMessage = config.timeoutErrorMessage;
  146. }
  147. reject(new AxiosError(
  148. timeoutErrorMessage,
  149. transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
  150. config,
  151. request));
  152. // Clean up request
  153. request = null;
  154. };
  155. // Add xsrf header
  156. // This is only done if running in a standard browser environment.
  157. // Specifically not if we're in a web worker, or react-native.
  158. if(platform.hasStandardBrowserEnv) {
  159. withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config));
  160. if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
  161. // Add xsrf header
  162. const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
  163. if (xsrfValue) {
  164. requestHeaders.set(config.xsrfHeaderName, xsrfValue);
  165. }
  166. }
  167. }
  168. // Remove Content-Type if data is undefined
  169. requestData === undefined && requestHeaders.setContentType(null);
  170. // Add headers to the request
  171. if ('setRequestHeader' in request) {
  172. utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
  173. request.setRequestHeader(key, val);
  174. });
  175. }
  176. // Add withCredentials to request if needed
  177. if (!utils.isUndefined(config.withCredentials)) {
  178. request.withCredentials = !!config.withCredentials;
  179. }
  180. // Add responseType to request if needed
  181. if (responseType && responseType !== 'json') {
  182. request.responseType = config.responseType;
  183. }
  184. // Handle progress if needed
  185. if (typeof config.onDownloadProgress === 'function') {
  186. request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
  187. }
  188. // Not all browsers support upload events
  189. if (typeof config.onUploadProgress === 'function' && request.upload) {
  190. request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
  191. }
  192. if (config.cancelToken || config.signal) {
  193. // Handle cancellation
  194. // eslint-disable-next-line func-names
  195. onCanceled = cancel => {
  196. if (!request) {
  197. return;
  198. }
  199. reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
  200. request.abort();
  201. request = null;
  202. };
  203. config.cancelToken && config.cancelToken.subscribe(onCanceled);
  204. if (config.signal) {
  205. config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  206. }
  207. }
  208. const protocol = parseProtocol(fullPath);
  209. if (protocol && platform.protocols.indexOf(protocol) === -1) {
  210. reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
  211. return;
  212. }
  213. // Send the request
  214. request.send(requestData || null);
  215. });
  216. }