Skip to content

Commit 32e85ea

Browse files
authored
Unify error behavior for queries (#12457)
1 parent 0595f39 commit 32e85ea

File tree

10 files changed

+453
-60
lines changed

10 files changed

+453
-60
lines changed

.changeset/four-ghosts-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": major
3+
---
4+
5+
Network errors triggered by queries now adhere to the `errorPolicy`. This means that GraphQL errors and network errors now behave the same way. Previously promise-based APIs, such as `client.query`, would reject the promise with the network error even if `errorPolicy` was set to `ignore`. The promise is now resolved with the `error` property set to the network error instead.

.size-limits.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 42028,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 37470,
4-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 32524,
5-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27415
2+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 42138,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 37491,
4+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 32521,
5+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27439
66
}

src/__tests__/client.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
print,
88
visit,
99
} from "graphql";
10+
import { GraphQLFormattedError } from "graphql";
1011
import { gql } from "graphql-tag";
1112
import { assign, cloneDeep } from "lodash";
1213
import { EMPTY, EmptyError, Observable, of, Subscription } from "rxjs";
@@ -2636,6 +2637,152 @@ describe("client", () => {
26362637
);
26372638
});
26382639

2640+
it("rejects network errors", async () => {
2641+
const query = gql`
2642+
query {
2643+
posts {
2644+
foo
2645+
__typename
2646+
}
2647+
}
2648+
`;
2649+
const error = new Error("Oops");
2650+
const link = mockSingleLink({
2651+
request: { query },
2652+
error,
2653+
});
2654+
const client = new ApolloClient({
2655+
link,
2656+
cache: new InMemoryCache(),
2657+
});
2658+
2659+
await expect(client.query({ query })).rejects.toThrow(error);
2660+
});
2661+
2662+
it("resolves partial data and GraphQL errors when errorPolicy is 'all'", async () => {
2663+
const query = gql`
2664+
query {
2665+
posts {
2666+
foo
2667+
__typename
2668+
}
2669+
}
2670+
`;
2671+
const errors: GraphQLFormattedError[] = [
2672+
{ message: 'Cannot query field "foo" on type "Post".' },
2673+
];
2674+
const link = mockSingleLink({
2675+
request: { query },
2676+
result: { data: { posts: null }, errors },
2677+
});
2678+
const client = new ApolloClient({
2679+
link,
2680+
cache: new InMemoryCache(),
2681+
});
2682+
2683+
await expect(
2684+
client.query({ query, errorPolicy: "all" })
2685+
).resolves.toEqualApolloQueryResult({
2686+
data: { posts: null },
2687+
error: new CombinedGraphQLErrors([
2688+
{ message: 'Cannot query field "foo" on type "Post".' },
2689+
]),
2690+
loading: false,
2691+
networkStatus: NetworkStatus.error,
2692+
partial: false,
2693+
});
2694+
});
2695+
2696+
it("resolves with network error when errorPolicy is 'all'", async () => {
2697+
const query = gql`
2698+
query {
2699+
posts {
2700+
foo
2701+
__typename
2702+
}
2703+
}
2704+
`;
2705+
const error = new Error("Oops");
2706+
const link = mockSingleLink({
2707+
request: { query },
2708+
error,
2709+
});
2710+
const client = new ApolloClient({
2711+
link,
2712+
cache: new InMemoryCache(),
2713+
});
2714+
2715+
await expect(
2716+
client.query({ query, errorPolicy: "all" })
2717+
).resolves.toEqualApolloQueryResult({
2718+
data: undefined,
2719+
error,
2720+
loading: false,
2721+
networkStatus: NetworkStatus.error,
2722+
partial: true,
2723+
});
2724+
});
2725+
2726+
it("resolves partial data and strips errors when errorPolicy is 'ignore'", async () => {
2727+
const query = gql`
2728+
query {
2729+
posts {
2730+
foo
2731+
__typename
2732+
}
2733+
}
2734+
`;
2735+
const errors: GraphQLFormattedError[] = [
2736+
{ message: 'Cannot query field "foo" on type "Post".' },
2737+
];
2738+
const link = mockSingleLink({
2739+
request: { query },
2740+
result: { data: { posts: null }, errors },
2741+
});
2742+
const client = new ApolloClient({
2743+
link,
2744+
cache: new InMemoryCache(),
2745+
});
2746+
2747+
await expect(
2748+
client.query({ query, errorPolicy: "ignore" })
2749+
).resolves.toEqualApolloQueryResult({
2750+
data: { posts: null },
2751+
loading: false,
2752+
networkStatus: NetworkStatus.ready,
2753+
partial: false,
2754+
});
2755+
});
2756+
2757+
it("resolves with no data or errors for network error when errorPolicy is 'ignore'", async () => {
2758+
const query = gql`
2759+
query {
2760+
posts {
2761+
foo
2762+
__typename
2763+
}
2764+
}
2765+
`;
2766+
const error = new Error("Oops");
2767+
const link = mockSingleLink({
2768+
request: { query },
2769+
error,
2770+
});
2771+
const client = new ApolloClient({
2772+
link,
2773+
cache: new InMemoryCache(),
2774+
});
2775+
2776+
await expect(
2777+
client.query({ query, errorPolicy: "ignore" })
2778+
).resolves.toEqualApolloQueryResult({
2779+
data: undefined,
2780+
loading: false,
2781+
networkStatus: NetworkStatus.ready,
2782+
partial: true,
2783+
});
2784+
});
2785+
26392786
it("should warn if server returns wrong data", async () => {
26402787
using _consoleSpies = spyOnConsole.takeSnapshots("error");
26412788
const query = gql`

src/core/QueryManager.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,7 @@ export class QueryManager {
12101210
>
12111211
): Observable<ApolloQueryResult<TData>> {
12121212
const requestId = (queryInfo.lastRequestId = this.generateRequestId());
1213+
const { errorPolicy } = options;
12131214

12141215
// Performing transformForLink here gives this.cache a chance to fill in
12151216
// missing fragment definitions (for example) before sending this document
@@ -1221,21 +1222,9 @@ export class QueryManager {
12211222
options.context,
12221223
options.variables
12231224
).pipe(
1224-
catchError((error) => {
1225-
error = maybeWrapError(error);
1226-
1227-
// Avoid storing errors from older interrupted queries.
1228-
if (requestId >= queryInfo.lastRequestId) {
1229-
queryInfo.resetLastWrite();
1230-
queryInfo.reset();
1231-
}
1232-
1233-
throw error;
1234-
}),
12351225
map((result) => {
12361226
const graphQLErrors = getGraphQLErrorsFromResult(result);
12371227
const hasErrors = graphQLErrors.length > 0;
1238-
const { errorPolicy } = options;
12391228

12401229
// If we interrupted this request by calling getResultsFromLink again
12411230
// with the same QueryInfo object, we ignore the old results.
@@ -1278,6 +1267,30 @@ export class QueryManager {
12781267
}
12791268

12801269
return aqr;
1270+
}),
1271+
catchError((error) => {
1272+
error = maybeWrapError(error);
1273+
1274+
// Avoid storing errors from older interrupted queries.
1275+
if (requestId >= queryInfo.lastRequestId && errorPolicy === "none") {
1276+
queryInfo.resetLastWrite();
1277+
queryInfo.reset();
1278+
throw error;
1279+
}
1280+
1281+
const aqr: ApolloQueryResult<TData> = {
1282+
data: undefined,
1283+
loading: false,
1284+
networkStatus: NetworkStatus.ready,
1285+
partial: true,
1286+
};
1287+
1288+
if (errorPolicy !== "ignore") {
1289+
aqr.error = error;
1290+
aqr.networkStatus = NetworkStatus.error;
1291+
}
1292+
1293+
return of(aqr);
12811294
})
12821295
);
12831296
}

src/core/__tests__/ApolloClient/general.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,61 @@ describe("ApolloClient", () => {
280280
await expect(stream).not.toEmitAnything();
281281
});
282282

283+
it("handles network errors with errorPolicy: 'all'", async () => {
284+
const stream = getObservableStream({
285+
query: gql`
286+
query people {
287+
allPeople(first: 1) {
288+
people {
289+
name
290+
}
291+
}
292+
}
293+
`,
294+
queryOptions: {
295+
errorPolicy: "all",
296+
},
297+
error: new Error("Network error"),
298+
});
299+
300+
await expect(stream).toEmitApolloQueryResult({
301+
data: undefined,
302+
error: new Error("Network error"),
303+
loading: false,
304+
networkStatus: NetworkStatus.error,
305+
partial: true,
306+
});
307+
308+
await expect(stream).not.toEmitAnything();
309+
});
310+
311+
it("handles network errors with errorPolicy: 'ignore'", async () => {
312+
const stream = getObservableStream({
313+
query: gql`
314+
query people {
315+
allPeople(first: 1) {
316+
people {
317+
name
318+
}
319+
}
320+
}
321+
`,
322+
queryOptions: {
323+
errorPolicy: "ignore",
324+
},
325+
error: new Error("Network error"),
326+
});
327+
328+
await expect(stream).toEmitApolloQueryResult({
329+
data: undefined,
330+
loading: false,
331+
networkStatus: NetworkStatus.ready,
332+
partial: true,
333+
});
334+
335+
await expect(stream).not.toEmitAnything();
336+
});
337+
283338
// Determine how/if we want to change this at all. ObservableQuery no longer
284339
// emits errors to its observers and instead emits a `next` event with an
285340
// `error` property.

src/react/hooks/__tests__/useLazyQuery.test.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,12 +1783,6 @@ describe("useLazyQuery Hook", () => {
17831783
});
17841784
}
17851785

1786-
// TODO: Determine if this is the correct behavior. This is different than
1787-
// 3.x where this resolves with an `ApolloQueryResult`.
1788-
// https://github.com/apollographql/apollo-client/issues/10787 wants this
1789-
// behavior
1790-
// https://github.com/apollographql/apollo-client/issues/9142#issuecomment-1118972947
1791-
// justifies the old behavior
17921786
await expect(execute()).rejects.toEqual(
17931787
new CombinedGraphQLErrors([{ message: "error 1" }])
17941788
);
@@ -2721,9 +2715,6 @@ describe("useLazyQuery Hook", () => {
27212715
await expect(takeSnapshot).not.toRerender();
27222716
});
27232717

2724-
// If there was any data to report, errorPolicy:"all" would report both
2725-
// result.data and result.error, but there is no GraphQL data when we
2726-
// encounter a network error, so the test again captures desired behavior.
27272718
it('handles errorPolicy:"all" appropriately', async () => {
27282719
const networkError = new Error("from the network");
27292720

@@ -2768,7 +2759,13 @@ describe("useLazyQuery Hook", () => {
27682759

27692760
const [execute] = getCurrentSnapshot();
27702761

2771-
await expect(execute()).rejects.toEqual(networkError);
2762+
await expect(execute()).resolves.toEqualApolloQueryResult({
2763+
data: undefined,
2764+
error: networkError,
2765+
loading: false,
2766+
networkStatus: NetworkStatus.error,
2767+
partial: true,
2768+
});
27722769

27732770
{
27742771
const [, result] = await takeSnapshot();
@@ -2787,9 +2784,6 @@ describe("useLazyQuery Hook", () => {
27872784
await expect(takeSnapshot).not.toRerender();
27882785
});
27892786

2790-
// Technically errorPolicy:"ignore" is supposed to throw away result.error,
2791-
// but in the case of network errors, since there's no actual data to
2792-
// report, it's useful/important that we report result.error anyway.
27932787
it('handles errorPolicy:"ignore" appropriately', async () => {
27942788
const networkError = new Error("from the network");
27952789

@@ -2834,17 +2828,21 @@ describe("useLazyQuery Hook", () => {
28342828

28352829
const [execute] = getCurrentSnapshot();
28362830

2837-
await expect(execute()).rejects.toEqual(networkError);
2831+
await expect(execute()).resolves.toEqualApolloQueryResult({
2832+
data: undefined,
2833+
loading: false,
2834+
networkStatus: NetworkStatus.ready,
2835+
partial: true,
2836+
});
28382837

28392838
{
28402839
const [, result] = await takeSnapshot();
28412840

28422841
expect(result).toEqualLazyQueryResult({
28432842
data: undefined,
2844-
error: networkError,
28452843
called: true,
28462844
loading: false,
2847-
networkStatus: NetworkStatus.error,
2845+
networkStatus: NetworkStatus.ready,
28482846
previousData: undefined,
28492847
variables: {},
28502848
});

0 commit comments

Comments
 (0)