in src/SimpleWebRequest.ts [730:910]
protected _respond(errorStatusText?: string): void {
if (this._finishHandled) {
// Aborted web requests often double-finish due to odd browser behavior, but non-aborted requests shouldn't...
// Unfortunately, this assertion fires frequently in the Safari browser, presumably due to a non-standard
// XHR implementation, so we need to comment it out.
// This also might get hit during browser feature detection process
// assert(this._aborted || this._timedOut, 'Double-finished XMLHttpRequest');
return;
}
this._finishHandled = true;
this._removeFromQueue();
if (this._retryTimer) {
SimpleWebRequestOptions.clearTimeout(this._retryTimer);
this._retryTimer = undefined;
}
if (this._requestTimeoutTimer) {
SimpleWebRequestOptions.clearTimeout(this._requestTimeoutTimer);
this._requestTimeoutTimer = undefined;
}
let statusCode = 0;
let statusText: string|undefined;
if (this._xhr) {
try {
statusCode = this._xhr.status;
statusText = this._xhr.statusText || errorStatusText;
} catch (e) {
// Some browsers error when you try to read status off aborted requests
}
} else {
statusText = errorStatusText || 'Browser Error - Possible CORS or Connectivity Issue';
}
let headers: Headers = {};
let body: any;
let responseParsingException: Error | undefined;
// Build the response info
if (this._xhr) {
// Parse out headers
const headerLines = (this._xhr.getAllResponseHeaders() || '').split(/\r?\n/);
headerLines.forEach(line => {
if (line.length === 0) {
return;
}
const index = line.indexOf(':');
if (index === -1) {
headers[line] = '';
} else {
headers[line.substr(0, index).toLowerCase()] = line.substr(index + 1).trim();
}
});
// Some browsers apparently don't set the content-type header in some error conditions from getAllResponseHeaders but do return
// it from the normal getResponseHeader. No clue why, but superagent mentions it as well so it's best to just conform.
if (!headers['content-type']) {
const check = this._xhr.getResponseHeader('content-type');
if (check) {
headers['content-type'] = check;
}
}
body = this._xhr.response;
if (headers['content-type'] && isJsonContentType(headers['content-type'])) {
if (!body || !isObject(body)) {
// Response can be null if the responseType does not match what the server actually sends
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
// Only access responseText if responseType is "text" or "", otherwise it will throw
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText
if ((this._xhr.responseType === 'text' || this._xhr.responseType === '') && this._xhr.responseText) {
try {
body = JSON.parse(this._xhr.responseText);
} catch (ex) {
// If a service returns invalid JSON in a payload, we can end up here - don't crash
// responseParsingException flag will indicate that we got response from the server that was corrupted.
// This will be manifested as null on receipient side and flag can help in understanding the problem.
responseParsingException = ex;
console.warn('Failed to parse XHR JSON response');
}
}
}
}
}
if (this._xhr && this._xhr.readyState === 4 && ((statusCode >= 200 && statusCode < 300) || statusCode === 304)) {
// Happy path!
const resp: WebResponse<TBody, TOptions> = {
url: this._xhr.responseURL || this._url,
method: this._action,
requestOptions: this._options,
requestHeaders: this._xhrRequestHeaders || {},
statusCode: statusCode,
statusText: statusText,
headers: headers,
body: body as TBody,
responseParsingException: responseParsingException,
};
this._resolve!!!(resp);
} else {
let errResp: WebErrorResponse<TOptions> = {
url: (this._xhr ? this._xhr.responseURL : undefined) || this._url,
method: this._action,
requestOptions: this._options,
requestHeaders: this._xhrRequestHeaders || {},
statusCode: statusCode,
statusText: statusText,
headers: headers,
body: body,
canceled: this._aborted,
timedOut: this._timedOut,
responseParsingException: responseParsingException,
};
if (this._options.augmentErrorResponse) {
this._options.augmentErrorResponse(errResp);
}
// Policy-adaptable failure
const handleResponse = this._options.customErrorHandler
? this._options.customErrorHandler(this, errResp)
: DefaultErrorHandler(this, errResp);
const retry = handleResponse !== ErrorHandlingType.DoNotRetry && (
(this._options.retries && this._options.retries > 0) ||
handleResponse === ErrorHandlingType.PauseUntilResumed ||
handleResponse === ErrorHandlingType.RetryUncountedImmediately ||
handleResponse === ErrorHandlingType.RetryUncountedWithBackoff);
if (retry) {
if (handleResponse === ErrorHandlingType.RetryCountedWithBackoff) {
this._options.retries!!!--;
}
if (this._requestTimeoutTimer) {
SimpleWebRequestOptions.clearTimeout(this._requestTimeoutTimer);
this._requestTimeoutTimer = undefined;
}
this._aborted = false;
this._timedOut = false;
this._finishHandled = false;
// Clear the XHR since we technically just haven't started again yet...
if (this._xhr) {
this._xhr.onabort = null;
this._xhr.onerror = null;
this._xhr.onload = null;
this._xhr.onprogress = null;
this._xhr.onreadystatechange = null;
this._xhr.ontimeout = null;
this._xhr = undefined;
this._xhrRequestHeaders = undefined;
}
if (handleResponse === ErrorHandlingType.PauseUntilResumed) {
this._paused = true;
} else if (handleResponse === ErrorHandlingType.RetryUncountedImmediately) {
this._enqueue();
} else {
this._retryTimer = SimpleWebRequestOptions.setTimeout(() => {
this._retryTimer = undefined;
this._enqueue();
}, this._retryExponentialTime.getTimeAndCalculateNext());
}
} else {
// No more retries -- fail.
this._reject!!!(errResp);
}
}
// Freed up a spot, so let's see if there's other stuff pending
SimpleWebRequestBase.checkQueueProcessing();
}