fix(event-stream): do not emit unhandled rejection when readable side is canceled#1376
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/internal/event-stream.ts (1)
24-28: LGTM — fix is correct and consistent with existing pattern.Attaching
.catch(_noop)to thewriter.closedchain correctly prevents the unhandled rejection that occurs when the underlyingTransformStream's readable side is cancelled (which rejectswriter.closedper WHATWG Streams). The pattern mirrors the existingonClosedimplementation at line 164 (this._writer.closed.then(cb).catch(_noop)) and the per-write.catch(...)handlers throughout the class, so the intent to swallow these errors is consistent.One minor note for consideration: the
.then(...)callback sets_writerIsClosed = trueonly on resolution, but on rejection (the cancel case)_writerIsClosedis left asfalse. In practice the per-write.catchhandlers also flip the flag andclose()guards on_disposed, so this is unlikely to cause issues — but if you want the flag to reflect "writer is no longer usable" in both outcomes, you could set it in the catch as well:Optional refinement
this._writer.closed .then(() => { this._writerIsClosed = true; }) - .catch(_noop); + .catch(() => { + this._writerIsClosed = true; + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/internal/event-stream.ts` around lines 24 - 28, The writer.closed promise only sets this._writerIsClosed = true on resolution, leaving the flag false on rejection; update the writer.closed handling (the this._writer.closed.then(...).catch(_noop) block) so that this._writerIsClosed is set when the promise rejects as well (either by setting the flag in the catch or using a finally-equivalent) to reflect that the writer is no longer usable; refer to this._writer.closed, this._writerIsClosed, _noop and mirror the pattern used by onClosed and the per-write .catch handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/internal/event-stream.ts`:
- Around line 24-28: The writer.closed promise only sets this._writerIsClosed =
true on resolution, leaving the flag false on rejection; update the
writer.closed handling (the this._writer.closed.then(...).catch(_noop) block) so
that this._writerIsClosed is set when the promise rejects as well (either by
setting the flag in the catch or using a finally-equivalent) to reflect that the
writer is no longer usable; refer to this._writer.closed, this._writerIsClosed,
_noop and mirror the pattern used by onClosed and the per-write .catch handlers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3e5ed762-e4a4-45c8-b75c-b28da2791ad5
📒 Files selected for processing (1)
src/utils/internal/event-stream.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/internal/event-stream.ts (1)
24-30: LGTM — correctly handles the rejection ofwriter.closed.This addresses the unhandled rejection cleanly and is consistent with the existing
.catch(() => { this._writerIsClosed = true })pattern used on per-write calls (lines 78, 95, 114, 137). On Deno specifically, an unhandled rejection from the closed promise (e.g.,"resource closed"after the readable side is cancelled) would terminate the process, so handling it here is the right fix.♻️ Optional: collapse the duplicated body into a single handler
Both branches do the same thing, so you can equivalently write it as:
- this._writer.closed - .then(() => { - this._writerIsClosed = true; - }) - .catch(() => { - this._writerIsClosed = true; - }); + this._writer.closed.finally(() => { + this._writerIsClosed = true; + });Note:
.finally()still propagates the rejection, so chain a.catch(_noop)if you adopt this — or keep the current explicit two-handler form, which is already clear and correct.- this._writer.closed - .then(() => { - this._writerIsClosed = true; - }) - .catch(() => { - this._writerIsClosed = true; - }); + this._writer.closed + .finally(() => { + this._writerIsClosed = true; + }) + .catch(_noop);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/internal/event-stream.ts` around lines 24 - 30, The current .then(...).catch(...) on this._writer.closed sets this._writerIsClosed in both branches; simplify by replacing the two-branch pattern with a single finally handler followed by a swallow-catch to avoid propagating rejections: call this._writer.closed.finally(() => set this._writerIsClosed = true) and then chain a .catch(_) noop to swallow any error; reference this._writer.closed and the this._writerIsClosed flag when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/internal/event-stream.ts`:
- Around line 24-30: The current .then(...).catch(...) on this._writer.closed
sets this._writerIsClosed in both branches; simplify by replacing the two-branch
pattern with a single finally handler followed by a swallow-catch to avoid
propagating rejections: call this._writer.closed.finally(() => set
this._writerIsClosed = true) and then chain a .catch(_) noop to swallow any
error; reference this._writer.closed and the this._writerIsClosed flag when
making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7f7b98b1-cf03-482a-838d-374ba59166c8
📒 Files selected for processing (1)
src/utils/internal/event-stream.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/utils/internal/event-stream.ts`:
- Around line 24-27: The call to this._writer.closed.finally(...) can still
produce an unhandled rejection if the closed promise rejects; update the chain
to swallow rejections by adding .catch(this._noop) after the .finally(...) so
that this._writerIsClosed = true is still set but any rejection from
this._writer.closed is handled (use the existing _noop helper, consistent with
the pattern used in onClosed()).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0f35a60b-68f9-4653-91fc-fe06be00f7c0
📒 Files selected for processing (1)
src/utils/internal/event-stream.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/utils/internal/event-stream.ts (1)
24-27:⚠️ Potential issue | 🔴 Critical
.finally()alone does not prevent the unhandled rejection — the bug from#1375is still present.
Promise.prototype.finally(onFinally)returns a promise that adopts the original promise's settled state. Ifthis._writer.closedrejects (which is exactly what happens when theTransformStreamis cancelled, per WHATWG Streams), the promise returned by.finally(...)rejects with the same reason. Since nothing catches it, the unhandled rejection still escapes and Deno will still terminate the process — defeating the stated purpose of this PR.The PR description even says "adds a noop
.catch", but the implementation uses.finally()without one. The file already exposes_noop(line 4) and the correct pattern is used inonClosed()at line 163 (.then(cb).catch(_noop)).🔧 Suggested fix
this._writer.closed .finally(() => { this._writerIsClosed = true; - }); + }) + .catch(_noop);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/internal/event-stream.ts` around lines 24 - 27, The current use of this._writer.closed.finally(() => { this._writerIsClosed = true; }) still allows rejections to escape; update the chain to swallow rejections by appending a catch that uses the existing _noop (e.g., this._writer.closed.finally(() => { this._writerIsClosed = true; }).catch(_noop)) so the rejection from the TransformStream cancellation is handled (mirror the pattern used in onClosed()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/utils/internal/event-stream.ts`:
- Around line 24-27: The current use of this._writer.closed.finally(() => {
this._writerIsClosed = true; }) still allows rejections to escape; update the
chain to swallow rejections by appending a catch that uses the existing _noop
(e.g., this._writer.closed.finally(() => { this._writerIsClosed = true;
}).catch(_noop)) so the rejection from the TransformStream cancellation is
handled (mirror the pattern used in onClosed()).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b10d8b37-2b5e-4dc7-88ad-8644a2dc981b
📒 Files selected for processing (1)
src/utils/internal/event-stream.ts
|
Can you please add a regression test? 🙏🏼 |
|
Done @pi0 :) |
Per WHATWG Streams, cancelling a TransformStream's readable errors its writable with the same reason. writer.closed therefore rejects with the string 'resource closed'.
H3's .then(() => { this._writerIsClosed = true }) has no .catch, so the rejection bubbles to a top-level unhandled promise rejection and Deno terminates the process.
H3 does correctly .catch(...) the per-write rejections (this._writer.write(...).catch(() => { this._writerIsClosed = true })), so the design intent is clearly to swallow these — it just forgot the same .catch on .closed. Adding one to that .then would fix this regardless of runtime.
commit: |
Per WHATWG Streams, cancelling a TransformStream's readable errors its writable with the same reason. writer.closed therefore rejects with the string 'resource closed'.
H3's .then(() => { this._writerIsClosed = true }) has no .catch, so the rejection bubbles to a top-level unhandled promise rejection and Deno terminates the process.
H3 does correctly .catch(...) the per-write rejections (this._writer.write(...).catch(() => { this._writerIsClosed = true })), so the design intent is clearly to swallow these — it just forgot the same .catch on .closed. Adding one to that .then would fix this regardless of runtime.
Summary by CodeRabbit