Summary
Any custom Resource that streams a plain async generator to natural completion over SSE (Accept: text/event-stream) hangs the HTTP response and throws a process uncaughtException the instant the generator returns. Data events + [DONE] are delivered, but the response never closes. This sits directly on the LLM/openaiStream completion pattern #631 is building toward, and on any bounded/export-over-SSE stream.
Root cause (code-confirmed)
server/serverHelpers/contentTypes.ts — transformIterable's next() unconditionally calls transform(step.value) even on the generator's terminal step { value: undefined, done: true }. SSE serialize() (contentTypes.ts:135) opens with if (message.acknowledge) message.acknowledge(), but message is undefined on that step → undefined.acknowledge throws:
TypeError: Cannot read properties of undefined (reading 'acknowledge')
at serialize (server/serverHelpers/contentTypes.ts:135)
← contentTypes.ts:627
← nextAsync (node:internal/streams/from:182)
Value-shape-independent: an AckShapeGen whose yielded items carry a real .acknowledge() succeeds 5/5 on the items — only the terminal undefined crashes.
Why it wasn't hit before
The common infinite-subscription SSE case (IterableEventQueue) never reaches a terminal done step. A plain generator returned over Accept: application/json closes cleanly (control) — the bug is confined to the SSE serialize/transformIterable path on finite generators.
Repro
A minimal static async *connect() yielding a few plain JSON objects then returning, requested with Accept: text/event-stream, hangs (response end never fires) + logs the uncaughtException. The same generator as application/json closes cleanly. Harper 228eacc0f.
Fix
Guard transformIterable to not transform the terminal done step (if (!step.done)), and/or null-guard serialize(message).
Found via exploratory QA. Related but distinct from #1399 (SSE teardown deferral).
— KrAIs 🤖 (exploratory QA, on Kris's behalf)
Summary
Any custom Resource that streams a plain async generator to natural completion over SSE (
Accept: text/event-stream) hangs the HTTP response and throws a processuncaughtExceptionthe instant the generator returns. Data events +[DONE]are delivered, but the response never closes. This sits directly on the LLM/openaiStreamcompletion pattern #631 is building toward, and on any bounded/export-over-SSE stream.Root cause (code-confirmed)
server/serverHelpers/contentTypes.ts—transformIterable'snext()unconditionally callstransform(step.value)even on the generator's terminal step{ value: undefined, done: true }. SSEserialize()(contentTypes.ts:135) opens withif (message.acknowledge) message.acknowledge(), butmessageisundefinedon that step →undefined.acknowledgethrows:Value-shape-independent: an
AckShapeGenwhose yielded items carry a real.acknowledge()succeeds 5/5 on the items — only the terminalundefinedcrashes.Why it wasn't hit before
The common infinite-subscription SSE case (
IterableEventQueue) never reaches a terminaldonestep. A plain generator returned overAccept: application/jsoncloses cleanly (control) — the bug is confined to the SSEserialize/transformIterablepath on finite generators.Repro
A minimal
static async *connect()yielding a few plain JSON objects then returning, requested withAccept: text/event-stream, hangs (responseendnever fires) + logs the uncaughtException. The same generator asapplication/jsoncloses cleanly. Harper228eacc0f.Fix
Guard
transformIterableto nottransformthe terminaldonestep (if (!step.done)), and/or null-guardserialize(message).Found via exploratory QA. Related but distinct from #1399 (SSE teardown deferral).
— KrAIs 🤖 (exploratory QA, on Kris's behalf)