Writing a Coprocessor
This guide walks you through creating your first coprocessor service and connecting it to Hive Router.
At the end, you will have a working setup where Hive Router calls your external service during request processing, and your service can either continue the request or stop it early with a custom response.
Before you start
You need three things:
- a running Hive Router
- a valid
supergraph.graphql - a small HTTP service that accepts and returns JSON
If you do not have a supergraph yet, you can use a test supergraph:
curl -sSL https://federation-demo.theguild.workers.dev/supergraph.graphql > supergraph.graphqlBuild your first coprocessor
Create a local service
Hive Router sends a JSON payload with fields like version, stage, control, and optional
request data. Your service should return JSON with at least:
{
"version": 1,
"control": "continue"
}This tells the router to continue processing.
Return OK 200 with valid JSON. Invalid responses are treated as coprocessor
failures.
Let's start by creating a small service in any language. It must expose one HTTP endpoint, for example
POST /coprocessor.
For local testing, run it on http://127.0.0.1:8081/coprocessor.
package main
import (
"encoding/json"
"log"
"net/http"
)
type CoprocessorRequest struct {
Version int `json:"version"`
Stage string `json:"stage"`
Headers map[string]string `json:"headers,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
}
type CoprocessorResponse struct {
Version int `json:"version"`
Control interface{} `json:"control"`
Headers map[string]string `json:"headers,omitempty"`
Body interface{} `json:"body,omitempty"`
Context map[string]interface{} `json:"context,omitempty"`
}
func main() {
http.HandleFunc("/coprocessor", handleCoprocessor)
log.Println("Coprocessor running on http://127.0.0.1:8081/coprocessor")
log.Fatal(http.ListenAndServe("127.0.0.1:8081", nil))
}
func handleCoprocessor(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
var payload CoprocessorRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
writeJSON(w, CoprocessorResponse{
Version: 1,
Control: "continue",
})
return
}
log.Printf("Received coprocessor stage: %s", payload.Stage)
writeJSON(w, CoprocessorResponse{
Version: 1,
Control: "continue",
})
}
func writeJSON(w http.ResponseWriter, response CoprocessorResponse) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("failed to write response: %v", err)
}
}The example coprocessor handles the router.request stage. It checks whether the incoming request (inbound request to Hive Router instance) includes an Authorization header. If the header is present, it adds "auth.checked": true to the context. If not, it stops the request early with a 401 Unauthorized response.
Configure Hive Router to call your service
Right now, Hive Router is not configured to send anything to the coprocessor.
Next step is to add coprocessor to the config file:
supergraph:
source: file
path: ./supergraph.graphql
coprocessor:
url: http://127.0.0.1:8081/coprocessor
protocol: http1
timeout: 1s
stages:
router:
request:
include:
headers: trueThis enables one stage (router.request) and sends headers to your service.
To modify context, you don't have to set context: true in include. The
same applies to headers or body.
Run Router and validate the call path
Start Hive Router and send any GraphQL request.
If everything is connected, your coprocessor service should receive one JSON payload for each
request that reaches router.request.
Add an early auth gate with control.break
Now implement a simple policy in your service:
- if
authorizationheader exists, returncontinueand add"auth.checked": trueto the context. - if it is missing, return
break
Example break response:
{
"version": 1,
"control": { "break": 401 },
"headers": {
"content-type": "application/json"
},
"body": {
"errors": [{ "message": "Unauthorized" }]
}
}func handleCoprocessor(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
var payload CoprocessorRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
writeJSON(w, CoprocessorResponse{
Version: 1,
Control: "continue",
})
return
}
log.Printf("Received coprocessor stage: %s", payload.Stage)
// Short-circuit with 403 Unauthorized when "Authorization" header is not set.
if payload.Headers["authorization"] == "" {
writeJSON(w, CoprocessorResponse{
Version: 1,
Control: map[string]int{
"break": http.StatusUnauthorized,
},
Headers: map[string]string{
"content-type": "application/json",
},
Body: map[string]interface{}{
"errors": []map[string]string{
{"message": "Unauthorized"},
},
},
})
return
}
// Otherwise insert `"auth.checked": true` to context.
writeJSON(w, CoprocessorResponse{
Version: 1,
Control: "continue",
Context: map[string]interface{}{
"auth.checked": true,
},
})
}This stops the pipeline and returns your response to the client immediately.
Operate it safely in production
Because coprocessors are in the request path, treat them like any critical service:
- set a strict timeout
- monitor request rate, latency, and error rate per stage
- use traces and structured logs to debug failures quickly