Rhai Script API Reference

APIs for router customizations


This reference documents the symbols and behaviors that are specific to Rhai customizations for the GraphOS Router and Apollo Router Core.

Entry point hooks

Your Rhai script's main file hooks into the individual services of the router's request-handling lifecycle. To do so, it defines whichever combination of the following entry point hooks it requires:

Rhai
1fn router_service(service) {}
2fn supergraph_service(service) {}
3fn execution_service(service) {}
4fn subgraph_service(service, subgraph) {}

Within each hook, you define custom logic to interact with the current active request and/or response as needed. This most commonly involves using methods of the provided service object to register service callbacks, like so:

Rhai
main.rhai
1fn supergraph_service(service) {
2  let request_callback = |request| {
3      print("Supergraph service: Client request received");
4  };
5  
6  let response_callback = |response| {
7      print("Supergraph service: Client response ready to send");
8  };
9
10  service.map_request(request_callback);
11  service.map_response(response_callback);
12}

Logging

If your script logs a message with Rhai's built-in print() function, it's logged to the router's logs at the "info" level:

Rhai
1print("logged at the info level");

For more control over a message's log level, you can use the following functions:

Rhai
1log_error("error-level log message");
2log_warn("warn-level log message");
3log_info("info-level log message");
4log_debug("debug-level log message");
5log_trace("trace-level log message");

Terminating client requests

Your Rhai script can terminate the associated client request that triggered it. To do so, it must throw an exception from the supergraph service. This returns an Internal Server Error to the client with a 500 response code.

For example:

Rhai
1// Must throw exception from supergraph service
2fn supergraph_service(service) {
3    // Define a closure to process our response
4    let f = |response| {
5        // Something goes wrong during response processing...
6        throw "An error occurred setting up the supergraph_service...";
7    };
8    // Map our response using our closure
9    service.map_response(f);
10}

If you wish to have more control over the HTTP status code returned to the client, then you can throw an error which is an objectmap with keys: status and message.

The key must be a number and the message must be something which can be converted to a string.

For example:

Rhai
1// Must throw exception from supergraph service
2fn supergraph_service(service) {
3    // Define a closure to process our response
4    let f = |response| {
5        // Something goes wrong during response processing...
6        throw #{
7            status: 400,
8            message: "An error occurred processing the response..."
9        };
10    };
11    // Map our response using our closure
12    service.map_response(f);
13}

You can also throw a valid GraphQL response, which will be deserialized and dealt with by the router.

For example:

Rhai
1// Must throw exception from supergraph service
2fn supergraph_service(service) {
3    // Define a closure to process our request
4    let f = |request| {
5        // Something goes wrong during request processing...
6        throw #{
7            status: 403,
8            body: #{
9                errors: [#{
10                    message: `I have raised a 403`,
11                    extensions: #{
12                        code: "ACCESS_DENIED"
13                    }
14                }]
15            }
16        };
17    };
18    // Map our request using our closure
19    service.map_request(f);
20}

Rhai throws at the map_request layer behave the same as ControlFlow::Break, which is explained in the external extensibility section.

If the supplied status code is not a valid HTTP status code, then a 500 response code will result.

Timing execution

Your Rhai customization can use the global Router.APOLLO_START constant to calculate durations. This is similar to Epoch in Unix environments.

Rhai
1// Define a closure to process our response
2// Note: We can't use a closure in this example because we are referencing our global Router during function execution
3fn process_response(response) {
4    let start = Router.APOLLO_START.elapsed;
5    // Do some processing here...
6    let duration = Router.APOLLO_START.elapsed - start;
7    print(`response processing took: ${duration}`);
8
9    // Log out any errors we may have
10    print(response.body.errors);
11}
12
13fn supergraph_service(service) {
14    const response_callback = Fn("process_response");
15    service.map_response(response_callback);
16}

Accessing the SDL

Your Rhai customization can use the global Router.APOLLO_SDL constant to examine the supergraph.

Rhai
1fn supergraph_service(service) {
2    print(`${Router.APOLLO_SDL}`);
3}

Accessing a TraceId

