graphql-ws

command
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 14 Imported by: 0

README

graphql-ws example

Demonstrates that gateway.Registry does not require gateway.WSHandler. This example hand-rolls the graphql-transport-ws subprotocol directly on top of github.com/coder/websocket and drives the Registry with its own connection id scheme, its own subscribe/unsubscribe bookkeeping, and its own authentication timing - none of which go through gateway.WSHandler.

Why this example exists

gateway.WSHandler authenticates a connection during the HTTP upgrade: WSAuthFunc only ever sees the *http.Request. That is the right shape for token-in-header or token-in-query-param auth, and it is what most WebSocket gateways look like.

It is not what mip-aio's real WebSocket endpoint looks like. There, the socket is hosted by gqlgen, and authentication happens on the first application-level frame: the client sends connection_init with payload.Authorization only after the upgrade has already completed. WSAuthFunc cannot express "accept the upgrade, then wait for a frame, then decide". Nothing in gateway.WSHandler's option set can either - subscribing to a topic, in graphql-ws, also happens on a later subscribe frame, per-subscription, not at registration time.

So this example proves the thing that actually matters for a consumer like mip-aio: Registry.Register, Unregister, Join, Leave, SendToConnection, and Broadcast are ordinary exported methods on *gateway.Registry. WSHandler is one caller of them, built for the common case; you are free to be another, built for whatever your transport actually does.

What it demonstrates
  • Negotiating a custom subprotocol (Sec-WebSocket-Protocol: graphql-transport-ws) with coder/websocket directly.
  • Deferring authentication until after the upgrade, on the first frame (connection_init.payload.Authorization), and closing with the graphql-ws-specified code 4401 on failure - not an HTTP 403, because the upgrade already succeeded.
  • Registering a connection with the Registry with no topics, then joining/leaving topics one at a time as subscribe/complete frames arrive, with per-topic reference counting so two subscriptions to the same topic only cost the Registry one Join.
  • Driving Registry.Broadcast from a plain net/http handler (/publish) and having the payload arrive at every subscribed connection's send function - the same cross-node fan-out WSHandler-based connections get, with no WSHandler involved on either side.
  • Routing a delivered payload back to the right graphql-ws subscription id(s). The Registry hands a connection's send function raw bytes with no indication of which topic they arrived on (one socket can be joined to several), so the topic travels inside the payload itself (publishEnvelope{Topic, Data}) and the connection looks up its own subscriptions by topic before framing a next message.
What it does not demonstrate
  • A spec-complete graphql-ws server. There is no query execution, no error frame, no rejection of a duplicate subscription id (4409 in the spec), no connection_init retry limiting (4429). Only the message types named in the task - connection_init / connection_ack / subscribe / next / complete / ping / pong - are implemented.
  • Real authentication. demoToken is a hardcoded string (const demoToken = "Bearer demo-token" in main.go); swap awaitInit's comparison for a real token/JWT check.
  • TLS. This example runs plain HTTP so go run needs no certificates; see examples/echo and the root cert_manager.go/server.go for the cluster-shared TLS story.

When to use WSHandler vs. writing your own handler

Use gateway.WSHandler when:

  • Authentication can be decided from the upgrade *http.Request alone (a header, a cookie, a query-string token) - i.e. WSAuthFunc can do the whole job.
  • Topics are known before or at registration time (query params, the auth result) and do not change per-message during the connection's life.
  • You want ping/pong keepalive, an inbound rate limiter, a read-size limit, origin checking, and takeover-on-reconnect handled for you.

Write your own handler (as this example does) when:

  • Authentication or topic membership is decided by an application-level frame sent after the upgrade - graphql-ws's connection_init, or any protocol where the first message on the wire carries the credentials.
  • You are hosting the WebSocket through another framework (gqlgen, a generated transport, a legacy protocol) that already owns the upgrade and the read/write loop, and the Registry is only there to give that framework cluster-wide delivery.
  • You need message framing WSHandler does not have an option for (this example's id/type/payload envelope, or anything else that is not "one payload in, one payload out").

In both cases the Registry does the identical job: track which connection ids are registered where in the cluster, and deliver payloads to them by id, by group, or by topic. What differs is only who calls Register/Join/Broadcast and when.

Running it

go run ./examples/graphql-ws
# gateway-graphql-ws listening on http://127.0.0.1:8090 (ws: /graphql, publish: /publish?topic=room:1&data=hello)

-addr overrides the listen address (default 127.0.0.1:8090).

Verifying it

The server exposes two endpoints:

  • ws://127.0.0.1:8090/graphql - the graphql-transport-ws socket. Requires the graphql-transport-ws subprotocol; a client that does not offer it is closed with websocket.StatusProtocolError.
  • GET /publish?topic=<topic>&data=<text> - broadcasts data (as a JSON string) to every connection subscribed to topic, wherever it is registered in the cluster.
With a browser console or Node
const ws = new WebSocket("ws://127.0.0.1:8090/graphql", "graphql-transport-ws");
ws.onmessage = (e) => console.log("recv", e.data);
ws.onclose = (e) => console.log("closed", e.code, e.reason);
ws.onopen = () => {
  ws.send(JSON.stringify({
    type: "connection_init",
    payload: { Authorization: "Bearer demo-token" },
  }));
};

Once you see {"type":"connection_ack"}, subscribe and trigger a broadcast from another terminal:

ws.send(JSON.stringify({ id: "sub-1", type: "subscribe", payload: { topic: "room:1" } }));
curl "http://127.0.0.1:8090/publish?topic=room:1&data=hello"
What success looks like
  • Connecting with a wrong (or missing) Authorization in connection_init gets the socket closed with code 4401 and reason Unauthorized, per graphql-ws's own convention for this case (see PROTOCOL.md, https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#invalid-message, which reserves 4401 for Unauthorized).
  • Connecting with Authorization: Bearer demo-token gets {"type":"connection_ack"}.
  • After subscribe with {"topic":"room:1"}, the curl /publish?topic=room:1&... above produces exactly one frame on the socket: {"id":"sub-1","type":"next","payload":"hello"} - the subscription id ties the delivery back to the subscribe that asked for it.
  • /publish on a topic nobody has subscribed to still returns 200 OK with delivered=0 dropped=0 remote=0; that is gateway.DeliveryResult.None() reporting correctly, not an error.
  • Sending complete for a subscription id and then publishing to its topic again produces no further next frames on that socket.

Documentation

Overview

Command graphql-ws demonstrates that gateway.Registry is usable without gateway.WSHandler at all. It hand-rolls the graphql-transport-ws subprotocol (https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) directly on top of github.com/coder/websocket and drives the Registry the same way WSHandler does internally: Register/Unregister/Join/Leave/Broadcast are all exported, ordinary methods, not something only the shipped handler can call.

The scenario this mirrors is mip-aio's real one: it hosts WebSocket through gqlgen, which owns the upgrade and the message framing, and authenticates on the first application-level frame (connection_init.payload.Authorization) rather than on the HTTP upgrade request - a shape gateway.WSHandler's WSAuthFunc (which only ever sees the *http.Request) cannot express. See README.md for the full write-up of when to reach for WSHandler versus writing a handler like this one.

This is a deliberately minimal subset of graphql-transport-ws: connection_init, connection_ack, subscribe, next, complete, ping and pong. It is enough to prove the Registry integration; it is not a spec-complete implementation (no query execution, no error/complete-on-error framing, no duplicate-subscription-id rejection). See README.md for the exact list of what is left out.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL