skip to content

What messages must a GraphQL-over-WebSocket client and server exchange before the first subscribe?

level: juniorimportance: must knowfreq 62%

answer

  1. The open socket is not enough
  2. Two messages before any operation
  3. Client speaks first, server acknowledges
  4. An optional free-form payload rides along
  5. 4401 and 4408 punish the mistakes

basics

~20 s

The client sends connection_init as its first message, optionally carrying credentials in a free-form payload object, and waits for the server's connection_ack. Only after that acknowledgement may it send a subscribe message; anything earlier is closed, not answered.

solid answer

~40 s

Over a WebSocket the `graphql-ws` subprotocol adds a handshake of its own on top of the open socket. The client's first message must be `connection_init`, whose optional `payload` object is free-form and is conventionally where credentials go, because browser JavaScript cannot set arbitrary headers on a WebSocket opening request and so has no `Authorization` header to use. The server answers `connection_ack`, also with an optional payload, and only then is the connection usable. A client that sends `subscribe` before the acknowledgement has its socket closed with code 4401; one that never sends `connection_init` inside the server's initialisation window gets 4408. None of this is in the GraphQL specification, which defines `subscription` as an operation type and an execution algorithm but no transport at all — the message flow is a community subprotocol document.

code

json · 9 lines
json
[
  {
    "type": "connection_init",
    "payload": { "authorization": "Bearer eyJhbGciOi..." }
  },
  {
    "type": "connection_ack"
  }
]

go deeper

for a junior

Recall the order: connection_init from the client, connection_ack from the server, and only then subscribe. Know that credentials conventionally sit in the init payload, and be able to say the socket opening is not the same as the connection being ready.

for a middle

Explain the mechanics: the payload is free-form and unstructured by the protocol, the acknowledgement gates every later message, and violations are closes with codes 4401, 4408 and 4429 rather than error responses. Be ready to say why a browser forces the credential into a message.

for a senior

Show you know the handshake is per socket and repeats on every reconnect, so a client that restores subscriptions must re-run it before replaying anything. Be able to say what you would log to tell a late init from a premature subscribe in production.

for a principal

Own the boundary claim: GraphQL specifies no transport, so this handshake is a community subprotocol your organisation has chosen, not a standard it inherits. Frame what that costs — client and server must agree out of band, and swapping subprotocols is a fleet-wide migration.

## The socket is open. Nothing works yet. Opening a WebSocket to a GraphQL endpoint gets you a duplex channel and nothing more. What travels over that channel is JSON messages whose meaning comes from a **subprotocol** — a small application-level protocol layered on top of the connection — and that subprotocol insists on a handshake of its own before it will accept a single operation. Engineers who have only ever used a client library often miss this, because the library runs the handshake invisibly and exposes nothing but `subscribe`. Say plainly where this comes from, because interviewers do ask: **the GraphQL specification defines `subscription` as an operation type and an execution algorithm, and defines no transport at all.** The GraphQL over HTTP working draft covers queries and mutations travelling over HTTP. The WebSocket message flow described here is the `graphql-ws` subprotocol document — a community protocol that the ecosystem converged on, not a ratified edition of anything. It is a convention with a written specification behind it, which is a different thing from being in the spec. ## The two messages `connection_init` is client to server. It must be the **first** message the client sends on the socket. It carries a `type` of `connection_init` and an optional `payload`, which is a free-form object: the subprotocol assigns it no structure and no meaning. `connection_ack` is server to client, sent in response, also with an optional free-form `payload`. Between the socket opening and that acknowledgement arriving, the connection is in a state the protocol calls *not acknowledged*, and the client is not permitted to send anything else — no `subscribe`, no operation, nothing. That is the whole handshake. Two messages, one round trip, per socket. ## Why the payload slot exists at all The reason is a browser limitation, and it is worth being able to state precisely. JavaScript opening a WebSocket may supply a URL and a list of subprotocol identifiers. It may **not** set arbitrary request headers, so there is no `Authorization` header available on the opening request. Cookies scoped to the origin do ride along, and a token can be stuffed into the query string — where it lands in access logs, referrers and browser history. The subprotocol's answer is to give the application a slot in its own first message, after the socket exists but before anything can be run on it. So the near-universal convention is that credentials go in the `connection_init` payload, the server reads them, and it either acknowledges or closes the socket. *Whether* a server should also re-check on each operation, and what an expiring credential costs mid-stream, is a policy question beyond the message flow. ## A worked sequence A clinical-trial registry graph publishes a live adverse-event feed. A monitoring dashboard opens a socket and wants `adverseEventReported(trialId: "NCT-48213")`. 1. The socket opens. 2. The client sends `{"type":"connection_init","payload":{"authorization":"Bearer …"}}`. 3. The server validates the payload and replies `{"type":"connection_ack"}`. 4. Only now does the client send its `subscribe` message, carrying an id it chose and the operation document. A well-written client queues subscriptions requested before step 3 and flushes them after it, which is exactly why the handshake is invisible to application code. ## What the server does when the sequence is broken The subprotocol defines close codes in the 4xxx range, which WebSocket reserves for application use, so these are the subprotocol's own vocabulary rather than the transport's: * **4401** — a message such as `subscribe` arrived before the connection was acknowledged. The socket is closed; no error message is sent for the operation. * **4408** — the client never sent `connection_init` inside the server's initialisation window. The window length is a server setting, not a protocol constant. * **4429** — the client sent `connection_init` more than once on the same socket. * **4400** — a message the server could not interpret at all: an unknown `type`, a missing `id` where one is required, a body that is not valid JSON. Notice that all four are **closes**, not error responses. The subprotocol treats a handshake violation as a reason to end the connection rather than to reply, which is why a broken client shows up in logs as sockets dying rather than as failed operations. ## The handshake is per socket There is no notion of resuming a previous session. Every new socket, including every reconnect after a network blip, repeats `connection_init` and `connection_ack` from scratch — and a client that restores a table of active subscriptions on reconnect must re-run the handshake before replaying any of them. ## What the interviewer is really checking Two things. First, that you know the socket being open is not the same as the connection being usable — the commonest junior bug in this area is firing `subscribe` from the socket's open event. Second, that you can separate the GraphQL specification from the conventions layered around it. A candidate who says "the spec defines the WebSocket handshake" has revealed they learned GraphQL entirely through one library.

  • Why do credentials usually travel in the connection_init payload rather than a request header?
    Because a browser cannot put them in one. The WebSocket API lets page JavaScript supply a URL and a list of subprotocol identifiers, and nothing else — no arbitrary request headers. Origin cookies do ride along on the opening request, and a query-string token is possible but leaks into logs and history. The subprotocol's answer is a free-form payload slot on its own first message, read after the socket exists but before anything can run on it.
  • What does the server do if a client sends connection_init twice on the same socket?
    It closes the socket with code 4429, too many initialisation requests. The handshake is once per connection: there is no re-authentication step and no way to replace the payload later. A client that wants to present a different credential opens a new socket and handshakes again, since the whole handshake is per socket and every reconnect repeats it from scratch.
  • Does the payload on connection_ack mean anything to the protocol?
    No. Like the payload on `connection_init` it is an optional, free-form object that the subprotocol assigns no structure to. Servers use it for whatever the application finds useful — a resolved user identity, a session id, negotiated limits — and clients that do not care simply ignore it. Nothing in the message flow branches on its contents.

The socket is the phone line being connected; connection_init and connection_ack are the two of you saying hello and confirming who is on the call. Talking before that is talking into a line nobody has picked up.

saying these in an interview costs you the question

  • Says the GraphQL specification defines the WebSocket handshake
  • Sends subscribe as soon as the socket opens
  • Claims a browser can set an Authorization header on a WebSocket
  • Thinks connection_ack is optional for the server
  • Treats an open socket as a ready connection
  • Expects an error message rather than a close on a violation

context