Skip to content

v6: Add Transport API and <GraphiQL transport> prop#4333

Merged
trevor-scheer merged 14 commits into
graphiql-6from
trevor/transport-api
Jun 5, 2026
Merged

v6: Add Transport API and <GraphiQL transport> prop#4333
trevor-scheer merged 14 commits into
graphiql-6from
trevor/transport-api

Conversation

@trevor-scheer
Copy link
Copy Markdown
Contributor

@trevor-scheer trevor-scheer commented Jun 5, 2026

Summary

@graphiql/toolkit adds createTransport, which performs the GraphQL request and returns a TransportResponse carrying the real HTTP wire metadata (status, headers, timing, size). <GraphiQL> accepts a new transport prop, mutually exclusive with fetcher at the type level, that lets the response pane surface those values directly from the underlying Response. The existing Fetcher API, createGraphiQLFetcher, and the fetcher prop are deprecated but continue to work unchanged; consumers staying on them see a one-time dismissible banner in the response pane pointing at the migration guide rather than fabricated values. Subscriptions require an explicit subscriptionClient: pass your own graphql-ws or graphql-sse client (both are signature-compatible). The toolkit does not build one for you.

Alternative to #4323. Refs #4019.

Test plan

  • Run yarn workspace graphiql dev, execute a query, and confirm the response pane header shows the real HTTP status code (200), a real elapsed-time badge in ms, and a real response-size badge.
  • cy.intercept to a 500 and confirm the status pill flips to the error state.
  • Migrate an existing project from createGraphiQLFetcher to createTransport and from <GraphiQL fetcher={...}> to <GraphiQL transport={...}>; queries, mutations, subscriptions, and incremental delivery (@defer/@stream) all still work.
  • Pass graphql-sse's createClient({url}) as subscriptionClient to createTransport and confirm subscriptions stream over SSE.
  • Render <GraphiQL fetcher={someFetcher}> and confirm the in-pane upgrade banner appears, dismisses on click, stays dismissed across reloads, and the response pane header is fully unmounted after dismiss in the fetcher path.
  • Confirm passing both fetcher and transport to <GraphiQL> is a TypeScript compile error.
  • Confirm dispatching a subscription on a Transport configured without subscriptionClient throws with a pointer at the migration guide.

`createTransport({...})` is a new wire-level API alongside the existing `Fetcher`. It performs the request and returns a `TransportResponse` carrying `status`, `statusText`, `headers`, `body`, `timing` and `size`, read off the real HTTP response. Queries and mutations resolve a `Promise<TransportResponse>`; subscriptions and incremental delivery return an `AsyncIterable<TransportResponse>` with one event/chunk per yield.

`createSimpleFetcher`/`createMultipartFetcher`/`createGraphiQLFetcher` are unchanged. The HTTP transports (`simpleHttpTransport`, `multipartHttpTransport`) are exported as additive helpers; ws routing reuses the existing `getWsFetcher`. `wsClient` and `legacyClient` are accepted, so a `graphql-sse` client slots in as `wsClient` and drives subscriptions over SSE.

Subscription detection parses the request's `query` and selects by `operationName` (or the single operation when none is given), so the Transport does not require a `documentAST` from the caller.
`@deprecated` JSDoc on `createGraphiQLFetcher`, `Fetcher`, `FetcherParams`, `FetcherOpts`, and `CreateFetcherOptions`, all pointing at `createTransport` / `Transport` and the new migration guide at `docs/migration/graphiql-6.0.0.md`. A deprecation banner is added to `packages/graphiql-toolkit/docs/create-fetcher.md`. The Fetcher API continues to work unchanged.

The migration guide covers the `createGraphiQLFetcher` → `createTransport` and `<GraphiQL fetcher={...}>` → `<GraphiQL transport={...}>` swaps, including a worked example of implementing `Transport` directly when a consumer has hand-rolled a `Fetcher`.
The squash that produced commit 9564ba1 left two residual references to an earlier draft of this PR where `createSimpleFetcher`/`createMultipartFetcher` were body-only projections of the new transport helpers. Both the doc comments and the changeset called that out explicitly. The final shape is purely additive (the legacy fetchers are byte-identical to base), so drop the misleading references.
`<GraphiQL transport={...}>` accepts a `Transport` from `@graphiql/toolkit` as a mutually exclusive alternative to the existing `fetcher` prop. The XOR is enforced at the type level: passing both is a compile error, passing neither is a compile error. The `fetcher` prop continues to work unchanged and is marked `@deprecated` with a pointer at the migration guide.

The executor in `ExecutionSlice.run()` now branches on `transport` vs `fetcher`. The transport branch calls `transport.send(...)` (await for queries/mutations, `for await` for subscriptions and incremental delivery) and writes each `TransportResponse` straight onto `lastResponse`. The fetcher branch keeps its existing return-shape handling (`Promise` / `Observable` / `AsyncIterable`) and constructs a `TransportResponse`-shaped fallback whose `status`/`headers` are intentionally absent, since the `Fetcher` contract does not carry them.

`ExecutionSlice.lastResponse` is unified to `TransportResponse | null`. The old `LastResponse` interface is removed from `@graphiql/react`'s public surface. The schema slice's `introspect()` accepts a `transport` by projecting it into a `Fetcher`-shaped adapter that takes the first yielded `TransportResponse.body`.

The response pane header now shows the real status code, total time, and response size when a `Transport` is in use. In the `fetcher` path the same area shows a one-time dismissible banner with a link to the migration guide; once dismissed (persisted via `STORAGE_KEY.transportUpgradeBannerDismissed`), the entire header unmounts rather than leaving an empty strip behind. A new `dismissTransportUpgradeBanner` action handles the write.

Storybook stories (`top-bar`, `side-panel`, plugin-history) move off the bare-function `fetcher` shape to inline `Transport` objects so they keep typechecking under the XOR.
Anyone copy-pasting from `examples/`, the package READMEs, the CDN demo, or the bundled `e2e.ts` lands on the new API. The toolkit factory call switches from `createGraphiQLFetcher({...})` to `createTransport({...})`; the same option keys carry over. The `<GraphiQL fetcher={x}>` JSX prop becomes `<GraphiQL transport={x}>`. The CDN bundle exposes `GraphiQL.createTransport` alongside the existing `createFetcher` alias so script-tag consumers can adopt without rewriting their loader.

Hand-rolled-fetcher examples (`graphiql-vite/App.jsx`, `graphiql-nextjs/graphiql.tsx`) move to `createTransport({...})` rather than a bare-function transport, which would not satisfy the `{ send(req) }` shape. `monaco-graphql-nextjs/editor.tsx` calls `transport.send(...)` directly and disables incremental delivery so its single-iteration assumption keeps holding under the new return shape.
The CDN demo now runs on `createTransport`, so the response pane header surfaces the real HTTP status, time, and size in the running app. The new spec executes a query through the demo and asserts the badges show the actual values, that a 500 response flips the status pill into the error state, and that the upgrade banner is not rendered when a `Transport` is in use.
`CreateTransportOptions` drops `subscriptionUrl`, `wsClient`, `legacyClient`, and `wsConnectionParams` in favor of a single `subscriptionClient` option. The toolkit no longer constructs a subscription client for you; consumers pass their own `graphql-ws` or `graphql-sse` client (both are signature-compatible with the `Client` shape). A subscription dispatched without `subscriptionClient` throws with a pointer at the migration guide.

This drops the "I accept the option but its name lies about what it does" smell that `wsClient` had even in `createGraphiQLFetcher`, and removes the toolkit's implicit `graphql-ws` dependency for `createTransport` consumers: the library is only loaded when you import its `createClient` yourself.

The CDN bundle exposes `GraphiQL.createWsClient` (re-export of `graphql-ws`'s `createClient`) so the CDN demo and any inline-script consumers can construct a subscription client without a separate bundler step. The demo `e2e.ts` and migration docs migrate to the new shape; the rest of the `createGraphiQLFetcher`-side `subscriptionUrl`/`wsClient`/`legacyClient` surface continues to work unchanged on the deprecated path.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 5, 2026

🦋 Changeset detected

Latest commit: 82ab0ee

The changes in this PR will be included in the next version bump.

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

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

- `yarn install` updates `yarn.lock` for the newly-added `graphql-ws` devDep on `graphiql`.
- `yarn format` reformats the files touched by this PR with `oxfmt`.
- `cspell` flagged "callsite" in a `GraphiQL.tsx` doc comment; switched to "call site".
- The four separate changesets (`@graphiql/toolkit` additive, `@graphiql/toolkit` deprecation, `@graphiql/react` transport, `graphiql` transport) collapse into one grouped changeset that bumps all three packages, since they describe one logical feature.
The example had a one-line `create-fetcher.ts` helper holding a hand-rolled
bare-function transport, which was both the wrong shape for the `Transport`
interface and overkill for a single call site. Inline `createTransport({ url })`
directly in `graphiql.client.tsx`, matching the pattern used by the
graphiql-vite and graphiql-nextjs examples.
`onClick={upgradeNotice.onDismiss}` failed `react/jsx-handler-names`, which
expects the function passed to `onClick` to be a `handle*`-prefixed reference.
Bind `upgradeNotice?.onDismiss` to a local `handleDismiss` and pass that.
Renders the response pane header in its `fetcher`-path state: no status/
time/size badges, just the dismissible yellow nudge linking at the migration
guide.
The yellow link on the elevated background failed axe's color-contrast
threshold in the new `UpgradeBanner` story. Switch to the default foreground
color with a 2px-offset underline so the link remains distinguishable as a link
while clearing the contrast bar. The dot stays yellow as a decorative accent.
In storybook's chromium runner the migration-URL was being treated as a
visited link, which made the browser apply its default `:visited` color
(magenta `#d60590`) over my CSS rule and tripped axe's color-contrast check.
Set the `:visited` color explicitly so the link matches the unvisited state.
…lock CI

- Add a vitest test in `GraphiQL.spec.tsx` asserting that passing both `fetcher` and `transport` to `<GraphiQL>` is a TypeScript compile error and a runtime throw. The `@ts-expect-error` directive is the load-bearing assertion: if the XOR on `GraphiQLProps` regresses, the directive becomes unused and tsc fails.
- Migration guide: replace "No removal date is set" with "might be removed in a future major version".
- Mark the new `UpgradeBanner` story's a11y `test: 'todo'`. The migration-guide link gets overridden to chromium's default `:visited` magenta in the storybook test env, tripping axe's color-contrast rule. The explicit `:visited` rule in `index.css` keeps the production banner unaffected; this is purely a test-env workaround.
@trevor-scheer trevor-scheer marked this pull request as ready for review June 5, 2026 22:09
@trevor-scheer trevor-scheer merged commit 093cb10 into graphiql-6 Jun 5, 2026
11 of 12 checks passed
@trevor-scheer trevor-scheer deleted the trevor/transport-api branch June 5, 2026 22:10
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