Skip to content

v6: fix transport error metadata, subscription teardown, and the spec accept header - #4429

Merged
trevor-scheer merged 7 commits into
graphiql-6from
trevor/transport-correctness
Aug 26, 2026
Merged

v6: fix transport error metadata, subscription teardown, and the spec accept header#4429
trevor-scheer merged 7 commits into
graphiql-6from
trevor/transport-correctness

Conversation

@trevor-scheer

@trevor-scheer trevor-scheer commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

A handful of independent correctness fixes to the v6 transport path, mostly around error handling and cancellation.

Non-JSON response bodies. The transports called response.json() unconditionally, so a body that isn't JSON (an HTML error page from a proxy, a plain-text 401) made send() reject and lose exactly the wire metadata the transport exists to provide. readResponseBody now reads the body as text and folds a parse failure into a synthetic { errors: [{ message: <raw text> }] } body, keeping the real status, headers, timing, and size. A plain-text Bad Gateway now renders as Bad Gateway, which also let the Cypress test stop pinning Electron's JSON-parser wording.

ok semantics. TransportResponse.ok only consulted the GraphQL body, so a 401 whose body parsed as JSON with no errors was ok: true. It's now response.ok && !hasErrors for HTTP responses. Subscription events have no HTTP envelope, so they stay body-only.

Subscription teardown. Stopping a subscription didn't actually stop it. The store's unsubscriber called iter[Symbol.asyncIterator]().return?.(), but TransportHookRegistry.wrap() mints a brand-new iterator on every [Symbol.asyncIterator]() call, so Stop disposed a throwaway while the one driving the for await loop kept delivering events forever. The store now captures the iterator once and disposes that same iterator. The legacy fetcher's iterable path had the identical pattern and gets the identical fix. New tests in execution.spec.ts assert delivery halts after stop(), both against a raw transport and through the real wrap().

Aborting queries/mutations. They had no cancellation at all. TransportRequest gains signal, threaded down to fetch, and stop() now aborts the in-flight request. stop() also bumps queryId so a transport that ignores the signal still can't paint a late response, and run()'s catch distinguishes a user-initiated abort (silent) from a genuine failure (surfaced). For streams the controller stays armed as an HTTP-level backstop; primary teardown is still iterator disposal.

onError plugin hook. Plugins could observe requests and responses but not failures. transport.onError fires when send() rejects outright (a network error, a throwing onBeforeSend hook), receiving the request as transformed by whichever hooks had run. Observer callbacks are now isolated: one that throws is logged instead of breaking the stream or masking the original error. wrap() also handles disposal races: return() during an awaited onBeforeSend prevents the underlying send() from firing, and late values/errors after disposal are dropped.

Spec accept header. With incremental delivery on (the default), the multipart transport sent accept: application/json, multipart/mixed with no mention of application/graphql-response+json, silently asking spec-compliant servers for legacy response semantics. It now sends multipart/mixed, application/graphql-response+json;q=0.9, application/json;q=0.8; without explicit q-values every type ties at q=1 and the server's tie-break can route an incremental response away from multipart.

Smaller items. TransportRequest and SubscriptionRequest gain extensions (in the URL for GET, in the body for POST/QUERY) for things like automatic persisted queries; the two near-identical parse passes in createTransport collapse into one classifySelectedOperation; the never-populated ResolverTrace type is removed from TransportResponse.timing.

@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: aded491

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 8 packages
Name Type
@graphiql/toolkit Minor
@graphiql/react Minor
graphiql Minor
@graphiql/plugin-history Major
@graphiql/plugin-code-exporter Major
@graphiql/plugin-collections Major
@graphiql/plugin-doc-explorer Major
@graphiql/plugin-query-builder Major

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@trevor-scheer
trevor-scheer force-pushed the trevor/transport-correctness branch from 0b32914 to aded491 Compare August 11, 2026 01:11
@trevor-scheer
trevor-scheer marked this pull request as ready for review August 26, 2026 13:59
@trevor-scheer
trevor-scheer merged commit 813c552 into graphiql-6 Aug 26, 2026
13 checks passed
@trevor-scheer
trevor-scheer deleted the trevor/transport-correctness branch August 26, 2026 14:00
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.

1 participant