forked from enisdenjo/graphql-ws
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Rewrite GraphQL over WebSocket Protocol (#2)
BREAKING CHANGE: This lib is no longer compatible with [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws). It follows a redesigned transport protocol aiming to improve security, stability and reduce ambiguity.
- Loading branch information
Showing
2 changed files
with
152 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,124 +1,203 @@ | ||
_Copied from the [subscriptions-transport-ws protocol specification](https://github.com/apollographql/subscriptions-transport-ws/blob/567fca89cb4578371877faee09e1630dcddff544/PROTOCOL.md)_ | ||
|
||
# GraphQL over WebSocket Protocol | ||
|
||
## Client-server communication | ||
## Nomenclature | ||
|
||
- **Socket** is the main WebSocket communication channel between the _server_ and the _client_ | ||
- **Connection** is a connection **within the established socket** describing a "connection" through which the operation requests will be communicated | ||
|
||
## Communication | ||
|
||
The WebSocket sub-protocol for this specification is: `graphql-transport-ws` | ||
|
||
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 conform to the specified message structure. | ||
|
||
**All** messages contain the `type` field outlining the type or action this message describes. Depending on the type, the message can contain two more _optional_ fields: | ||
|
||
- `id` used for uniquely identifying server responses and connecting them with the client requests | ||
- `payload` holding the extra "payload" information to go with the specific message type | ||
|
||
The server can terminate 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. | ||
|
||
Each message has a `type` field, which defined in the protocol of this package, as well as associated fields inside `payload` field, depending on the message type, and `id` field so the client can identify each response from the server. | ||
The client terminates the socket and closes the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. | ||
|
||
Each WebSocket message is represented in JSON structure, and being stringified before sending it over the network. | ||
## Message types | ||
|
||
This is the structure of each message: | ||
### `ConnectionInit` | ||
|
||
Direction: **Client -> Server** | ||
|
||
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 subscription operation requests. | ||
|
||
The client can specify additional `connectionParams` which are sent through the `payload` field in the outgoing message. | ||
|
||
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 terminate the socket with the close event: `4408: Connection initialisation timeout`. | ||
|
||
```typescript | ||
export interface OperationMessage { | ||
payload?: any; | ||
id?: string; | ||
type: string; | ||
interface ConnectionInitMessage { | ||
type: 'connection_init'; | ||
payload?: Record<string, unknown>; // connectionParams | ||
} | ||
``` | ||
|
||
### Client -> Server | ||
The server will respond by either: | ||
|
||
- Dispatching a `ConnectionAck` message acknowledging that the connection has been successfully established. The server does not implement the `onConnect` callback or the implemented callback has returned `true`. | ||
- Closing the socket with a close event `4403: Forbidden` indicating that the connection request has been denied because of access control. The server has returned `false` in the `onConnect` callback. | ||
- Closing the socket with a close event `4400: <error-message>` indicating that the connection request has been denied because of an implementation specific error. The server has thrown an error in the `onConnect` callback, the thrown error's message is the `<error-message>` in the close event. | ||
|
||
### `ConnectionAck` | ||
|
||
Direction: **Server -> Client** | ||
|
||
#### GQL_CONNECTION_INIT | ||
Potential response to the `ConnectionInit` message from the client acknowledging a successful connection with the server. | ||
|
||
Client sends this message after plain websocket connection to start the communication with the server | ||
```typescript | ||
interface ConnectionAckMessage { | ||
type: 'connection_ack'; | ||
} | ||
``` | ||
|
||
The server will response only with `GQL_CONNECTION_ACK` + `GQL_CONNECTION_KEEP_ALIVE` (if used) or `GQL_CONNECTION_ERROR` to this message. | ||
The client is now **ready** to request subscription operations. | ||
|
||
- `payload: Object` : optional parameters that the client specifies in `connectionParams` | ||
### `Subscribe` | ||
|
||
#### GQL_START | ||
Direction: **Client -> Server** | ||
|
||
Client sends this message to execute GraphQL operation | ||
Requests a operation specified in the message `payload`. This message leverages the unique ID field to connect future server messages to the operation started by this message. | ||
|
||
- `id: string` : The id of the GraphQL operation to start | ||
- `payload: Object`: | ||
- `query: string` : GraphQL operation as string or parsed GraphQL document node | ||
- `variables?: Object` : Object with GraphQL variables | ||
- `operationName?: string` : GraphQL operation name | ||
```typescript | ||
import { DocumentNode } from 'graphql'; | ||
|
||
interface SubscribeMessage { | ||
id: '<unique-operation-id>'; | ||
type: 'subscribe'; | ||
payload: { | ||
operationName: string; | ||
query: string | DocumentNode; | ||
variables: Record<string, unknown>; | ||
}; | ||
} | ||
``` | ||
|
||
#### GQL_STOP | ||
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 terminated immediately with a close event `4401: Unauthorized`. | ||
|
||
Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe) | ||
### `Next` | ||
|
||
- `id: string` : operation id | ||
Direction: **Server -> Client** | ||
|
||
#### GQL_CONNECTION_TERMINATE | ||
Operation execution result message. | ||
|
||
Client sends this message to terminate the connection. | ||
- If the operation is a `query` or `mutation`, the message can be seen as the final execution result. This message is followed by the `Complete` message indicating the completion of the operation. | ||
- If the operation is a `subscription`, the message can be seen as an event in the source stream requested by the `Subscribe` message. | ||
|
||
### Server -> Client | ||
```typescript | ||
import { ExecutionResult } from 'graphql'; | ||
|
||
#### GQL_CONNECTION_ERROR | ||
interface NextMessage { | ||
id: '<unique-operation-id>'; | ||
type: 'next'; | ||
payload: ExecutionResult; | ||
} | ||
``` | ||
|
||
The server may responses with this message to the `GQL_CONNECTION_INIT` from client, indicates the server rejected the connection. | ||
### `Error` | ||
|
||
It server also respond with this message in case of a parsing errors of the message (which does not disconnect the client, just ignore the message). | ||
Direction: **Server -> Client** | ||
|
||
- `payload: Object`: the server side error | ||
Operation execution error(s) triggered by the `Next` message happening before the actual execution, usually due to validation errors. | ||
|
||
#### GQL_CONNECTION_ACK | ||
```typescript | ||
import { GraphQLError } from 'graphql'; | ||
|
||
The server may responses with this message to the `GQL_CONNECTION_INIT` from client, indicates the server accepted the connection. | ||
interface ErrorMessage { | ||
id: '<unique-operation-id>'; | ||
type: 'error'; | ||
payload: GraphQLError[]; | ||
} | ||
``` | ||
|
||
#### GQL_DATA | ||
### `Complete` | ||
|
||
The server sends this message to transfter the GraphQL execution result from the server to the client, this message is a response for `GQL_START` message. | ||
Direction: **bidirectional** | ||
|
||
For each GraphQL operation send with `GQL_START`, the server will respond with at least one `GQL_DATA` message. | ||
- **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**. | ||
|
||
- `id: string` : ID of the operation that was successfully set up | ||
- `payload: Object` : | ||
- `data: any`: Execution result | ||
- `errors?: Error[]` : Array of resolvers errors | ||
- **Client -> Server** (for `subscription` operations only) indicating that the client has stopped listening to the events and wants to complete the source stream. No further data events, relevant to the original subscription, should be sent through. | ||
|
||
#### GQL_ERROR | ||
```typescript | ||
interface CompleteMessage { | ||
id: '<unique-operation-id>'; | ||
type: 'complete'; | ||
} | ||
``` | ||
|
||
Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation errors (resolver errors are part of `GQL_DATA` message, and will be added as `errors` array) | ||
### Invalid message | ||
|
||
- `payload: Error` : payload with the error attributed to the operation failing on the server | ||
- `id: string` : operation ID of the operation that failed on the server | ||
Direction: **bidirectional** | ||
|
||
#### GQL_COMPLETE | ||
Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket termination with a close event `4400: <error-message>`. The `<error-message>` can be vagouly descriptive on why the received message is invalid. | ||
|
||
Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the specific operation. | ||
## Examples | ||
|
||
- `id: string` : operation ID of the operation that completed | ||
For the sake of clarity, the following examples demonstrate the communication protocol. | ||
|
||
#### GQL_CONNECTION_KEEP_ALIVE | ||
<h3 id="successful-connection-initialisation">Successful connection initialisation</h3> | ||
|
||
Server message that should be sent right after each `GQL_CONNECTION_ACK` processed and then periodically to keep the client connection alive. | ||
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` | ||
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") | ||
1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation | ||
1. _Server_ validates the connection initialisation request and dispatches a `ConnectionAck` message to the client on successful connection | ||
1. _Client_ has received the acknowledgement message and is now ready to request operation executions | ||
|
||
The client starts to consider the keep alive message only upon the first received keep alive message from the server. | ||
### Forbidden connection initialisation | ||
|
||
### Messages Flow | ||
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` | ||
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") | ||
1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation | ||
1. _Server_ validates the connection initialisation request and decides that the client is not allowed to establish a connection | ||
1. _Server_ terminates the socket by dispatching the close event `4403: Forbidden` | ||
1. _Client_ reports an error using the close event reason (which is `Forbidden`) | ||
|
||
This is a demonstration of client-server communication, in order to get a better understanding of the protocol flow: | ||
### Erroneous connection initialisation | ||
|
||
#### Session Init Phase | ||
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` | ||
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") | ||
1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation | ||
1. _Server_ tries validating the connection initialisation request but an error `I'm a teapot` is thrown | ||
1. _Server_ terminates the socket by dispatching the close event `4400: I'm a teapot` | ||
1. _Client_ reports an error using the close event reason (which is `I'm a teapot`) | ||
|
||
The phase initializes the connection between the client and server, and usually will also build the server-side `context` for the execution. | ||
### Connection initialisation timeout | ||
|
||
- Client connected immediately, or stops and wait if using lazy mode (until first operation execution) | ||
- Client sends `GQL_CONNECTION_INIT` message to the server. | ||
- Server calls `onConnect` callback with the init arguments, waits for init to finish and returns it's return value with `GQL_CONNECTION_ACK` + `GQL_CONNECTION_KEEP_ALIVE` (if used), or `GQL_CONNECTION_ERROR` in case of `false` or thrown exception from `onConnect` callback. | ||
- Client gets `GQL_CONNECTION_ACK` + `GQL_CONNECTION_KEEP_ALIVE` (if used) and waits for the client's app to create subscriptions. | ||
1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` | ||
1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") | ||
1. _Client_ does not dispatch a `ConnectionInit` message | ||
1. _Server_ waits for the `ConnectionInit` message for the duration specified in the `connectionInitWaitTimeout` parameter | ||
1. _Server_ waiting time has passed | ||
1. _Server_ terminates the socket by dispatching the close event `4408: Connection initialisation timeout` | ||
1. _Client_ reports an error using the close event reason (which is `Connection initialisation timeout`) | ||
|
||
#### Connected Phase | ||
### Query/Mutation operation | ||
|
||
This phase called per each operation the client request to execute: | ||
_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ | ||
|
||
- App creates a subscription using `subscribe` or `query` client's API, and the `GQL_START` message sent to the server. | ||
- Server calls `onOperation` callback, and responds with `GQL_DATA` in case of zero errors, or `GQL_ERROR` if there is a problem with the operation (is might also return `GQL_ERROR` with `errors` array, in case of resolvers errors). | ||
- Client get `GQL_DATA` and handles it. | ||
- Server calls `onOperationDone` if the operation is a query or mutation (for subscriptions, this called when unsubscribing) | ||
- Server sends `GQL_COMPLETE` if the operation is a query or mutation (for subscriptions, this sent when unsubscribing) | ||
1. _Client_ generates a unique ID for the following operation | ||
1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested `query`/`mutation` operation passed through the `payload` field | ||
1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation | ||
1. _Server_ validates the request and executes the GraphQL operation | ||
1. _Server_ dispatches a `Next` message with the execution result matching the client's unique ID | ||
1. _Server_ dispatches the `Complete` message with the matching unique ID indicating that the execution has completed | ||
1. _Server_ triggers the `onComplete` callback, if specified | ||
|
||
For subscriptions: | ||
### Subscribe operation | ||
|
||
- App triggers `PubSub`'s publication method, and the server publishes the event, passing it through the `subscribe` executor to create GraphQL execution result | ||
- Client receives `GQL_DATA` with the data, and handles it. | ||
- When client unsubscribe, the server triggers `onOperationDone` and sends `GQL_COMPLETE` message to the client. | ||
_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ | ||
|
||
When client done with executing GraphQL, it should close the connection and terminate the session using `GQL_CONNECTION_TERMINATE` message. | ||
1. _Client_ generates a unique ID for the following operation | ||
1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested subscription operation passed through the `payload` field | ||
1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation | ||
1. _Server_ validates the request, establishes a GraphQL subscription and listens for events in the source stream | ||
1. _Server_ dispatches `Next` messages for every event in the underlying subscription source stream matching the client's unique ID | ||
1. _Client_ stops the subscription by dispatching a `Complete` message with the matching unique ID | ||
1. _Server_ effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources | ||
1. _Server_ triggers the `onComplete` callback, if specified |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters