于10月8日至10日在纽约市加入我们,了解关于GraphQL联邦和API平台工程的最新技巧、趋势和新闻。加入我们参加2024年在纽约市的GraphQL峰会
文档
免费开始

Rhai 脚本以自定义路由器

直接将自定义功能添加到您的路由器


您可以自定义您的's 的行为与使用 Rhai 脚本语言. 在基于 Rust 的项目中,Rhai 是执行常见的脚本任务的理想选择,例如字符串操作和处理头信息。您的 Rhai 脚本还可以挂钩到 's 的 请求处理生命周期.

Rhai 语言参考

要了解 Rhai,请查看 Rhai 语言参考 以及一些 示例脚本.

用例

中 Rhai 脚本的一些常见用例包括:

  • 修改 HTTP 请求和响应的细节。这包括从客户端发送到您的 router 的请求,以及从您的 router 发送到您的 的请求。您可以修改以下内容的任何组合:
    • 请求和响应体
    • 头部
    • 状态码
    • 请求上下文
  • 在整个请求生命周期中记录各种信息
  • 执行checkpoint-风格的请求短路

设置

要启用您的 router 中的 Rhai 脚本,您需要将以下键添加到路由器的 YAML 配置文件:

config.yaml
# 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"
  1. rhai 最高级键添加到您的 routerYAML 配置文件 中。
    • 此键 必须 包含以下键之一:scriptsmain(请参阅上面的示例)。
  2. 将所有 Rhai 脚本文件放置在特定目录中。
    • 默认情况下,router 会查找相对路径为 ./rhai 的目录(router 命令执行的目录)。
    • 您可以使用 scripts 键覆盖此默认设置(见上面)。
  3. 在您的 router 项目中定义一个 "main" Rhai 文件
    • 此文件定义了所有路由器用于调用您的脚本的 "入口点" 钩子。
    • 默认情况下,router 在 Rhai 脚本目录中查找 main.rhai
    • 您可以使用 main 键覆盖此默认设置(见上面)。

主文件

您的 Rhai 脚本的主文件定义了您想要使用的请求生命周期的哪些组合钩子。以下是一个包含所有可用钩子的 main.rhai 文件骨架,并注册了所有可用的 回调

您可以向路由器提供 一个 主 Rhai 文件。这意味着所有自定义功能的来源都必须来自这些钩子定义。

为了在您的 Rhai 自定义中组织相关的功能,主文件可以从任何数量的其他 Rhai 文件(称为 模块)导入并使用符号,这些模块位于您的脚本目录中:

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);
}

Router 请求生命周期

在构建您的 Rhai 脚本之前,了解 router 如何处理每个传入的 请求有所帮助。在每个请求的执行过程中,服务 在路由器中相互通信,如图所示:

SubgraphService(s)ExecutionServiceSupergraphServiceRouterServiceSubgraphService(s)ExecutionServiceSupergraphServiceRouterServiceparClientSends requestConverts raw HTTP requestto GraphQL/JSON requestInitiates queryplan executionInitiatessub-operationInitiatessub-operationInitiatessub-operationAssembles andreturns responseReturns GraphQL/JSONresponseReturns HTTPresponseClient

执行过程从 RouterService 开始,向 "从左到右" 到每个单独的 SubgraphService,每个服务将客户端的原始请求传递到下一个服务。同样,当执行从 "从右到左" 从 SubgraphServiceRouterService 时,每个服务将向客户端传递生成的响应。

您可以将 Rhai 脚本挂接到上述任何组合的服务中(如果您使用的是 Apollo Router Core v1.30.0 及更高版本)。脚本可以在传递过程中修改请求、响应和/或相关的元数据。

服务描述

路由器中的每个服务都有一个 Rhai 脚本可以定义的对应功能来挂接到该服务:

服务 /
功能
描述
RouterService

router_service

运行在 HTTP 请求生命周期开始和结束时。

例如, JWT 认证RouterService 内执行。

如果您需要对 HTTP contextheaders 进行交互,请定义 router_service。它不支持访问 body 属性(请参见 这个 GitHub 问题 以了解详细信息)。

SupergraphService

supergraph_service

在 GraphQL 请求生命周期的开始和结束时运行。

如果您需要对 GraphQL 请求或 GraphQL 响应进行交互,请定义 supergraph_service。例如,您可以添加检查匿名查询。

ExecutionService

execution_service

处理生成后的查询计划执行。

如果您的定制包含用于执行(例如,如果根据策略决定要阻止特定 )的逻辑,请定义 execution_service

SubgraphService

subgraph_service

处理路由器与您的子图之间的通信。

定义 subgraph_service 来配置此通信(例如,以动态添加传递到子图的 HTTP 标头)。

与其他服务每次调用每个客户端请求不同,此服务每次需要解决客户端请求的 子图 请求时才调用。每次调用都会传递一个 subgraph 参数,指示相应的子图名称。

每个服务使用请求和响应数据的数据结构,该结构包含

  • 一个上下文对象,该对象在请求开始时创建,并在整个请求生命周期中传递。它包含
    • 来自客户端的原始请求
    • 一个数据包,该数据包可以通过插件填充,用于在整个请求生命周期中进行通信
  • 以及该服务特定的任何其他数据(例如,和下游请求数据/响应数据)

服务回调

