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:
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:
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:
1print("logged at the info level");
For more control over a message's log level, you can use the following functions:
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:
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:
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:
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.
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.
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.
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.
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.
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.
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}
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 specifiedSTANDARD_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 4648URL_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:
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.
1fn supergraph_service(service){
2 service.map_request(|request|{
3 let sha = sha256::digest("hello world");
4 log_info(sha);
5 });
6}
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.
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.
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.
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.
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}
- 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.
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:
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
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:
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
1if response.status_code.to_string() == "200" {
2 print(`ok`);
3}
Also useful if you want to convert response status code to a number
1if parse_int(response.status_code.to_string()) == 200 {
2 print(`ok`);
3}
You can also create your own status code from an integer:
1if response.status_code == status_code_from_int(200) {
2 print(`ok`);
3}
response.status_code
object is available only for map_response
callbacks registered in router_service
or subgraph_service
.