Your Rhai customization can use the function traceid() to retrieve an opentelemetry span id. This will throw an exception if a span is not available, so always handle exceptions when using this function.

Rhai
1fn supergraph_service(service) {
2    try {
3        let id = traceid();
4        print(`id: ${id}`);
5    }
6    catch(err)
7    {
8        // log any errors
9        log_error(`span id error: ${err}`);
10    }
11}

url encode/decode strings

Your Rhai customization can use the functions urlencode() and urldecode() to encode/decode strings. encode() does not fail, but decode() can fail, so always handle exceptions when using the decode() function.

Rhai
1fn supergraph_service(service) {
2    let original = "alice and bob";
3    let encoded = urlencode(original);
4    // encoded will be "alice%20and%20bob"
5    try {
6        let and_back = urldecode(encoded);
7        // and_back will be "alice and bob"
8    }
9    catch(err)
10    {
11        // log any errors
12        log_error(`urldecode error: ${err}`);
13    }
14}

json encode/decode strings

Your Rhai customization can use the functions json::encode() and json::decode() to convert Rhai objects to/from valid JSON encoded strings. Both functions can fail, so always handle exceptions when using them.

Rhai
1fn router_service(service) {
2    let original = `{"valid":"object"}`;
3    try {
4        let encoded = json::decode(original);
5        // encoded is a Rhai object, with a property (or key) named valid with a String value of "object"
6        print(`encoded.valid: ${encoded.valid}`);
7        let and_back = json::encode(encoded);
8        // and_back will be a string == original.
9        if and_back != original {
10            throw "something has gone wrong";
11        }
12    }
13    catch(err)
14    {
15        // log any errors
16        log_error(`json coding error: ${err}`);
17    }
18}

base64 encode/decode strings

Your Rhai customization can use the functions base64::encode() and base64::decode() to encode/decode strings. encode() does not fail, but decode() can fail, so always handle exceptions when using the decode() function.

Rhai
1fn supergraph_service(service) {
2    let original = "alice and bob";
3    let encoded = base64::encode(original);
4    // encoded will be "YWxpY2UgYW5kIGJvYgo="
5    try {
6        let and_back = base64::decode(encoded);
7        // and_back will be "alice and bob"
8    }
9    catch(err)
10    {
11        // log any errors
12        log_error(`base64::decode error: ${err}`);
13    }
14}
 note
You don't need to import the "base64" module. It is imported in the router.

Different alphabets

Base64 supports multiple alphabets to encode data, depending on the supported characters where it is used. The router supports the following alphabets:

  • STANDARD: the "base64" encoding as defined in RFC 4648. This is the default when not specified

  • STANDARD_NO_PAD: the "base64" encoding as defined in RFC 4648 without the padding (= or == characters at the end)

  • URL_SAFE: the "base64url" encoding as defined in RFC 4648

  • URL_SAFE_NO_PAD: the "base64url" encoding as defined in RFC 4648 without the padding (= or == characters at the end)

To use them, we can add an argument to the encode and decode methods:

Rhai
1fn supergraph_service(service) {
2    let original = "alice and bob";
3    let encoded = base64::encode(original, base64::URL_SAFE);
4    // encoded will be "YWxpY2UgYW5kIGJvYgo="
5    try {
6        let and_back = base64::decode(encoded, base64::URL_SAFE);
7        // and_back will be "alice and bob"
8    }
9    catch(err)
10    {
11        // log any errors
12        log_error(`base64::decode error: ${err}`);
13    }
14}

sha256 hash strings

Your Rhai customization can use the function sha256::digest() to hash strings using the SHA256 hashing algorithm.

Rhai
1fn supergraph_service(service){
2    service.map_request(|request|{
3        let sha = sha256::digest("hello world");
4        log_info(sha);
5    });
6}
 note
You don't need to import the "sha256" module. It is imported in the router.

Headers with multiple values

The simple get/set api for dealing with single value headers is sufficient for most use cases. If you wish to set multiple values on a key then you should do this by supplying an array of values.

If you wish to get multiple values for a header key, then you must use the values() fn, NOT the indexed accessor. If you do use the indexed accessor, it will only return the first value (as a string) associated with the key.

Look at the examples to see how this works in practice.

Unix timestamp

Your Rhai customization can use the function unix_now() to obtain the current Unix timestamp in seconds since the Unix epoch.

Rhai
1fn supergraph_service(service) {
2    let now = unix_now();
3}

Unix timestamp (in milliseconds)

Your Rhai customization can use the function unix_ms_now() to obtain the current Unix timestamp in milliseconds since the Unix epoch.

Rhai
1fn supergraph_service(service) {
2    let now = unix_ms_now();
3}

Unique IDs (UUID)

Your Rhai customization can use the function uuid_v4() to obtain a UUIDv4 ID.

Rhai
1fn supergraph_service(service) {
2    let id = uuid_v4();
3}

Environment Variables

Your Rhai customization can access environment variables using the env module. Use the env::get() function.

Rhai
1fn router_service(service) {
2    try {
3        print(`HOME: ${env::get("HOME")}`);
4        print(`LANG: ${env::get("LANG")}`);
5    } catch(err) {
6        print(`exception: ${err}`);
7    }
8}
 note
  • You don't need to import the "env" module. It is imported in the router.
  • get() may fail, so it's best to handle exceptions when using it.

Available constants

The router provides constants for your Rhai scripts that mostly help you fetch data from the context.

Text
1Router.APOLLO_SDL // Context key to access the SDL
2Router.APOLLO_START // Constant to calculate durations
3Router.APOLLO_AUTHENTICATION_JWT_CLAIMS // Context key to access authentication jwt claims
4Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS // Context key to modify or access the custom connection params when using subscriptions in WebSocket to subgraphs (cf subscription docs)
5Router.APOLLO_ENTITY_CACHE_KEY // Context key to access the entity cache key
6Router.APOLLO_OPERATION_ID // Context key to get the value of apollo operation id (studio trace id) from the context
7Router.APOLLO_COST_ESTIMATED_KEY // Context key to get the estimated cost of an operation
8Router.APOLLO_COST_ACTUAL_KEY // Context key to get the actual cost of an operation
9Router.APOLLO_COST_STRATEGY_KEY // Context key to get the strategy used to calculate cost
10Router.APOLLO_COST_RESULT_KEY // Context key to get the cost result of an operation

Request interface

All callback functions registered via map_request are passed a request object that represents the request sent by the client. This object provides the following fields:

Text
1request.context
2request.id
3request.headers
4request.method
5request.body.query
6request.body.operation_name
7request.body.variables
8request.body.extensions
9request.uri.host
10request.uri.path
11request.uri.port
 note
These fields are typically modifiable, apart from method which is always read-only. However, when the callback service is subgraph_service, the only modifiable field is request.context.

For subgraph_service callbacks only, the request object provides additional modifiable fields for interacting with the request that will be sent to the corresponding subgraph:

Text
1request.subgraph.headers
2request.subgraph.body.query
3request.subgraph.body.operation_name
4request.subgraph.body.variables
5request.subgraph.body.extensions
6request.subgraph.uri.host
7request.subgraph.uri.path
8request.subgraph.uri.port

request.context

The context is a generic key/value store that exists for the entire lifespan of a particular client request. You can use this to share information between multiple callbacks throughout the request's lifespan.

Keys must be strings, but values can be any Rhai object.

Rhai
1// You can interact with request.context as an indexed variable
2request.context["contextual"] = 42; // Adds value 42 to the context with key "contextual"
3print(`${request.context["contextual"]}`); // Writes 42 to the router log at info level
4// Rhai also supports extended dot notation for indexed variables, so this is equivalent
5request.context.contextual = 42;

upsert()

The context provides an upsert() function for resolving situations where one of an update or an insert is required when setting the value for a particular key.

To use upsert(), you define a callback function that receives a key's existing value (if any) and makes changes as required before returning the final value to set.

