Skip to content

Commit 61a0b6d

Browse files
arturovtalxhub
authored andcommitted
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
1 parent f76f2eb commit 61a0b6d

File tree

3 files changed

+24
-3
lines changed

3 files changed

+24
-3
lines changed

packages/common/http/src/xhr.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export class HttpXhrBackend implements HttpBackend {
311311
// By default, register for load and error events.
312312
xhr.addEventListener('load', onLoad);
313313
xhr.addEventListener('error', onError);
314+
xhr.addEventListener('timeout', onError);
314315

315316
// Progress events are only enabled if requested.
316317
if (req.reportProgress) {
@@ -333,6 +334,7 @@ export class HttpXhrBackend implements HttpBackend {
333334
// On a cancellation, remove all registered event listeners.
334335
xhr.removeEventListener('error', onError);
335336
xhr.removeEventListener('load', onLoad);
337+
xhr.removeEventListener('timeout', onError);
336338
if (req.reportProgress) {
337339
xhr.removeEventListener('progress', onDownProgress);
338340
if (reqBody !== null && xhr.upload) {

packages/common/http/test/xhr_mock.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class MockXMLHttpRequest {
5454

5555
listeners: {
5656
error?: (event: ErrorEvent) => void,
57+
timeout?: (event: ErrorEvent) => void,
5758
load?: () => void,
5859
progress?: (event: ProgressEvent) => void,
5960
uploadProgress?: (event: ProgressEvent) => void,
@@ -70,11 +71,12 @@ export class MockXMLHttpRequest {
7071
this.body = body;
7172
}
7273

73-
addEventListener(event: 'error'|'load'|'progress'|'uploadProgress', handler: Function): void {
74+
addEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress', handler: Function):
75+
void {
7476
this.listeners[event] = handler as any;
7577
}
7678

77-
removeEventListener(event: 'error'|'load'|'progress'|'uploadProgress'): void {
79+
removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'): void {
7880
delete this.listeners[event];
7981
}
8082

@@ -129,6 +131,12 @@ export class MockXMLHttpRequest {
129131
}
130132
}
131133

134+
mockTimeoutEvent(error: any): void {
135+
if (this.listeners.timeout) {
136+
this.listeners.timeout(error);
137+
}
138+
}
139+
132140
abort() {
133141
this.mockAborted = true;
134142
}

packages/common/http/test/xhr_spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {HttpRequest} from '@angular/common/http/src/request';
1010
import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpResponse, HttpResponseBase, HttpStatusCode, HttpUploadProgressEvent} from '@angular/common/http/src/response';
1111
import {HttpXhrBackend} from '@angular/common/http/src/xhr';
12-
import {describe, it} from '@angular/core/testing/src/testing_internal';
12+
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
1313
import {Observable} from 'rxjs';
1414
import {toArray} from 'rxjs/operators';
1515

@@ -151,6 +151,17 @@ const XSSI_PREFIX = ')]}\'\n';
151151
});
152152
factory.mock.mockErrorEvent(new Error('blah'));
153153
});
154+
it('emits timeout if the request times out', done => {
155+
backend.handle(TEST_POST).subscribe({
156+
error: (error: HttpErrorResponse) => {
157+
expect(error instanceof HttpErrorResponse).toBeTrue();
158+
expect(error.error instanceof Error).toBeTrue();
159+
expect(error.url).toBe('/test');
160+
done();
161+
},
162+
});
163+
factory.mock.mockTimeoutEvent(new Error('timeout'));
164+
});
154165
it('avoids abort a request when fetch operation is completed', done => {
155166
const abort = jasmine.createSpy('abort');
156167

0 commit comments

Comments
 (0)