你Rhai脚本的主文件中的主文件中的每个钩子都会传递一个service对象,该对象提供两个方法:map_requestmap_response。在钩子中,你通常使用一个或两个这些方法来注册在回调函数中调用的GraphQL操作生命周期中的

  • map_request回调在每个服务中以router接收到客户端请求为起点,“向右”执行时被调用:

    Router
    supergraph_service
    router_service
    execution_service
    subgraph_service
    subgraph_service
    Client
    Subgraph A
    Subgraph B

    这些回调各自接收到客户端当前request状态(可能已由链中的早期回调修改)。每个回调都可以直接修改这个request对象。

    此外,subgraph_service的回调可以访问和修改router将通过request.subgraph发送给相应子图的子-operation请求。

    参阅以下以获取参考:request

  • map_response回调在执行回“向左”从subgraphs解决它们各自的子-operation时在每个服务中被调用:

    Router
    execution_service
    supergraph_service
    router_service
    subgraph_service
    subgraph_service
    Client
    Subgraph A
    Subgraph B

    首先,为subgraph_service传递每个的对应子图response

    之后,将execution_servicesupergraph_service然后是router_service的回调传递给客户端的合并response,该响应是由所有subgraphresponse组装而成的。

示例脚本

除了下面的示例之外,请参阅router存储库的示例目录中的更多示例。Rhai特定的示例列在README.md中。

处理入站请求

本示例演示了如何注册router请求处理。

// 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");
}

操作头和请求上下文

此示例操作头和请求上下文

// 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 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 log
throw "Error: we lost our custom header from our context"; // This will appear in the errors response and short-circuit the response
}
}

⚠️ 注意

访问不存在的头会抛出异常

在读取之前安全地检查头是否存在,请使用request.headers.contains("header-name")

在使用 containssubgraph_service 中时,必须将您的头分配给一个临时的本地 。 否则,contains 会抛出一个异常,表明它“不能更改”原始请求。”

例如,以下 subgraph_service 函数在处理之前检查是否存在 x-custom-header

// Ensure existence of header before processing
fn subgraph_service(service, subgraph){
service.map_request(|request|{
// Reassign to local variable, as contains cannot modify request
let headers = request.headers;
if headers.contains("x-custom-header") {
// Process existing header
}
});
}

将cookie转换为头

此示例将cookie转换为可用于传输到子图的头。在 examples/cookies-to-headers 目录 中有一个完整的完整示例(带有测试)。

// 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];
}
}
}

热重新加载

router “监控”您的rhai.scripts 目录(以及所有子目录),并在它检测到以下任何更改之一时启动解释器重新加载:

  • 创建具有 .rhai 后缀的新文件
  • 修改或删除具有 .rhai 后缀的现有文件

在应用更改之前,router 会尝试识别您脚本中的任何错误。如果检测到错误,则 router 会记录它们并继续使用其现有 脚本集。

注意

每次您修改脚本时,请检查 router 的日志输出以确保它们已应用。

限制

目前,Rhai 脚本不能执行以下操作:

  • 使用Rust crates
  • 执行网络请求
  • 读取或写入磁盘

如果您的 router 定制需要执行任何这些操作,您可以使用外部协处理器(这是一个企业功能)。

全局变量

router 的 Rhai 接口可以模拟闭包:https://rhai.rs/book/language/fn-closure.html

然而,这是一个重要的限制

"匿名函数的语法,然而,会自动捕获匿名函数中未定义但在外部作用域中定义的变量,即该匿名函数被创建的作用域。"

因此,Rhai闭包无法引用全局变量的。例如:可能会尝试以下操作:

fn supergraph_service(service){
let f = |request| {
let v = Router.APOLLO_SDL;
print(v);
};
service.map_request(f);
}

注意: Router 是一个全局变量。

这将无法工作,您会得到类似以下错误: service callback failed: Variable not found: Router (line 4, position 17)

有两种解决方案。这两种方案是:

  1. 创建一个全局变量的本地副本,使其可以被闭包捕获
fn supergraph_service(service){
let v = Router.APOLLO_SDL;
let f = |request| {
print(v);
};
service.map_request(f);
}

  1. 使用函数指针而不是闭包语法
fn supergraph_service(service) {
const request_callback = Fn("process_request");
service.map_request(request_callback);
}
fn process_request(request) {
print(`${Router.APOLLO_SDL}`);
}

避免死锁

路由器需要其Rhai引擎实现同步功能来确保路由器在多线程执行环境中的数据完整性。这意味着Rhai中的共享值可能导致死锁。

这在回调中引用外部数据时使用闭包尤其危险。当需要时,请特别小心,通过对所需数据进行副本来避免这种情况。在examples/surrogate-cache-key目录中有一个这样的例子,'封闭' response.headers将导致死锁。为了避免这种情况,必须获得所需的本地数据的副本并在闭包中使用它。

服务

`router_service`的回调无法访问请求或响应的主体。在`router`服务阶段,请求或响应的主体是一个不可透明的字节序列。

调试

理解错误

如果在 Rhai 脚本中存在语法错误,router将在启动时记录一条错误消息,提到apollo.rhai插件。错误消息中的行号描述了错误被检测的位置,而不是错误存在的位置。例如,如果第10行缺少分号,错误消息将提到第11行(一旦 Rhai 发现错误)。

语法高亮

语法高亮可以使您更容易在脚本中查找错误。我们推荐使用在线Rhai 游戏场或使用带有Rhai 扩展的 VS Code。

日志记录

为了追踪运行时错误,插入日志语句以缩小问题范围。

上一页
概览
下一页
Rhai API 参考文档
评价文章评价在 GitHub 上编辑编辑论坛Discord

©2024Apollo Graph Inc.,以Apollo GraphQL的名义。

隐私政策

公司