fix(streaming): expose Last-Event-ID and make write-after-abort a no-op - #5289
Open
SynthLuvr wants to merge 2 commits into
Open
fix(streaming): expose Last-Event-ID and make write-after-abort a no-op#5289SynthLuvr wants to merge 2 commits into
SynthLuvr wants to merge 2 commits into
Conversation
Addresses the SSE lifecycle pain points behind honojs#3765, extracted from the workarounds apps had to build on top of streamSSE: - Last-Event-ID exposure: the SSE callback could not see the `Last-Event-ID` header that EventSource sends automatically on reconnect, so resuming a stream required reading `c.req.header('last-event-id')` outside `streamSSE` and threading the cursor in. `SSEStreamingApi` now carries `lastEventId` (optional third constructor argument, populated by `streamSSE`), so handlers can resume natively from the last event the client received. - Write-after-abort semantics: writing after the stream was aborted still touched the underlying writer, relying on the runtime to error it once the disconnect propagates. On runtimes where cancellation is not propagated to the writable (e.g. Bun < 1.1.27, the same versions hono already special-cases), a backpressured write after abort never settles and the handler hangs forever — the "SSE does not work" class of failure. `StreamingApi.write()`/`writeln()` now short-circuit once `aborted` is set: writes are defined no-ops that can neither throw nor hang, and `close()` after abort no longer touches the torn-down writer.
- Condense the write()/close() abort-guard comments to the essential runtime-behavior rationale - Shorten the lastEventId doc to the reconnect semantics - Reset lifecycle test counters in beforeEach instead of inside the route - Drop narration comments; simplify SSE frame data extraction in tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3765
What
Two fixes to the SSE streaming lifecycle:
streamSSEnow exposes theLast-Event-IDrequest header asstream.lastEventId. When anEventSourcereconnects, the browser re-sends theidof the last event it received in this header, so handlers can resume the event stream instead of replaying everything. It isundefinedwhen the header is absent.StreamingApi.write()andclose()are no-ops afterabort(). Once the client is gone the writable may be errored, and touching the writer can throw or hang depending on the runtime. Both methods now return early whenabortedis set, so a handler that keeps producing after a disconnect completes cleanly instead of hanging or crashing.Testing
src/utils/stream.test.ts:write()/close()afterabort()and after the client cancels the response stream resolve as no-ops without touching the writer.src/helper/streaming/sse.test.tsx:lastEventIdis exposed from the header andundefinedwhen absent;writeSSE()after a client disconnect resolves as a no-op and the handler finishes.runtime-tests/node/index.test.ts: end-to-end over a real Node server — resume fromLast-Event-IDon reconnect, full replay without the header, and a handler finishing cleanly after a mid-stream disconnect.