Skip to content

Commit ea0fd02

Browse files
authored
Merge pull request #2395 from JesseZomer/convert-httperrorresponse-to-apollo-servererror
feat!: convert HTTP errors to ServerError for Apollo Client
2 parents 14207e4 + 9f3ff23 commit ea0fd02

5 files changed

Lines changed: 870 additions & 4 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'apollo-angular': major
3+
---
4+
5+
**BREAKING CHANGE**: HTTP errors now return Apollo Client's `ServerError` instead of Angular's `HttpErrorResponse`
6+
7+
When Apollo Server returns non-2xx HTTP status codes (status >= 300), apollo-angular's HTTP links now return `ServerError` from `@apollo/client/errors` instead of Angular's `HttpErrorResponse`. This enables proper error detection in errorLinks using `ServerError.is(error)` and provides consistent error handling with Apollo Client's ecosystem.
8+
9+
**Migration Guide:**
10+
11+
Before:
12+
```typescript
13+
import { HttpErrorResponse } from '@angular/common/http';
14+
15+
link.request(operation).subscribe({
16+
error: (err) => {
17+
if (err instanceof HttpErrorResponse) {
18+
console.log(err.status);
19+
console.log(err.error);
20+
}
21+
}
22+
});
23+
```
24+
25+
After:
26+
```typescript
27+
import { ServerError } from '@apollo/client/errors';
28+
29+
link.request(operation).subscribe({
30+
error: (err) => {
31+
if (ServerError.is(err)) {
32+
console.log(err.statusCode);
33+
console.log(err.bodyText);
34+
console.log(err.response.headers);
35+
}
36+
}
37+
});
38+
```
39+
40+
**Properties Changed:**
41+
- `err.status``err.statusCode`
42+
- `err.error``err.bodyText` (always string, JSON stringified for objects)
43+
- `err.headers` (Angular HttpHeaders) → `err.response.headers` (native Headers)
44+
- Access response via `err.response` which includes: `status`, `statusText`, `ok`, `url`, `type`, `redirected`
45+
46+
**Note:** This only affects HTTP-level errors (status >= 300). Network errors and other error types remain unchanged. GraphQL errors in the response body are still processed normally through Apollo Client's error handling.
47+
48+
Fixes #2394

packages/apollo-angular/http/src/http-batch-link.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { print } from 'graphql';
22
import { Observable } from 'rxjs';
3-
import { HttpClient, HttpContext, HttpHeaders } from '@angular/common/http';
3+
import { HttpClient, HttpContext, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
44
import { Injectable } from '@angular/core';
55
import { ApolloLink } from '@apollo/client';
6+
import { ServerError } from '@apollo/client/errors';
67
import { BatchLink } from '@apollo/client/link/batch';
78
import type { HttpLink } from './http-link';
89
import { Body, Context, OperationPrinter, Request } from './types';
@@ -14,6 +15,41 @@ import {
1415
prioritize,
1516
} from './utils';
1617

18+
function convertHttpErrorToApolloError(err: HttpErrorResponse): Error {
19+
// Create a Response-like object that satisfies Apollo Client's expectations
20+
const mockResponse = {
21+
status: err.status,
22+
statusText: err.statusText,
23+
ok: err.ok,
24+
url: err.url || '',
25+
headers: new Headers(),
26+
type: 'error' as ResponseType,
27+
redirected: false,
28+
} as Response;
29+
30+
// Convert Angular's HttpHeaders to native Headers
31+
err.headers.keys().forEach(key => {
32+
const values = err.headers.getAll(key);
33+
if (values) {
34+
values.forEach(value => mockResponse.headers.append(key, value));
35+
}
36+
});
37+
38+
// Get the body text
39+
const bodyText = typeof err.error === 'string' ? err.error : JSON.stringify(err.error || {});
40+
41+
// Return ServerError for non-2xx status codes (following Apollo Client's logic)
42+
if (err.status >= 300) {
43+
return new ServerError(`Response not successful: Received status code ${err.status}`, {
44+
response: mockResponse,
45+
bodyText,
46+
});
47+
}
48+
49+
// For other HttpErrorResponse cases, return a generic error
50+
return new Error(err.message);
51+
}
52+
1753
export declare namespace HttpBatchLink {
1854
export type Options = {
1955
batchMax?: number;
@@ -89,7 +125,11 @@ export class HttpBatchLinkHandler extends ApolloLink {
89125
throw new Error('File upload is not available when combined with Batching');
90126
}).subscribe({
91127
next: result => observer.next(result.body),
92-
error: err => observer.error(err),
128+
error: err => {
129+
if (err instanceof HttpErrorResponse)
130+
observer.error(convertHttpErrorToApolloError(err));
131+
else observer.error(err);
132+
},
93133
complete: () => observer.complete(),
94134
});
95135

packages/apollo-angular/http/src/http-link.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { print } from 'graphql';
22
import { Observable } from 'rxjs';
3-
import { HttpClient, HttpContext } from '@angular/common/http';
3+
import { HttpClient, HttpContext, HttpErrorResponse } from '@angular/common/http';
44
import { Injectable } from '@angular/core';
55
import { ApolloLink } from '@apollo/client';
6+
import { ServerError } from '@apollo/client/errors';
67
import { pick } from './http-batch-link';
78
import {
89
Body,
@@ -15,6 +16,41 @@ import {
1516
} from './types';
1617
import { createHeadersWithClientAwareness, fetch, mergeHeaders, mergeHttpContext } from './utils';
1718

19+
function convertHttpErrorToApolloError(err: HttpErrorResponse): Error {
20+
// Create a Response-like object that satisfies Apollo Client's expectations
21+
const mockResponse = {
22+
status: err.status,
23+
statusText: err.statusText,
24+
ok: err.ok,
25+
url: err.url || '',
26+
headers: new Headers(),
27+
type: 'error' as ResponseType,
28+
redirected: false,
29+
} as Response;
30+
31+
// Convert Angular's HttpHeaders to native Headers
32+
err.headers.keys().forEach(key => {
33+
const values = err.headers.getAll(key);
34+
if (values) {
35+
values.forEach(value => mockResponse.headers.append(key, value));
36+
}
37+
});
38+
39+
// Get the body text
40+
const bodyText = typeof err.error === 'string' ? err.error : JSON.stringify(err.error || {});
41+
42+
// Return ServerError for non-2xx status codes (following Apollo Client's logic)
43+
if (err.status >= 300) {
44+
return new ServerError(`Response not successful: Received status code ${err.status}`, {
45+
response: mockResponse,
46+
bodyText,
47+
});
48+
}
49+
50+
// For other HttpErrorResponse cases, return a generic error
51+
return new Error(err.message);
52+
}
53+
1854
export declare namespace HttpLink {
1955
export interface Options extends FetchOptions, HttpRequestOptions {
2056
operationPrinter?: OperationPrinter;
@@ -94,7 +130,11 @@ export class HttpLinkHandler extends ApolloLink {
94130
operation.setContext({ response });
95131
observer.next(response.body);
96132
},
97-
error: err => observer.error(err),
133+
error: err => {
134+
if (err instanceof HttpErrorResponse)
135+
observer.error(convertHttpErrorToApolloError(err));
136+
else observer.error(err);
137+
},
98138
complete: () => observer.complete(),
99139
});
100140

0 commit comments

Comments
 (0)