Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/http-error-server-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'apollo-angular': major
---

**BREAKING CHANGE**: HTTP errors now return Apollo Client's `ServerError` instead of Angular's `HttpErrorResponse`

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.

**Migration Guide:**

Before:
```typescript
import { HttpErrorResponse } from '@angular/common/http';

link.request(operation).subscribe({
error: (err) => {
if (err instanceof HttpErrorResponse) {
console.log(err.status);
console.log(err.error);
}
}
});
```

After:
```typescript
import { ServerError } from '@apollo/client/errors';

link.request(operation).subscribe({
error: (err) => {
if (ServerError.is(err)) {
console.log(err.statusCode);
console.log(err.bodyText);
console.log(err.response.headers);
}
}
});
```

**Properties Changed:**
- `err.status` → `err.statusCode`
- `err.error` → `err.bodyText` (always string, JSON stringified for objects)
- `err.headers` (Angular HttpHeaders) → `err.response.headers` (native Headers)
- Access response via `err.response` which includes: `status`, `statusText`, `ok`, `url`, `type`, `redirected`

**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.

Fixes #2394
45 changes: 43 additions & 2 deletions packages/apollo-angular/http/src/http-batch-link.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient, HttpContext, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpContext, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApolloLink } from '@apollo/client';
import { BatchLink } from '@apollo/client/link/batch';
import { ServerError } from '@apollo/client/errors';
import type { HttpLink } from './http-link';
import { Body, Context, OperationPrinter, Request } from './types';
import {
Expand All @@ -14,6 +15,43 @@ import {
prioritize,
} from './utils';

function convertHttpErrorToApolloError(err: HttpErrorResponse): Error {
// Create a Response-like object that satisfies Apollo Client's expectations
const mockResponse = {
status: err.status,
statusText: err.statusText,
ok: err.ok,
url: err.url || '',
headers: new Headers(),
type: 'error' as ResponseType,
redirected: false,
} as Response;

// Convert Angular's HttpHeaders to native Headers
err.headers.keys().forEach(key => {
const values = err.headers.getAll(key);
if (values) {
values.forEach(value => mockResponse.headers.append(key, value));
}
});

// Get the body text
const bodyText = typeof err.error === 'string'
? err.error
: JSON.stringify(err.error || {});

// Return ServerError for non-2xx status codes (following Apollo Client's logic)
if (err.status >= 300) {
return new ServerError(
`Response not successful: Received status code ${err.status}`,
{ response: mockResponse, bodyText }
);
}

// For other HttpErrorResponse cases, return a generic error
return new Error(err.message);
}

export declare namespace HttpBatchLink {
export type Options = {
batchMax?: number;
Expand Down Expand Up @@ -89,7 +127,10 @@ export class HttpBatchLinkHandler extends ApolloLink {
throw new Error('File upload is not available when combined with Batching');
}).subscribe({
next: result => observer.next(result.body),
error: err => observer.error(err),
error: err => {
if (err instanceof HttpErrorResponse) observer.error(convertHttpErrorToApolloError(err));
else observer.error(err);
},
complete: () => observer.complete(),
});

Expand Down
45 changes: 43 additions & 2 deletions packages/apollo-angular/http/src/http-link.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient, HttpContext } from '@angular/common/http';
import { HttpClient, HttpContext, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApolloLink } from '@apollo/client';
import { ServerError } from '@apollo/client/errors';
import { pick } from './http-batch-link';
import {
Body,
Expand All @@ -15,6 +16,43 @@ import {
} from './types';
import { createHeadersWithClientAwareness, fetch, mergeHeaders, mergeHttpContext } from './utils';

function convertHttpErrorToApolloError(err: HttpErrorResponse): Error {
// Create a Response-like object that satisfies Apollo Client's expectations
const mockResponse = {
status: err.status,
statusText: err.statusText,
ok: err.ok,
url: err.url || '',
headers: new Headers(),
type: 'error' as ResponseType,
redirected: false,
} as Response;

// Convert Angular's HttpHeaders to native Headers
err.headers.keys().forEach(key => {
const values = err.headers.getAll(key);
if (values) {
values.forEach(value => mockResponse.headers.append(key, value));
}
});

// Get the body text
const bodyText = typeof err.error === 'string'
? err.error
: JSON.stringify(err.error || {});

// Return ServerError for non-2xx status codes (following Apollo Client's logic)
if (err.status >= 300) {
return new ServerError(
`Response not successful: Received status code ${err.status}`,
{ response: mockResponse, bodyText }
);
}

// For other HttpErrorResponse cases, return a generic error
return new Error(err.message);
}

export declare namespace HttpLink {
export interface Options extends FetchOptions, HttpRequestOptions {
operationPrinter?: OperationPrinter;
Expand Down Expand Up @@ -94,7 +132,10 @@ export class HttpLinkHandler extends ApolloLink {
operation.setContext({ response });
observer.next(response.body);
},
error: err => observer.error(err),
error: err => {
if (err instanceof HttpErrorResponse) observer.error(convertHttpErrorToApolloError(err));
else observer.error(err);
},
complete: () => observer.complete(),
});

Expand Down
Loading