External Coprocessing
Customize your router's behavior in any language
With external coprocessing, you can hook into the GraphOS Router's request-handling lifecycle by writing standalone code in any language and framework. This code (i.e., your coprocessor) can run anywhere on your network that's accessible to the router over HTTP.
You can configure your router to "call out" to your coprocessor at different stages throughout the request-handling lifecycle, enabling you to perform custom logic based on a client request's headers, query string, and other details. This logic can access disk and perform network requests, all while safely isolated from the critical router process.
When your coprocessor responds to these requests, its response body can modify various details of the client's request or response. You can even terminate a client request.
Recommended locations for hosting your coprocessor include:
On the same host as your router (minimal request latency)
In the same Pod as your router, as a "sidecar" container (minimal request latency)
In the same availability zone as your router (low request latency with increased deployment isolation)
How it works
Whenever your router receives a client request, at various stages in the request-handling lifecycle it can send HTTP POST requests to your coprocessor:
This diagram shows request execution proceeding "down" from a client, through the router, to individual subgraphs. Execution then proceeds back "up" to the client in the reverse order.
As shown in the diagram above, the RouterService
, SupergraphService
, ExecutionService
, and SubgraphService
steps of the request-handling lifecycle can send these POST requests (also called coprocessor requests).
Each supported service can send its coprocessor requests at two different stages:
As execution proceeds "down" from the client to individual subgraphs
Here, the coprocessor can inspect and modify details of requests before GraphQL operations are processed.
The coprocessor can also instruct the router to terminate a client request immediately.
As execution proceeds back "up" from subgraphs to the client
Here, the coprocessor can inspect and modify details of the router's response to the client.
At every stage, the router waits for your coprocessor's response before it continues processing the corresponding request. Because of this, you should maximize responsiveness by configuring only whichever coprocessor requests your customization requires.
Multiple requests with SubgraphService
If your coprocessor hooks into your router's SubgraphService
, the router sends a separate coprocessor request for each subgraph request in its query plan. In other words, if your router needs to query three separate subgraphs to fully resolve a client operation, it sends three separate coprocessor requests. Each coprocessor request includes the name and URL of the subgraph being queried.
Setup
First, make sure your router is connected to a GraphOS Enterprise organization.
You configure external coprocessing in your router's YAML config file, under the coprocessor
key.
Typical configuration
This example configuration sends commonly used request and response details to your coprocessor (see the comments below for explanations of each field):
1coprocessor:
2 url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint.
3 timeout: 2s # The timeout for all coprocessor requests. Defaults to 1 second (1s)
4 router: # This coprocessor hooks into the `RouterService`
5 request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request.
6 headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
7 body: false
8 context: false
9 sdl: false
10 path: false
11 method: false
12 response: # By including this key, the `RouterService` sends a coprocessor request whenever it's about to send response data to a client (including incremental data via @defer).
13 headers: true
14 body: false
15 context: false
16 sdl: false
17 status_code: false
18 supergraph: # This coprocessor hooks into the `SupergraphService`
19 request: # By including this key, the `SupergraphService` sends a coprocessor request whenever it first receives a client request.
20 headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
21 body: false
22 context: false
23 sdl: false
24 method: false
25 response: # By including this key, the `SupergraphService` sends a coprocessor request whenever it's about to send response data to a client (including incremental data via @defer).
26 headers: true
27 body: false
28 context: false
29 sdl: false
30 status_code: false
31 subgraph:
32 all:
33 request: # By including this key, the `SubgraphService` sends a coprocessor request whenever it is about to make a request to a subgraph.
34 headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
35 body: false
36 context: false
37 uri: false
38 method: false
39 service_name: false
40 response: # By including this key, the `SubgraphService` sends a coprocessor request whenever receives a subgraph response.
41 headers: true
42 body: false
43 context: false
44 service_name: false
45 status_code: false
Minimal configuration
You can confirm that your router can reach your coprocessor by setting this minimal configuration before expanding it as needed:
1coprocessor:
2 url: http://127.0.0.1:8081 # Replace with the URL of your coprocessor's HTTP endpoint.
3 router:
4 request:
5 headers: false
In this case, the RouterService
only sends a coprocessor request whenever it receives a client request. The coprocessor request body includes no data related to the client request (only "control" data, which is covered below).
Conditions
You can define conditions for a stage of the request lifecycle that you want to run the coprocessor. You can set coprocessor conditions with selectors based on headers or context entries.
Execution
stage doesn't support coprocessor conditions.Example configurations:
Run during the
SupergraphResponse
stage only for the first event of a supergraph response. Useful for handling only the first subscription event when a subscription is opened:
1coprocessor:
2 url: http://127.0.0.1:3000
3 supergraph:
4 response:
5 condition:
6 eq:
7 - true
8 - is_primary_response: true # Will be true only for the first event received on a supergraph response (like classical queries and mutations for example)
9 body: true
10 headers: true
Run during the
Request
stage only if the request contains a request header:
1coprocessor:
2 url: http://127.0.0.1:3000
3 router:
4 request:
5 condition:
6 eq:
7 - request_header: should-execute-copro # Header name
8 - "enabled" # Header value
9 body: true
10 headers: true
Client configuration
The router supports coprocessor connections over:
- HTTP/1.1
- HTTP/1.1 with TLS
- HTTP/2 with TLS
- HTTP/2 Cleartext protocol (h2c). This uses HTTP/2 over plaintext connections.
Use the table below to look up the resulting protocol of a coprocessor connection, based on the URL and the experimental_http2
configuration:
URL with http:// | URL with https:// | |
---|---|---|
experimental_http2: disable | HTTP/1.1 | HTTP/1.1 with TLS |
experimental_http2: enable | HTTP/1.1 | Either HTTP/1.1 or HTTP/2 with TLS, as determined by the TLS handshake |
experimental_http2: http2only | h2c | HTTP/2 with TLS |
experimental_http2 not set | HTTP/1.1 | Either HTTP/1.1 or HTTP/2 with TLS, as determined by the TLS handshake |
Configuring experimental_http2: http2only
for a network that doesn't support HTTP/2 results in a failed coprocessor connection.
For example, to enable h2c (http2 cleartext) communication with a coprocessor you can use this configuration::
1coprocessor:
2 url: http://127.0.0.1:8081
3 # Using an HTTP (not HTTPS) URL and experimental_http2: http2only results in connections that use h2c
4 client:
5 experimental_http2: http2only
6
Coprocessor request format
The router communicates with your coprocessor via HTTP POST requests (called coprocessor requests). The body of each coprocessor request is a JSON object with properties that describe either the current client request or the current router response.
Properties of the JSON body are divided into two high-level categories:
"Control" properties
These provide information about the context of the specific router request or response. They provide a mechanism to influence the router's execution flow.
The router always includes these properties in coprocessor requests.
Data properties
These provide information about the substance of a request or response, such as the GraphQL query string and any HTTP headers. Aside from
sdl
, your coprocessor can modify all of these properties.You configure which of these fields the router includes in its coprocessor requests. By default, the router includes none of them.
Example requests by stage
RouterRequest
Click to expand
1{
2 // Control properties
3 "version": 1,
4 "stage": "RouterRequest",
5 "control": "continue",
6 "id": "1b19c05fdafc521016df33148ad63c1b",
7
8 // Data properties
9 "headers": {
10 "cookie": [
11 "tasty_cookie=strawberry"
12 ],
13 "content-type": [
14 "application/json"
15 ],
16 "host": [
17 "127.0.0.1:4000"
18 ],
19 "apollo-federation-include-trace": [
20 "ftv1"
21 ],
22 "apollographql-client-name": [
23 "manual"
24 ],
25 "accept": [
26 "*/*"
27 ],
28 "user-agent": [
29 "curl/7.79.1"
30 ],
31 "content-length": [
32 "46"
33 ]
34 },
35 "body": "{
36 \"query\": \"query GetActiveUser {\n me {\n name\n}\n}\"
37 }",
38 "context": {
39 "entries": {
40 "accepts-json": false,
41 "accepts-wildcard": true,
42 "accepts-multipart": false
43 }
44 },
45 "sdl": "...", // String omitted due to length
46 "path": "/",
47 "method": "POST"
48}
RouterResponse
Click to expand
1{
2 // Control properties
3 "version": 1,
4 "stage": "RouterResponse",
5 "control": "continue",
6 "id": "1b19c05fdafc521016df33148ad63c1b",
7
8 // Data properties
9 "headers": {
10 "vary": [
11 "origin"
12 ],
13 "content-type": [
14 "application/json"
15 ]
16 },
17 "body": "{
18 \"data\": {
19 \"me\": {
20 \"name\": \"Ada Lovelace\"
21 }
22 }
23 }",
24 "context": {
25 "entries": {
26 "apollo_telemetry::subgraph_metrics_attributes": {},
27 "accepts-json": false,
28 "accepts-multipart": false,
29 "apollo_telemetry::client_name": "manual",
30 "apollo_telemetry::usage_reporting": {
31 "statsReportKey": "# Long\nquery Long{me{name}}",
32 "referencedFieldsByType": {
33 "User": {
34 "fieldNames": [
35 "name"
36 ],
37 "isInterface": false
38 },
39 "Query": {
40 "fieldNames": [
41 "me"
42 ],
43 "isInterface": false
44 }
45 }
46 },
47 "apollo_telemetry::client_version": "",
48 "accepts-wildcard": true
49 }
50 },
51 "statusCode": 200,
52 "sdl": "..." // Omitted due to length
53}
SupergraphRequest
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "SupergraphRequest",
6 "control": "continue",
7
8 // Data properties
9 "headers": {
10 "cookie": [
11 "tasty_cookie=strawberry"
12 ],
13 "content-type": [
14 "application/json"
15 ],
16 "host": [
17 "127.0.0.1:4000"
18 ],
19 "apollo-federation-include-trace": [
20 "ftv1"
21 ],
22 "apollographql-client-name": [
23 "manual"
24 ],
25 "accept": [
26 "*/*"
27 ],
28 "user-agent": [
29 "curl/7.79.1"
30 ],
31 "content-length": [
32 "46"
33 ]
34 },
35 "body": {
36 "query": "query Long {\n me {\n name\n}\n}",
37 "operationName": "MyQuery",
38 "variables": {}
39 },
40 "context": {
41 "entries": {
42 "accepts-json": false,
43 "accepts-wildcard": true,
44 "accepts-multipart": false,
45 "this-is-a-test-context": 42
46 }
47 },
48 "serviceName": "service name shouldn't change",
49 "uri": "http://thisurihaschanged"
50}
51
SupergraphResponse
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "SupergraphResponse",
6 "control": {
7 "break": 200
8 },
9
10 // Data properties
11 "body": {
12 "errors": [{ "message": "my error message" }]
13 },
14 "context": {
15 "entries": {
16 "testKey": true
17 }
18 },
19 "headers": {
20 "aheader": ["a value"]
21 }
22}
23
ExecutionRequest
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "ExecutionRequest",
6 "control": "continue",
7
8 // Data properties
9 "headers": {
10 "cookie": [
11 "tasty_cookie=strawberry"
12 ],
13 "content-type": [
14 "application/json"
15 ],
16 "host": [
17 "127.0.0.1:4000"
18 ],
19 "apollo-federation-include-trace": [
20 "ftv1"
21 ],
22 "apollographql-client-name": [
23 "manual"
24 ],
25 "accept": [
26 "*/*"
27 ],
28 "user-agent": [
29 "curl/7.79.1"
30 ],
31 "content-length": [
32 "46"
33 ]
34 },
35 "body": {
36 "query": "query Long {\n me {\n name\n}\n}",
37 "operationName": "MyQuery"
38 },
39 "context": {
40 "entries": {
41 "accepts-json": false,
42 "accepts-wildcard": true,
43 "accepts-multipart": false,
44 "this-is-a-test-context": 42
45 }
46 },
47 "serviceName": "service name shouldn't change",
48 "uri": "http://thisurihaschanged",
49 "queryPlan": {
50 "usage_reporting":{"statsReportKey":"# Me\nquery Me{me{name username}}","referencedFieldsByType":{"User":{"fieldNames":["name","username"],"isInterface":false},"Query":{"fieldNames":["me"],"isInterface":false}}},
51 "root":{
52 "kind":"Fetch",
53 "serviceName":"accounts",
54 "variableUsages":[],
55 "operation":"query Me__accounts__0{me{name username}}",
56 "operationName":"Me__accounts__0",
57 "operationKind":"query",
58 "id":null,
59 "inputRewrites":null,
60 "outputRewrites":null,
61 "authorization":{"is_authenticated":false,"scopes":[],"policies":[]}},
62 "formatted_query_plan":"QueryPlan {\n Fetch(service: \"accounts\") {\n {\n me {\n name\n username\n }\n }\n },\n}",
63 "query":{
64 "string":"query Me {\n me {\n name\n username\n }\n}\n","fragments":{"map":{}},"operations":[{"name":"Me","kind":"query","type_name":"Query","selection_set":[{"Field":{"name":"me","alias":null,"selection_set":[{"Field":{"name":"name","alias":null,"selection_set":null,"field_type":{"Named":"String"},"include_skip":{"include":"Yes","skip":"No"}}},{"Field":{"name":"username","alias":null,"selection_set":null,"field_type":{"Named":"String"},"include_skip":{"include":"Yes","skip":"No"}}}],"field_type":{"Named":"User"},"include_skip":{"include":"Yes","skip":"No"}}}],"variables":{}}],"subselections":{},"unauthorized":{"paths":[],"errors":{"log":true,"response":"errors"}},"filtered_query":null,"defer_stats":{"has_defer":false,"has_unconditional_defer":false,"conditional_defer_variable_names":[]},"is_original":true}
65 }
66}
67
ExecutionResponse
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "ExecutionResponse",
6 "control": {
7 "break": 200
8 },
9
10 // Data properties
11 "body": {
12 "errors": [{ "message": "my error message" }]
13 },
14 "context": {
15 "entries": {
16 "testKey": true
17 }
18 },
19 "headers": {
20 "aheader": ["a value"]
21 }
22}
23
SubgraphRequest
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "SubgraphRequest",
6 "control": "continue",
7 "id": "666d677225c1bc6d7c54a52b409dbd4e",
8
9 // Data properties
10 "headers": {},
11 "body": {
12 "query": "query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{body id}}}}",
13 "operationName": "TopProducts__reviews__1",
14 "variables": {
15 "representations": [
16 {
17 "__typename": "Product",
18 "upc": "1"
19 },
20 {
21 "__typename": "Product",
22 "upc": "2"
23 },
24 {
25 "__typename": "Product",
26 "upc": "3"
27 }
28 ]
29 }
30 },
31 "context": {
32 "entries": {
33 "apollo_telemetry::usage_reporting": {
34 "statsReportKey": "# TopProducts\nquery TopProducts{topProducts{name price reviews{body id}}}",
35 "referencedFieldsByType": {
36 "Query": {
37 "fieldNames": [
38 "topProducts"
39 ],
40 "isInterface": false
41 },
42 "Review": {
43 "fieldNames": [
44 "body",
45 "id"
46 ],
47 "isInterface": false
48 },
49 "Product": {
50 "fieldNames": [
51 "price",
52 "name",
53 "reviews"
54 ],
55 "isInterface": false
56 }
57 }
58 },
59 "apollo_telemetry::client_version": "",
60 "apollo_telemetry::subgraph_metrics_attributes": {},
61 "apollo_telemetry::client_name": ""
62 }
63 },
64 "uri": "https://reviews.demo.starstuff.dev/",
65 "method": "POST",
66 "serviceName": "reviews"
67}
68
SubgraphResponse
Click to expand
1
2{
3 // Control properties
4 "version": 1,
5 "stage": "SubgraphResponse",
6 "id": "b7810c6f7f95640fd6c6c8781e3953c0",
7 "control": "continue",
8
9 // Data properties
10 "headers": {
11 "etag": [
12 "W/\"d3-7aayASjs0+e2c/TpiAYgEu/yyo0\""
13 ],
14 "via": [
15 "2 fly.io"
16 ],
17 "server": [
18 "Fly/90d459b3 (2023-03-07)"
19 ],
20 "date": [
21 "Thu, 09 Mar 2023 14:28:46 GMT"
22 ],
23 "x-powered-by": [
24 "Express"
25 ],
26 "x-ratelimit-limit": [
27 "10000000"
28 ],
29 "access-control-allow-origin": [
30 "*"
31 ],
32 "x-ratelimit-remaining": [
33 "9999478"
34 ],
35 "content-type": [
36 "application/json; charset=utf-8"
37 ],
38 "fly-request-id": [
39 "01GV3CCG5EM3ZNVZD2GH0B00E2-lhr"
40 ],
41 "x-ratelimit-reset": [
42 "1678374007"
43 ]
44 },
45 "body": {
46 "data": {
47 "_entities": [
48 {
49 "reviews": [
50 {
51 "body": "Love it!",
52 "id": "1"
53 },
54 {
55 "body": "Prefer something else.",
56 "id": "4"
57 }
58 ]
59 },
60 {
61 "reviews": [
62 {
63 "body": "Too expensive.",
64 "id": "2"
65 }
66 ]
67 },
68 {
69 "reviews": [
70 {
71 "body": "Could be better.",
72 "id": "3"
73 }
74 ]
75 }
76 ]
77 }
78 },
79 "context": {
80 "entries": {
81 "apollo_telemetry::usage_reporting": {
82 "statsReportKey": "# TopProducts\nquery TopProducts{topProducts{name price reviews{body id}}}",
83 "referencedFieldsByType": {
84 "Product": {
85 "fieldNames": [
86 "price",
87 "name",
88 "reviews"
89 ],
90 "isInterface": false
91 },
92 "Query": {
93 "fieldNames": [
94 "topProducts"
95 ],
96 "isInterface": false
97 },
98 "Review": {
99 "fieldNames": [
100 "body",
101 "id"
102 ],
103 "isInterface": false
104 }
105 }
106 },
107 "apollo_telemetry::client_version": "",
108 "apollo_telemetry::subgraph_metrics_attributes": {},
109 "apollo_telemetry::client_name": ""
110 }
111 },
112 "serviceName": "reviews",
113 "statusCode": 200
114}
115
Property reference
Property / Type | Description |
---|---|
Control properties | |
| Indicates whether the router should continue processing the current client request. In coprocessor request bodies from the router, this value is always the string value continue .In your coprocessor's response, you can instead return an object with the following format:JSON
body you specify.For details, see Terminating a client request. |
| A unique ID corresponding to the client request associated with this coprocessor request.Do not return a different value for this property. If you do, the router treats the coprocessor request as if it failed. |
| Indicates which stage of the router's request-handling lifecycle this coprocessor request corresponds to.This value is one of the following:
|
| Indicates which version of the coprocessor request protocol the router is using.Currently, this value is always 1 .Do not return a different value for this property. If you do, the router treats the coprocessor request as if it failed. |
Data properties | |
| The body of the corresponding request or response.This field is populated when the underlying HTTP method is POST . If you are looking for operation data on GET requests, that info will be populated in the path parameter per the GraphQL over HTTP spec.If your coprocessor returns a different value for body , the router replaces the existing body with that value. This is common when terminating a client request.This field's type depends on the coprocessor request's stage :
RouterResponse stage returns redacted errors within the errors field. To process subgraph errors manually in your coprocessor, enable subgraph error inclusion. |
| An object representing the router's shared context for the corresponding client request.If your coprocessor returns a different value for context , the router replaces the existing context with that value. |
| When stage is SupergraphResponse , if present and true then there will be subsequent SupergraphResponse calls to the co-processor for each multi-part (@defer /subscriptions) response. |
| An object mapping of all HTTP header names and values for the corresponding request or response.Ensure headers are handled like HTTP headers in general. For example, normalize header case before your coprocessor operates on them.If your coprocessor returns a different value for headers , the router replaces the existing headers with that value.The router discards any |
| The HTTP method that is used by the request. |
| The RouterService or SupergraphService path that this coprocessor request pertains to. |
| A string representation of the router's current supergraph schema.This value can be very large, so you should avoid including it in coprocessor requests if possible.The router ignores modifications to this value. |
| The name of the subgraph that this coprocessor request pertains to.This value is present only for coprocessor requests from the router's SubgraphService .Do not return a different value for this property. If you do, the router treats the coprocessor request as if it failed. |
| The HTTP status code returned with a response. |
| When stage is SubgraphRequest , this is the full URI of the subgraph the router will query. |
| When stage is ExecutionRequest , this contains the query plan for the client query. It cannot be modified by the coprocessor. |
Responding to coprocessor requests
The router expects your coprocessor to respond with a 200
status code and a JSON body that matches the structure of the request body.
In the response body, your coprocessor can return modified values for certain properties. By doing so, you can modify the remainder of the router's execution for the client request.
The router supports modifying the following properties from your coprocessor:
Modify this property to immediately terminate a client request.
If you omit a property from your response body entirely, the router uses its existing value for that property.
Terminating a client request
Every coprocessor request body includes a control
property with the string value continue
. If your coprocessor's response body also sets control
to continue
, the router continues processing the client request as usual.
Alternatively, your coprocessor's response body can set control
to an object with a break
property, like so:
1{
2 "control": { "break": 401 },
3 "body": {
4 "errors": [
5 {
6 "message": "Not authenticated.",
7 "extensions": {
8 "code": "ERR_UNAUTHENTICATED"
9 }
10 }
11 ]
12 }
13}
If the router receives an object with this format for control
, it immediately terminates the request-handling lifecycle for the client request. It sends an HTTP response to the client with the following details:
The HTTP status code is set to the value of the
break
property (401
in the example above).The response body is the coprocessor's returned value for
body
.The value of
body
should adhere to the standard GraphQL JSON response format (see the example above).Alternatively, you can specify a string value for
body
. If you do, the router returns an error response with that string as the error'smessage
.
The example response above sets the HTTP status code to 400
, which indicates a failed request.
You can also use this mechanism to immediately return a successful response:
1{
2 "control": { "break": 200 },
3 "body": {
4 "data": {
5 "currentUser": {
6 "name": "Ada Lovelace"
7 }
8 }
9 }
10}
data
property matches the structure expected by the client query.body
in the RouterRequest
and RouterResponse
stages is always a string, but you can still break
with a GraphQL response if it's encoded as JSON.Examples of coprocessor responses for Router stages
1{
2 "control": { "break": 500 },
3 "body": "{ \"errors\": [ { \"message\": \"Something went wrong\", \"extensions\": { \"code\": \"INTERNAL_SERVER_ERRROR\" } } ] }"
4}
1{
2 "control": { "break": 200 },
3 "body": "{ \"data\": { \"currentUser\": { \"name\": \"Ada Lovelace\" } }"
4}
data
property matches the structure expected by the client query.Failed responses
If a request to a coprocessor results in a failed response, which is seperate from a control break, the router will return an error to the client making the supergraph request. The router considers all of the following scenarios to be a failed response from your coprocessor:
Your coprocessor doesn't respond within the amount of time specified by the
timeout
key in your configuration (default one second).Your coprocessor responds with a non-
2xx
HTTP code.Your coprocessor's response body doesn't match the JSON structure of the corresponding request body.
Your coprocessor's response body sets different values for control properties that must not change, such as
stage
andversion
.
Handling deferred query responses
GraphOS Router and Apollo Router Core support the incremental delivery of query response data via the @defer
directive:
For a single query with deferred fields, your router sends multiple "chunks" of response data to the client. If you enable coprocessor requests for the RouterResponse
stage, your router sends a separate coprocessor request for each chunk it returns as part of a deferred query.
Note the following about handling deferred response chunks:
The
status_code
andheaders
fields are included only in the coprocessor request for any response's first chunk. These values can't change after the first chunk is returned to the client, so they're subsequently omitted.If your coprocessor modifes the response
body
for a response chunk, it must provide the new value as a string, not as an object. This is because response chunk bodies include multipart boundary information in addition to the actual serialized JSON response data. See examples.Many responses will not contain deferred streams and for these the body string can usually be fairly reliably transformed into a JSON object for easy manipulation within the coprocessor. Coprocessors should be carefully coded to allow for the presence of a body that is not a valid JSON object.
Because the data is a JSON string at both
RouterRequest
andRouterResponse
, it's entirely possible for a coprocessor to rewrite the body from invalid JSON content into valid JSON content. This is one of the primary use cases forRouterRequest
body processing.
Examples of deferred response chunks
The examples below illustrate the differences between the first chunk of a deferred response and all subsequent chunks:
First response chunk
The first response chunk includes headers
and statusCode
fields:
1{
2 "version": 1,
3 "stage": "RouterResponse",
4 "id": "8dee7fe947273640a5c2c7e1da90208c",
5 "sdl": "...", // String omitted due to length
6 "headers": {
7 "content-type": [
8 "multipart/mixed;boundary=\"graphql\";deferSpec=20220824"
9 ],
10 "vary": [
11 "origin"
12 ]
13 },
14 "body": "\r\n--graphql\r\ncontent-type: application/json\r\n\r\n{\"data\":{\"me\":{\"id\":\"1\"}},\"hasNext\":true}\r\n--graphql\r\n",
15 "context": {
16 "entries": {
17 "operation_kind": "query",
18 "apollo_telemetry::client_version": "",
19 "apollo_telemetry::client_name": "manual"
20 }
21 },
22 "statusCode": 200
23}
Subsequent response chunk
Subsequent response chunks omit the headers
and statusCode
fields:
1{
2 "version": 1,
3 "stage": "RouterResponse",
4 "id": "8dee7fe947273640a5c2c7e1da90208c",
5 "sdl": "...", // String omitted due to length
6 "body": "content-type: application/json\r\n\r\n{\"hasNext\":false,\"incremental\":[{\"data\":{\"name\":\"Ada Lovelace\"},\"path\":[\"me\"]}]}\r\n--graphql--\r\n",
7 "context": {
8 "entries": {
9 "operation_kind": "query",
10 "apollo_telemetry::client_version": "",
11 "apollo_telemetry::client_name": "manual"
12 }
13 }
14}
Adding authorization claims via coprocessor
To use the authorization directives, a request needs to include claims—the details of its authentication and scope. The most straightforward way to add claims is with JWT authentication. You can also add claims with a RouterService
or SupergraphService
coprocessor since they hook into the request lifecycle before the router applies authorization logic.
An example configuration of the router calling a coprocessor for authorization claims:
1coprocessor:
2 url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint.
3 router: # By including this key, a coprocessor can hook into the `RouterService`. You can also use `SupergraphService` for authorization.
4 request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request.
5 headers: false # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
6 context: true # The authorization directives works with claims stored in the request's context
This configuration prompts the router to send an HTTP POST request to your coprocessor whenever it receives a client request. For example, your coprocessor may receive a request with this format:
1{
2 "version": 1,
3 "stage": "RouterRequest",
4 "control": "continue",
5 "id": "d0a8245df0efe8aa38a80dba1147fb2e",
6 "context": {
7 "entries": {
8 "accepts-json": true
9 }
10 }
11}
When your coprocessor receives this request from the router, it should add claims to the request's context
and return them in the response to the router. Specifically, the coprocessor should add an entry with a claims object. The key must be apollo_authentication::JWT::claims
, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use @requireScopes
, the response may look something like this:
1{
2 "version": 1,
3 "stage": "RouterRequest",
4 "control": "continue",
5 "id": "d0a8245df0efe8aa38a80dba1147fb2e",
6 "context": {
7 "entries": {
8 "accepts-json": true,
9 "apollo_authentication::JWT::claims": {
10 "scope": "profile:read profile:write"
11 }
12 }
13 }
14}