skip to content

With Apollo Client 4's GraphQLWsLink, how do you authenticate the support-chat socket and keep messages flowing when the token expires or the socket drops?

level: seniorimportance: should knowfreq 20%

answer

  1. headers never reach the socket link
  2. credentials ride on connection init
  3. read the token per connection
  4. graphql-ws retries before Apollo notices
  5. after a gap refetch, after giving up restart

basics

~20 s

GraphQLWsLink ignores context headers, so SetContextLink auth never reaches it; send the token in graphql-ws connectionParams, read at each connection. graphql-ws retries drops itself; once it gives up, useSubscription reports a socket-closed error and completes, so restart and refetch.

solid answer

~50 s

`GraphQLWsLink` hands the `graphql-ws` client only the query, variables, operation name and extensions, so an auth header set by `SetContextLink` reaches `HttpLink` and nothing else. The socket authenticates once, when it connects, with the `connectionParams` given to `createClient`; make it a function so the current token is read at every connection instead of being captured at startup. Refreshing the token does not re-authenticate an open socket: the server decides whether to keep or close it, and a new token only travels on the next connection. Drops are retried inside `graphql-ws`, invisibly to Apollo, and events pushed during the gap are not replayed, so refetch the room's messages after a reconnect. When `graphql-ws` gives up, `GraphQLWsLink` errors with "Socket closed with event <code> <reason>"; `useSubscription` sets `error`, calls `onComplete`, and stays stopped until `restart()` or a remount.

code

tsx · 22 lines
tsx
import { useSubscription } from "@apollo/client/react";

export function LiveStatus({ roomId, refetch }: {
  roomId: string;
  refetch: () => Promise<unknown>;
}) {
  const { error, restart } = useSubscription(MESSAGE_ADDED, {
    variables: { roomId },
  });

  if (!error) return null;
  return (
    <button
      onClick={() => {
        restart();
        void refetch();
      }}
    >
      Live updates stopped. Reconnect
    </button>
  );
}

go deeper

for a junior

Recall that the WebSocket is authenticated with connectionParams on the graphql-ws client, not with an HTTP header.

for a middle

Explain why SetContextLink headers never reach GraphQLWsLink, and why connectionParams should be a function that reads the current token.

for a senior

Walk through the failure path: silent retries, lost events, the socket-closed error and completion, then recover with restart and a refetch, and handle sign-out explicitly.

for a principal

Weigh connection-time authentication against the product's need to revoke access mid-session, and decide what the client and the server each own.

## Two transports, two authentication paths A support console usually runs queries and mutations over `HttpLink` and subscriptions over `GraphQLWsLink`, chosen per operation by `ApolloLink.split`. Credentials reach the two very differently: | | HTTP operations | WebSocket subscriptions | |---|---|---| | Where the token goes | a request header, typically set by `SetContextLink` | `connectionParams` on the `graphql-ws` client | | When it is read | on every operation | when the socket connects | | What a refresh changes | the next request | only the next connection | `GraphQLWsLink` passes the `graphql-ws` client the query, variables, operation name and extensions. It does **not** pass the operation's context, so headers added by `SetContextLink` in front of the split never reach the socket. ## Authenticating the socket `createClient` from `graphql-ws` accepts `connectionParams`, which the client sends to the server when the connection is set up, before any subscription runs. The server authenticates the connection from it. - A **plain object** is built once. If it holds the token as a string, every reconnect sends that same, possibly expired, token. - A **function**, which may be `async`, is called on each connection attempt, so a reconnect picks up the token your auth code has refreshed since. - The shape of the payload, such as `{ authToken }`, is a contract with the server; the client's only job is to send a valid one on every connection. ```ts const wsLink = new GraphQLWsLink( createClient({ url: "wss://support.example.com/graphql", connectionParams: async () => ({ authToken: await getAccessToken() }), }) ); ``` ## When the token expires on an open socket The socket was authenticated when it connected. Refreshing the token in the browser changes nothing for that connection; whether it may continue is the server's decision. Two client-side consequences follow: - If the server closes the socket when the credential expires, the next connection must carry a fresh token, which the function form provides. - If you need the new identity to apply now, for example after a role change or a **sign-out**, you have to force a new connection yourself. The link exposes its `graphql-ws` client as `wsLink.client`, and the `graphql-ws` documentation has recipes for restarting it. On sign-out also clear the cache, for example with `client.clearStore()`, so the next user never sees the previous agent's rooms. ## When the socket drops 1. The network blips and the socket closes. 2. `graphql-ws` retries the connection on its own, according to its retry settings, and re-subscribes the active operations when it reconnects. Apollo sees nothing: no error, no event. 3. Messages published while the socket was down are **not** replayed; the protocol has no history. 4. If the retries run out, `graphql-ws` reports the close to the link, and `GraphQLWsLink` errors with a message like `Socket closed with event 1006`. 5. Apollo delivers that as an error result: `useSubscription` sets `error`, calls `onError`, then completes the subscription and calls `onComplete`. 6. The hook stays stopped. It does not resubscribe on the next render. ## Recovering in the UI - **After a silent reconnect**, refetch the room's message query so the gap is filled. Hook into the `graphql-ws` client's connection events, or refetch when the tab becomes active again. - **After the socket gives up**, show that live updates stopped and offer a retry that calls the hook's `restart()`; for a completed subscription, `restart` creates a new one. Refetch at the same time. - **Deduplicated subscriptions** share one connection, so `restart` restarts it for every subscriber. - Apollo's docs prefer retrying inside `graphql-ws` over `RetryLink`, because the WebSocket client knows why the connection failed. - **A "reconnecting" indicator** cannot come from the hook, which reports nothing while `graphql-ws` retries. Drive it from the `graphql-ws` client's own connection events instead. ## Common mistakes - Adding the WebSocket token in `SetContextLink` and wondering why the server sees an anonymous socket. - Building `connectionParams` once from a token read at startup. - Assuming reconnects replay missed messages. - Leaving an authenticated socket open after sign-out. - Creating a new `graphql-ws` client whenever the token changes, which can leave the old client and its socket running beside the new one.

  • What must an Apollo Client 4 app do with the subscription socket when an agent signs out?
    Force it closed or reconnected. The socket was authenticated as that agent when it connected, and any subscription still mounted, a notification badge say, keeps it open with that identity. Restart the `graphql-ws` client exposed as `wsLink.client`, following its recipes, and clear the cache with `client.clearStore()` so the next agent sees none of the previous one's data.
  • Should you put RetryLink in front of the split to survive dropped sockets?
    Apollo's docs prefer retrying inside `graphql-ws`, since the WebSocket client knows why the connection failed and reconnects without tearing the operation down. `RetryLink` is the generic alternative: it resends a failed operation to the terminating link. In front of the split it also retries HTTP operations, so limit it with its retry condition if you use it.
  • In Apollo Client 4, what does restart() do on a deduplicated subscription?
    It terminates the shared connection to the link and recreates the request, which restarts it for every subscriber attached through deduplication, not just the component that called it. If the subscription had already completed, the hook creates a new subscription instead.

saying these in an interview costs you the question

  • An auth header from SetContextLink is sent with every WebSocket subscription.
  • A plain connectionParams object picks up the refreshed token on reconnect.
  • Refreshing the token in the browser re-authenticates the already open socket.
  • graphql-ws resends the events that were pushed while the socket was down.
  • After the socket gives up, useSubscription reconnects on its own at the next render.