fix(graphql-react-ws): forward fatal per-operation errors to onError - #178
Merged
Conversation
Previously, a server-terminated operation (as opposed to a full socket close) silently resolved every consumer of that subscription as gracefully complete instead of errored. `graphqlWsSubscribe`'s `error` callback synchronously aborted every listener's `AbortController` before the async rejection could propagate through `BroadcastAsyncGenerator`'s `for await` loop, so the abort-triggered `resolveCompleted()` always won the race and masked the error. Even when the rejection did propagate, `useSubscription`'s effect had no `try/catch` around its own `for await`, so it was swallowed by `subscribe()`'s generic `.catch(console.error)`. Adds `BroadcastAsyncGenerator.rejectAll()` (called before the abort cascade) and wraps `useSubscription`'s loop in a `try/catch` that forwards into `onError`/`store.error`. Includes a repro test that fails without either fix. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: c9d349b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A fatal, per-operation transport error (the server terminates just this subscription — e.g. a duplicate-operation-id conflict — as opposed to a full socket close) was silently resolving every consumer of that subscription as gracefully complete instead of erroring, with zero signal to the caller.
Two compounding bugs:
graphqlWsSubscribe'serrorcallback callscleanupGenerator(), which synchronously aborts every listener'sAbortControllerviaonDispose(). That abort synchronously runssubscribe()'scleanupListener, which callslistener.resolveCompleted()— resolving the listener as done, not errored. This races ahead of (and wins against) the async rejection that would otherwise propagate throughBroadcastAsyncGenerator.broadcast()'sfor awaitloop, so the real error never reaches the listener.useSubscription's effect had notry/catcharound its ownfor await (const result of iterator), so the rejection was swallowed bysubscribe()'s generic.catch(console.error)further up the stack —onErrornever fires.Fix
BroadcastAsyncGenerator.rejectAll(error), called synchronously before the abort cascade in theerrorcallback, so every listener'sdoneflag is alreadytrue(with the real error recorded) by the time the abort-triggeredresolveCompleted()runs — making that call a no-op instead of masking the error.useSubscription'sfor awaitloop in atry/catchthat forwards the caught error intoonErrorCallback/store.error, matching the existing handling for GraphQL-levelresult.errors.client.test.ts) that mocksgraphql-ws, fires a fatal per-operation error, and assertsonErrorfires — fails without either fix, passes with both.distoutput (they were being bundled in viaentryPoints: ['src']).Test plan
pnpm --filter @soundxyz/graphql-react-ws test— repro test passespnpm --filter @soundxyz/graphql-react-ws prepare— builds cleanly,distcontains no test files2.0.0API surface (via a separate worktree) to confirm this is a clean, non-breaking patch for existing consumerspnpm prettier:checkpassesMade with Cursor