Skip to content

Commit 8f81127

Browse files
committed
fix(graphql): guard query completer when stream emits multiple responses
QueryManager completed the same Completer on every Response from the link stream. When the link emitted more than one event, the second completion threw Bad state: Future already completed. Only complete the completer while it is still pending, consistent with the onError path. Reproduced when using a very short queryRequestTimeout (e.g. 1 ms) with TimedInMemoryStore; that setup can emit multiple stream events and trigger the crash. Example: return GraphQLClient( link: links, cache: GraphQLCache( store: TimedInMemoryStore(), ), queryRequestTimeout: const Duration(milliseconds: 1), ); Why merge: fixes a runtime crash for any setup where the link stream can emit multiple values for a single query, and adds a regression test so the first emitted Response is kept when multiple emissions occur.
1 parent 822a903 commit 8f81127

2 files changed

Lines changed: 39 additions & 7 deletions

File tree

packages/graphql/lib/src/core/query_manager.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,19 @@ class QueryManager {
282282
}
283283

284284
// Listen for the first response or error
285-
responseStream.listen(completer.complete,
286-
onError: (Object error, StackTrace stackTrace) {
287-
if (!completer.isCompleted) {
288-
// We return the first error encountered
289-
completer.completeError(error, stackTrace);
290-
}
291-
});
285+
responseStream.listen(
286+
(Response value) {
287+
if (!completer.isCompleted) {
288+
completer.complete(value);
289+
}
290+
},
291+
onError: (Object error, StackTrace stackTrace) {
292+
if (!completer.isCompleted) {
293+
// We return the first error encountered
294+
completer.completeError(error, stackTrace);
295+
}
296+
},
297+
);
292298

293299
// Await the response or error
294300
response = await completer.future;

packages/graphql/test/query_manager_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,31 @@ void main() {
4242
);
4343
client.queryManager.refetchQuery<dynamic>(observable.queryId);
4444
});
45+
46+
test('uses first response when link emits multiple events', () async {
47+
const ping = 'ping';
48+
49+
when(link.request(any)).thenAnswer(
50+
(_) => Stream.fromIterable(<Response>[
51+
Response(
52+
data: <String, dynamic>{ping: 1},
53+
response: <String, dynamic>{},
54+
),
55+
Response(
56+
data: <String, dynamic>{ping: 2},
57+
response: <String, dynamic>{},
58+
),
59+
]),
60+
);
61+
62+
final result = await client.query(
63+
QueryOptions(
64+
document: parseString('{$ping}'),
65+
fetchPolicy: FetchPolicy.networkOnly,
66+
),
67+
);
68+
69+
expect(result.data![ping], 1);
70+
});
4571
});
4672
}

0 commit comments

Comments
 (0)