Follow-up from #1478 / #1484 / #1488. Now that onDispose (#1488) gives EventStream a portable end-of-event signal, the autoclose option looks vestigial — and its false branch is arguably harmful. Raising it before v2 stable, while removing it is still free.
What it does today
EventStreamOptions.autoclose (default true) gates two things in src/utils/internal/event-stream.ts:
- L42-47 — registers
onDispose(event, () => this.close()), i.e. auto-close when the event ends.
- L35-37 — whether
_disposed is set inside the writer-closed handler.
Effect 2 is nearly unobservable: _writerIsClosed is set outside the gate on L34, and _isClosed is _writerIsClosed || _disposed, so push/pushComment/flush no-op either way. The only difference is whether a later manual close() early-returns or falls through to a no-op branch.
Why autoclose: false may no longer be meaningful
-
It cannot keep the stream writable after a disconnect. Cancelling the readable errors the writable side regardless of the option, so pushes no-op either way. The transport is gone; no flag brings it back.
-
It cannot cause a premature close. For a stream that is the response body, onDispose fires only once the body finishes streaming — at which point close() is a no-op. So autoclose: true can never cut a live stream short, which was the original reason to offer an opt-out.
-
It does not control onClosed delivery for a sent stream; those callbacks fire from the writer-closed handler independently.
-
It silently disables cleanup for unsent streams. A stream created but never send()-ed with autoclose: false never fires onClosed at all — and nothing the user can do recovers it, because the response is already gone and there is no other signal that the event ended. Verified:
| scenario |
onClosed runs |
| unsent, handler returns normally |
yes |
| unsent, handler throws after registering |
yes |
| sent stream, normal completion |
yes |
unsent, autoclose: false |
no |
So the remaining behavioral surface of autoclose: false is: h3 does not call close() for you at end of event — in the one case where that matters, it produces a leak.
Current coverage and exposure
- Not referenced anywhere in
docs/ — only the JSDoc on the type.
- No test passes
autoclose: false. The opt-out branch is entirely uncovered (test/sse.test.ts:163 is named "autocloses…" but exercises the default).
Historical note
The option made sense when introduced in #586 (Feb 2024): h3 v1 was Node-only, event.node.req.on("close") was the only lifecycle signal available, and opting out of a Node listener was a coherent choice. The v2 rewrite and #1488 replaced that mechanism entirely; the option outlived its mechanism.
Options
- Remove it. Cheapest before v2 stable.
EventStream always registers onDispose.
- Keep the type, ignore the value, with a deprecation notice, and drop it in a later major.
- Keep it, but fix the leak — always register
onDispose and have the callback respect autoclose (so cleanup/onClosed always runs, while a live stream is still not force-closed). Preserves the opt-out with no dead end.
- Keep as-is and document that
autoclose: false disables onClosed for streams that are never sent.
I lean 1, with 3 as the fallback if there is a real consumer. Worth checking nitro and other downstreams for autoclose usage before deciding — I only verified this repo.
Follow-up from #1478 / #1484 / #1488. Now that
onDispose(#1488) givesEventStreama portable end-of-event signal, theautocloseoption looks vestigial — and itsfalsebranch is arguably harmful. Raising it before v2 stable, while removing it is still free.What it does today
EventStreamOptions.autoclose(defaulttrue) gates two things insrc/utils/internal/event-stream.ts:onDispose(event, () => this.close()), i.e. auto-close when the event ends._disposedis set inside the writer-closed handler.Effect 2 is nearly unobservable:
_writerIsClosedis set outside the gate on L34, and_isClosedis_writerIsClosed || _disposed, sopush/pushComment/flushno-op either way. The only difference is whether a later manualclose()early-returns or falls through to a no-op branch.Why
autoclose: falsemay no longer be meaningfulIt cannot keep the stream writable after a disconnect. Cancelling the readable errors the writable side regardless of the option, so pushes no-op either way. The transport is gone; no flag brings it back.
It cannot cause a premature close. For a stream that is the response body,
onDisposefires only once the body finishes streaming — at which pointclose()is a no-op. Soautoclose: truecan never cut a live stream short, which was the original reason to offer an opt-out.It does not control
onCloseddelivery for a sent stream; those callbacks fire from the writer-closed handler independently.It silently disables cleanup for unsent streams. A stream created but never
send()-ed withautoclose: falsenever firesonClosedat all — and nothing the user can do recovers it, because the response is already gone and there is no other signal that the event ended. Verified:onClosedrunsautoclose: falseSo the remaining behavioral surface of
autoclose: falseis: h3 does not callclose()for you at end of event — in the one case where that matters, it produces a leak.Current coverage and exposure
docs/— only the JSDoc on the type.autoclose: false. The opt-out branch is entirely uncovered (test/sse.test.ts:163is named "autocloses…" but exercises the default).Historical note
The option made sense when introduced in #586 (Feb 2024): h3 v1 was Node-only,
event.node.req.on("close")was the only lifecycle signal available, and opting out of a Node listener was a coherent choice. The v2 rewrite and #1488 replaced that mechanism entirely; the option outlived its mechanism.Options
EventStreamalways registersonDispose.onDisposeand have the callback respectautoclose(so cleanup/onClosedalways runs, while a live stream is still not force-closed). Preserves the opt-out with no dead end.autoclose: falsedisablesonClosedfor streams that are never sent.I lean 1, with 3 as the fallback if there is a real consumer. Worth checking nitro and other downstreams for
autocloseusage before deciding — I only verified this repo.