Skip to content

fix(standard-server): throw error for waiting operation if event iterator cancelled#974

Merged
dinwwwh merged 2 commits intomainfrom
fix/standard-server/event-iterator-if-cancell-while-waiting-for-new-event
Sep 11, 2025
Merged

fix(standard-server): throw error for waiting operation if event iterator cancelled#974
dinwwwh merged 2 commits intomainfrom
fix/standard-server/event-iterator-if-cancell-while-waiting-for-new-event

Conversation

@dinwwwh
Copy link
Copy Markdown
Member

@dinwwwh dinwwwh commented Sep 8, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Cancelling an ongoing iteration now reliably stops the stream and surfaces a clear cancellation error instead of appearing to complete normally.
    • Distinguishes explicit cancellation from natural completion to prevent delivering a final value after cancellation and ensures the stream closes promptly and instrumentation remains consistent.
  • Tests

    • Added tests verifying that cancelling while awaiting the next item rejects with the cancellation error, closes the stream, and updates instrumentation as expected.

@vercel
Copy link
Copy Markdown

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
orpc Ready Ready Preview Comment Sep 8, 2025 8:46am

@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Sep 8, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds explicit cancellation handling to toEventIterator: introduces an internal isCancelled flag, marks cancellation when generator.return() is invoked (not a natural completion), causes pending next() calls to reject with AbortError("Stream was cancelled"), and updates tests (note: the same test is inserted twice).

Changes

Cohort / File(s) Summary
Iterator cancellation logic
packages/standard-server-fetch/src/event-iterator.ts
Imports AbortError, adds isCancelled flag, sets it when cancellation (reason !== 'next') occurs, logs a 'cancelled' event, and on reader.read() returning done: true throws AbortError('Stream was cancelled') if cancelled; adds explanatory comment block and separates cancellation vs natural completion control flow.
Cancellation tests (duplicated)
packages/standard-server-fetch/src/event-iterator.test.ts
Inserts two identical tests that call generator.return() while a next() is pending and assert the pending next() rejects with a cancellation error, the underlying stream reader closes, and instrumentation counters match expectations; duplication noted.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Consumer
  participant Iterator as toEventIterator
  participant Reader as StreamReader

  Note over Iterator,Reader: Iterator wraps a ReadableStream and uses a reader

  Consumer->>Iterator: next() (pending)
  Iterator->>Reader: read() (awaiting data)

  rect rgb(230,245,255)
    Note over Consumer,Iterator: Cancellation initiated
    Consumer->>Iterator: return()
    Iterator->>Reader: cancel() / releaseLock()
    Iterator->>Iterator: set isCancelled = true
  end

  Reader-->>Iterator: read() resolves { done: true }
  alt isCancelled == true
    Iterator-->>Consumer: reject pending next() with AbortError("Stream was cancelled")
  else
    Iterator-->>Consumer: resolve { done: true, value: undefined }
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • unnoq/orpc#968 — similar fixes around async-iterator cancellation semantics and ensuring .return cancels pending .next().
  • unnoq/orpc#464 — also modifies event iterator cancellation logic (adds cancellation flag / prevents final value after cancel).
  • unnoq/orpc#619 — touches the same toEventIterator implementation and adjusts iterator creation/cancellation behavior.

Poem

I munched on bytes beneath the moon, so merry,
A flag went up — "cancel!" — soft and airy.
Next waited patient, then found the gate closed,
Stream folded kindly, no value proposed.
Hoppity hop — tidy ends, no query. 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/standard-server/event-iterator-if-cancell-while-waiting-for-new-event

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @unnoq, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the behavior of the toEventIterator function to ensure robust error handling when an asynchronous stream operation is cancelled. Previously, cancelling an AsyncIterator while it was waiting for the next value could lead to an ambiguous done: true state without indicating the cancellation. The changes introduce explicit error throwing in such scenarios, making the stream's state clearer and more predictable for consumers.

Highlights

  • Error Handling for Stream Cancellation: Ensures that calling .return() on an AsyncIterator while .next() is awaiting a value now correctly throws an error, preventing silent completion in cancelled stream scenarios.
  • event-iterator.ts Logic Update: Introduced an isCancelled flag and updated the toEventIterator function to explicitly check this flag when the stream reports done, throwing an error if cancellation occurred.
  • New Test Case: Added a dedicated test case in event-iterator.test.ts to validate the new error-throwing behavior when an event iterator is cancelled during a pending next() operation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses an issue where cancelling a waiting event iterator would not result in an error. The introduction of an isCancelled flag to track the state and throw an error upon cancellation is a solid approach. The new test case effectively validates this fix. I have a suggestion to enhance maintainability by using a custom error class, which would allow for more robust error handling by consumers.

Comment thread packages/standard-server-fetch/src/event-iterator.ts Outdated
Comment thread packages/standard-server-fetch/src/event-iterator.test.ts Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Sep 8, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Sep 8, 2025

More templates

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@974

@orpc/client

npm i https://pkg.pr.new/@orpc/client@974

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@974

@orpc/experimental-durable-event-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-event-iterator@974

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@974

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@974

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@974

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@974

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@974

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@974

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@974

@orpc/react

npm i https://pkg.pr.new/@orpc/react@974

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@974

