Skip to content

Commit 5c56b32

Browse files
authored
Ignore data values set in subsequent chunks in incremental responses (#12982)
1 parent 072da24 commit 5c56b32

File tree

9 files changed

+177
-13
lines changed

9 files changed

+177
-13
lines changed

.api-reports/api-report-incremental.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ namespace Defer20220824Handler {
5252
};
5353
// (undocumented)
5454
type SubsequentResult<TData = Record<string, unknown>> = {
55-
data?: TData | null | undefined;
56-
errors?: ReadonlyArray<GraphQLFormattedError>;
5755
extensions?: Record<string, unknown>;
5856
hasNext: boolean;
5957
incremental?: Array<IncrementalResult<TData>>;

.changeset/neat-windows-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Ignore top-level `data` values on subsequent chunks in incremental responses.

.changeset/shaggy-brooms-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Fix the `Defer20220824Handler.SubsequentResult` type to match the `FormattedSubsequentIncrementalExecutionResult` type in `[email protected]`.

.size-limits.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44831,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39452,
2+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44773,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39426,
44
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33875,
55
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27756
66
}

src/incremental/handlers/__tests__/defer20220824/defer.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,3 +1037,78 @@ test("handles final chunk of { hasNext: false } correctly in usage with Apollo C
10371037
});
10381038
await expect(observableStream).not.toEmitAnything();
10391039
});
1040+
1041+
// Servers that return a `data` property in subsequent payloads are technically
1042+
// invalid, but we still want to handle cases where the server misbehaves.
1043+
//
1044+
// See the following issue for more information:
1045+
// https://github.com/apollographql/apollo-client/issues/12976
1046+
test("ignores `data` property added to subsequent chunks by misbehaving servers", async () => {
1047+
const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } =
1048+
mockDefer20220824();
1049+
const client = new ApolloClient({
1050+
link: httpLink,
1051+
cache: new InMemoryCache(),
1052+
incrementalHandler: new Defer20220824Handler(),
1053+
});
1054+
1055+
const query = gql`
1056+
query HeroNameQuery {
1057+
hero {
1058+
id
1059+
... @defer {
1060+
name
1061+
}
1062+
}
1063+
}
1064+
`;
1065+
1066+
const observableStream = new ObservableStream(client.watchQuery({ query }));
1067+
1068+
enqueueInitialChunk({
1069+
data: { hero: { __typename: "Hero", id: "1" } },
1070+
hasNext: true,
1071+
});
1072+
1073+
await expect(observableStream).toEmitTypedValue({
1074+
loading: true,
1075+
data: undefined,
1076+
dataState: "empty",
1077+
networkStatus: NetworkStatus.loading,
1078+
partial: true,
1079+
});
1080+
1081+
await expect(observableStream).toEmitTypedValue({
1082+
loading: true,
1083+
data: markAsStreaming({
1084+
hero: {
1085+
__typename: "Hero",
1086+
id: "1",
1087+
},
1088+
}),
1089+
dataState: "streaming",
1090+
networkStatus: NetworkStatus.streaming,
1091+
partial: true,
1092+
});
1093+
1094+
enqueueSubsequentChunk({
1095+
// @ts-expect-error simulate misbehaving server
1096+
data: null,
1097+
incremental: [{ data: { name: "Luke" }, path: ["hero"] }],
1098+
hasNext: false,
1099+
});
1100+
1101+
await expect(observableStream).toEmitTypedValue({
1102+
loading: false,
1103+
data: {
1104+
hero: {
1105+
__typename: "Hero",
1106+
id: "1",
1107+
name: "Luke",
1108+
},
1109+
},
1110+
dataState: "complete",
1111+
networkStatus: NetworkStatus.ready,
1112+
partial: false,
1113+
});
1114+
});

src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,3 +2629,79 @@ test("handles final chunk of { hasNext: false } correctly in usage with Apollo C
26292629
});
26302630
await expect(observableStream).not.toEmitAnything();
26312631
});
2632+
2633+
// Servers that return a `data` property in subsequent payloads are technically
2634+
// invalid, but we still want to handle cases where the server misbehaves.
2635+
//
2636+
// See the following issue for more information:
2637+
// https://github.com/apollographql/apollo-client/issues/12976
2638+
test("ignores `data` property added to subsequent chunks by misbehaving servers", async () => {
2639+
const stream = mockDeferStreamGraphQL17Alpha9();
2640+
const client = new ApolloClient({
2641+
link: stream.httpLink,
2642+
cache: new InMemoryCache(),
2643+
incrementalHandler: new GraphQL17Alpha9Handler(),
2644+
});
2645+
2646+
const query = gql`
2647+
query HeroNameQuery {
2648+
hero {
2649+
id
2650+
... @defer {
2651+
name
2652+
}
2653+
}
2654+
}
2655+
`;
2656+
2657+
const observableStream = new ObservableStream(client.watchQuery({ query }));
2658+
2659+
stream.enqueueInitialChunk({
2660+
data: { hero: { __typename: "Hero", id: "1" } },
2661+
pending: [{ id: "0", path: ["hero"] }],
2662+
hasNext: true,
2663+
});
2664+
2665+
await expect(observableStream).toEmitTypedValue({
2666+
loading: true,
2667+
data: undefined,
2668+
dataState: "empty",
2669+
networkStatus: NetworkStatus.loading,
2670+
partial: true,
2671+
});
2672+
2673+
await expect(observableStream).toEmitTypedValue({
2674+
loading: true,
2675+
data: markAsStreaming({
2676+
hero: {
2677+
__typename: "Hero",
2678+
id: "1",
2679+
},
2680+
}),
2681+
dataState: "streaming",
2682+
networkStatus: NetworkStatus.streaming,
2683+
partial: true,
2684+
});
2685+
2686+
stream.enqueueSubsequentChunk({
2687+
// @ts-expect-error simulate misbehaving server
2688+
data: null,
2689+
incremental: [{ data: { name: "Luke" }, id: "0" }],
2690+
completed: [{ id: "0" }],
2691+
hasNext: false,
2692+
});
2693+
2694+
await expect(observableStream).toEmitTypedValue({
2695+
loading: false,
2696+
data: {
2697+
hero: {
2698+
__typename: "Hero",
2699+
id: "1",
2700+
name: "Luke",
2701+
},
2702+
},
2703+
dataState: "complete",
2704+
networkStatus: NetworkStatus.ready,
2705+
partial: false,
2706+
});
2707+
});

src/incremental/handlers/defer20220824.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ export declare namespace Defer20220824Handler {
3333
};
3434

3535
export type SubsequentResult<TData = Record<string, unknown>> = {
36-
data?: TData | null | undefined;
37-
errors?: ReadonlyArray<GraphQLFormattedError>;
3836
extensions?: Record<string, unknown>;
3937
hasNext: boolean;
4038
incremental?: Array<IncrementalResult<TData>>;
@@ -99,7 +97,6 @@ class DeferRequest<TData extends Record<string, unknown>>
9997
): FormattedExecutionResult<TData> {
10098
this.hasNext = chunk.hasNext;
10199
this.data = cacheData;
102-
this.merge(chunk);
103100

104101
if (hasIncrementalChunks(chunk)) {
105102
for (const incremental of chunk.incremental) {
@@ -135,6 +132,8 @@ class DeferRequest<TData extends Record<string, unknown>>
135132
arrayMerge
136133
);
137134
}
135+
} else {
136+
this.merge(chunk);
138137
}
139138

140139
const result: FormattedExecutionResult<TData> = { data: this.data };
@@ -178,7 +177,9 @@ export class Defer20220824Handler
178177
}
179178
};
180179
if (this.isIncrementalResult(result)) {
181-
push(result);
180+
if ("errors" in result) {
181+
push(result);
182+
}
182183
if (hasIncrementalChunks(result)) {
183184
result.incremental.forEach(push);
184185
}

src/incremental/handlers/graphql17Alpha9.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ class IncrementalRequest<TData>
119119
}
120120
}
121121

122-
this.merge(chunk, "truncate");
123-
124122
if (hasIncrementalChunks(chunk)) {
125123
for (const incremental of chunk.incremental) {
126124
const pending = this.pending.find(({ id }) => incremental.id === id);
@@ -194,6 +192,8 @@ class IncrementalRequest<TData>
194192
arrayMerge
195193
);
196194
}
195+
} else {
196+
this.merge(chunk, "truncate");
197197
}
198198

199199
if ("completed" in chunk && chunk.completed) {
@@ -286,7 +286,7 @@ export class GraphQL17Alpha9Handler
286286

287287
if (this.isIncrementalResult(result)) {
288288
push(new IncrementalRequest().handle(undefined, result));
289-
} else {
289+
} else if ("errors" in result) {
290290
push(result);
291291
}
292292

src/link/context/__tests__/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import assert from "node:assert";
2+
13
import { gql } from "graphql-tag";
24
import { Observable, of } from "rxjs";
35

@@ -268,6 +270,8 @@ test("can access the client from operation argument", async () => {
268270
const link = withContext.concat(mockLink);
269271
const stream = new ObservableStream(execute(link, { query }, { client }));
270272

271-
const { data } = await stream.takeNext();
272-
expect(data!.client).toBe(client);
273+
const result = await stream.takeNext();
274+
275+
assert("data" in result);
276+
expect(result.data!.client).toBe(client);
273277
});

0 commit comments

Comments
 (0)