UNPKG

simplerestclients

Version:

A library of components for accessing RESTful services with javascript/typescript.

912 lines (751 loc) 35.6 kB
/** * SimpleWebRequest.ts * Author: David de Regt * Copyright: Microsoft 2016 * * Simple client for issuing web requests. */ import { attempt, isObject, isString, remove, assert, clone } from './utils'; import { ExponentialTime } from './ExponentialTime'; interface Dictionary<T> { [key: string]: T; } export interface Headers extends Dictionary<string> {} export interface Params extends Dictionary<any> {} export interface WebTransportResponseBase { url: string; method: string; statusCode: number; statusText: string | undefined; headers: Headers; responseParsingException?: Error; } export interface WebTransportResponse<TBody> extends WebTransportResponseBase { body: TBody; } export interface WebTransportErrorResponse extends WebTransportResponseBase { body: any; canceled: boolean; timedOut: boolean; } export interface RestRequestInResponse<TOptions = WebRequestOptions> { requestOptions: TOptions; requestHeaders: Headers; } export interface WebResponseBase<TOptions = WebRequestOptions> extends WebTransportResponseBase, RestRequestInResponse<TOptions> {} export interface WebErrorResponse<TOptions = WebRequestOptions> extends WebTransportErrorResponse, RestRequestInResponse<TOptions> {} export interface WebResponse<TBody, TOptions = WebRequestOptions> extends WebTransportResponse<TBody>, RestRequestInResponse<TOptions> {} export enum WebRequestPriority { DontCare = 0, Low = 1, Normal = 2, High = 3, Critical = 4 } export enum ErrorHandlingType { // Ignore retry policy, if any, and fail immediately DoNotRetry, // Retry immediately, without counting it as a failure (used when you've made some sort of change to the ) RetryUncountedImmediately, // Retry with exponential backoff, but don't count it as a failure (for 429 handling) RetryUncountedWithBackoff, // Use standard retry policy (count it as a failure, exponential backoff as policy dictates) RetryCountedWithBackoff, // Return this if you need to satisfy some condition before this request will retry (then call .resumeRetrying()). PauseUntilResumed } export interface NativeBlobFileData { uri: string; size: number; name: string; type: string; } export interface NativeFileData { file: NativeBlobFileData | File; } export interface XMLHttpRequestProgressEvent extends ProgressEvent { lengthComputable: boolean; loaded: number; path: string[]; percent: number; position: number; total: number; totalSize: number; } export type SendDataType = Params | string | NativeFileData; export interface WebRequestOptions { withCredentials?: boolean; retries?: number; priority?: WebRequestPriority; timeout?: number; acceptType?: string; customResponseType?: XMLHttpRequestResponseType; contentType?: string; sendData?: SendDataType; /* Deprecated: use overrideGetHeaders */ headers?: Headers; // Used instead of calling getHeaders. overrideGetHeaders?: Headers; // Overrides all other headers. augmentHeaders?: Headers; streamingDownloadProgress?: (responseText: string) => void; onProgress?: (progressEvent: XMLHttpRequestProgressEvent) => void; customErrorHandler?: (webRequest: SimpleWebRequestBase, errorResponse: WebErrorResponse) => ErrorHandlingType; augmentErrorResponse?: (resp: WebErrorResponse) => void; } function isJsonContentType(ct: string): boolean { return !!ct && ct.indexOf('application/json') === 0; } function isFormContentType(ct: string): boolean { return !!ct && ct.indexOf('application/x-www-form-urlencoded') === 0; } function isFormDataContentType(ct: string): boolean { return !!ct && ct.indexOf('multipart/form-data') === 0; } export const DefaultOptions: WebRequestOptions = { priority: WebRequestPriority.Normal, }; // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISimpleWebRequestOptions { // Maximum executing requests allowed. Other requests will be queued until free spots become available. MaxSimultaneousRequests: number; // We've seen cases where requests have reached completion but callbacks haven't been called (typically during failed // CORS preflight) Directly call the respond function to kick these requests and unblock the reserved slot in our queues HungRequestCleanupIntervalMs: number; // Use this to shim calls to setTimeout/clearTimeout with any other service/local function you want. setTimeout: (callback: () => void, timeoutMs?: number) => number; clearTimeout: (id: number) => void; } export let SimpleWebRequestOptions: ISimpleWebRequestOptions = { MaxSimultaneousRequests: 5, HungRequestCleanupIntervalMs: 10000, setTimeout: (callback: () => void, timeoutMs?: number) => setTimeout(callback, timeoutMs), clearTimeout: (id: number) => clearTimeout(id), }; export function DefaultErrorHandler(webRequest: SimpleWebRequestBase, errResp: WebTransportErrorResponse): ErrorHandlingType { if (errResp.canceled || !errResp.statusCode || errResp.statusCode >= 400 && errResp.statusCode < 600) { // Fail canceled/0/4xx/5xx requests immediately. // These are permenent failures, and shouldn't have retry logic applied to them. return ErrorHandlingType.DoNotRetry; } // Possible transient failure -- just retry as normal with backoff. return ErrorHandlingType.RetryCountedWithBackoff; } // Note: The ordering of this enum is used for detection logic const enum FeatureSupportStatus { Unknown, Detecting, NotSupported, Supported } // List of pending requests, sorted from most important to least important (numerically descending) let requestQueue: SimpleWebRequestBase[] = []; // List of requests blocked on _blockUNtil promises let blockedList: SimpleWebRequestBase[] = []; // List of executing (non-finished) requests -- only to keep track of number of requests to compare to the max let executingList: SimpleWebRequestBase[] = []; let hungRequestCleanupTimer: number | undefined; // Feature flag checkers for whether the current environment supports various types of XMLHttpRequest features let onLoadErrorSupportStatus = FeatureSupportStatus.Unknown; let timeoutSupportStatus = FeatureSupportStatus.Unknown; export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions = WebRequestOptions> { protected _xhr: XMLHttpRequest | undefined; protected _xhrRequestHeaders: Headers | undefined; protected _requestTimeoutTimer: number | undefined; protected _options: TOptions; protected _aborted = false; protected _timedOut = false; protected _paused = false; protected _created = Date.now(); // De-dupe result handling for two reasons so far: // 1. Various platforms have bugs where they double-resolves aborted xmlhttprequests // 2. Safari seems to have a bug where sometimes it double-resolves happily-completed xmlhttprequests protected _finishHandled = false; protected _retryTimer: number | undefined; protected _retryExponentialTime = new ExponentialTime(1000, 300000); constructor(protected _action: string, protected _url: string, protected readonly options: TOptions, protected readonly _getHeaders?: () => Headers, protected readonly _blockRequestUntil?: () => Promise<void> | undefined) { this._options = { ...DefaultOptions, ...options }; } getPriority(): WebRequestPriority { return this._options.priority || WebRequestPriority.DontCare; } abstract abort(): void; protected static checkQueueProcessing(): void { while (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) { const req = requestQueue.shift(); if (!req) { return; } blockedList.push(req); const blockPromise = (req._blockRequestUntil && req._blockRequestUntil()) || Promise.resolve(); blockPromise.then(() => { remove(blockedList, req); if (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests && !req._aborted) { executingList.push(req); SimpleWebRequest._scheduleHungRequestCleanupIfNeeded(); req._fire(); } else { req._enqueue(); } }, (err: any) => { remove(blockedList, req); // fail the request if the block promise is rejected req._respond('_blockRequestUntil rejected: ' + err); }); } } private static _scheduleHungRequestCleanupIfNeeded(): void { // Schedule a cleanup timer if needed if (executingList.length > 0 && hungRequestCleanupTimer === undefined) { hungRequestCleanupTimer = SimpleWebRequestOptions.setTimeout(this._hungRequestCleanupTimerCallback, SimpleWebRequestOptions.HungRequestCleanupIntervalMs); } else if (executingList.length === 0 && hungRequestCleanupTimer) { SimpleWebRequestOptions.clearTimeout(hungRequestCleanupTimer); hungRequestCleanupTimer = undefined; } } private static _hungRequestCleanupTimerCallback = (): void => { hungRequestCleanupTimer = undefined; executingList.filter(request => { if (request._xhr && request._xhr.readyState === 4) { console.warn('SimpleWebRequests found a completed XHR that hasn\'t invoked it\'s callback functions, manually responding'); return true; } return false; }).forEach(request => { // We need to respond outside of the initial iteration across the array since _respond mutates exeutingList request._respond(); }); SimpleWebRequest._scheduleHungRequestCleanupIfNeeded(); }; protected _removeFromQueue(): void { // Only pull from request queue and executing queue here - pulling from the blocked queue can result in requests // being queued up multiple times if _respond fires more than once (it shouldn't, but does happen in the wild) remove(executingList, this); remove(requestQueue, this); } protected _assertAndClean(expression: any, message: string): void { if (!expression) { this._removeFromQueue(); console.error(message); assert(expression, message); } } // TSLint thinks that this function is unused. Silly tslint. // tslint:disable-next-line private _fire(): void { this._xhr = new XMLHttpRequest(); this._xhrRequestHeaders = {}; // xhr.open() can throw an exception for a CSP violation. const openError = attempt(() => { // Apparently you're supposed to open the connection before adding events to it. If you don't, the node.js implementation // of XHR actually calls this.abort() at the start of open()... Bad implementations, hooray. this._xhr!!!.open(this._action, this._url, true); }); if (openError) { this._respond(openError.toString()); return; } if (this._options.timeout) { const timeoutSupported = timeoutSupportStatus; // Use manual timer if we don't know about timeout support if (timeoutSupported !== FeatureSupportStatus.Supported) { this._assertAndClean(!this._requestTimeoutTimer, 'Double-fired requestTimeoutTimer'); this._requestTimeoutTimer = SimpleWebRequestOptions.setTimeout(() => { this._requestTimeoutTimer = undefined; this._timedOut = true; this.abort(); }, this._options.timeout); } // This is our first completed request. Use it for feature detection if (timeoutSupported === FeatureSupportStatus.Supported || timeoutSupported <= FeatureSupportStatus.Detecting) { // timeout and ontimeout are part of the XMLHttpRequest Level 2 spec, should be supported in most modern browsers this._xhr.timeout = this._options.timeout; this._xhr.ontimeout = () => { timeoutSupportStatus = FeatureSupportStatus.Supported; if (timeoutSupported !== FeatureSupportStatus.Supported) { // When this request initially fired we didn't know about support, bail & let the fallback method handle this return; } this._timedOut = true; // Set aborted flag to match simple timer approach, which aborts the request and results in an _respond call this._aborted = true; this._respond('TimedOut'); }; } } const onLoadErrorSupported = onLoadErrorSupportStatus; // Use onreadystatechange if we don't know about onload support or it onload is not supported if (onLoadErrorSupported !== FeatureSupportStatus.Supported) { if (onLoadErrorSupported === FeatureSupportStatus.Unknown) { // Set global status to detecting, leave local state so we can set a timer on finish onLoadErrorSupportStatus = FeatureSupportStatus.Detecting; } this._xhr.onreadystatechange = () => { if (!this._xhr) { return; } if (this._xhr.readyState === 3 && this._options.streamingDownloadProgress && !this._aborted) { // This callback may result in cancelling the connection, so keep that in mind with any handling after it // if we decide to stop using the return after this someday down the line. i.e. this._xhr may be undefined // when we come back from this call. this._options.streamingDownloadProgress(this._xhr.responseText); return; } if (this._xhr.readyState !== 4) { // Wait for it to finish return; } // This is the first request completed (unknown status when fired, detecting now), use it for detection if (onLoadErrorSupported === FeatureSupportStatus.Unknown && onLoadErrorSupportStatus === FeatureSupportStatus.Detecting) { // If onload hasn't fired within 10 seconds of completion, detect as not supported SimpleWebRequestOptions.setTimeout(() => { if (onLoadErrorSupportStatus !== FeatureSupportStatus.Supported) { onLoadErrorSupportStatus = FeatureSupportStatus.NotSupported; } }, 10000); } this._respond(); }; } else if (this._options.streamingDownloadProgress) { // If we support onload and such, but have a streaming download handler, still trap the oRSC. this._xhr.onreadystatechange = () => { if (!this._xhr) { return; } if (this._xhr.readyState === 3 && this._options.streamingDownloadProgress && !this._aborted) { // This callback may result in cancelling the connection, so keep that in mind with any handling after it // if we decide to stop using the return after this someday down the line. i.e. this._xhr may be undefined // when we come back from this call. this._options.streamingDownloadProgress(this._xhr.responseText); } }; } if (onLoadErrorSupported !== FeatureSupportStatus.NotSupported) { // onLoad and onError are part of the XMLHttpRequest Level 2 spec, should be supported in most modern browsers this._xhr.onload = () => { onLoadErrorSupportStatus = FeatureSupportStatus.Supported; if (onLoadErrorSupported !== FeatureSupportStatus.Supported) { // When this request initially fired we didn't know about support, bail & let the fallback method handle this return; } this._respond(); }; this._xhr.onerror = () => { onLoadErrorSupportStatus = FeatureSupportStatus.Supported; if (onLoadErrorSupported !== FeatureSupportStatus.Supported) { // When this request initially fired we didn't know about support, bail & let the fallback method handle this return; } this._respond(); }; } this._xhr.onabort = () => { // If the browser cancels us (page navigation or whatever), it sometimes calls both the readystatechange and this, // so make sure we know that this is an abort. this._aborted = true; this._respond('Aborted'); }; if (this._xhr.upload && this._options.onProgress) { this._xhr.upload.onprogress = this._options.onProgress as (ev: ProgressEvent) => void; } const acceptType = this._options.acceptType || 'json'; const responseType = this._options.customResponseType || SimpleWebRequestBase._getResponseType(acceptType); const responseTypeError = attempt(() => { this._xhr!!!.responseType = responseType; }); if (responseTypeError) { // WebKit added support for the json responseType value on 09/03/2013 // https://bugs.webkit.org/show_bug.cgi?id=73648. // Versions of Safari prior to 7 and Android 4 Samsung borwsers are // known to throw an Error when setting the value "json" as the response type. // // The json response type can be ignored if not supported, because JSON payloads // are handled by mapBody anyway if (responseType !== 'json') { throw responseTypeError; } } SimpleWebRequest._setRequestHeader(this._xhr, this._xhrRequestHeaders, 'Accept', SimpleWebRequestBase.mapContentType(acceptType)); this._xhr.withCredentials = !!this._options.withCredentials; const nextHeaders = this.getRequestHeaders(); // check/process headers let headersCheck: Dictionary<boolean> = {}; Object.keys(nextHeaders).forEach(key => { const value = nextHeaders[key]; const headerLower = key.toLowerCase(); if (headerLower === 'content-type') { this._assertAndClean(false, `Don't set Content-Type with options.headers -- use it with the options.contentType property`); return; } if (headerLower === 'accept') { this._assertAndClean(false, `Don't set Accept with options.headers -- use it with the options.acceptType property`); return; } this._assertAndClean(!headersCheck[headerLower], `Setting duplicate header key: ${ headersCheck[headerLower] } and ${ key }`); if (value === undefined || value === null) { console.warn(`Tried to set header "${ key }" on request with "${ value }" value, header will be dropped`); return; } headersCheck[headerLower] = true; SimpleWebRequest._setRequestHeader(this._xhr!!!, this._xhrRequestHeaders!!!, key, value); }); if (this._options.sendData) { const contentType = SimpleWebRequestBase.mapContentType(this._options.contentType || 'json'); SimpleWebRequest._setRequestHeader(this._xhr, this._xhrRequestHeaders, 'Content-Type', contentType); const sendData = SimpleWebRequestBase.mapBody(this._options.sendData, contentType); this._xhr.send(sendData as BodyInit); } else { this._xhr.send(); } } private static _setRequestHeader(xhr: XMLHttpRequest, xhrRequestHeaders: Headers, key: string, val: string): void { xhr.setRequestHeader(key, val); xhrRequestHeaders[key] = val; } static mapContentType(contentType: string): string { if (contentType === 'json') { return 'application/json'; } else if (contentType === 'form') { return 'application/x-www-form-urlencoded'; } else { return contentType; } } static mapBody(sendData: SendDataType, contentType: string): SendDataType { if (isJsonContentType(contentType)) { if (!isString(sendData)) { return JSON.stringify(sendData); } } else if (isFormContentType(contentType)) { if (!isString(sendData) && isObject(sendData)) { const params = sendData as Params; return Object.keys(params) .map(param => encodeURIComponent(param) + (params[param] ? '=' + encodeURIComponent(params[param].toString()) : '')) .join('&'); } } else if (isFormDataContentType(contentType)) { if (isObject(sendData)) { // Note: This only works for IE10 and above. const formData = new FormData(); const params = sendData as Params; Object.keys(params) .forEach(param => formData.append(param, params[param])); return formData; } else { assert(false, 'contentType multipart/form-data must include an object as sendData'); } } return sendData; } setUrl(newUrl: string): void { this._url = newUrl; } setHeader(key: string, val: string|undefined): void { if (!this._options.augmentHeaders) { this._options.augmentHeaders = {}; } if (val) { this._options.augmentHeaders[key] = val; } else { delete this._options.augmentHeaders[key]; } } getRequestHeaders(): Headers { let headers: Headers = {}; if (this._getHeaders && !this._options.overrideGetHeaders && !this._options.headers) { headers = { ...headers, ...this._getHeaders() }; } if (this._options.overrideGetHeaders) { headers = { ...headers, ...this._options.overrideGetHeaders }; } if (this._options.headers) { headers = { ...headers, ...this._options.headers }; } if (this._options.augmentHeaders) { headers = { ...headers, ...this._options.augmentHeaders }; } return headers; } getOptions(): Readonly<WebRequestOptions> { return clone(this._options); } setPriority(newPriority: WebRequestPriority): void { if (this._options.priority === newPriority) { return; } this._options.priority = newPriority; if (this._paused) { return; } if (this._xhr) { // Already fired -- wait for it to retry for the new priority to matter return; } // Remove and re-queue remove(requestQueue, this); this._enqueue(); } resumeRetrying(): void { if (!this._paused) { assert(false, 'resumeRetrying() called but not paused!'); return; } this._paused = false; this._enqueue(); } protected _enqueue(): void { // It's possible for a request to be canceled before it's queued since onCancel fires synchronously and we set up the listener // before queueing for execution // An aborted request should never be queued for execution if (this._aborted) { return; } // Check if the current queues, if the request is already in there, nothing to enqueue if (executingList.indexOf(this) >= 0 || blockedList.indexOf(this) >= 0 || requestQueue.indexOf(this) >= 0) { return; } // Throw it on the queue const index = requestQueue.findIndex(request => // find a request with the same priority, but newer (request.getPriority() === this.getPriority() && request._created > this._created) || // or a request with lower priority (request.getPriority() < this.getPriority()), ); if (index > -1) { // add me before the found request requestQueue.splice(index, 0, this); } else { // add me at the end requestQueue.push(this); } // See if it's time to execute it SimpleWebRequestBase.checkQueueProcessing(); } private static _getResponseType(acceptType: string): XMLHttpRequestResponseType { if (acceptType === 'blob') { return 'arraybuffer'; } if (acceptType === 'text/xml' || acceptType === 'application/xml') { return 'document'; } if (acceptType === 'text/plain') { return 'text'; } return 'json'; } protected abstract _respond(errorStatusText?: string): void; } export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebRequestOptions> extends SimpleWebRequestBase<TOptions> { private _resolve?: (resp: WebResponse<TBody, TOptions>) => void; private _reject?: (resp?: any) => void; constructor(action: string, url: string, options: TOptions, getHeaders?: () => Headers, blockRequestUntil?: () => Promise<void> | undefined) { super(action, url, options, getHeaders, blockRequestUntil); } abort(): void { if (this._aborted) { assert(false, 'Already aborted ' + this._action + ' request to ' + this._url); return; } this._aborted = true; if (this._retryTimer) { SimpleWebRequestOptions.clearTimeout(this._retryTimer); this._retryTimer = undefined; } if (this._requestTimeoutTimer) { SimpleWebRequestOptions.clearTimeout(this._requestTimeoutTimer); this._requestTimeoutTimer = undefined; } if (!this._resolve) { assert(false, 'Haven\'t even fired start() yet -- can\'t abort'); return; } // Cannot rely on this._xhr.abort() to trigger this._xhr.onAbort() synchronously, thus we must trigger an early response here this._respond('Aborted'); if (this._xhr) { // Abort the in-flight request this._xhr.abort(); } } start(): Promise<WebResponse<TBody, TOptions>> { if (this._resolve) { assert(false, 'WebRequest already started'); return Promise.reject('WebRequest already started'); } const promise = new Promise<WebResponse<TBody, TOptions>>((res, rej) => { this._resolve = res; this._reject = rej; }); this._enqueue(); return promise; } 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(); } }