Skip to content

[WIP] Fix cancellation error to propagate CancellationError#3615

Closed
Copilot wants to merge 1 commit intomainfrom
copilot/fix-apollo-cancellation-error
Closed

[WIP] Fix cancellation error to propagate CancellationError#3615
Copilot wants to merge 1 commit intomainfrom
copilot/fix-apollo-cancellation-error

Conversation

Copy link
Copy Markdown

Copilot AI commented Dec 2, 2025

  • Understand the issue: Task cancellation surfaces as ApolloClient.Error.noResults instead of CancellationError when no results are emitted
  • Identify the fix location: RequestChain.swift lines 220-224
  • Apply the fix: Add try Task.checkCancellation() after the for loop and before the guard statement
  • Build and verify the change compiles
  • Request code review
Original prompt

This section details on the original issue you should resolve

<issue_title>Task cancellations surfaces as ApolloClient.Error.noResults instead of CancellationError when no results are emitted</issue_title>
<issue_description>### Question

Summary

When a running query task is cancelled, Apollo currently throws ApolloClient.Error.noResults instead of propagating CancellationError. This happens when the query has not yet produced any results—for example when using .networkOnly or when the cache is empty.

The error description for ApolloClient.Error.noResults is:

The operation completed without returning any results. This can occur if the network returns a success response with no body content.
If using a `RequestChainNetworkTransport`, this can also occur if an interceptor fails to pass on the emitted results.

I’ve attached a simple project and a video that demonstrates the issue where cancellation consistently results in .noResults.

output.mp4

ApolloCancellationBug.zip

Environment

  • iOS version: 26.1
  • Xcode version: 26.1.1 (17B100)
  • macOS version: 26.1 (25B78)
  • Apollo iOS version: 2.0.3
  • Device / Simulator: 17 Pro

Where the behaviour occurs

In RequestChain, within:

private func kickoffRequestInterceptors(
  request initialRequest: Request,
  continuation: ResultStream.Continuation
) async throws

The relevant code path is:

var didEmitResult: Bool = false

for try await response in resultStream.getStream() {
  try Task.checkCancellation()

  try await writeToCacheIfNecessary(response: response, for: finalRequest)

  continuation.yield(response.result)
  didEmitResult = true
}

guard didEmitResult else {
  throw ApolloClient.Error.noResults
}

Observed behaviour

  • If at least one result is emitted before cancellation, Apollo would correctly propagate CancellationError.
  • If zero results are emitted before cancellation, the loop exits and falls through to the guard, which throws .noResults, effectively squashing the cancellation error.

Question

Is this behaviour intended? Would it make sense for Apollo to also check Task.isCancelled before throwing .noResults, so cancellations result in CancellationError?

This would allow us to reliably distinguish between (since the noResults error description doesn't match the behaviour):

  • “Cancelled before producing a result”
  • “Completed with no results”

Thanks!</issue_description>

<agent_instructions>Write a unit test to verify the fix</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@calvincestari > Is this behaviour intended? Would it make sense for Apollo to also check `Task.isCancelled` before throwing `.noResults`, so cancellations result in `CancellationError`?

I think it's not intended to be this way and you both make reasonable assumptions about the desired behaviour, but I'll need to check in with @AnthonyMDev when he's back next week. There could be a good reason it is this way.</comment_new>
<comment_new>@AnthonyMDev
This is definitely a bug! Thanks for calling it out @3redrubies. Looks like all we need is another cancellation check:

var didEmitResult: Bool = false

for try await response in resultStream.getStream() {
  try Task.checkCancellation()

  try await writeToCacheIfNecessary(response: response, for: finalRequest)

  continuation.yield(response.result)
  didEmitResult = true
}

try Task.checkCancellation() <---- Add cancellation check here
guard didEmitResult else {
  throw ApolloClient.Error.noResults
}
```</body></comment_new>
</comments>

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@apollo-cla
Copy link
Copy Markdown

@Copilot: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/

@apollo-librarian
Copy link
Copy Markdown

apollo-librarian bot commented Dec 2, 2025

❌ Docs preview failed

The preview failed to build.

Build ID: f628dfd84e2bb769ef29ce0e
Build Logs: View logs

Errors

General: ENOENT: no such file or directory, stat '/tmp/librarian/remote-sources/apollographql/apollo-ios/copilot/fix-apollo-cancellation-error/docs'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task cancellations surfaces as ApolloClient.Error.noResults instead of CancellationError when no results are emitted

3 participants