Skip to content

feat(fetch)!: always send close event, remove emptyCloseEventEnabled option - #125

Merged
dinwwwh merged 1 commit into
mainfrom
claude/remove-empty-close-event-enabled-e779ea
Sep 29, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/remove-empty-close-event-enabled-e779ea

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026

Copy link
Copy Markdown
Member

toEventStream now always sends a close event when the iterator completes, even if it returns undefined (sent as event: close with no data). The emptyCloseEventEnabled option is removed, so clients can always tell a finished stream from a dropped connection.

Breaking change

  • Passing emptyCloseEventEnabled is now a type error in @standard-server/fetch and @standard-server/node, which share the same options type.
  • Callers that set it to false now receive a trailing event: close they didn't get before.

Testing

  • Removed the tests that only covered the option. The existing with return and without return tests still cover the close event, both with a return value and without one.
  • Type check, lint and the full test suite (739 tests) pass.

…` option

`toEventStream` now always emits a `close` event when the iterator
completes, including when it returns `undefined`, so clients can
reliably tell a finished stream from a dropped connection.
@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026

Copy link
Copy Markdown
@standard-server/aws-lambda

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

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@125

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@125

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@125

commit: 35be66d

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

This PR makes toEventStream always emit the terminal close event and removes the emptyCloseEventEnabled option.

  • Always-on close event — packages/fetch/src/event-stream.ts:187 now unconditionally enqueues { ...meta, event: result.done ? 'close' : 'message', data: stringifyJSON(data) }, dropping the emptyCloseEventEnabled gate and its default. Behavior under default options is unchanged; only callers that passed false gain a trailing event: close.
  • Option removal — emptyCloseEventEnabled is gone from ToEventStreamOptions (packages/fetch/src/event-stream.ts:116). Since @standard-server/node re-exports this type (packages/node/src/event-stream.ts:18), the break is marked correctly as !.
  • Test cleanup — the empty close event describe block (5 tests) is removed. The surviving with return and without return cases (packages/fetch/src/event-stream.test.ts:218, :238) still pin the close event both with a return value and without, so coverage is retained.

No remaining references to emptyCloseEventEnabled exist anywhere in the repo, and packages/core/README.md:185 already documents that return emits a close event, so the docs get strictly more accurate. pnpm exec vitest run packages/fetch/src/event-stream.test.ts packages/node/src/event-stream.test.ts passes (26 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@codspeed

codspeed Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/remove-empty-close-event-enabled-e779ea (35be66d) with main (b5b4035)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@dinwwwh
dinwwwh merged commit ddd0a09 into main Sep 29, 2026
11 checks passed
dinwwwh added a commit that referenced this pull request Oct 18, 2026
…169)

`toAsyncIteratorObject` now only finishes cleanly on a `close` event, or
throws `ErrorEvent` on an `error` event. A stream that ends without
either now rejects with `EventStreamDecoderError('Event Stream ended
before complete')`. That is the same error a stream gets when it ends
partway through a message. The rule also covers a missing (`null`) body.

Since #125, `toEventStream` always sends a `close` event, so streams
from standard-server senders are unaffected. This change lets the client
actually use that signal: a dropped connection no longer looks like a
finished stream.

## Why

When the server's pipeline errors, the client already sees a network
error. Many cut-offs still reach the client as a clean end of stream,
though, and before this change they read as a finished stream. In a
local probe with Node's `fetch` and `http2` client:
- an HTTP/1.1 response ended early (`res.end()`)
- a connection-close-delimited body (an HTTP/1.0 hop) whose connection
drops
- an HTTP/2 stream ended early, or its session torn down

Proxy timeouts, serverless time limits and graceful shutdowns can end
responses in these ways.

## Scope

- Applies to `@standard-server/fetch`, `@standard-server/node` and
`@standard-server/aws-lambda`, which share the fetch implementation.
- `@standard-server/peer` is unchanged. It already rejects when its
queue closes before a `close` event.
- Stopping early with `return()` still resolves as done.

## Breaking change

- Reading an event stream that ends without a `close` event now throws
instead of resolving `{ done: true, value: undefined }`.
- This affects SSE senders that never send `event: close`: generic SSE
APIs, and standard-server senders that set the old
`emptyCloseEventEnabled: false` option.
- `toAsyncIteratorObject(null)` now throws on the first `next()`. This
happens, for example, with a HEAD or 204 response labelled
`text/event-stream`.

## Testing

- Updated the tests that relied on a close-less stream finishing
quietly. They now either send `event: close` or expect the error.
- Added an empty-stream case and changed the null-stream case to expect
the error.
- `pnpm run check` (sherif, oxlint, oxfmt, tsc) and the full vitest
suite pass locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01NYpqRELEp4zdwCYWeqRMAj

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

1 participant