Docs
Launch GraphOS Studio

External coprocessing in the Apollo Router

Customize your router's behavior in any language


This feature is only available with a GraphOS Dedicated or Enterprise plan.
To compare GraphOS feature support across all plan types, see the

.

With external coprocessing, you can hook into the '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 over HTTP.

You can configure your 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, string, and other details. This logic can access disk and perform network requests, all while safely isolated from the critical process.

When your coprocessor responds to these requests, its response body can modify

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 (minimal request latency)
  • In the same Pod as your , as a "sidecar" container (minimal request latency)
  • In the same availability zone as your (low request latency with increased deployment isolation)

How it works

Whenever your receives a client request, at various stages in the

it can send HTTP POST requests to your coprocessor:

1. Sends request
2. Can send request
details to coprocessor
and receive modifications
3
4. Can send request
details to coprocessor
and receive modifications
5
6. Can send request
details to coprocessor
and receive modifications
7
8. Can send request
details to coprocessor
and receive modifications
9
RouterService
SupergraphService
ExecutionService
SubgraphService(s)
Client
Coprocessor
Subgraphs

This diagram shows request execution proceeding "down" from a client, through the , to individual . 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

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 s
    • Here, the coprocessor can inspect and modify details of requests before are processed.
    • The coprocessor can also instruct the to
      terminate a client request
      immediately.
  • As execution proceeds back "up" from to the client
    • Here, the coprocessor can inspect and modify details of the 's response to the client.

At every stage, the 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 's SubgraphService, the sends a separate coprocessor request for each subgraph request in its query plan. In other words, if your needs to three separate to fully resolve a client , it sends three separate coprocessor requests. Each coprocessor request includes the

and
URL
of the being queried.

Setup

First, make sure your is

.

You configure external coprocessing in your 's

, 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 ):

router.yaml
coprocessor:
url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint.
timeout: 2s # The timeout for all coprocessor requests. Defaults to 1 second (1s)
router: # This coprocessor hooks into the `RouterService`
request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request.
headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
body: false
context: false
sdl: false
path: false
method: false
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).
headers: true
body: false
context: false
sdl: false
status_code: false
supergraph: # This coprocessor hooks into the `SupergraphService`
request: # By including this key, the `SupergraphService` sends a coprocessor request whenever it first receives a client request.
headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
body: false
context: false
sdl: false
method: false
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).
headers: true
body: false
context: false
sdl: false
status_code: false
subgraph:
all:
request: # By including this key, the `SubgraphService` sends a coprocessor request whenever it is about to make a request to a subgraph.
headers: true # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
body: false
context: false
uri: false
method: false
service_name: false
response: # By including this key, the `SubgraphService` sends a coprocessor request whenever receives a subgraph response.
headers: true
body: false
context: false
service_name: false
status_code: false

Minimal configuration

You can confirm that your can reach your coprocessor by setting this minimal configuration before expanding it as needed:

router.yaml
coprocessor:
url: http://127.0.0.1:8081 # Replace with the URL of your coprocessor's HTTP endpoint.
router:
request:
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

).

Coprocessor request format

The 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 response.

NOTE

Body properties vary by the router's current execution stage.

Properties of the JSON body are divided into two high-level categories:

  • "Control" properties
    • These provide information about the context of the specific request or response. They provide a mechanism to influence the router's execution flow.
    • The always includes these properties in coprocessor requests.
  • Data properties
    • These provide information about the substance of a request or response, such as the string and any HTTP headers. Aside from sdl, your coprocessor can modify all of these properties.
    • You
      configure which of these fields
      the includes in its coprocessor requests. By default, the router includes none of them.

Example requests by stage

RouterRequest

RouterResponse

SupergraphRequest

SupergraphResponse

ExecutionRequest

ExecutionResponse

SubgraphRequest

SubgraphResponse

Property reference

Property / TypeDescription

Control properties

control

string | object

Indicates whether the 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:

{ "break": 400 }

If you do this, the terminates the request-handling lifecycle and immediately responds to the client with the provided HTTP code and response

you specify.

For details, see

.

id

string

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 treats the coprocessor request as if it failed.

stage

string

Indicates which stage of the 's

this coprocessor request corresponds to.

This value is one of the following:

  • RouterRequest: The RouterService has just received a client request.
  • RouterResponse: The RouterService is about to send response data to a client.
  • SupergraphRequest: The SupergraphService is about to send a request.
  • SupergraphResponse: The SupergraphService has just received a response.
  • SubgraphRequest: The SubgraphService is about to send a request to a .
  • SubgraphResponse: The SubgraphService has just received a response.

Do not return a different value for this property. If you do, the treats the coprocessor request as if it failed.

version

number

Indicates which version of the coprocessor request protocol the is using.

Currently, this value is always 1.

Do not return a different value for this property. If you do, the treats the coprocessor request as if it failed.

Data properties

body

string | object

The body of the corresponding request or response.

This is populated when the underlying HTTP method is POST. If you are looking for data on GET requests, that info will be populated in the path parameter per the

.

If your coprocessor

for body, the replaces the existing body with that value. This is common when
terminating a client request
.

This 's type depends on the coprocessor request's

:

  • For SubgraphService stages, body is a JSON object.
  • For SupergraphService stages, body is a JSON object.
  • For RouterService stages, body is a JSON string.
    • This is necessary to support handling
      deferred queries
      .
    • If you modify body during the RouterRequest stage, the new value must be a valid string serialization of a JSON object. If it isn't, the detects that the body is malformed and returns an error to the client.

This 's structure depends on whether the coprocessor request corresponds to a request, a standard response, or a response "chunk" for a :

  • If a request, body usually contains a query property containing the string.
  • If a standard response, body usually contains data and/or errors properties for the result.
  • If a response "chunk", body contains data for some of the .

