Docs
Try Apollo Studio

Rhai scripts for the Apollo Router

Extend your router without building a custom binary


You can customize the Apollo Router's behavior with scripts that use the Rhai scripting language. Rhai is ideal for performing common scripting tasks (manipulating strings, processing headers, etc.) in a Rust-based project.

To start familiarizing yourself with Rhai, see the language reference and some basic syntax examples.

Rhai scripts can hook into any combination of services in the Apollo Router's request-handling pipeline.

Use cases

Common use cases for Apollo Router Rhai scripts 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

Configuration

config.yaml
# This is a top-level key. It MUST define at least one of the two
# sub-keys shown, even if you don't modify its 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"

To use Rhai scripts with the Apollo Router, you must do the following:

  • 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 a main key (see the example above).
  • Place all of your Rhai script files in a specific directory.
    • By default, the Apollo Router looks in the ./rhai directory (relative to the directory the router command is executed from).
    • You can override this default with the scripts key (see above).
  • Define a "main" Rhai file in your router project.
    • This file defines all of the "entry point" hooks that the Apollo Router uses to call into your script.
    • By default, the Apollo 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 supported entry point 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 Apollo 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:

my_module.rhai
// Module file
fn process_request(request) {
print("Supergraph service: Client request received");
}
main.rhai
// Main file
import "my_module" as my_mod;
fn process_request(request) {
my_mod::process_request(request)
}
fn supergraph_service(service) {
// Rhai convention for creating a function pointer
const request_callback = Fn("process_request");
service.map_request(request_callback);
}

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:

    Apollo Router
    execution_service
    supergraph_service
    subgraph_service
    subgraph_service
    Client
    Subgraph A
    Subgraph B

    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 this request object directly.

    Additionally, callbacks for subgraph_service can access and modify the sub-operation request that the router will send to the corresponding subgraph via request.subgraph.

  • map_response callbacks are called in each service as execution proceeds back "to the left" from subgraphs resolving their individual sub-operations:

    Apollo Router
    supergraph_service
    execution_service
    subgraph_service
    subgraph_service
    Client
    Subgraph A
    Subgraph B

    First, callbacks for subgraph_service are each passed the response from the corresponding subgraph.

    Afterward, callbacks for execution_service and then supergraph_service are passed the combined response for the client that's assembled from all subgraph responses.

Examples

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 requests
fn supergraph_service(service) {
const request_callback = Fn("process_request"); // This is standard Rhai functionality for creating a function pointer
service.map_request(request_callback); // Register the callback
}
// Generate a log for each request
fn 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 pointer
service.map_request(request_callback); // Register the request callback
const response_callback = Fn("process_response"); // This is standard Rhai functionality for creating a function pointer
service.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 response
fn 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 value
if 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 log
throw "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 response
request.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 response
fn 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 value
if request.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 log
throw "Error: we lost our custom header from our context"; // This will appear in the errors response and short-circuit the request
}
}

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 callback
fn 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 cookies
let cookies = request.headers["cookie"].split(';');
for cookie in cookies {
// Split our cookies into name and value
let k_v = cookie.split('=', 2);
if k_v.len() == 2 {
// trim off any whitespace
k_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 request
request.subgraph.headers[k_v[0]] = k_v[1];
}
}
}

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 create a native Rust plugin.

Avoiding deadlocks

The Apollo Router requires that its Rhai engine implements 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 a good 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.

Edit on GitHub
Previous
Overview
Next
Rhai API reference