Rhai
1// Get a reference to a cache-key
2let my_cache_key = response.headers["cache-key"];
3
4// Define an upsert resolver callback
5// The `current` parameter is the current value for the specified key.
6// This particular callback checks whether `current` is an ObjectMap 
7// (default is the unit value of ()). If not, assign an empty ObjectMap.
8// Finally, update the stored ObjectMap with our subgraph name as key
9// and the returned cache-key as a value.
10let resolver = |current| {
11  if current == () {
12      // No map found. Create an empty object map
13      current = #{};
14  }
15  // Update our object map with a key and value
16  current[subgraph] = my_cache_key;
17  return current;
18};
19
20// Upsert our context with our resolver
21response.context.upsert("surrogate-cache-key", resolver);

request.id

The id is a string which uniquely identifies a request/response context for its entire lifespan. If you have a request (or a response) you can access the ID as follows.

Rhai
1print(`request id is: ${request.id}`);

request.headers

The headers of a request are accessible as a read/write indexed variable. The keys and values must be valid header name and value strings.

Rhai
1// You can interact with request.headers as an indexed variable
2request.headers["x-my-new-header"] = 42.to_string(); // Inserts a new header "x-my-new-header" with value "42"
3print(`${request.headers["x-my-new-header"]}`); // Writes "42" into the router log at info level
4// Rhai also supports extended dot notation for indexed variables, so this is equivalent
5request.headers.x-my-new-header = 42.to_string();
6// You can also set an header value from an array. Useful with the "set-cookie" header,
7// Note: It's probably more useful to do this on response headers. Simply illustrating the syntax here.
8request.headers["set-cookie"] = [
9  "foo=bar; Domain=localhost; Path=/; Expires=Wed, 04 Jan 2023 17:25:27 GMT; HttpOnly; Secure; SameSite=None",
10  "foo2=bar2; Domain=localhost; Path=/; Expires=Wed, 04 Jan 2023 17:25:27 GMT; HttpOnly; Secure; SameSite=None",
11];
12// You can also get multiple header values for a header using the values() fn
13// Note: It's probably more useful to do this on response headers. Simply illustrating the syntax here.
14print(`${request.headers.values("set-cookie")}`);

request.method

This is the HTTP method of the client request.

Rhai
1print(`${request.method}`); // Log the HTTP method

request.body.query

This is the client-provided GraphQL operation string to execute.

To modify this value before query planning occurs, you must do so within a supergraph_service() request callback. If you modify it later, the query plan is generated using the original provided operation string.

The following example modifies an incoming query and transforms it into a completely invalid query:

Rhai
1print(`${request.body.query}`); // Log the query string before modification
2request.body.query="query invalid { _typnam }}"; // Update the query string (in this case to an invalid query)
3print(`${request.body.query}`); // Log the query string after modification

request.body.operation_name

This is the name of the GraphQL operation to execute, if a name is provided in the request. This value must be present if request.body.query contains more than one operation definition.

For an example of interacting with operation_name, see the examples/op-name-to-header directory.

Rhai
1print(`${request.body.operation_name}`); // Log the operation_name before modification
2request.body.operation_name +="-my-suffix"; // Append "-my-suffix" to the operation_name
3print(`${request.body.operation_name}`); // Log the operation_name after modification

request.body.variables

These are the values of any GraphQL variables provided for the operation. They are exposed to Rhai as an Object Map.

Rhai
1print(`${request.body.variables}`); // Log all GraphQL variables

request.body.extensions

Request extensions may be read or modified. They are exposed to Rhai as an Object Map.

Rhai
1print(`${request.body.extensions}`); // Log all extensions

request.uri.host

This is the host component of the request's URI, as a string.

Modifying this value for a client request has no effect, because the request has already reached the router. However, modifying request.subgraph.uri.host in a subgraph_service callback does modify the URI that the router uses to communicate with the corresponding subgraph.

Rhai
1print(`${request.uri.host}`); // Log the request host

request.uri.path

This is the path component of the request's URI, as a string.

Modifying this value for a client request has no effect, because the request has already reached the router. However, modifying request.subgraph.uri.path in a subgraph_service callback does modify the URI that the router uses to communicate with the corresponding subgraph.