By default, the RouterResponse stage returns redacted errors within the errors . To process errors manually in your coprocessor, enable

.

context

object

An object representing the 's shared context for the corresponding client request.

If your coprocessor

for context, the replaces the existing context with that value.

hasNext

bool

When stage is SupergraphResponse, if present and true then there will be subsequent SupergraphResponse calls to the co-processor for each multi-part (@defer/) response.

headers

object

An object mapping of all HTTP header names and values for the corresponding request or response.

If your coprocessor

for headers, the router replaces the existing headers with that value. ⚠️ content-length sent by Coprocessors are discarded by the . (Wrong content-length values could lead to failure for HTTP requests to be sent)

method

string

The HTTP method that is used by the request.

path

string

The RouterService or SupergraphService path that this coprocessor request pertains to.

sdl

string

A string representation of the 's current .

This value can be very large, so you should avoid including it in coprocessor requests if possible.

The ignores modifications to this value.

serviceName

string

The name of the that this coprocessor request pertains to.

This value is present only for coprocessor requests from the 's SubgraphService.

Do not return a different value for this property. If you do, the treats the coprocessor request as if it failed.

statusCode

number

The HTTP status code returned with a response.

uri

string

When stage is SubgraphRequest, this is the full URI of the the will .

query_plan

string

When stage is ExecutionRequest, this contains the for the client query. It cannot be modified by the coprocessor.

Responding to coprocessor requests

The expects your coprocessor to respond with a 200 status code and a JSON body that matches the structure of the

.

In the response body, your coprocessor can return modified values for certain properties. By doing so, you can modify the remainder of the 's execution for the client request.

The supports modifying the following properties from your coprocessor:

⚠️ CAUTION

Do not modify other

. Doing so can cause the client request to fail.

If you omit a property from your response body entirely, the 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 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:

{
"control": { "break": 401 },
"body": {
"errors": [
{
"message": "Not authenticated.",
"extensions": {
"code": "ERR_UNAUTHENTICATED"
}
}
]
}
}

If the 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 JSON response format (see the example above).
    • Alternatively, you can specify a string value for body. If you do, the returns an error response with that string as the error's message.

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:

{
"control": { "break": 200 },
"body": {
"data": {
"currentUser": {
"name": "Ada Lovelace"
}
}
}
}

NOTE

If you return a successful response, make sure the structure of the data property matches the structure expected by the client .

💡 TIP

The body in the RouterRequest and RouterResponse stages is always a string, but you can still break with a response if it's encoded as JSON.

NOTE

If you return a successful response, make sure the structure of the data property matches the structure expected by the client .

Failed responses

If a request to a coprocessor results in a failed response, which is seperate from a control break, the will return an error to the client making the 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 and version.

Handling deferred query responses

The supports the incremental delivery of response data via

:

RouterClientRouterClientResolves non-deferredfieldsResolves deferredfieldsSends a query thatdefers some fieldsReturns data fornon-deferred fieldsReturns data for deferred fields

For a single with deferred , your sends multiple "chunks" of response data to the client. If you enable coprocessor requests for the RouterResponse stage, your sends a separate coprocessor request for each chunk it returns as part of a .

Note the following about handling deferred response chunks:

  • The

    and
    headers
    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

    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 and RouterResponse, 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 for RouterRequest 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 :

{
"version": 1,
"stage": "RouterResponse",
"id": "8dee7fe947273640a5c2c7e1da90208c",
"sdl": "...", // String omitted due to length
"headers": {
"content-type": [
"multipart/mixed;boundary=\"graphql\";deferSpec=20220824"
],
"vary": [
"origin"
]
},
"body": "\r\n--graphql\r\ncontent-type: application/json\r\n\r\n{\"data\":{\"me\":{\"id\":\"1\"}},\"hasNext\":true}\r\n--graphql\r\n",
"context": {
"entries": {
"operation_kind": "query",
"apollo_telemetry::client_version": "",
"apollo_telemetry::client_name": "manual"
}
},
"statusCode": 200
}

Subsequent response chunk

Subsequent response chunks omit the headers and statusCode :

{
"version": 1,
"stage": "RouterResponse",
"id": "8dee7fe947273640a5c2c7e1da90208c",
"sdl": "...", // String omitted due to length
"body": "content-type: application/json\r\n\r\n{\"hasNext\":false,\"incremental\":[{\"data\":{\"name\":\"Ada Lovelace\"},\"path\":[\"me\"]}]}\r\n--graphql--\r\n",
"context": {
"entries": {
"operation_kind": "query",
"apollo_telemetry::client_version": "",
"apollo_telemetry::client_name": "manual"
}
}
}

Adding authorization claims via coprocessor

To use the

, 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 applies authorization logic.

An example configuration of the calling a coprocessor for authorization claims:

router.yaml
coprocessor:
url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint.
router: # By including this key, a coprocessor can hook into the `RouterService`. You can also use `SupergraphService` for authorization.
request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request.
headers: false # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default.
context: true # The authorization directives works with claims stored in the request's context

This configuration prompts the 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:

{
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"id": "d0a8245df0efe8aa38a80dba1147fb2e",
"context": {
"entries": {
"accepts-json": true
}
}
}

When your coprocessor receives this request from the , it should add claims to the request's

and return them in the response to the . 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 you intend to use. For example, if you want to use
@requireScopes
, the response may look something like this:

{
"version": 1,
"stage": "RouterRequest",
"control": "continue",
"id": "d0a8245df0efe8aa38a80dba1147fb2e",
"context": {
"entries": {
"accepts-json": true,
"apollo_authentication::JWT::claims": {
"scope": "profile:read profile:write"
}
}
}
}
Previous
Rhai API reference
Next
Native Rust plugins
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company