Skip to content

fix: apply strict-mode abort guard to SSE message handlers#362

Open
atilafassina wants to merge 2 commits intomainfrom
fix-strict-query
Open

fix: apply strict-mode abort guard to SSE message handlers#362
atilafassina wants to merge 2 commits intomainfrom
fix-strict-query

Conversation

@atilafassina
Copy link
Copy Markdown
Contributor

Summary

  • Mirror the existing signal.aborted early-return from onError into onMessage for useAnalyticsQuery (use-analytics-query.ts:121) and useGenieChat's three SSE call sites (sendMessage, pollPendingMessage, fetchConversationPage). Without this, a final SSE error envelope emitted by the server in response to our own cleanup-driven abort lands on the still-attached handler under React StrictMode's double-mount and surfaces a transient user-visible error before the second mount's data arrives.
  • Port of d8a3bc30 from the analytics-exp branch (which applied the same fix to useMetricView); v0.30.0 ships with the gap on main.
  • Three adjacent fixes from the same investigation are deferred to follow-up PRs and intentionally not included here: server-side AbortError classification (sql-warehouse/client.ts + stream-manager.ts), cache singleflight AbortError resilience (cache/index.ts), and a useGenieChat guard for the early-error envelope. Each addresses an independently-mergeable layer; this PR closes the user-visible client-side gap.

Test plan

  • New __tests__/use-analytics-query.test.ts covers the regression: late error, result, and arrow envelopes after signal.aborted = true are all dropped (no state mutation, no ArrowClient.fetchArrow issued); plus happy paths and the unmount-aborts-controller contract. 8 tests.
  • pnpm test (appkit-ui suite) — 276/276 green.
  • pnpm build && pnpm docs:build — clean.
  • pnpm check:fix && pnpm -r typecheck — clean (1 pre-existing biome warning unrelated to this PR).
  • Manual repro under React StrictMode in dev-playground: load /analytics, confirm no transient error flash on first paint.

This pull request and its description were written by Isaac.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents late SSE envelopes from mutating React hook state after the owning AbortController has been aborted (notably under React StrictMode’s double-mount), avoiding transient user-visible errors caused by server “final error” messages emitted during abort-driven cleanup.

Changes:

  • Add an AbortSignal.aborted early-return guard to useAnalyticsQuery’s onMessage handler (mirroring the existing onError guard).
  • Add the same abort guard to useGenieChat’s three SSE onMessage call sites (fetchConversationPage, sendMessage, pollPendingMessage).
  • Add a new useAnalyticsQuery test suite covering late error/result/arrow envelopes after abort (including ensuring no Arrow fetch occurs), plus basic happy-path and unmount behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/appkit-ui/src/react/hooks/use-analytics-query.ts Drops SSE messages after abort to prevent post-cleanup state updates/errors under StrictMode.
packages/appkit-ui/src/react/hooks/tests/use-analytics-query.test.ts Adds regression tests ensuring late envelopes are ignored after abort and verifying unmount abort behavior.
packages/appkit-ui/src/react/genie/use-genie-chat.ts Applies the same “ignore messages after abort” guard across Genie SSE message handlers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ler in useAnalyticsQuery

useAnalyticsQuery's `onError` callback bails silently when
`abortController.signal.aborted` is true, but `onMessage` had no
matching guard. Under React StrictMode's double-mount, the first
mount's cleanup aborted controller A, but the server-side SSE writer
still emitted a final `event: error` envelope on the already-open
stream (cancellation hand-off). That envelope arrived at the hook's
onMessage, sailed past the missing guard, hit the
`parsed.type === "error"` branch, and surfaced a user-visible error
before the second mount's data arrived. Production builds (StrictMode
no-op) didn't see it; dev did.

Mirror the `onError` guard at the top of `onMessage`. Once the
controller is aborted, drop ALL events from that stream uniformly
(`result`, `arrow`, `error`) — the next mount/refetch creates a fresh
controller and runs cleanly. The `arrow` branch matters specifically
because it triggers a side-effectful `ArrowClient.fetchArrow` network
call, which the consumer has already given up on.

Port of d8a3bc3 from analytics-exp, which applied the same fix to
useMetricView. The remaining cross-mount race (cache singleflight
propagating mount-1's AbortError to mount-2's awaited promise),
server-side AbortError mis-classification as UPSTREAM_ERROR, and the
analogous gap in useGenieChat are tracked as follow-up PRs.

Tests: new file use-analytics-query.test.ts covering the regression
(late `error`, `result`, `arrow` envelopes after abort all dropped),
the happy paths (normal `error` and `result` envelopes still set
state), URL/payload assertions, and the unmount-aborts-controller
contract. 8 tests; 276 appkit-ui tests pass; build, biome, typecheck
green.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
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.

2 participants