Rhai 脚本以自定义路由器
直接将自定义功能添加到您的路由器
您可以自定义您的GraphOS 路由器 或 Apollo 路由器核心's 的行为与使用 Rhai 脚本语言. 在基于 Rust 的项目中,Rhai 是执行常见的脚本任务的理想选择,例如字符串操作和处理头信息。您的 Rhai 脚本还可以挂钩到 路由器's 的 请求处理生命周期.
Rhai 语言参考
要了解 Rhai,请查看 Rhai 语言参考 以及一些 示例脚本.
用例
在 路由器 中 Rhai 脚本的一些常见用例包括:
- 修改 HTTP 请求和响应的细节。这包括从客户端发送到您的 router 的请求,以及从您的 router 发送到您的 子图 的请求。您可以修改以下内容的任何组合:
- 请求和响应体
- 头部
- 状态码
- 请求上下文
- 在整个请求生命周期中记录各种信息
- 执行
checkpoint
-风格的请求短路
设置
要启用您的 router 中的 Rhai 脚本,您需要将以下键添加到路由器的 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"
- 将
rhai
最高级键添加到您的 router 的 YAML 配置文件 中。- 此键 必须 包含以下键之一:
scripts
或main
(请参阅上面的示例)。
- 此键 必须 包含以下键之一:
- 将所有 Rhai 脚本文件放置在特定目录中。
- 默认情况下,router 会查找相对路径为
./rhai
的目录(router
命令执行的目录)。 - 您可以使用
scripts
键覆盖此默认设置(见上面)。
- 默认情况下,router 会查找相对路径为
- 在您的 router 项目中定义一个 "main" Rhai 文件。
- 此文件定义了所有路由器用于调用您的脚本的 "入口点" 钩子。
- 默认情况下,router 在 Rhai 脚本目录中查找
main.rhai
。 - 您可以使用
main
键覆盖此默认设置(见上面)。
主文件
您的 Rhai 脚本的主文件定义了您想要使用的请求生命周期的哪些组合钩子。以下是一个包含所有可用钩子的 main.rhai
文件骨架,并注册了所有可用的 回调:
您可以向路由器提供 一个 主 Rhai 文件。这意味着所有自定义功能的来源都必须来自这些钩子定义。
为了在您的 Rhai 自定义中组织相关的功能,主文件可以从任何数量的其他 Rhai 文件(称为 模块)导入并使用符号,这些模块位于您的脚本目录中:
// 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 请求生命周期
在构建您的 Rhai 脚本之前,了解 router 如何处理每个传入的 GraphQL 请求有所帮助。在每个请求的执行过程中,服务 在路由器中相互通信,如图所示:
执行过程从 RouterService
开始,向 "从左到右" 到每个单独的 SubgraphService
,每个服务将客户端的原始请求传递到下一个服务。同样,当执行从 "从右到左" 从 SubgraphService
到 RouterService
时,每个服务将向客户端传递生成的响应。
您可以将 Rhai 脚本挂接到上述任何组合的服务中(如果您使用的是 Apollo Router Core v1.30.0 及更高版本)。脚本可以在传递过程中修改请求、响应和/或相关的元数据。
服务描述
路由器中的每个服务都有一个 Rhai 脚本可以定义的对应功能来挂接到该服务:
服务 / 功能 | 描述 |
---|---|
| 运行在 HTTP 请求生命周期开始和结束时。 例如, JWT 认证 在 如果您需要对 HTTP |
| 在 GraphQL 请求生命周期的开始和结束时运行。 如果您需要对 GraphQL 请求或 GraphQL 响应进行交互,请定义 |
| 处理生成后的查询计划执行。 如果您的定制包含用于执行(例如,如果根据策略决定要阻止特定 查询)的逻辑,请定义 |
| 处理路由器与您的子图之间的通信。 定义 与其他服务每次调用每个客户端请求不同,此服务每次需要解决客户端请求的 子图 请求时才调用。每次调用都会传递一个 |
每个服务使用请求和响应数据的数据结构,该结构包含
- 一个上下文对象,该对象在请求开始时创建,并在整个请求生命周期中传递。它包含
- 来自客户端的原始请求
- 一个数据包,该数据包可以通过插件填充,用于在整个请求生命周期中进行通信
- 以及该服务特定的任何其他数据(例如,查询计划和下游请求数据/响应数据)
服务回调
你Rhai脚本的主文件中的主文件中的每个钩子都会传递一个service
对象,该对象提供两个方法:map_request
和map_response
。在钩子中,你通常使用一个或两个这些方法来注册在回调函数中调用的GraphQL操作生命周期中的operation。
map_request
回调在每个服务中以router接收到客户端请求为起点,“向右”执行时被调用:这些回调各自接收到客户端当前
request
状态(可能已由链中的早期回调修改)。每个回调都可以直接修改这个request
对象。此外,
subgraph_service
的回调可以访问和修改router将通过request.subgraph
发送给相应子图subgraph的子-operation请求。参阅以下字段以获取参考:
request
。map_response
回调在执行回“向左”从subgraphs解决它们各自的子-operation时在每个服务中被调用:首先,为
subgraph_service
传递每个的对应子图response
。之后,将
execution_service
、supergraph_service
然后是router_service
的回调传递给客户端的合并response
,该响应是由所有subgraph的response
组装而成的。
示例脚本
除了下面的示例之外,请参阅router存储库的示例目录中的更多示例。Rhai特定的示例列在README.md
中。
处理入站请求
本示例演示了如何注册router请求处理。
// 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");}
操作头和请求上下文
此示例操作头和请求上下文
// 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}}
⚠️ 注意
访问不存在的头会抛出异常。
在读取之前安全地检查头是否存在,请使用request.headers.contains("header-name")
。
在使用 contains
在 subgraph_service
中时,必须将您的头分配给一个临时的本地 变量。 否则,contains
会抛出一个异常,表明它“不能更改”原始请求。”
例如,以下 subgraph_service
函数在处理之前检查是否存在 x-custom-header
。
// 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}});}
将cookie转换为头
此示例将cookie转换为可用于传输到子图的头。在 examples/cookies-to-headers 目录 中有一个完整的完整示例(带有测试)。
// 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];}}}
热重新加载
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)
有两种解决方案。这两种方案是:
- 创建一个全局变量的本地副本,使其可以被闭包捕获
fn supergraph_service(service){let v = Router.APOLLO_SDL;let f = |request| {print(v);};service.map_request(f);}
或
- 使用函数指针而不是闭包语法
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。
日志记录
为了追踪运行时错误,插入日志语句以缩小问题范围。