fix(http): emit error on XMLHttpRequest abort event (#40767)

Before this change, when Google Chrome cancels a XMLHttpRequest, an Observable of the response
never finishes. This happens, for example, when you put your computer to sleep or just press
Ctrl+S to save the browser page. After this commit, if request is canceled or aborted an
appropriate Observable will be completed with an error.

Fixes #22324

PR Close #40767
This commit is contained in:
Dmitry Drobyshev 2021-02-09 23:20:03 +03:00 committed by Alex Rickabaugh
parent ddff6b63d7
commit 38972653fa
3 changed files with 20 additions and 3 deletions

View File

@ -312,6 +312,7 @@ export class HttpXhrBackend implements HttpBackend {
xhr.addEventListener('load', onLoad); xhr.addEventListener('load', onLoad);
xhr.addEventListener('error', onError); xhr.addEventListener('error', onError);
xhr.addEventListener('timeout', onError); xhr.addEventListener('timeout', onError);
xhr.addEventListener('abort', 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 {
return () => { return () => {
// 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('abort', onError);
xhr.removeEventListener('load', onLoad); xhr.removeEventListener('load', onLoad);
xhr.removeEventListener('timeout', onError); xhr.removeEventListener('timeout', onError);
if (req.reportProgress) { if (req.reportProgress) {

View File

@ -55,6 +55,7 @@ export class MockXMLHttpRequest {
listeners: { listeners: {
error?: (event: ErrorEvent) => void, error?: (event: ErrorEvent) => void,
timeout?: (event: ErrorEvent) => void, timeout?: (event: ErrorEvent) => void,
abort?: () => void,
load?: () => void, load?: () => void,
progress?: (event: ProgressEvent) => void, progress?: (event: ProgressEvent) => void,
uploadProgress?: (event: ProgressEvent) => void, uploadProgress?: (event: ProgressEvent) => void,
@ -71,12 +72,13 @@ export class MockXMLHttpRequest {
this.body = body; this.body = body;
} }
addEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress', handler: Function): addEventListener(
void { event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'|'abort',
handler: Function): void {
this.listeners[event] = handler as any; this.listeners[event] = handler as any;
} }
removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'): void { removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'|'abort'): void {
delete this.listeners[event]; delete this.listeners[event];
} }
@ -137,6 +139,12 @@ export class MockXMLHttpRequest {
} }
} }
mockAbortEvent(): void {
if (this.listeners.abort) {
this.listeners.abort();
}
}
abort() { abort() {
this.mockAborted = true; this.mockAborted = true;
} }

View File

@ -173,6 +173,13 @@ const XSSI_PREFIX = ')]}\'\n';
factory.mock.abort = abort; factory.mock.abort = abort;
factory.mock.mockFlush(HttpStatusCode.Ok, 'OK', 'Done'); factory.mock.mockFlush(HttpStatusCode.Ok, 'OK', 'Done');
}); });
it('emits an error when browser cancels a request', done => {
backend.handle(TEST_POST).subscribe(undefined, (err: HttpErrorResponse) => {
expect(err instanceof HttpErrorResponse).toBe(true);
done();
});
factory.mock.mockAbortEvent();
});
describe('progress events', () => { describe('progress events', () => {
it('are emitted for download progress', done => { it('are emitted for download progress', done => {
backend.handle(TEST_POST.clone({reportProgress: true})) backend.handle(TEST_POST.clone({reportProgress: true}))