fix(http): complete the request on timeout (#39807)
When using the [timeout attribute](https://xhr.spec.whatwg.org/#the-timeout-attribute) and an XHR request times out, browsers trigger the `timeout` event (and execute the XHR's `ontimeout` callback). Additionally, Safari 9 handles timed-out requests in the same way, even if no `timeout` has been explicitly set on the XHR. In the above cases, `HttpClient` would fail to capture the XHR's completing (with an error), so the corresponding `Observable` would never complete. PR Close #26453 PR Close #39807
This commit is contained in:
parent
f76f2eb381
commit
61a0b6de6d
|
@ -311,6 +311,7 @@ export class HttpXhrBackend implements HttpBackend {
|
||||||
// By default, register for load and error events.
|
// By default, register for load and error events.
|
||||||
xhr.addEventListener('load', onLoad);
|
xhr.addEventListener('load', onLoad);
|
||||||
xhr.addEventListener('error', onError);
|
xhr.addEventListener('error', onError);
|
||||||
|
xhr.addEventListener('timeout', onError);
|
||||||
|
|
||||||
// Progress events are only enabled if requested.
|
// Progress events are only enabled if requested.
|
||||||
if (req.reportProgress) {
|
if (req.reportProgress) {
|
||||||
|
@ -333,6 +334,7 @@ export class HttpXhrBackend implements HttpBackend {
|
||||||
// On a cancellation, remove all registered event listeners.
|
// On a cancellation, remove all registered event listeners.
|
||||||
xhr.removeEventListener('error', onError);
|
xhr.removeEventListener('error', onError);
|
||||||
xhr.removeEventListener('load', onLoad);
|
xhr.removeEventListener('load', onLoad);
|
||||||
|
xhr.removeEventListener('timeout', onError);
|
||||||
if (req.reportProgress) {
|
if (req.reportProgress) {
|
||||||
xhr.removeEventListener('progress', onDownProgress);
|
xhr.removeEventListener('progress', onDownProgress);
|
||||||
if (reqBody !== null && xhr.upload) {
|
if (reqBody !== null && xhr.upload) {
|
||||||
|
|
|
@ -54,6 +54,7 @@ export class MockXMLHttpRequest {
|
||||||
|
|
||||||
listeners: {
|
listeners: {
|
||||||
error?: (event: ErrorEvent) => void,
|
error?: (event: ErrorEvent) => void,
|
||||||
|
timeout?: (event: ErrorEvent) => void,
|
||||||
load?: () => void,
|
load?: () => void,
|
||||||
progress?: (event: ProgressEvent) => void,
|
progress?: (event: ProgressEvent) => void,
|
||||||
uploadProgress?: (event: ProgressEvent) => void,
|
uploadProgress?: (event: ProgressEvent) => void,
|
||||||
|
@ -70,11 +71,12 @@ export class MockXMLHttpRequest {
|
||||||
this.body = body;
|
this.body = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(event: 'error'|'load'|'progress'|'uploadProgress', handler: Function): void {
|
addEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress', handler: Function):
|
||||||
|
void {
|
||||||
this.listeners[event] = handler as any;
|
this.listeners[event] = handler as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEventListener(event: 'error'|'load'|'progress'|'uploadProgress'): void {
|
removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'): void {
|
||||||
delete this.listeners[event];
|
delete this.listeners[event];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +131,12 @@ export class MockXMLHttpRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockTimeoutEvent(error: any): void {
|
||||||
|
if (this.listeners.timeout) {
|
||||||
|
this.listeners.timeout(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abort() {
|
abort() {
|
||||||
this.mockAborted = true;
|
this.mockAborted = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {HttpRequest} from '@angular/common/http/src/request';
|
import {HttpRequest} from '@angular/common/http/src/request';
|
||||||
import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpResponse, HttpResponseBase, HttpStatusCode, HttpUploadProgressEvent} from '@angular/common/http/src/response';
|
import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpResponse, HttpResponseBase, HttpStatusCode, HttpUploadProgressEvent} from '@angular/common/http/src/response';
|
||||||
import {HttpXhrBackend} from '@angular/common/http/src/xhr';
|
import {HttpXhrBackend} from '@angular/common/http/src/xhr';
|
||||||
import {describe, it} from '@angular/core/testing/src/testing_internal';
|
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {toArray} from 'rxjs/operators';
|
import {toArray} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@ -151,6 +151,17 @@ const XSSI_PREFIX = ')]}\'\n';
|
||||||
});
|
});
|
||||||
factory.mock.mockErrorEvent(new Error('blah'));
|
factory.mock.mockErrorEvent(new Error('blah'));
|
||||||
});
|
});
|
||||||
|
it('emits timeout if the request times out', done => {
|
||||||
|
backend.handle(TEST_POST).subscribe({
|
||||||
|
error: (error: HttpErrorResponse) => {
|
||||||
|
expect(error instanceof HttpErrorResponse).toBeTrue();
|
||||||
|
expect(error.error instanceof Error).toBeTrue();
|
||||||
|
expect(error.url).toBe('/test');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
factory.mock.mockTimeoutEvent(new Error('timeout'));
|
||||||
|
});
|
||||||
it('avoids abort a request when fetch operation is completed', done => {
|
it('avoids abort a request when fetch operation is completed', done => {
|
||||||
const abort = jasmine.createSpy('abort');
|
const abort = jasmine.createSpy('abort');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue