Skip to content

Commit 66e04b9

Browse files
authored
RFC: GraphQL over WebSocket (#140)
1 parent 412ebd2 commit 66e04b9

File tree

1 file changed

+250
-0
lines changed

1 file changed

+250
-0
lines changed

rfcs/GraphQLOverWebSocket.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# GraphQL over WebSocket Protocol
2+
3+
## Nomenclature
4+
5+
- **Socket** is the main WebSocket communication channel between the _server_ and the _client_
6+
- **Connection** is a connection **within the established socket** describing a "connection" through which the operation requests will be communicated
7+
8+
## Communication
9+
10+
The WebSocket sub-protocol for this specification is: `graphql-transport-ws`.
11+
12+
Messages are represented through the JSON structure and are stringified before being sent over the network. They are bidirectional, meaning both the server and the client must conform to the specified message structure.
13+
14+
**All** messages contain the `type` field outlining the action this message describes. Depending on the type, the message can contain two more _optional_ fields:
15+
16+
- `id` used for uniquely identifying server responses and connecting them with the client's requests
17+
- `payload` holding the extra "payload" information to go with the specific message type
18+
19+
Multiple operations identified with separate IDs can be active at any time and their messages can be interleaved on the connection.
20+
21+
The server can close the socket (kick the client off) at any time. The close event dispatched by the server is used to describe the fatal error to the client.
22+
23+
The client closes the socket and the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure.
24+
25+
## Message types
26+
27+
### `ConnectionInit`
28+
29+
Direction: **Client -> Server**
30+
31+
Indicates that the client wants to establish a connection within the existing socket. This connection is **not** the actual WebSocket communication channel, but is rather a frame within it asking the server to allow future operation requests.
32+
33+
The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will close the socket with the event: `4408: Connection initialisation timeout`.
34+
35+
If the server receives more than one `ConnectionInit` message at any given time, the server will close the socket with the event `4429: Too many initialisation requests`.
36+
37+
```typescript
38+
interface ConnectionInitMessage {
39+
type: 'connection_init';
40+
payload?: Record<string, unknown>;
41+
}
42+
```
43+
44+
### `ConnectionAck`
45+
46+
Direction: **Server -> Client**
47+
48+
Expected response to the `ConnectionInit` message from the client acknowledging a successful connection with the server.
49+
50+
The server can use the optional `payload` field to transfer additional details about the connection.
51+
52+
```typescript
53+
interface ConnectionAckMessage {
54+
type: 'connection_ack';
55+
payload?: Record<string, unknown>;
56+
}
57+
```
58+
59+
The client is now **ready** to request subscription operations.
60+
61+
### `Ping`
62+
63+
Direction: **bidirectional**
64+
65+
Useful for detecting failed connections, displaying latency metrics or other types of network probing.
66+
67+
A `Pong` must be sent in response from the receiving party as soon as possible.
68+
69+
The `Ping` message can be sent at any time within the established socket.
70+
71+
The optional `payload` field can be used to transfer additional details about the ping.
72+
73+
```typescript
74+
interface PingMessage {
75+
type: 'ping';
76+
payload?: Record<string, unknown>;
77+
}
78+
```
79+
80+
### `Pong`
81+
82+
Direction: **bidirectional**
83+
84+
The response to the `Ping` message. Must be sent as soon as the `Ping` message is received.
85+
86+
The `Pong` message can be sent at any time within the established socket. Furthermore, the `Pong` message may even be sent unsolicited as an unidirectional heartbeat.
87+
88+
The optional `payload` field can be used to transfer additional details about the pong.
89+
90+
```typescript
91+
interface PongMessage {
92+
type: 'pong';
93+
payload?: Record<string, unknown>;
94+
}
95+
```
96+
97+
### `Subscribe`
98+
99+
Direction: **Client -> Server**
100+
101+
Requests an operation specified in the message `payload`. This message provides a unique ID field to connect published messages to the operation requested by this message.
102+
103+
If there is already an active subscriber for an operation matching the provided ID, regardless of the operation type, the server **must** close the socket immediately with the event `4409: Subscriber for <unique-operation-id> already exists`.
104+
105+
The server needs only keep track of IDs for as long as the subscription is active. Once a client completes an operation, it is free to re-use that ID.
106+
107+
```typescript
108+
interface SubscribeMessage {
109+
id: '<unique-operation-id>';
110+
type: 'subscribe';
111+
payload: {
112+
operationName?: string | null;
113+
query: string;
114+
variables?: Record<string, unknown> | null;
115+
extensions?: Record<string, unknown> | null;
116+
};
117+
}
118+
```
119+
120+
Executing operations is allowed **only** after the server has acknowledged the connection through the `ConnectionAck` message, if the connection is not acknowledged, the socket will be closed immediately with the event `4401: Unauthorized`.
121+
122+
### `Next`
123+
124+
Direction: **Server -> Client**
125+
126+
Operation execution result(s) from the source stream created by the binding `Subscribe` message. After all results have been emitted, the `Complete` message will follow indicating stream completion.
127+
128+
```typescript
129+
import { ExecutionResult } from 'graphql';
130+
131+
interface NextMessage {
132+
id: '<unique-operation-id>';
133+
type: 'next';
134+
payload: ExecutionResult;
135+
}
136+
```
137+
138+
### `Error`
139+
140+
Direction: **Server -> Client**
141+
142+
Operation execution error(s) in response to the `Subscribe` message. This can occur _before_ execution starts, usually due to validation errors, or _during_ the execution of the request. This message terminates the operation and no further messages will be sent.
143+
144+
```typescript
145+
import { GraphQLError } from 'graphql';
146+
147+
interface ErrorMessage {
148+
id: '<unique-operation-id>';
149+
type: 'error';
150+
payload: GraphQLError[];
151+
}
152+
```
153+
154+
### `Complete`
155+
156+
Direction: **bidirectional**
157+
158+
- **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, no `Complete` message will be emitted.
159+
160+
- **Client -> Server** indicates that the client has stopped listening and wants to complete the subscription. No further events, relevant to the original subscription, should be sent through. Even if the client sent a `Complete` message for a _single-result-operation_ before it resolved, the result should not be sent through once it does.
161+
162+
Note: The asynchronous nature of the full-duplex connection means that a client can send a `Complete` message to the server even when messages are in-flight to the client, or when the server has itself completed the operation (via a `Error` or `Complete` message). Both client and server must therefore be prepared to receive (and ignore) messages for operations that they consider already completed.
163+
164+
```typescript
165+
interface CompleteMessage {
166+
id: '<unique-operation-id>';
167+
type: 'complete';
168+
}
169+
```
170+
171+
### Invalid message
172+
173+
Direction: **bidirectional**
174+
175+
Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket closure with the event `4400: <error-message>`. The `<error-message>` can be vaguely descriptive on why the received message is invalid.
176+
177+
Receiving a message (other than `Subscribe`) with an ID that belongs to an operation that has been previously completed does not constitute an error. It is permissable to simply ignore all _unknown_ IDs without closing the connection.
178+
179+
## Examples
180+
181+
For the sake of clarity, the following examples demonstrate the communication protocol.
182+
183+
<h3 id="successful-connection-initialisation">Successful connection initialisation</h3>
184+
185+
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws`
186+
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
187+
1. _Client_ immediately dispatches a `ConnectionInit` message optionally providing a payload as agreed with the server
188+
1. _Server_ validates the connection initialisation request and dispatches a `ConnectionAck` message to the client on successful connection
189+
1. _Client_ has received the acknowledgement message and is now ready to request operation executions
190+
191+
### Connection initialisation timeout
192+
193+
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws`
194+
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
195+
1. _Client_ does not dispatch a `ConnectionInit` message
196+
1. _Server_ waits for the `ConnectionInit` message for the duration specified in the `connectionInitWaitTimeout` parameter
197+
1. _Server_ waiting time has passed
198+
1. _Server_ closes the socket by dispatching the event `4408: Connection initialisation timeout`
199+
200+
### Streaming operation
201+
202+
#### `subscription` operation and queries with streaming directives
203+
204+
_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._
205+
206+
1. _Client_ generates a unique ID for the following operation
207+
1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
208+
<br>_All future communication is linked through this unique ID_
209+
1. _Server_ executes the streaming GraphQL operation
210+
1. _Server_ checks if the generated ID is unique across active streaming subscriptions
211+
212+
- If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for <generated-id> already exists`
213+
- If unique, continue...
214+
215+
1. _Server_ _optionally_ checks if the operation is valid before starting executing it, e.g. checking permissions
216+
217+
- If **not** valid, the _server_ sends an `Error` message and deems the operation complete.
218+
- If valid, continue...
219+
220+
1. _Server_ dispatches results over time with the `Next` message
221+
1. - _Server_ dispatches the `Complete` message indicating that the source stream has completed
222+
- _Client_ completes the stream observer
223+
<br>**or**
224+
- _Client_ stops the subscription by dispatching a `Complete` message
225+
- _Server_ receives `Complete` message and completes the source stream
226+
- _Client_ ignores all further messages that it recives with this ID
227+
<br>**or**
228+
- _Server_ dispatches the `Complete` message indicating that the source stream has completed
229+
- **Simultaneously** _client_ stops the subscription by dispatching a `Complete` message
230+
- _Client_ ignores all further messages that it recives with this ID
231+
- _Server_ ignores the `Complete` message from the client
232+
233+
### Single result operation
234+
235+
#### `query` and `mutation` operations without streaming directives
236+
237+
A single result operation is identical to a streaming operation except that _at most one_ `Next` message is sent.
238+
239+
It shares the same name-space for IDs as streaming operations and can be multiplexed with other operations on the connection.
240+
241+
_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._
242+
243+
1. _Client_ generates a unique ID for the following operation
244+
1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
245+
<br>_All future communication is linked through this unique ID_
246+
1. _Server_ executes the single result GraphQL operation
247+
1. _Server_ dispatches the result with the `Next` message
248+
1. _Server_ dispatches the `Complete` message indicating that the execution has completed
249+
250+
The _client_ may dispatch a `Complete` message at any time, just as shown in the streaming operations examples above, and the same interactions ensue.

0 commit comments

Comments
 (0)