Fix SSE client cancellation#9503
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to fix cancellation behavior for GraphQL over SSE result streaming by ensuring the cancellation token provided to ReadAsResultStreamAsync actually influences the underlying SSE read loop.
Changes:
- Pass the
ReadAsResultStreamAsynccancellation token into the SSE reader. - Link the request-level cancellation token with the enumerator cancellation token inside
SseReader. - Update the SSE subscription cancellation test to pass the token directly to
ReadAsResultStreamAsync.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs | Updates the SSE subscription test to pass a token directly to ReadAsResultStreamAsync. |
| src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs | Adds a request-level cancellation token and links it with enumerator cancellation. |
| src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs | For SSE responses, forwards the provided cancellation token into SseReader. |
Comments suppressed due to low confidence (1)
src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs:723
- This test now passes the token to ReadAsResultStreamAsync but no longer uses WithCancellation.
await foreachwon’t pass that token to the enumerator’sGetAsyncEnumeratorunless you useWithCancellation, and the SSE reader currently can also end the sequence without throwing on cancellation. As a result, theOperationCanceledExceptioncatch/assert here is brittle and may not trigger. Consider either reintroducing.WithCancellation(cts.Token)(to make the exception expectation deterministic) or changing the assertion to verify that enumeration stops promptly aftercts.CancelAsync()without requiring an exception.
await foreach (var result in subscriptionResponse.ReadAsResultStreamAsync(cts.Token))
{
result.MatchInlineSnapshot(
"""
Data: {"onReview":{"stars":5}}
""");
await cts.CancelAsync();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (contentType?.MediaType.EqualsOrdinal(ContentType.EventStream) ?? false) | ||
| { | ||
| return new SseReader(_message); | ||
| return new SseReader(_message, cancellationToken); | ||
| } |
There was a problem hiding this comment.
ReadAsResultStreamAsync now forwards the provided cancellationToken into SseReader, but SseReader currently treats a canceled PipeReader read as a normal end of stream (yield break) rather than propagating an OperationCanceledException. That makes cancellation behavior inconsistent with the non-SSE paths (which throw when the token is canceled) and makes it hard for callers to distinguish cancellation vs completion. Consider updating the SSE reader to throw when cancellation is requested (e.g., when the read is canceled) so this token has the expected observable effect.
src/HotChocolate/AspNetCore/src/Transport.Http/Sse/SseReader.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
No description provided.