Rhai
1print(`${request.uri.path}`); // log the request path
2request.uri.path += "/added-context"; // Add an extra element to the query path

request.uri.port

This is the port component of the request's URI, as an integer. If no port is explicitly defined in the URI, this value defaults to an empty value.

Modifying this value for a client request has no effect, because the request has already reached the router. However, modifying request.subgraph.uri.port in a subgraph_service callback does modify the URI that the router uses to communicate with the corresponding subgraph.

Rhai
1print(`${request.uri.port}`); // log the request port
2request.uri.port = 4040; // Changes the port to be 4040

request.subgraph.*

The request.subgraph object is available only for map_request callbacks registered in subgraph_service. This object has the exact same fields as request itself, but these fields apply to the HTTP request that the router will send to the corresponding subgraph.

Rhai
1// You can interact with request.subgraph.headers as an indexed variable
2request.subgraph.headers["x-my-new-header"] = 42.to_string(); // Inserts a new header "x-my-new-header" with value "42"
3print(`${request.subgraph.headers["x-my-new-header"]}`); // Writes "42" into the router log at info level
4// Rhai also supports extended dot notation for indexed variables, so this is equivalent
5request.subgraph.headers.x-my-new-header = 42.to_string();

Response interface

All callback functions registered via map_response are passed a response object that represents an HTTP response.

  • For callbacks in subgraph_service, this object represents the response sent to the router by the corresponding subgraph.

  • In all other services, this object represents the response that the router will send to the requesting client.

The response object includes the following fields:

Text
1response.context
2response.id
3response.status_code
4response.headers
5response.body.label
6response.body.data
7response.body.errors
8response.body.extensions

All of the above fields are read/write.

The following fields are identical in behavior to their request counterparts:

response.is_primary()

Be particularly careful when interacting with headers or status_code in a response context. For router_service(), supergraph_service() and execution_service(), response headers and status_code only exist for the first response in a deferred response stream. You can handle this by making use of the is_primary() function which will return true if a response is the first (or primary) response. If you do try to access the headers or status_code in a non-primary response, then you'll raise an exception which can be handled like any other rhai exception, but is not so convenient as using the is_primary() method.

Rhai
1    if response.is_primary() {
2        print(`all response headers: ${response.headers}`);
3    } else {
4        print(`don't try to access headers`);
5    }

Other fields are described below.

response.body.label

A response may contain a label and this may be read/written as a string.

Rhai
1print(`${response.body.label}`); // logs the response label

response.body.data

A response may contain data (some responses with errors do not contain data). Be careful when manipulating data (and errors) to make sure that response remain valid. data is exposed to Rhai as an Object Map.

There is a complete example of interacting with the response data in the examples/data-response-mutate directory.

Rhai
1print(`${response.body.data}`); // logs the response data

response.body.errors

A response may contain errors. Errors are represented in rhai as an array of Object Maps.

Each Error must contain at least:

  • a message (String)

  • a location (Array)

(The location can be an empty array.)

Optionally, an error may also contain extensions, which are represented as an Object Map.

There is a complete example of interacting with the response errors in the examples/error-response-mutate directory.

Rhai
1// Create an error with our message
2let error_to_add = #{
3    message: "this is an added error",
4    locations: [],
5    // Extensions are optional, adding some arbitrary extensions to illustrate syntax
6    extensions: #{
7        field_1: "field 1",
8        field_2: "field_2"
9    }
10};
11// Add this error to any existing errors
12response.body.errors += error_to_add;
13print(`${response.body.errors}`); // logs the response errors

response.status_code.to_string()

Convert response status code to a string.

Rhai
1if response.status_code.to_string() == "200" {
2    print(`ok`);
3}

Also useful if you want to convert response status code to a number

Rhai
1if parse_int(response.status_code.to_string()) == 200 {
2    print(`ok`);
3}

You can also create your own status code from an integer:

Rhai
1if response.status_code == status_code_from_int(200) {
2    print(`ok`);
3}
 note
The response.status_code object is available only for map_response callbacks registered in router_service or subgraph_service.
Feedback

Edit on GitHub

Forums