Skip to content

Commit 489cf42

Browse files
devversionthePunderWoman
authored andcommittedNov 19, 2021
fix(common): incorrect error type for XHR errors in TestRequest (#36082)
Currently the `HttpClient` always wraps errors from XHR requests, but the underlying errors are always of type `ProgressEvent`, or don't have a native error if the status code is just indicating failure (e.g. 404). This behavior does not match in the `TestRequest` class provided by `@angular/common/http/testing` where errors are considered being of type `ErrorEvent`. This is incorrect because `ErrorEvent`s provide information for errors in scripts or files which are evaluated. Since the `HttpClient` never evaluates scripts/files, and also since XHR requests clearly are documented to emit `ProgressEvent`'s, we should change the `TestSupport` to retrieve such `ProgressEvent`'s instead of incompatible objects of type `ErrorEvent`. In favor of having a deprecation period, we keep supporting `ErrorEvent` in the `TestRequest.error` signature. Eventually, we can remove this signature in the future. Resources: * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/error_event * https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent * https://xhr.spec.whatwg.org/#event-xhr-errpr Related to: #34748. DEPRECATED: `TestRequest` from `@angular/common/http/testing` no longer accepts `ErrorEvent` when simulating XHR errors. Instead instances of `ProgressEvent` should be passed, matching with the native browser behavior. PR Close #36082
1 parent 53bdbc6 commit 489cf42

File tree

8 files changed

+69
-61
lines changed

8 files changed

+69
-61
lines changed
 

Diff for: ‎aio/content/examples/http/src/testing/http-client.spec.ts

+6-16
Original file line numberDiff line numberDiff line change
@@ -137,31 +137,21 @@ describe('HttpClient testing', () => {
137137
// #enddocregion 404
138138

139139
// #docregion network-error
140-
it('can test for network error', () => {
141-
const emsg = 'simulated network error';
140+
it('can test for network error', done => {
141+
// Create mock ProgressEvent with type `error`, raised when something goes wrong
142+
// at network level. e.g. Connection timeout, DNS error, offline, etc.
143+
const mockError = new ProgressEvent('error');
142144

143145
httpClient.get<Data[]>(testUrl).subscribe(
144146
data => fail('should have failed with the network error'),
145147
(error: HttpErrorResponse) => {
146-
expect(error.error.message).toEqual(emsg, 'message');
148+
expect(error.error).toBe(mockError);
149+
done();
147150
}
148151
);
149152

150153
const req = httpTestingController.expectOne(testUrl);
151154

152-
// Create mock ErrorEvent, raised when something goes wrong at the network level.
153-
// Connection timeout, DNS error, offline, etc
154-
const mockError = new ErrorEvent('Network error', {
155-
message: emsg,
156-
// #enddocregion network-error
157-
// The rest of this is optional and not used.
158-
// Just showing that you could provide this too.
159-
filename: 'HeroService.ts',
160-
lineno: 42,
161-
colno: 21
162-
// #docregion network-error
163-
});
164-
165155
// Respond with mock error
166156
req.error(mockError);
167157
});

Diff for: ‎aio/content/examples/testing/src/app/model/hero.service.spec.ts

+8-14
Original file line numberDiff line numberDiff line change
@@ -186,28 +186,22 @@ describe('HeroesService (with mocks)', () => {
186186
req.flush(msg, {status: 404, statusText: 'Not Found'});
187187
});
188188

189-
it('should turn network error into user-facing error', () => {
190-
const emsg = 'simulated network error';
189+
it('should turn network error into user-facing error', done => {
190+
// Create mock ProgressEvent with type `error`, raised when something goes wrong at
191+
// the network level. Connection timeout, DNS error, offline, etc.
192+
const errorEvent = new ProgressEvent('error');
191193

192194
const updateHero: Hero = { id: 1, name: 'A' };
193195
heroService.updateHero(updateHero).subscribe(
194196
heroes => fail('expected to fail'),
195-
error => expect(error.message).toContain(emsg)
197+
error => {
198+
expect(error).toBe(errorEvent);
199+
done();
200+
}
196201
);
197202

198203
const req = httpTestingController.expectOne(heroService.heroesUrl);
199204

200-
// Create mock ErrorEvent, raised when something goes wrong at the network level.
201-
// Connection timeout, DNS error, offline, etc
202-
const errorEvent = new ErrorEvent('so sad', {
203-
message: emsg,
204-
// The rest of this is optional and not used.
205-
// Just showing that you could provide this too.
206-
filename: 'HeroService.ts',
207-
lineno: 42,
208-
colno: 21
209-
});
210-
211205
// Respond with mock error
212206
req.error(errorEvent);
213207
});

Diff for: ‎aio/content/examples/testing/src/app/model/hero.service.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ export class HeroService {
8181
// TODO: send the error to remote logging infrastructure
8282
console.error(error); // log to console instead
8383

84-
const message = (error.error instanceof ErrorEvent) ?
85-
error.error.message :
86-
`server returned code ${error.status} with body "${error.error}"`;
84+
// If a native error is caught, do not transform it. We only want to
85+
// transform response errors that are not wrapped in an `Error`.
86+
if (error.error instanceof Event) {
87+
throw error.error;
88+
}
8789

90+
const message = `server returned code ${error.status} with body "${error.error}"`;
8891
// TODO: better job of transforming error for user consumption
8992
throw new Error(`${operation} failed: ${message}`);
9093
};

Diff for: ‎aio/content/examples/testing/src/app/model/testing/http-client.spec.ts

+6-14
Original file line numberDiff line numberDiff line change
@@ -121,29 +121,21 @@ describe('HttpClient testing', () => {
121121
req.flush(emsg, { status: 404, statusText: 'Not Found' });
122122
});
123123

124-
it('can test for network error', () => {
125-
const emsg = 'simulated network error';
124+
it('can test for network error', done => {
125+
// Create mock ProgressEvent with type `error`, raised when something goes wrong at
126+
// the network level. Connection timeout, DNS error, offline, etc.
127+
const errorEvent = new ProgressEvent('error');
126128

127129
httpClient.get<Data[]>(testUrl).subscribe(
128130
data => fail('should have failed with the network error'),
129131
(error: HttpErrorResponse) => {
130-
expect(error.error.message).toEqual(emsg, 'message');
132+
expect(error.error).toBe(errorEvent);
133+
done();
131134
}
132135
);
133136

134137
const req = httpTestingController.expectOne(testUrl);
135138

136-
// Create mock ErrorEvent, raised when something goes wrong at the network level.
137-
// Connection timeout, DNS error, offline, etc
138-
const errorEvent = new ErrorEvent('so sad', {
139-
message: emsg,
140-
// The rest of this is optional and not used.
141-
// Just showing that you could provide this too.
142-
filename: 'HeroService.ts',
143-
lineno: 42,
144-
colno: 21
145-
});
146-
147139
// Respond with mock error
148140
req.error(errorEvent);
149141
});

Diff for: ‎aio/content/guide/deprecations.md

+21
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ v13 -> v16
3838
| `@angular/common` | [`ReflectiveInjector`](#reflectiveinjector) | <!--v8--> v11 |
3939
| `@angular/common` | [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | <!--v9--> v11 |
4040
| `@angular/common/http` | [`XhrFactory`](api/common/http/XhrFactory) | <!--v12--> v15 |
41+
| `@angular/common/http/testing` | [`TestRequest` accepting `ErrorEvent` for error simulation](#testrequest-errorevent) | <!--v13--> v16 |
4142
| `@angular/core` | [`DefaultIterableDiffer`](#core) | <!--v7--> v11 |
4243
| `@angular/core` | [`ReflectiveKey`](#core) | <!--v8--> v11 |
4344
| `@angular/core` | [`RenderComponentType`](#core) | <!--v7--> v11 |
@@ -469,6 +470,26 @@ In ViewEngine, [JIT compilation](https://angular.io/guide/glossary#jit) required
469470

470471
Important note: this deprecation doesn't affect JIT mode in Ivy (JIT remains available with Ivy, however we are exploring a possibility of deprecating it in the future. See [RFC: Exploration of use-cases for Angular JIT compilation mode](https://github.com/angular/angular/issues/43133)).
471472

473+
{@a testrequest-errorevent}
474+
475+
### `TestRequest` accepting `ErrorEvent`
476+
477+
Angular provides utilities for testing `HttpClient`. The `TestRequest` class from
478+
`@angular/common/http/testing` mocks HTTP request objects for use with `HttpTestingController`.
479+
480+
`TestRequest` provides an API for simulating an HTTP response with an error. In earlier versions
481+
of Angular, this API accepted objects of type `ErrorEvent`, which does not match the type of
482+
error event that browsers return natively. If you use `ErrorEvent` with `TestRequest`,
483+
you should switch to `ProgressEvent`.
484+
485+
Here is an example using a `ProgressEvent`:
486+
487+
```ts
488+
const mockError = new ProgressEvent('error');
489+
const mockRequest = httpTestingController.expectOne(..);
490+
491+
mockRequest.error(mockError);
492+
```
472493

473494
{@a deprecated-cli-flags}
474495

Diff for: ‎goldens/public-api/common/http/testing/testing.md

+3-7
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,9 @@ export interface RequestMatch {
4949
export class TestRequest {
5050
constructor(request: HttpRequest<any>, observer: Observer<HttpEvent<any>>);
5151
get cancelled(): boolean;
52-
error(error: ErrorEvent, opts?: {
53-
headers?: HttpHeaders | {
54-
[name: string]: string | string[];
55-
};
56-
status?: number;
57-
statusText?: string;
58-
}): void;
52+
// @deprecated
53+
error(error: ErrorEvent, opts?: TestRequestErrorOptions): void;
54+
error(error: ProgressEvent, opts?: TestRequestErrorOptions): void;
5955
event(event: HttpEvent<any>): void;
6056
flush(body: ArrayBuffer | Blob | boolean | string | number | Object | (boolean | string | number | Object | null)[] | null, opts?: {
6157
headers?: HttpHeaders | {

Diff for: ‎packages/common/http/test/xhr_mock.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export class MockXMLHttpRequest {
5353
mockResponseHeaders: string = '';
5454

5555
listeners: {
56-
error?: (event: ErrorEvent) => void,
57-
timeout?: (event: ErrorEvent) => void,
56+
error?: (event: ProgressEvent) => void,
57+
timeout?: (event: ProgressEvent) => void,
5858
abort?: () => void,
5959
load?: () => void,
6060
progress?: (event: ProgressEvent) => void,

Diff for: ‎packages/common/http/testing/src/request.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
import {HttpErrorResponse, HttpEvent, HttpHeaders, HttpRequest, HttpResponse, HttpStatusCode} from '@angular/common/http';
1010
import {Observer} from 'rxjs';
1111

12+
/**
13+
* Type that describes options that can be used to create an error
14+
* in `TestRequest`.
15+
*/
16+
type TestRequestErrorOptions = {
17+
headers?: HttpHeaders|{[name: string]: string | string[]},
18+
status?: number,
19+
statusText?: string,
20+
};
21+
1222
/**
1323
* A mock requests that was received and is ready to be answered.
1424
*
@@ -78,12 +88,14 @@ export class TestRequest {
7888

7989
/**
8090
* Resolve the request by returning an `ErrorEvent` (e.g. simulating a network failure).
91+
* @deprecated Http requests never emit an `ErrorEvent`. Please specify a `ProgressEvent`.
92+
*/
93+
error(error: ErrorEvent, opts?: TestRequestErrorOptions): void;
94+
/**
95+
* Resolve the request by returning an `ProgressEvent` (e.g. simulating a network failure).
8196
*/
82-
error(error: ErrorEvent, opts: {
83-
headers?: HttpHeaders|{[name: string]: string | string[]},
84-
status?: number,
85-
statusText?: string,
86-
} = {}): void {
97+
error(error: ProgressEvent, opts?: TestRequestErrorOptions): void;
98+
error(error: ProgressEvent|ErrorEvent, opts: TestRequestErrorOptions = {}): void {
8799
if (this.cancelled) {
88100
throw new Error(`Cannot return an error for a cancelled request.`);
89101
}

0 commit comments

Comments
 (0)
Please sign in to comment.