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:
arturovt 2020-11-23 00:05:39 +02:00 committed by Alex Rickabaugh
parent f76f2eb381
commit 61a0b6de6d
3 changed files with 24 additions and 3 deletions

View File

@ -311,6 +311,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) {
@ -333,6 +334,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

@ -9,7 +9,7 @@
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 {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 {toArray} from 'rxjs/operators';
@ -151,6 +151,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');