fix(sse): mark writer as closed on write failure#1322
Conversation
…erations - onClosed() called .then(cb) without .catch(), so if the callback threw, it became an unhandled promise rejection - Write operations used .catch() with no callback argument, which is technically valid but unclear — replaced with explicit _noop for clarity Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughReplace silent, potentially unhandled writer promise rejections with explicit catches that mark the internal writer as closed; add tests asserting Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/utils/internal/event-stream.ts (1)
74-106:⚠️ Potential issue | 🟠 MajorSet
_writerIsClosed = truewhen write operations fail to prevent silent retry loops.At lines 74, 89, and 106, write errors are suppressed with
.catch(_noop)but the instance state is not updated. If a write fails, subsequentpush()calls will pass the_writerIsClosedcheck (stillfalse) and attempt to write again, repeating silently until the writer closes. Update the catch handlers to set_writerIsClosed = trueto short-circuit these failed write attempts and prevent silent failures.Suggested patch
- await this._writer.write(this._encoder.encode(formatEventStreamComment(comment))).catch(_noop); + await this._writer + .write(this._encoder.encode(formatEventStreamComment(comment))) + .catch(() => { + this._writerIsClosed = true; + }); - await this._writer.write(this._encoder.encode(formatEventStreamMessage(message))).catch(_noop); + await this._writer + .write(this._encoder.encode(formatEventStreamMessage(message))) + .catch(() => { + this._writerIsClosed = true; + }); - await this._writer.write(this._encoder.encode(payload)).catch(_noop); + await this._writer.write(this._encoder.encode(payload)).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 74 - 106, The write operations in _sendEvent and _sendEvents (and the comment-writing call that uses formatEventStreamComment) currently suppress errors with .catch(_noop) which leaves _writerIsClosed false and allows silent retry loops; update each catch handler to set this._writerIsClosed = true (and then optionally log or swallow) so a failed write short-circuits future pushes — modify the three write calls that call this._writer.write(this._encoder.encode(...)).catch(_noop) to instead set this._writerIsClosed = true in their catch handlers.
🤖 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`:
- Line 154: The writer.closed promise rejection is ignored; update both places
that call this._writer.closed.then(...) (in the constructor where you set
_writerIsClosed and in onClosed) to invoke the same callback/state-setter on
both fulfillment and rejection so cleanup/state changes always run. Concretely,
change the current .then(cb).catch(_noop) usage to call the callback as the
rejection handler as well (e.g., pass cb as the second argument to .then or call
.catch(cb)), ensuring the code that sets _writerIsClosed and the onClosed
callback always executes even if writer.closed rejects; this will restore
correct behavior for methods like pushComment and _sendEvent and guarantee
cleanup in onClosed.
---
Outside diff comments:
In `@src/utils/internal/event-stream.ts`:
- Around line 74-106: The write operations in _sendEvent and _sendEvents (and
the comment-writing call that uses formatEventStreamComment) currently suppress
errors with .catch(_noop) which leaves _writerIsClosed false and allows silent
retry loops; update each catch handler to set this._writerIsClosed = true (and
then optionally log or swallow) so a failed write short-circuits future pushes —
modify the three write calls that call
this._writer.write(this._encoder.encode(...)).catch(_noop) to instead set
this._writerIsClosed = true in their catch handlers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 38f24f9d-3672-4156-9e99-8643bbd506f2
📒 Files selected for processing (1)
src/utils/internal/event-stream.ts
|
Can you please provide reproduction + add regression test? (each line of change should be covered by it. it fails without change and fixes after) |
When a write operation fails, mark _writerIsClosed = true to prevent subsequent push() calls from silently retrying writes on a broken stream. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- onClosed: verify throwing callback doesn't cause unhandled rejection
- push/pushComment: verify operations don't throw after stream close
- write failure: verify _writerIsClosed is set to true on write error,
preventing subsequent push() calls from silently retrying writes
The last test fails without the .catch(() => { this._writerIsClosed = true })
fix and passes with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/unit/sse.test.ts (1)
120-137: Accessing private internals is acceptable here for regression testing.This test verifies the core mechanism added by this PR: setting
_writerIsClosed = trueon write failure. While accessing private fields via(stream as any)is fragile and will break if internals are renamed, it's justified here since:
- The PR objective specifically requires testing that
_writerIsClosedis set on write error- There's no public API to observe this internal state
- This is a regression test that "fails without change and fixes after" as requested by the reviewer
Consider adding a brief comment explaining why internal access is necessary:
📝 Suggested documentation
it("marks writer as closed on write failure to prevent silent retries", async () => { const event = mockEvent("/"); const stream = new EventStream(event); - // Access internals to verify state + // Access internals to verify state — required because _writerIsClosed is private + // and this regression test needs to confirm the fix for issue `#1322` const writeSpy = vi.fn().mockRejectedValue(new Error("write failed"));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/sse.test.ts` around lines 120 - 137, Add a short explanatory comment above the lines that access private internals in the test to justify using (stream as any) to inspect _writerIsClosed and _writer.write: explain this is a regression test for EventStream.push that must verify the private flag _writerIsClosed is set on write failure and there is no public API to observe this state. Keep the test logic unchanged (references: EventStream, push, _writer.write, _writerIsClosed, writeSpy) and ensure the comment is concise and placed immediately before the internal access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/unit/sse.test.ts`:
- Around line 78-95: The test registers a Node-only handler with process.on/off
which will blow up in non-Node runtimes; update the test around EventStream,
stream.onClosed and the unhandledRejection handler to be runtime-safe by
guarding access to process: only call process.on("unhandledRejection",
unhandled) and process.off("unhandledRejection", unhandled) if typeof
globalThis.process !== "undefined" && typeof (globalThis.process as any).on ===
"function" (or use globalThis.process?.on/off), or alternatively skip the test
when process is undefined (e.g., wrap the it(...) with a runtime check). Ensure
the guard surrounds both registration and teardown so the EventStream.onClosed
behavior is still asserted in Node while avoiding ReferenceError in other
runtimes.
---
Nitpick comments:
In `@test/unit/sse.test.ts`:
- Around line 120-137: Add a short explanatory comment above the lines that
access private internals in the test to justify using (stream as any) to inspect
_writerIsClosed and _writer.write: explain this is a regression test for
EventStream.push that must verify the private flag _writerIsClosed is set on
write failure and there is no public API to observe this state. Keep the test
logic unchanged (references: EventStream, push, _writer.write, _writerIsClosed,
writeSpy) and ensure the comment is concise and placed immediately before the
internal access.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8481988f-4807-47a3-9b61-222f7851d12b
📒 Files selected for processing (2)
src/utils/internal/event-stream.tstest/unit/sse.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/utils/internal/event-stream.ts
Add dedicated regression tests for each write failure catch handler:
- L74: pushComment write failure sets _writerIsClosed
- L89: push (single event) write failure sets _writerIsClosed
- L110: push (batch events) write failure sets _writerIsClosed
All three tests fail without the .catch(() => { _writerIsClosed = true })
fix and pass with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/unit/sse.test.ts (1)
77-95:⚠️ Potential issue | 🟠 MajorMake this test matrix-safe; raw
process.on/offis runtime-fragile.Line 83 and Line 93 rely on Node-only
processAPIs. This can fail in non-Node targets and bypasses the standardctx.errorsflow. Please move this case to thedescribeMatrixpattern and use runtime-aware skipping plus matrix error tracking. Also, ensure listener cleanup runs infinally.#!/bin/bash set -euo pipefail echo "Check for runtime-fragile Node process hooks in this test:" rg -n 'process\.(on|off)\("unhandledRejection"' test/unit/sse.test.ts echo echo "Check whether matrix test utilities are used in this file:" rg -n 'describeMatrix|ctx\.errors|it\.skipIf\(' test/unit/sse.test.tsAs per coding guidelines:
test/**/*.test.tsshould usedescribeMatrix, track unhandled errors withctx.errors, and use runtime-specific skips withit.skipIf(...).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/sse.test.ts` around lines 77 - 95, The test currently attaches Node-only process handlers directly which is fragile; refactor the "EventStream onClosed" test to use describeMatrix and the test matrix utilities: wrap the case in describeMatrix, use it.skipIf(runtime !== "node") or equivalent to skip non-Node runtimes, replace raw process.on/off with the matrix error tracker (ctx.errors) to capture unhandled rejections, register the listener via ctx.errors (or equivalent helper) when testing EventStream.onClosed and ensure listener removal occurs in a finally block so cleanup always runs; locate references to EventStream, stream.onClosed, and the test case in this file to apply the changes.
🧹 Nitpick comments (2)
test/unit/sse.test.ts (2)
8-8: Avoid barrel import fromsrc/index.tsin this.tstest.Please import
mockEventdirectly from its defining module instead of../../src/index.ts.As per coding guidelines:
**/*.ts: “Do not use barrel files — import directly from specific modules”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/sse.test.ts` at line 8, The test currently uses a barrel import of mockEvent from ../../src/index.ts; change the import in test/unit/sse.test.ts to import mockEvent directly from the module that defines it (the file where mockEvent is declared) instead of the barrel file — update the import statement to reference that specific module (the module that exports mockEvent) so the test imports mockEvent directly from its defining source.
97-118: “Stops retrying” needs an observable assertion.These tests currently validate “does not throw” but don’t directly prove no retry/write attempt occurred. Consider asserting write-call behavior (similar to Lines 120-166) to make the test intent explicit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/sse.test.ts` around lines 97 - 118, The tests should assert that no write attempts are made after the stream is closed rather than only asserting no throw; update both test cases to spy/mock the underlying writer write method and verify it was not called after close. Specifically, locate the EventStream instance and its internal writer (refer to EventStream, close, push, pushComment) and replace or spyOn the writer.write (or the WritableStreamDefaultWriter.write) with a jest.fn(), call await stream.close(), then call stream.push/stream.pushComment and assert the mocked write was not invoked (call count remains 0) for both messages to prove no retry/write occurred.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@test/unit/sse.test.ts`:
- Around line 77-95: The test currently attaches Node-only process handlers
directly which is fragile; refactor the "EventStream onClosed" test to use
describeMatrix and the test matrix utilities: wrap the case in describeMatrix,
use it.skipIf(runtime !== "node") or equivalent to skip non-Node runtimes,
replace raw process.on/off with the matrix error tracker (ctx.errors) to capture
unhandled rejections, register the listener via ctx.errors (or equivalent
helper) when testing EventStream.onClosed and ensure listener removal occurs in
a finally block so cleanup always runs; locate references to EventStream,
stream.onClosed, and the test case in this file to apply the changes.
---
Nitpick comments:
In `@test/unit/sse.test.ts`:
- Line 8: The test currently uses a barrel import of mockEvent from
../../src/index.ts; change the import in test/unit/sse.test.ts to import
mockEvent directly from the module that defines it (the file where mockEvent is
declared) instead of the barrel file — update the import statement to reference
that specific module (the module that exports mockEvent) so the test imports
mockEvent directly from its defining source.
- Around line 97-118: The tests should assert that no write attempts are made
after the stream is closed rather than only asserting no throw; update both test
cases to spy/mock the underlying writer write method and verify it was not
called after close. Specifically, locate the EventStream instance and its
internal writer (refer to EventStream, close, push, pushComment) and replace or
spyOn the writer.write (or the WritableStreamDefaultWriter.write) with a
jest.fn(), call await stream.close(), then call stream.push/stream.pushComment
and assert the mocked write was not invoked (call count remains 0) for both
messages to prove no retry/write occurred.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 78b90414-64bc-4971-9cf7-7b9b80a0b4b9
📒 Files selected for processing (1)
test/unit/sse.test.ts
|
This seems like a valid fix but still tests are not covering (reverting some changes in src, still tests pass) |
Mark writer as closed when flush() write fails, consistent with the other write paths. Remove line number references from test descriptions.
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes