HTTP callback protocol for GraphQL subscriptions
For federated subgraphs communicating with the Apollo Router
This protocol is currently in preview. Breaking changes might be introduced during the preview period.
You only need to read this reference if you're adding support for this protocol to a GraphQL server library or other system!
This reference describes a protocol for GraphQL servers (or subgraphs) to send subscription data to a subscribing graph router (such as the Apollo Router) via HTTP callbacks.
For routers with many simultaneous open subscriptions, this protocol scales better than WebSocket-based protocols, which require long-lasting open connections.
The Apollo Router provides preview support for this protocol as part of its support for federated subscriptions:
Protocol flow
All actions described in these steps are performed by one of Router, Subgraph, or Emitter.
Emitter is a system that sends new subscription data to Router. Emitter is commonly also Subgraph, but it doesn't have to be.
Initialization
Before it executes a subscription operation, Router generates a unique ID (version 4 UUID recommended) to represent that operation.
Router sends a GraphQL subscription operation to Subgraph via HTTP POST using the standard GraphQL request payload:
{"query": "subscription { userWasCreated { name reviews { body } } }","extensions": {"subscription": {"callback_url": "http://localhost:4000/callback/c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","subscription_id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX"}}}The
extensions
property of this payload includes asubscription
object with the following:callback_url
: The URL that Emitter will send subscription data tosubscription_id
: The generated unique ID for the subscription operationverifier
: A string that Emitter will include in all HTTP callback requests to verify its identity
Before Subgraph responds to Router's request, it sends a
check
message to the providedcallback_url
for confirmation from Router:{"kind": "subscription","action": "check","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX"}All messages sent to
callback_url
are HTTP POST requests. Message types are documented below.This helps ensure that Subgraph is able to send callbacks successfully, and that the
id
andverifier
fields are correct.Router validates the
check
message using itsid
andverifier
fields.Again, Subgraph has not yet responded to Router's original request!
- If the
check
message is valid, Router responds with the following details:- A 204 HTTP status code
- An empty response body
- The response header
subscription-protocol: callback
- If the
check
message is invalid, Router responds with a status code besides 204 (400-level recommended).- If this occurs, Subgraph then responds to Router's request with a 400-level status code, and the subscription is canceled.
- If the
If validation succeeds, Subgraph spawns a background process or notifies a separate system (Emitter) to begin listening for subscription events.
Subgraph finally responds to Router with a 200-level status code. This indicates to Router that the subscription has been initialized.
With initialization complete, the protocol commences its main loop.
Main loop
The protocol's main loop remains active for the duration of the subscription. During the main loop, all of the following occur:
- Every five seconds, Emitter must send a
heartbeat
message to Router to confirm that Router is still listening. - Whenever new subscription data is available, Emitter sends a
next
message to Router containing the new data. - If an error occurs and the subscription must be terminated, Emitter sends a
complete
message to Router and includes theerrors
field. - If the subscription reaches the end of its stream and no new data is forthcoming, Emitter sends a
complete
message to Router and omits theerrors
field. - If Router terminates a particular subscription, it should return a 404 status code for all future HTTP callbacks sent for that subscription.
- Relatedly, if Emitter receives a 404 status code from Router for an HTTP callback, it should consider the associated subscription terminated.
Message types
During the protocol flow, Subgraph and Emitter send various messages to Router's callback URL via HTTP POST requests.
All of these messages include the following base properties in their JSON body:
{"kind": "subscription","action": "check","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX"}
Property | Description |
---|---|
| This value is currently always |
| The message type, which is one of the following:
The example body above is for a Router should respond with an error if this value is not one of the above. |
| The identifier for the message's associated subscription, generated by Router during protocol initialization. Router should respond with a 404 status code if this value does not match the ID of an active subscription. |
| A string value provided by Router during initialization so it can validate callback requests from Subgraph and Emitter. Router should respond with an error if this does not match the value it provided when initializing the subscription with the corresponding |
Fields and behaviors specific to individual message types are documented below.
check
During protocol initialization, Subgraph sends a synchronous check
message to Router to help ensure that it can send callbacks successfully, and that the id
and verifier
fields provided by Router are correct.
While subscriptions are active, you can also use a check
message in place of a heartbeat
message if you only need to verify one active subscription. (If you want to verify multiple active subscriptions with a single message, use heartbeat
.)
A check
message includes only base message fields:
{"kind": "subscription","action": "check","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX"}
If id
and verifier
both match Router's provided values, Router should respond with the following details:
- A 204 HTTP status code
- An empty response body
- The response header
subscription-protocol: callback
Otherwise, Router should respond with an error.
During initialization, Subgraph must send this message synchronously. That's because it sends this message to Router to confirm a subscription request from Router, before responding to that request.
heartbeat
As long as at least one subscription is active, Emitter must send a heartbeat
message to Router every five seconds. This enables Emitter to confirm both that it can still reach Router's callback endpoint, and that all known subscriptions are still active.
The heartbeat
message includes an ids
field, which is an array of Emitter's active subscription IDs:
{"kind": "subscription","action": "heartbeat","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX","ids": ["c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945", "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b254"]}
This message still includes an id
field! Even though this value is also present in the ids
list, Emitter must specify whichever subscription is associated with the verifier
it's providing.
On receiving a heartbeat
message, Router checks whether all subscriptions listed in ids
are indeed still active on its end.
- If all listed subscriptions are active, Router should respond with the following details:
- A 204 HTTP status code
- An empty response body
- If the list includes both active and inactive subscriptions, Router should respond with the following details:
- A 400 HTTP status code
- A JSON response body containing:
- An
invalid_ids
field. This is a list containing all IDs of inactive subscriptions. - A
verifier
field. This value replaces the value ofverifier
provided by Emitter.
- An
- If all listed subscriptions are inactive, Router should respond with the following details:
- A 404 HTTP status code
- An empty response body
Here's an example payload sent with a 400 HTTP status code if ids
contains both active and inactive subscription IDs:
{"id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","invalid_ids": ["c4a9d1b8-dc57-44ab-9e5a-6e6189b2b254"],"verifier": "XXX"}
next
Whenever a new subscription event occurs, Emitter sends the associated data to Router in a next
message.
The next
message includes a payload
field, which contains the subscription data in standard GraphQL JSON response format:
{"kind": "subscription","action": "next","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX","payload": {"data": {"numberIncremented": 5}}}
complete
Emitter sends a complete
message to Router to terminate an active subscription. Emitter might terminate a subscription for the following reasons:
- The subscription has reached the end of its stream and no new data is forthcoming.
- An Emitter error occurred that caused the subscription to fail.
A complete
message can include an errors
field containing an array of GraphQL errors. This field is required if the subscription failed and optional if it completed successfully (it's usually an empty list in this case):
{"kind": "subscription","action": "complete","id": "c4a9d1b8-dc57-44ab-9e5a-6e6189b2b945","verifier": "XXX","errors": [{ // Optional if subscription completed successfully"message": "Something went wrong"}]}
On receiving a complete
message, Router terminates the associated subscription.
Error states
The following are common error states that can occur with this protcol:
- Emitter can't communicate with Router's callback endpoint, either because the endpoint isn't available or because its provided credentials (
id
and/orverifier
) are invalid. - Emitter receives an error HTTP status code from Router's callback endpoint.
- If the error code is 404, Emitter should consider the associated subscription terminated by Router.
- If the error code is 400 in response to a
heartbeat
message, Emitter should terminate all subscriptions listed in the response body'sinvalid_ids
field. - In other error cases, Emitter should consider the subscription terminated due to an unexpected error.
- Emitter fails to send Router a
heartbeat
orcheck
message for an active subscription every five seconds, causing Router to terminate that subscription.