skip to content

With graphql_flutter, how do you receive GraphQL subscriptions in a Flutter app, and what must you configure on the WebSocketLink?

level: middleimportance: should knowfreq 34%

answer

  1. subscriptions need their own link
  2. Link.split on request.isSubscription
  3. default subProtocol is old graphql-ws
  4. initialPayload carries auth
  5. SocketClientConfig: autoReconnect, 5 s delay

basics

~20 s

Split subscriptions to a WebSocketLink with Link.split((r) => r.isSubscription, wsLink, httpChain), match subProtocol to the server (the default is the old graphql-ws protocol), send auth in SocketClientConfig.initialPayload, and render with the Subscription widget or useSubscription.

solid answer

~40 s

Subscriptions do not go over HTTP, so the link must route them: `Link.split((request) => request.isSubscription, wsLink, authLink.concat(httpLink))`; otherwise the HTTP link swallows them. `WebSocketLink(url, config: SocketClientConfig(...), subProtocol: ...)` needs two checks. Its `subProtocol` defaults to `GraphQLProtocol.graphqlWs`, the old protocol the package marks as unmaintained, so a server built on the newer protocol needs `GraphQLProtocol.graphqlTransportWs`. And `AuthLink` sits on the HTTP branch, so credentials for the socket go in `initialPayload`, which may be a function returning the payload. `SocketClientConfig` defaults to `autoReconnect: true` with 5 seconds between attempts and a 30-second `inactivityTimeout`. In the UI, `Subscription(options: SubscriptionOptions(document: ...), builder: (result) => ...)` gives only the latest event; `ResultAccumulator` or `onSubscriptionResult` collects them.

code

dart · 22 lines
dart
final wsLink = WebSocketLink(
  'wss://api.bookclub.example/graphql',
  subProtocol: GraphQLProtocol.graphqlTransportWs,
  config: SocketClientConfig(
    initialPayload: () async => {'authorization': 'Bearer ${await tokenStore.read()}'},
  ),
);

final link = Link.split(
  (request) => request.isSubscription,
  wsLink,
  authLink.concat(httpLink),
);

Widget newReviews() => Subscription(
      options: SubscriptionOptions(document: reviewAdded, variables: {'bookId': bookId}),
      builder: (result) {
        if (result.hasException) return const SizedBox.shrink();
        final review = result.data?['reviewAdded'];
        return review == null ? const SizedBox.shrink() : NewReviewBanner(review: review);
      },
    );

go deeper

for a junior

Recall that subscriptions need a WebSocketLink, chosen with Link.split, and are rendered with the Subscription widget or useSubscription.

for a middle

Explain the subProtocol default and when to change it, how the socket is authenticated, and what SocketClientConfig does on connection loss.

for a senior

Make subscriptions robust: reconnect behaviour, token refresh through initialPayload, writing events into the cache, and verifying the protocol against the server.

for a principal

Decide when subscriptions are worth their connection cost compared with polling or push notifications for the product's freshness needs.

## Routing subscriptions In `graphql_flutter` every operation passes through the client's **link chain**. Queries and mutations end at an `HttpLink`; a **subscription** needs a long-lived connection, which `WebSocketLink` provides. The two are combined with **`Link.split`**, which sends each request down one of two branches: ```dart final link = Link.split( (request) => request.isSubscription, wsLink, authLink.concat(httpLink), ); ``` The package README is explicit that the split is required: without it, the HTTP link receives the subscription and swallows it. ## Configuring WebSocketLink `WebSocketLink(url, {config, subProtocol})` has three knobs that matter in practice: | Setting | Default | Why it matters | |---|---|---| | `subProtocol` | `GraphQLProtocol.graphqlWs` (`graphql-ws`) | must match the server's protocol | | `config.initialPayload` | none | where auth for the socket goes | | `config.autoReconnect` | `true` | reconnects after a lost connection | | `config.delayBetweenReconnectionAttempts` | 5 seconds | spacing between reconnect attempts | | `config.inactivityTimeout` | 30 seconds | closes the socket if no keep-alive arrives | | `config.queryAndMutationTimeout` | 10 seconds | for operations sent over the socket | **The protocol default is the classic trap.** Two GraphQL-over-WebSocket protocols are in use, and the package's constants name them: `GraphQLProtocol.graphqlWs`, documented as the **old, no-longer-maintained** protocol, and `GraphQLProtocol.graphqlTransportWs`, the newer one implemented by the widely used `graphql-ws` server library. The default is the old one, so against a server that speaks only the new protocol, the socket connects and subscriptions never deliver. How the protocols differ on the wire is a transport topic; on the client the fix is one argument. **Authentication.** `AuthLink` adds an HTTP header, and in the usual chain it sits only on the HTTP branch of the split. The socket's credentials go in **`initialPayload`**, sent in the connection-init message. It can be a value or a function, including an `async` one, so it can read the current token each time the socket (re)connects. ## Rendering events - **`Subscription` widget**: `Subscription(options: SubscriptionOptions(document: ...), builder: (QueryResult result) => ...)`. The builder receives **only the most recent** result; check `result.hasException` and `result.isLoading` as with queries. - **`useSubscription(options, onSubscriptionResult: ...)`** is the hook version for `HookWidget`s. - **`onSubscriptionResult`** runs for every event and receives the client, which makes it the place to write the event into the cache or trigger side effects. - **`ResultAccumulator`** is a helper widget that collects successive results into a list; its docs warn that it is stateful and loses what it collected if its state is disposed. The subscription hook also listens to `connectivity_plus`: when the device goes from offline to Wi-Fi or mobile it re-subscribes, on Android after a DNS lookup confirms real connectivity. ## A book-club example A "new reviews" banner on a book page subscribes to `reviewAdded(bookId:)`. The `Subscription` widget shows the latest review; `onSubscriptionResult` writes each event into the cached reviews list so the `Query` below updates too. Because the server uses the newer protocol, the link sets `subProtocol: GraphQLProtocol.graphqlTransportWs`, and `initialPayload` returns `{'authorization': 'Bearer <token>'}` from the auth service. ## Connection loss and app lifecycle `SocketClientConfig` gives more control than the defaults: - **`onConnectionLost(code, reason)`** is called when the socket closes and returns a `Future<Duration?>`, letting the app pick the delay before the next attempt, for example a growing backoff instead of the fixed 5 seconds; - **`toggleConnection`** takes a `Stream<ToggleConnectionState>`; emitting `disconnect` pauses reconnection and closes the socket, and `connect` resumes it, which suits closing the socket while the app is in the background and reopening it on return; - **`connectFn`** replaces the default connect call, for example to add headers on `dart:io` platforms, with the caveat in its docs that a channel you listen to yourself must be wrapped with `forGraphQL()`. After a reconnect, events published while the socket was down are not replayed, so a screen that must be complete refetches its `Query` as well. ## Pitfalls - Forgetting the split: subscriptions silently never arrive. - Wrong `subProtocol`: the socket opens, events never come. - Auth only in `AuthLink`: the socket connects unauthenticated and the server rejects the subscription. - Treating the builder's `result` as a history: it is the latest event only.

  • Why doesn't AuthLink authenticate the subscription socket?
    `AuthLink` sets an HTTP header on requests that pass through it, and in the usual setup it is chained in front of `HttpLink` on the non-subscription branch of `Link.split`. The socket's credentials go in `SocketClientConfig.initialPayload`, which is sent when the connection is initialised.
  • What does the Subscription builder's result contain after several events?
    Only the most recent event. To show a history, wrap the output in `ResultAccumulator`, or use `onSubscriptionResult` to append each event to state or to the cache, remembering that `ResultAccumulator` loses its list if its state is disposed.

saying these in an interview costs you the question

  • HttpLink can carry subscriptions if the server supports them
  • WebSocketLink defaults to the newer graphql-transport-ws protocol
  • AuthLink headers are applied to the WebSocket connection automatically
  • The Subscription builder receives every event as a list
  • WebSocketLink never reconnects on its own