@orpc/experimental-react-swr

npm i https://pkg.pr.new/@orpc/experimental-react-swr@974

@orpc/server

npm i https://pkg.pr.new/@orpc/server@974

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@974

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@974

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@974

@orpc/standard-server-aws-lambda

npm i https://pkg.pr.new/@orpc/standard-server-aws-lambda@974

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@974

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@974

@orpc/standard-server-peer

npm i https://pkg.pr.new/@orpc/standard-server-peer@974

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@974

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@974

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@974

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@974

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@974

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@974

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@974

commit: 18e587b

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/standard-server-fetch/src/event-iterator.ts (2)

30-41: Nit: tighten comment wording

Consider replacing “resolve a value” with “resolve completion” to reduce ambiguity, and briefly note that some runtimes resolve read() with done=true on cancel.


43-45: Treat cancellation as non-error in telemetry; tag the error

Throwing is correct for API behavior, but the catch block will currently mark this as an error (setSpanError). Recommend tagging/special-casing this cancellation so spans aren’t flagged as errors.

Apply this local diff and the supplemental edits below:

-            throw new Error('Stream was cancelled')
+            throw new Error(CANCELLATION_ERROR_MESSAGE)

Outside the selected range, add a constant and skip setSpanError for this case:

// near the top of the file
const CANCELLATION_ERROR_MESSAGE = 'Stream was cancelled' as const
// inside catch (e) { ... }
if (
  !(e instanceof ErrorEvent) &&
  !(e instanceof Error && e.message === CANCELLATION_ERROR_MESSAGE)
) {
  setSpanError(span, e, options)
}
packages/standard-server-fetch/src/event-iterator.test.ts (1)

183-205: Speed up test to reduce CI wall time

The 1000ms sleep makes the suite slower than needed. A shorter delay still guarantees a pending read but keeps tests snappy.

-        await new Promise(resolve => setTimeout(resolve, 1000))
+        await new Promise(resolve => setTimeout(resolve, 25))
...
-    await new Promise(resolve => setTimeout(resolve, 1))
+    await new Promise(resolve => setTimeout(resolve, 0))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85c0fd7 and d0ddbfa.

📒 Files selected for processing (2)
  • packages/standard-server-fetch/src/event-iterator.test.ts (1 hunks)
  • packages/standard-server-fetch/src/event-iterator.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/standard-server-fetch/src/event-iterator.test.ts (2)
packages/standard-server-fetch/src/event-iterator.ts (1)
  • toEventIterator (7-116)
packages/shared/src/iterator.ts (1)
  • isAsyncIteratorObject (6-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: publish-commit
  • GitHub Check: lint
🔇 Additional comments (3)
packages/standard-server-fetch/src/event-iterator.ts (2)

17-17: Good: explicit cancellation flag

Clear separation of cancellation vs. natural completion. This avoids silently resolving a cancelled in-flight read.


102-103: The requested extraction of the AsyncIteratorClass definition, its next/return/throw methods, and cleanup invocations has been queued. Once those results are in, I’ll confirm exactly which reason values are used and whether treating any non-‘next’ as cancellation is safe.

packages/standard-server-fetch/src/event-iterator.test.ts (1)

183-205: LGTM: cancellation during pending next() is correctly asserted

Accurately reproduces the race, asserts rejection message, and validates span usage counts.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/standard-server-fetch/src/event-iterator.test.ts (1)

195-199: Make the test deterministic without setTimeout(0)

Relying on a 0ms timeout is slightly racy. Wait until the first reader.read() has actually started by asserting the spy count, then cancel.

-    const promise = expect(generator.next()).rejects.toBeInstanceOf(shared.AbortError)
-
-    await new Promise(resolve => setTimeout(resolve, 0))
-    await generator.return(undefined)
-    await promise
+    const promise = expect(generator.next()).rejects.toBeInstanceOf(shared.AbortError)
+    // Ensure the in-flight read has begun before cancelling.
+    await vi.waitFor(() => expect(runInSpanContextSpy).toHaveBeenCalledTimes(1))
+    await generator.return(undefined)
+    await promise
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0ddbfa and 18e587b.

📒 Files selected for processing (2)
  • packages/standard-server-fetch/src/event-iterator.test.ts (1 hunks)
  • packages/standard-server-fetch/src/event-iterator.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/standard-server-fetch/src/event-iterator.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/standard-server-fetch/src/event-iterator.test.ts (2)
packages/standard-server-fetch/src/event-iterator.ts (1)
  • toEventIterator (7-116)
packages/shared/src/iterator.ts (1)
  • isAsyncIteratorObject (6-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: publish-commit
  • GitHub Check: lint
🔇 Additional comments (2)
packages/standard-server-fetch/src/event-iterator.test.ts (2)

183-205: Good: verifies pending next() rejects with AbortError on cancellation

The assertion against shared.AbortError accurately reflects the new cancellation semantics. Instrumentation call counts also look consistent with one in-flight read and one cancel.


183-205: Duplicate test name check passed – no duplicates found.

@dinwwwh dinwwwh added the lgtm This PR has been approved by a maintainer label Sep 10, 2025
@dinwwwh dinwwwh merged commit 06aad3a into main Sep 11, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant