Skip to content

fix(js-client): stabilize hook headers, handle rejections, tolerate [DONE] streams#31

Open
cabljac wants to merge 1 commit into
js-clientfrom
fix/js-client-streaming-and-header-stability
Open

fix(js-client): stabilize hook headers, handle rejections, tolerate [DONE] streams#31
cabljac wants to merge 1 commit into
js-clientfrom
fix/js-client-streaming-and-header-stability

Conversation

@cabljac

@cabljac cabljac commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Three correctness fixes on top of the js-client branch, from a review of the client SDK + React hooks.

1. 🔴 Inline headers silently abort streams (blocker)

useStream/useAction listed the raw HeadersInit in their dependency arrays. Callers almost always pass an inline object literal — a new reference every render — so the stream executor was re-created and any in-flight request aborted on every re-render. Now they depend on a content-derived serializeHeaders(...) key (latest value read via a ref), so equal headers keep the executor/execute referentially stable.

2. 🟠 Unhandled rejection on fire-and-forget execute()

execute() re-throws after storing the error in error state. A caller that doesn't await (e.g. onClick={() => execute()}) got an unhandled promise rejection. We now attach an internal no-op .catch to the returned promise; errors remain observable via error state and awaiting callers still see the rejection.

3. 🟠 Stream end falsely errors without a {result} envelope

parseStreamResponse threw "Stream did not terminate correctly" whenever a stream closed without a {result} envelope. Streaming-only flows that close gracefully after a [DONE] sentinel now resolve to undefined. A genuinely truncated stream (no result, no terminator) still throws, with a clearer message.

Tests

  • client unit: +2 (parse-stream graceful [DONE], truncated-stream error) — 28 pass
  • react: +2 (header stability for both hooks) — 8 pass
  • client type tests pass

Verified locally (client unit + type tests, react tests run against client source). Note: a separate pre-existing ESM build issue in @genkit-ai/client (index.mjs importing ./aliases.js, cf. genkit-ai#5405) blocks running the react suite against the built lib — out of scope for this PR but worth a follow-up.

@cabljac cabljac force-pushed the fix/js-client-streaming-and-header-stability branch 2 times, most recently from 2c9256c to e7e47b9 Compare June 1, 2026 15:49
…DONE] streams

Three fixes to the client SDK and React hooks:

- useStream/useAction: depend on a content-derived headers key instead of
  the raw HeadersInit. Callers almost always pass an inline object literal
  (new reference every render), which re-created the stream executor and
  aborted any in-flight request on every render.

- useStream/useAction: attach an internal no-op catch to the promise
  returned from execute(), so a fire-and-forget caller that ignores the
  promise does not trigger an unhandled rejection. Errors remain observable
  via `error` state, and awaiting callers still see the rejection.

- parseStreamResponse: tolerate streaming-only flows that close gracefully
  after a [DONE] sentinel without a result envelope, resolving to undefined
  rather than throwing. A truncated stream (no result, no terminator) still
  throws, now with a clearer message.

Adds regression tests for each.
@cabljac cabljac force-pushed the fix/js-client-streaming-and-header-stability branch from e7e47b9 to 00c9221 Compare June 1, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant