fix(http): complete the request on timeout (#40771)

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 #40771
This commit is contained in:
arturovt 2021-02-10 01:08:48 +02:00 committed by Joey Perrott
parent ce80d5ee19
commit a2a5b4add5
3 changed files with 23 additions and 2 deletions

View file

@ -310,6 +310,7 @@ export class HttpXhrBackend implements HttpBackend {
// By default, register for load and error events.
xhr.addEventListener('load', onLoad);
xhr.addEventListener('error', onError);
xhr.addEventListener('timeout', onError);
// Progress events are only enabled if requested.
if (req.reportProgress) {
@ -332,6 +333,7 @@ export class HttpXhrBackend implements HttpBackend {
// On a cancellation, remove all registered event listeners.
xhr.removeEventListener('error', onError);
xhr.removeEventListener('load', onLoad);
xhr.removeEventListener('timeout', onError);
if (req.reportProgress) {
xhr.removeEventListener('progress', onDownProgress);
if (reqBody !== null && xhr.upload) {

View file

@ -54,6 +54,7 @@ export class MockXMLHttpRequest {
listeners: {
error?: (event: ErrorEvent) => void,
timeout?: (event: ErrorEvent) => void,
load?: () => void,
progress?: (event: ProgressEvent) => void,
uploadProgress?: (event: ProgressEvent) => void,
@ -70,11 +71,12 @@ export class MockXMLHttpRequest {
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;
}
removeEventListener(event: 'error'|'load'|'progress'|'uploadProgress'): void {
removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'): void {
delete this.listeners[event];
}
@ -129,6 +131,12 @@ export class MockXMLHttpRequest {
}
}
mockTimeoutEvent(error: any): void {
if (this.listeners.timeout) {
this.listeners.timeout(error);
}
}
abort() {
this.mockAborted = true;
}

View file

@ -147,6 +147,17 @@ const XSSI_PREFIX = ')]}\'\n';
});
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 => {
const abort = jasmine.createSpy('abort');