Rhai Scripts to customize routers
Add custom functionality directly to your router
You can customize your GraphOS Router or Apollo Router Core's behavior with scripts that use the Rhai scripting language. In a Rust-based project, Rhai is ideal for performing common scripting tasks such as manipulating strings and processing headers. Your Rhai scripts can also hook into multiple stages of the router's request handling lifecycle.
Rhai language reference
To learn about Rhai, see the Rhai language reference and some example scripts.
Use cases
Common use cases for Rhai scripts in routers include:
- Modifying the details of HTTP requests and responses. This includes requests sent from clients to your router, along with requests sent from your router to your subgraphs. You can modify any combination of the following:
- Request and response bodies
- Headers
- Status codes
- Request context
- Logging various information throughout the request lifecycle
- Performing
checkpoint
-style short circuiting of requests
Setup
To enable Rhai scripts in your router, you add the following keys to the router's YAML config file:
# This is a top-level key. It MUST define at least one of the two# sub-keys shown, even if you use that subkey's default value.rhai:# Specify a different Rhai script directory path with this key.# The path can be relative or absolute.scripts: "/rhai/scripts/directory"# Specify a different name for your "main" Rhai file with this key.# The router looks for this filename in your Rhai script directory.main: "test.rhai"
- Add the
rhai
top-level key to your router's YAML config file.- This key must contain at least one of a
scripts
key or amain
key (see the example above).
- This key must contain at least one of a
- Place all of your Rhai script files in a specific directory.
- By default, the router looks in the
./rhai
directory (relative to the directory therouter
command is executed from). - You can override this default with the
scripts
key (see above).
- By default, the router looks in the
- Define a "main" Rhai file in your router project.
- This file defines all of the "entry point" hooks that the router uses to call into your script.
- By default, the router looks for
main.rhai
in your Rhai script directory. - You can override this default with the
main
key (see above).
The main file
Your Rhai script's main file defines whichever combination of request lifecycle hooks you want to use. Here's a skeleton main.rhai
file that includes all available hooks and also registers all available callbacks:
You can provide exactly one main Rhai file to the router. This means that all of your customization's functionality must originate from these hook definitions.
To organize unrelated functionality within your Rhai customization, your main file can import and use symbols from any number of other Rhai files (known as modules) in your script directory:
// Module filefn process_request(request) {print("Supergraph service: Client request received");}
// Main fileimport "my_module" as my_mod;fn process_request(request) {my_mod::process_request(request)}fn supergraph_service(service) {// Rhai convention for creating a function pointerconst request_callback = Fn("process_request");service.map_request(request_callback);}
Router request lifecycle
Before you build your Rhai script, it helps to understand how the router handles each incoming GraphQL request. During each request's execution, different services in the router communicate with each other as shown:
As execution proceeds "left to right" from the RouterService
to individual SubgraphService
s, each service passes the client's original request along to the next service. Similarly, as execution continues "right to left" from SubgraphService
s to the RouterService
, each service passes the generated response for the client.
Your Rhai scripts can hook into any combination of the above services (if you are using the Apollo Router Core v1.30.0 and later). The scripts can modify the request, response, and/or related metadata as they're passed along.
Service descriptions
Each service of the router has a corresponding function that a Rhai script can define to hook into that service:
Service / Function | Description |
---|---|
| Runs at the very beginning and very end of the HTTP request lifecycle. For example, JWT authentication is performed within the Define |
| Runs at the very beginning and very end of the GraphQL request lifecycle. Define |
| Handles initiating the execution of a query plan after it's been generated. Define |
| Handles communication between the router and your subgraphs. Define Whereas other services are called once per client request, this service is called once per subgraph request that's required to resolve the client's request. Each call is passed a |
Each service uses data structures for request and response data that contain:
- A context object that was created at the start of the request and is propagated throughout the entire request lifecycle. It holds:
- The original request from the client
- A bag of data that can be populated by plugins for communication across the request lifecycle
- Any other specific data to that service (e.g., query plans and downstream requests/responses)
Service callbacks
Each hook in your Rhai script's main file is passed a service
object, which provides two methods: map_request
and map_response
. Most of the time in a hook, you use one or both of these methods to register callback functions that are called during the lifecycle of a GraphQL operation.
map_request
callbacks are called in each service as execution proceeds "to the right" from the router receiving a client request:These callbacks are each passed the current state of the client's
request
(which might have been modified by an earlier callback in the chain). Each callback can modify thisrequest
object directly.Additionally, callbacks for
subgraph_service
can access and modify the sub-operation request that the router will send to the corresponding subgraph viarequest.subgraph
.For reference, see the fields of
request
.map_response
callbacks are called in each service as execution proceeds back "to the left" from subgraphs resolving their individual sub-operations:First, callbacks for
subgraph_service
are each passed theresponse
from the corresponding subgraph.Afterward, callbacks for
execution_service
,supergraph_service
and thenrouter_service
are passed the combinedresponse
for the client that's assembled from all subgraphresponse
s.
Example scripts
In addition to the examples below, see more examples in the router repo's examples directory. Rhai-specific examples are listed in README.md
.
Handling incoming requests
This example illustrates how to register router request handling.
// At the supergraph_service stage, register callbacks for processing requestsfn supergraph_service(service) {const request_callback = Fn("process_request"); // This is standard Rhai functionality for creating a function pointerservice.map_request(request_callback); // Register the callback}// Generate a log for each requestfn process_request(request) {log_info("this is info level log message");}
Manipulating headers and the request context
This example manipulates headers and the request context:
// At the supergraph_service stage, register callbacks for processing requests and// responses.fn supergraph_service(service) {const request_callback = Fn("process_request"); // This is standard Rhai functionality for creating a function pointerservice.map_request(request_callback); // Register the request callbackconst response_callback = Fn("process_response"); // This is standard Rhai functionality for creating a function pointerservice.map_response(response_callback); // Register the response callback}// Ensure the header is present in the request// If an error is thrown, then the request is short-circuited to an error responsefn process_request(request) {log_info("processing request"); // This will appear in the router log as an INFO log// Verify that x-custom-header is present and has the expected valueif request.headers["x-custom-header"] != "CUSTOM_VALUE" {log_error("Error: you did not provide the right custom header"); // This will appear in the router log as an ERROR logthrow "Error: you did not provide the right custom header"; // This will appear in the errors response and short-circuit the request}// Put the header into the context and check the context in the responserequest.context["x-custom-header"] = request.headers["x-custom-header"];}// Ensure the header is present in the response context// If an error is thrown, then the response is short-circuited to an error responsefn process_response(response) {log_info("processing response"); // This will appear in the router log as an INFO log// Verify that x-custom-header is present and has the expected valueif response.context["x-custom-header"] != "CUSTOM_VALUE" {log_error("Error: we lost our custom header from our context"); // This will appear in the router log as an ERROR logthrow "Error: we lost our custom header from our context"; // This will appear in the errors response and short-circuit the response}}
⚠️ CAUTION
Accessing a non-existent header throws an exception.
To safely check for the existence of a header before reading it, use request.headers.contains("header-name")
.
When using contains
within subgraph_service
, you must assign your headers to a temporary local variable. Otherwise, contains
throws an exception that it "cannot mutate" the original request.
For example, the following subgraph_service
function checks whether the x-custom-header
exists before processing it.
// Ensure existence of header before processingfn subgraph_service(service, subgraph){service.map_request(|request|{// Reassign to local variable, as contains cannot modify requestlet headers = request.headers;if headers.contains("x-custom-header") {// Process existing header}});}
Converting cookies to headers
This example converts cookies into headers for transmission to subgraphs. There is a complete working example (with tests) of this in the examples/cookies-to-headers directory.
// Call map_request with our service and pass in a string with the name// of the function to callbackfn subgraph_service(service, subgraph) {// Choose how to treat each subgraph using the "subgraph" parameter.// In this case we are doing the same thing for all subgraphs// and logging out details for each.print(`registering request callback for: ${subgraph}`); // print() is the same as using log_info()const request_callback = Fn("process_request");service.map_request(request_callback);}// This will convert all cookie pairs into headers.// If you only wish to convert certain cookies, you// can add logic to modify the processing.fn process_request(request) {print("adding cookies as headers");// Find our cookieslet cookies = request.headers["cookie"].split(';');for cookie in cookies {// Split our cookies into name and valuelet k_v = cookie.split('=', 2);if k_v.len() == 2 {// trim off any whitespacek_v[0].trim();k_v[1].trim();// update our headers// Note: we must update subgraph.headers, since we are// setting a header in our subgraph requestrequest.subgraph.headers[k_v[0]] = k_v[1];}}}
Hot reloading
The router "watches" your rhai.scripts
directory (along with all subdirectories), and it initiates an interpreter reload whenever it detects one of the following changes:
- Creation of a new file with a
.rhai
suffix - Modification or deletion of an existing file with a
.rhai
suffix
The router attempts to identify any errors in your scripts before applying changes. If errors are detected, the router logs them and continues using its existing set of scripts.
ⓘ NOTE
Whenever you make changes to your scripts, check your router's log output to make sure they were applied.
Limitations
Currently, Rhai scripts cannot do the following:
- Use Rust crates
- Execute network requests
- Read or write to disk
If your router customization needs to do any of these, you can instead use external co-processing (this is an Enterprise feature).
Global variables
The router's Rhai
interface can simulate closures: https://rhai.rs/book/language/fn-closure.html
However, and this is an important restriction:
" The anonymous function syntax, however, automatically captures variables that are not defined within the current scope, but are defined in the external scope – i.e. the scope where the anonymous function is created. "
Thus it's not possible for a Rhai
closure to reference a global variable. For example: this kind of thing might be attempted:
fn supergraph_service(service){let f = |request| {let v = Router.APOLLO_SDL;print(v);};service.map_request(f);}
Note: Router
is a global variable.
That won't work and you'll get an error something like: service callback failed: Variable not found: Router (line 4, position 17)
There are two workarounds. Either:
- Create a local copy of the global that can be captured by the closure:
fn supergraph_service(service){let v = Router.APOLLO_SDL;let f = |request| {print(v);};service.map_request(f);}
Or:
- Use a function pointer rather than closure syntax:
fn supergraph_service(service) {const request_callback = Fn("process_request");service.map_request(request_callback);}fn process_request(request) {print(`${Router.APOLLO_SDL}`);}
Avoiding deadlocks
The router requires its Rhai engine to implement the sync feature to guarantee data integrity within the router's multi-threading execution environment. This means that shared values within Rhai might cause a deadlock.
This is particularly risky when using closures within callbacks while referencing external data. Take particular care to avoid this kind of situation by making copies of data when required. The examples/surrogate-cache-key directory contains an example of this, where "closing over" response.headers
would cause a deadlock. To avoid this, a local copy of the required data is obtained and used in the closure.
Services
Callbacks of router_service
cannot access the body of a request or response. At the router service stage, a request or response body is an opaque sequence of bytes.
Debugging
Understanding errors
If there is a syntax error in a Rhai script, the router will log an error message at startup mentioning the apollo.rhai
plugin. The line number in the error message describes where the error was detected, not where the error is present. For example, if you're missing a semicolon at the end of line 10, the error will mention line 11 (once Rhai realizes the mistake).
Syntax highlighting
Syntax highlighting can make it easier to spot errors in a script. We recommend using the online Rhai playground or using VS Code with the Rhai extension.
Logging
For tracking down runtime errors, insert logging statements to narrow down the issue.