axios
Version:
Promise based HTTP client for the browser and node.js
385 lines (344 loc) • 13.5 kB
JavaScript
import utils from '../utils.js';
import settle from '../core/settle.js';
import transitionalDefaults from '../defaults/transitional.js';
import AxiosError from '../core/AxiosError.js';
import CanceledError from '../cancel/CanceledError.js';
import normalizeURLForProtocolCheck from '../helpers/normalizeURLForProtocolCheck.js';
import parseProtocol from '../helpers/parseProtocol.js';
import platform from '../platform/index.js';
import AxiosHeaders from '../core/AxiosHeaders.js';
import cookies from '../helpers/cookies.js';
import { progressEventReducer } from '../helpers/progressEventReducer.js';
import resolveConfig from '../helpers/resolveConfig.js';
import { toByteStringHeaderObject } from '../helpers/sanitizeHeaderValue.js';
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported &&
function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let useAsyncXsrf;
try {
const store =
platform.hasStandardBrowserEnv && typeof window !== 'undefined' && window.cookieStore;
useAsyncXsrf = store && utils.isFunction(store.get);
} catch (e) {
// A restricted Cookie Store getter must retain the synchronous fallback.
}
let xsrfCookieName;
const _config = resolveConfig(
config,
useAsyncXsrf
? (name) => {
xsrfCookieName = name;
return null;
}
: undefined
);
let requestData = _config.data;
const requestHeaders = AxiosHeaders.from(_config.headers).normalize();
let { responseType, onUploadProgress, onDownloadProgress } = _config;
let onCanceled;
let timeoutId;
let uploadThrottled, downloadThrottled;
let flushUpload, flushDownload, flushDownloadWithEvent;
function done() {
clearTimeout(timeoutId);
// A final progress callback must not prevent the remaining cleanup.
// Report errors asynchronously, as onloadend and the throttle timer do.
try {
flushUpload && flushUpload(); // flush events
} catch (err) {
setTimeout(() => {
throw err;
});
}
try {
flushDownload && flushDownload(); // flush events
} catch (err) {
setTimeout(() => {
throw err;
});
}
// A flush can synchronously cancel the request and reenter done().
// In that case the inner call has already detached these listeners.
if (request) {
downloadThrottled && request.removeEventListener('progress', downloadThrottled);
if (request.upload) {
uploadThrottled && request.upload.removeEventListener('progress', uploadThrottled);
flushUpload && request.upload.removeEventListener('loadend', flushUpload);
}
}
_config.cancelToken && _config.cancelToken.unsubscribe(onCanceled);
_config.signal && _config.signal.removeEventListener('abort', onCanceled);
}
let request = new XMLHttpRequest();
request.open(_config.method.toUpperCase(), _config.url, true);
// Set the request timeout in MS
request.timeout = _config.timeout;
function onloadend(event) {
if (!request) {
return;
}
// Status 0 means no response was received, which onerror and onabort normally
// reject before this runs. Firefox 152 fires only readystatechange and loadend for
// navigation-canceled requests (https://bugzilla.mozilla.org/show_bug.cgi?id=1505389),
// leaving settle() to resolve them as an empty success. ECONNABORTED is the error
// onabort raised on Firefox 151. Reads over file:, which some environments report as
// status 0 on success, are excluded by the request URL's scheme after browser-style
// preprocessing, by the page origin's scheme for relative URLs (which inherit it), or
// by responseURL where implemented.
if (
request.status === 0 &&
(parseProtocol(normalizeURLForProtocolCheck(_config.url)) ||
parseProtocol(platform.origin)) !== 'file' &&
!(request.responseURL && request.responseURL.startsWith('file:'))
) {
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
done();
// Clean up request
request = null;
return;
}
// When loadend is still dispatching, flushing with it gives progress
// listeners a final delivery whose event has a live target. The legacy
// ready-state fallback has no event, so replay its pending progress.
// A throwing listener must not block settlement; rethrow asynchronously,
// matching how listener errors surface on the throttle timer path.
try {
if (event) {
flushDownloadWithEvent && flushDownloadWithEvent(event);
} else {
flushDownload && flushDownload();
}
} catch (err) {
setTimeout(() => {
throw err;
});
}
// A final progress callback can cancel the request synchronously.
if (!request) {
return;
}
// Prepare the response
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
const responseData =
!responseType || responseType === 'text' || responseType === 'json'
? request.responseText
: request.response;
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request,
};
settle(
function _resolve(value) {
resolve(value);
done();
},
function _reject(err) {
reject(err);
done();
},
response
);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.startsWith('file:'))
) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
done();
// Clean up request
request = null;
};
// Handle low level network errors
request.onerror = function handleError(event) {
// Browsers deliver a ProgressEvent in XHR onerror
// (message may be empty; when present, surface it)
// See https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/error_event
const msg = event && event.message ? event.message : 'Network Error';
const err = new AxiosError(msg, AxiosError.ERR_NETWORK, config, request);
// attach the underlying event for consumers who want details
err.event = event || null;
reject(err);
done();
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
if (!request) return;
let timeoutErrorMessage = _config.timeout
? 'timeout of ' + _config.timeout + 'ms exceeded'
: 'timeout exceeded';
const transitional = _config.transitional || transitionalDefaults;
if (_config.timeoutErrorMessage) {
timeoutErrorMessage = _config.timeoutErrorMessage;
}
reject(
new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request
)
);
done();
// Clean up request
request = null;
};
// Remove Content-Type if data is undefined
requestData === undefined && requestHeaders.setContentType(null);
// Add withCredentials to request if needed
if (!utils.isUndefined(_config.withCredentials)) {
request.withCredentials = !!_config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = _config.responseType;
}
// Handle progress if needed
if (onDownloadProgress) {
[downloadThrottled, flushDownload, flushDownloadWithEvent] = progressEventReducer(
onDownloadProgress,
true
);
request.addEventListener('progress', downloadThrottled);
}
// Not all browsers support upload events
if (onUploadProgress && request.upload) {
[uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress);
request.upload.addEventListener('progress', uploadThrottled);
request.upload.addEventListener('loadend', flushUpload);
}
if (_config.cancelToken || _config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = (cancel) => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
const xhr = request;
done();
request = null;
xhr.abort();
};
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
if (!request) return;
if (_config.signal) {
_config.signal.aborted
? onCanceled()
: _config.signal.addEventListener('abort', onCanceled);
}
}
if (!request) return;
const protocol = parseProtocol(_config.url);
if (protocol && !platform.protocols.includes(protocol)) {
reject(
new AxiosError(
'Unsupported protocol ' + protocol + ':',
AxiosError.ERR_BAD_REQUEST,
config
)
);
done();
return;
}
const sendRequest = () => {
if (!request) return;
// Apply headers after an asynchronous XSRF read so existing headers are
// replaced through AxiosHeaders rather than appended by the native XHR.
if ('setRequestHeader' in request) {
utils.forEach(
toByteStringHeaderObject(requestHeaders),
function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
}
);
}
request.send(requestData || null);
};
if (xsrfCookieName) {
// Native XHR timeout starts at send(). Cover cookie preparation and the
// subsequent request with the same budget and existing timeout handler.
const now =
typeof performance !== 'undefined' && utils.isFunction(performance.now)
? () => performance.now()
: Date.now;
const deadline = request.timeout > 0 ? now() + Number(request.timeout) : undefined;
const abortOnTimeout = () => {
if (!request) return;
const xhr = request;
xhr.ontimeout();
xhr.abort();
};
if (deadline !== undefined) {
const scheduleTimeout = () => {
const remaining = deadline - now();
if (remaining > 0) {
// Browser timers overflow above the signed 32-bit delay limit.
timeoutId = setTimeout(scheduleTimeout, Math.min(remaining, 0x7fffffff));
} else {
abortOnTimeout();
}
};
scheduleTimeout();
}
cookies
.readAsync(xsrfCookieName)
.then((value) => {
if (!request) return;
// Promise callbacks may run before an overdue timer after a busy task.
if (deadline !== undefined && now() >= deadline) {
abortOnTimeout();
return;
}
if (value) {
const headerName =
utils.hasOwnProp(_config, 'xsrfHeaderName') && _config.xsrfHeaderName;
requestHeaders.set(headerName, value);
}
sendRequest();
})
.catch((error) => {
if (!request) return;
reject(AxiosError.from(error, null, config, request));
done();
request = null;
});
} else {
sendRequest();
}
});
};