xhr.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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. const responseType = config.responseType;
  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.isStandardBrowserEnv || platform.isStandardBrowserWebWorkerEnv) {
  56. requestHeaders.setContentType(false); // Let the browser set it
  57. } else if(!requestHeaders.getContentType(/^\s*multipart\/form-data/)){
  58. requestHeaders.setContentType('multipart/form-data'); // mobile/desktop app frameworks
  59. } else if(utils.isString(contentType = requestHeaders.getContentType())){
  60. // fix semicolon duplication issue for ReactNative FormData implementation
  61. requestHeaders.setContentType(contentType.replace(/^\s*(multipart\/form-data);+/, '$1'))
  62. }
  63. }
  64. let request = new XMLHttpRequest();
  65. // HTTP basic authentication
  66. if (config.auth) {
  67. const username = config.auth.username || '';
  68. const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
  69. requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
  70. }
  71. const fullPath = buildFullPath(config.baseURL, config.url);
  72. request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
  73. // Set the request timeout in MS
  74. request.timeout = config.timeout;
  75. function onloadend() {
  76. if (!request) {
  77. return;
  78. }
  79. // Prepare the response
  80. const responseHeaders = AxiosHeaders.from(
  81. 'getAllResponseHeaders' in request && request.getAllResponseHeaders()
  82. );
  83. const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
  84. request.responseText : request.response;
  85. const response = {
  86. data: responseData,
  87. status: request.status,
  88. statusText: request.statusText,
  89. headers: responseHeaders,
  90. config,
  91. request
  92. };
  93. settle(function _resolve(value) {
  94. resolve(value);
  95. done();
  96. }, function _reject(err) {
  97. reject(err);
  98. done();
  99. }, response);
  100. // Clean up request
  101. request = null;
  102. }
  103. if ('onloadend' in request) {
  104. // Use onloadend if available
  105. request.onloadend = onloadend;
  106. } else {
  107. // Listen for ready state to emulate onloadend
  108. request.onreadystatechange = function handleLoad() {
  109. if (!request || request.readyState !== 4) {
  110. return;
  111. }
  112. // The request errored out and we didn't get a response, this will be
  113. // handled by onerror instead
  114. // With one exception: request that using file: protocol, most browsers
  115. // will return status as 0 even though it's a successful request
  116. if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
  117. return;
  118. }
  119. // readystate handler is calling before onerror or ontimeout handlers,
  120. // so we should call onloadend on the next 'tick'
  121. setTimeout(onloadend);
  122. };
  123. }
  124. // Handle browser request cancellation (as opposed to a manual cancellation)
  125. request.onabort = function handleAbort() {
  126. if (!request) {
  127. return;
  128. }
  129. reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
  130. // Clean up request
  131. request = null;
  132. };
  133. // Handle low level network errors
  134. request.onerror = function handleError() {
  135. // Real errors are hidden from us by the browser
  136. // onerror should only fire if it's a network error
  137. reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
  138. // Clean up request
  139. request = null;
  140. };
  141. // Handle timeout
  142. request.ontimeout = function handleTimeout() {
  143. let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
  144. const transitional = config.transitional || transitionalDefaults;
  145. if (config.timeoutErrorMessage) {
  146. timeoutErrorMessage = config.timeoutErrorMessage;
  147. }
  148. reject(new AxiosError(
  149. timeoutErrorMessage,
  150. transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
  151. config,
  152. request));
  153. // Clean up request
  154. request = null;
  155. };
  156. // Add xsrf header
  157. // This is only done if running in a standard browser environment.
  158. // Specifically not if we're in a web worker, or react-native.
  159. if (platform.isStandardBrowserEnv) {
  160. // Add xsrf header
  161. const xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath))
  162. && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
  163. if (xsrfValue) {
  164. requestHeaders.set(config.xsrfHeaderName, xsrfValue);
  165. }
  166. }
  167. // Remove Content-Type if data is undefined
  168. requestData === undefined && requestHeaders.setContentType(null);
  169. // Add headers to the request
  170. if ('setRequestHeader' in request) {
  171. utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
  172. request.setRequestHeader(key, val);
  173. });
  174. }
  175. // Add withCredentials to request if needed
  176. if (!utils.isUndefined(config.withCredentials)) {
  177. request.withCredentials = !!config.withCredentials;
  178. }
  179. // Add responseType to request if needed
  180. if (responseType && responseType !== 'json') {
  181. request.responseType = config.responseType;
  182. }
  183. // Handle progress if needed
  184. if (typeof config.onDownloadProgress === 'function') {
  185. request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
  186. }
  187. // Not all browsers support upload events
  188. if (typeof config.onUploadProgress === 'function' && request.upload) {
  189. request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
  190. }
  191. if (config.cancelToken || config.signal) {
  192. // Handle cancellation
  193. // eslint-disable-next-line func-names
  194. onCanceled = cancel => {
  195. if (!request) {
  196. return;
  197. }
  198. reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
  199. request.abort();
  200. request = null;
  201. };
  202. config.cancelToken && config.cancelToken.subscribe(onCanceled);
  203. if (config.signal) {
  204. config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  205. }
  206. }
  207. const protocol = parseProtocol(fullPath);
  208. if (protocol && platform.protocols.indexOf(protocol) === -1) {
  209. reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
  210. return;
  211. }
  212. // Send the request
  213. request.send(requestData || null);
  214. });
  215. }