diff --git a/.changesets/feat_garypen_2278_rhai_router_service.md b/.changesets/feat_garypen_2278_rhai_router_service.md new file mode 100644 index 0000000000..c105892e4a --- /dev/null +++ b/.changesets/feat_garypen_2278_rhai_router_service.md @@ -0,0 +1,43 @@ +### Provide a rhai interface to the router service ([Issue #2278](https://github.com/apollographql/router/issues/2278)) + +Adds `Rhai` support for the `router_service`. + +It is now possible to interact with requests and responses at the `router_service` level from `Rhai`. The functionality is very similar to that provided for interacting with existing services, for example `supergraph_service`. For instance, you may map requests and responses as follows: + +```rust +fn router_service(service) { + const request_callback = Fn("process_request"); + service.map_request(request_callback); + const response_callback = Fn("process_response"); + service.map_response(response_callback); +} + +``` +The main difference from existing services is that the router_service is dealing with HTTP Bodies, not well formatted GraphQL objects. This means that the `Request.body` or `Response.body` is not a well structured object that you may interact with, but is simply a String. + +This makes it more complex to deal with Request and Response bodies with the tradeoff being that a script author has more power and can perform tasks which are just not possible within the confines of a well-formed GraphQL object. + +This simple example, simply logs the bodies: + +```rust +// Generate a log for each request at this stage +fn process_request(request) { + print(`body: ${request.body}`); +} + +// Generate a log for each response at this stage +fn process_response(response) { + print(`body: ${response.body}`); +} +``` + +This PR also introduces two new Rhai functions: + +```rust +json_encode(Object) +json_decode(String) -> Object + +``` +Which will respectively encode a `Rhai` Object or decode a JSON string into a `Rhai` Object. These functions may be helpful when dealing with String bodies which represent encoded JSON objects. + +By [@garypen](https://github.com/garypen) in https://github.com/apollographql/router/pull/3234 diff --git a/apollo-router/src/plugins/rhai/engine.rs b/apollo-router/src/plugins/rhai/engine.rs index 135dda4556..ab1dd67194 100644 --- a/apollo-router/src/plugins/rhai/engine.rs +++ b/apollo-router/src/plugins/rhai/engine.rs @@ -6,6 +6,7 @@ use std::time::SystemTime; use base64::prelude::BASE64_STANDARD; use base64::Engine as _; +use bytes::Bytes; use http::header::InvalidHeaderName; use http::uri::Authority; use http::uri::Parts; @@ -30,6 +31,7 @@ use tower::BoxError; use uuid::Uuid; use super::execution; +use super::router; use super::subgraph; use super::supergraph; use super::Rhai; @@ -293,6 +295,16 @@ mod router_json { pub(crate) fn value_to_string(x: &mut Value) -> String { format!("{x:?}") } + + #[rhai_fn(pure, return_raw)] + pub(crate) fn json_encode(input: &mut Dynamic) -> Result> { + serde_json::to_string(input).map_err(|e| e.to_string().into()) + } + + #[rhai_fn(pure, return_raw)] + pub(crate) fn json_decode(input: &mut ImmutableString) -> Result> { + serde_json::from_str(input).map_err(|e| e.to_string().into()) + } } #[export_module] @@ -350,7 +362,67 @@ mod router_context { format!("{x:?}") } + #[rhai_fn(get = "context", pure, return_raw)] + pub(crate) fn router_first_response_context_get( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + #[rhai_fn(set = "context", return_raw)] + pub(crate) fn router_first_response_context_set( + obj: &mut SharedMut, + context: Context, + ) -> Result<(), Box> { + obj.with_mut(|response| response.context = context); + Ok(()) + } + + #[rhai_fn(get = "context", pure, return_raw)] + pub(crate) fn supergraph_first_response_context_get( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + #[rhai_fn(set = "context", return_raw)] + pub(crate) fn supergraph_first_response_context_set( + obj: &mut SharedMut, + context: Context, + ) -> Result<(), Box> { + obj.with_mut(|response| response.context = context); + Ok(()) + } + + #[rhai_fn(get = "context", pure, return_raw)] + pub(crate) fn execution_first_response_context_get( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + #[rhai_fn(set = "context", return_raw)] + pub(crate) fn execution_first_response_context_set( + obj: &mut SharedMut, + context: Context, + ) -> Result<(), Box> { + obj.with_mut(|response| response.context = context); + Ok(()) + } + // Add context getter/setters for deferred responses + #[rhai_fn(get = "context", pure, return_raw)] + pub(crate) fn router_deferred_response_context_get( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + #[rhai_fn(set = "context", return_raw)] + pub(crate) fn router_deferred_response_context_set( + obj: &mut SharedMut, + context: Context, + ) -> Result<(), Box> { + obj.with_mut(|response| response.context = context); + Ok(()) + } + #[rhai_fn(get = "context", pure, return_raw)] pub(crate) fn supergraph_deferred_response_context_get( obj: &mut SharedMut, @@ -468,22 +540,48 @@ mod router_plugin { } // End of SubgraphRequest specific section + #[rhai_fn(get = "headers", pure, return_raw)] + pub(crate) fn get_originating_headers_router_response( + obj: &mut SharedMut, + ) -> Result> { + Ok(obj.with_mut(|response| response.response.headers().clone())) + } + + #[rhai_fn(name = "is_primary", pure)] + pub(crate) fn router_response_is_primary(_obj: &mut SharedMut) -> bool { + true + } + + #[rhai_fn(get = "headers", pure, return_raw)] + pub(crate) fn get_originating_headers_router_deferred_response( + _obj: &mut SharedMut, + ) -> Result> { + Err(CANNOT_ACCESS_HEADERS_ON_A_DEFERRED_RESPONSE.into()) + } + + #[rhai_fn(name = "is_primary", pure)] + pub(crate) fn router_deferred_response_is_primary( + _obj: &mut SharedMut, + ) -> bool { + false + } + #[rhai_fn(get = "headers", pure, return_raw)] pub(crate) fn get_originating_headers_supergraph_response( - obj: &mut SharedMut, + obj: &mut SharedMut, ) -> Result> { Ok(obj.with_mut(|response| response.response.headers().clone())) } #[rhai_fn(name = "is_primary", pure)] pub(crate) fn supergraph_response_is_primary( - _obj: &mut SharedMut, + _obj: &mut SharedMut, ) -> bool { true } #[rhai_fn(get = "headers", pure, return_raw)] - pub(crate) fn get_originating_headers_router_deferred_response( + pub(crate) fn get_originating_headers_supergraph_deferred_response( _obj: &mut SharedMut, ) -> Result> { Err(CANNOT_ACCESS_HEADERS_ON_A_DEFERRED_RESPONSE.into()) @@ -498,13 +596,15 @@ mod router_plugin { #[rhai_fn(get = "headers", pure, return_raw)] pub(crate) fn get_originating_headers_execution_response( - obj: &mut SharedMut, + obj: &mut SharedMut, ) -> Result> { Ok(obj.with_mut(|response| response.response.headers().clone())) } #[rhai_fn(name = "is_primary", pure)] - pub(crate) fn execution_response_is_primary(_obj: &mut SharedMut) -> bool { + pub(crate) fn execution_response_is_primary( + _obj: &mut SharedMut, + ) -> bool { true } @@ -529,16 +629,24 @@ mod router_plugin { Ok(obj.with_mut(|response| response.response.headers().clone())) } + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + #[rhai_fn(get = "body", pure, return_raw)] + pub(crate) fn get_originating_body_router_response( + obj: &mut SharedMut, + ) -> Result, Box> { + Ok(obj.with_mut(|response| response.response.body().to_vec())) + }*/ + #[rhai_fn(get = "body", pure, return_raw)] pub(crate) fn get_originating_body_supergraph_response( - obj: &mut SharedMut, + obj: &mut SharedMut, ) -> Result> { Ok(obj.with_mut(|response| response.response.body().clone())) } #[rhai_fn(get = "body", pure, return_raw)] pub(crate) fn get_originating_body_execution_response( - obj: &mut SharedMut, + obj: &mut SharedMut, ) -> Result> { Ok(obj.with_mut(|response| response.response.body().clone())) } @@ -550,8 +658,24 @@ mod router_plugin { Ok(obj.with_mut(|response| response.response.body().clone())) } + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided #[rhai_fn(get = "body", pure, return_raw)] pub(crate) fn get_originating_body_router_deferred_response( + obj: &mut SharedMut, + ) -> Result> { + // Get the body + let bytes = obj.with_mut(|response| { + let bytes = std::mem::take(&mut response.response); + // Copy back the response so it can continue to be used + response.response = bytes.clone(); + Ok::>(bytes) + })?; + + String::from_utf8(bytes.to_vec()).map_err(|err| err.to_string().into()) + }*/ + + #[rhai_fn(get = "body", pure, return_raw)] + pub(crate) fn get_originating_body_supergraph_deferred_response( obj: &mut SharedMut, ) -> Result> { Ok(obj.with_mut(|response| response.response.clone())) @@ -565,8 +689,8 @@ mod router_plugin { } #[rhai_fn(set = "headers", return_raw)] - pub(crate) fn set_originating_headers_supergraph_response( - obj: &mut SharedMut, + pub(crate) fn set_originating_headers_router_response( + obj: &mut SharedMut, headers: HeaderMap, ) -> Result<(), Box> { obj.with_mut(|response| *response.response.headers_mut() = headers); @@ -575,6 +699,23 @@ mod router_plugin { #[rhai_fn(set = "headers", return_raw)] pub(crate) fn set_originating_headers_router_deferred_response( + _obj: &mut SharedMut, + _headers: HeaderMap, + ) -> Result<(), Box> { + Err(CANNOT_ACCESS_HEADERS_ON_A_DEFERRED_RESPONSE.into()) + } + + #[rhai_fn(set = "headers", return_raw)] + pub(crate) fn set_originating_headers_supergraph_response( + obj: &mut SharedMut, + headers: HeaderMap, + ) -> Result<(), Box> { + obj.with_mut(|response| *response.response.headers_mut() = headers); + Ok(()) + } + + #[rhai_fn(set = "headers", return_raw)] + pub(crate) fn set_originating_headers_supergraph_deferred_response( _obj: &mut SharedMut, _headers: HeaderMap, ) -> Result<(), Box> { @@ -583,7 +724,7 @@ mod router_plugin { #[rhai_fn(set = "headers", return_raw)] pub(crate) fn set_originating_headers_execution_response( - obj: &mut SharedMut, + obj: &mut SharedMut, headers: HeaderMap, ) -> Result<(), Box> { obj.with_mut(|response| *response.response.headers_mut() = headers); @@ -607,9 +748,20 @@ mod router_plugin { Ok(()) } + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + #[rhai_fn(set = "body", return_raw)] + pub(crate) fn set_originating_body_router_response( + obj: &mut SharedMut, + body: String, + ) -> Result<(), Box> { + let bytes = Bytes::from(body); + obj.with_mut(|response| *response.response.body_mut() = bytes); + Ok(()) + }*/ + #[rhai_fn(set = "body", return_raw)] pub(crate) fn set_originating_body_supergraph_response( - obj: &mut SharedMut, + obj: &mut SharedMut, body: Response, ) -> Result<(), Box> { obj.with_mut(|response| *response.response.body_mut() = body); @@ -618,7 +770,7 @@ mod router_plugin { #[rhai_fn(set = "body", return_raw)] pub(crate) fn set_originating_body_execution_response( - obj: &mut SharedMut, + obj: &mut SharedMut, body: Response, ) -> Result<(), Box> { obj.with_mut(|response| *response.response.body_mut() = body); @@ -634,8 +786,19 @@ mod router_plugin { Ok(()) } + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided #[rhai_fn(set = "body", return_raw)] pub(crate) fn set_originating_body_router_deferred_response( + obj: &mut SharedMut, + body: String, + ) -> Result<(), Box> { + let bytes = Bytes::from(body); + obj.with_mut(|response| response.response = bytes); + Ok(()) + }*/ + + #[rhai_fn(set = "body", return_raw)] + pub(crate) fn set_originating_body_supergraph_deferred_response( obj: &mut SharedMut, body: Response, ) -> Result<(), Box> { @@ -678,7 +841,18 @@ mod router_plugin { } #[rhai_fn(name = "headers_are_available", pure)] - pub(crate) fn supergraph_response(_: &mut SharedMut) -> bool { + pub(crate) fn router_response(_: &mut SharedMut) -> bool { + true + } + + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + #[rhai_fn(name = "headers_are_available", pure)] + pub(crate) fn router_deferred_response(_: &mut SharedMut) -> bool { + false + }*/ + + #[rhai_fn(name = "headers_are_available", pure)] + pub(crate) fn supergraph_response(_: &mut SharedMut) -> bool { true } @@ -690,7 +864,7 @@ mod router_plugin { } #[rhai_fn(name = "headers_are_available", pure)] - pub(crate) fn execution_response(_: &mut SharedMut) -> bool { + pub(crate) fn execution_response(_: &mut SharedMut) -> bool { true } @@ -934,15 +1108,29 @@ mod router_plugin { } #[derive(Default)] -pub(crate) struct RhaiExecutionResponse { +pub(crate) struct RhaiRouterFirstRequest { pub(crate) context: Context, - pub(crate) response: http_ext::Response, + pub(crate) request: http::Request<()>, } +#[allow(dead_code)] #[derive(Default)] -pub(crate) struct RhaiExecutionDeferredResponse { +pub(crate) struct RhaiRouterChunkedRequest { pub(crate) context: Context, - pub(crate) response: Response, + pub(crate) request: Bytes, +} + +#[derive(Default)] +pub(crate) struct RhaiRouterResponse { + pub(crate) context: Context, + pub(crate) response: http::Response<()>, +} + +#[allow(dead_code)] +#[derive(Default)] +pub(crate) struct RhaiRouterChunkedResponse { + pub(crate) context: Context, + pub(crate) response: Bytes, } #[derive(Default)] @@ -957,6 +1145,18 @@ pub(crate) struct RhaiSupergraphDeferredResponse { pub(crate) response: Response, } +#[derive(Default)] +pub(crate) struct RhaiExecutionResponse { + pub(crate) context: Context, + pub(crate) response: http_ext::Response, +} + +#[derive(Default)] +pub(crate) struct RhaiExecutionDeferredResponse { + pub(crate) context: Context, + pub(crate) response: Response, +} + macro_rules! if_subgraph { ( subgraph => $subgraph: block else $not_subgraph: block ) => { $subgraph @@ -966,6 +1166,152 @@ macro_rules! if_subgraph { }; } +macro_rules! register_rhai_router_interface { + ($engine: ident, $($base: ident), *) => { + $( + // Context stuff + $engine.register_get( + "context", + |obj: &mut SharedMut<$base::FirstRequest>| -> Result> { + Ok(obj.with_mut(|request| request.context.clone())) + } + ) + .register_get( + "context", + |obj: &mut SharedMut<$base::ChunkedRequest>| -> Result> { + Ok(obj.with_mut(|request| request.context.clone())) + } + ).register_get( + "context", + |obj: &mut SharedMut<$base::Response>| -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + ) + .register_get( + "context", + |obj: &mut SharedMut<$base::DeferredResponse>| -> Result> { + Ok(obj.with_mut(|response| response.context.clone())) + } + ); + + $engine.register_set( + "context", + |obj: &mut SharedMut<$base::FirstRequest>, context: Context| { + obj.with_mut(|request| request.context = context); + Ok(()) + } + ) + .register_set( + "context", + |obj: &mut SharedMut<$base::ChunkedRequest>, context: Context| { + obj.with_mut(|request| request.context = context); + Ok(()) + } + ) + .register_set( + "context", + |obj: &mut SharedMut<$base::Response>, context: Context| { + obj.with_mut(|response| response.context = context); + Ok(()) + } + ).register_set( + "context", + |obj: &mut SharedMut<$base::DeferredResponse>, context: Context| { + obj.with_mut(|response| response.context = context); + Ok(()) + } + ); + + // Originating Request + $engine.register_get( + "headers", + |obj: &mut SharedMut<$base::FirstRequest>| -> Result> { + Ok(obj.with_mut(|request| request.request.headers().clone())) + } + ).register_get( + "headers", + |obj: &mut SharedMut<$base::Response>| -> Result> { + Ok(obj.with_mut(|response| response.response.headers().clone())) + } + ); + + $engine.register_set( + "headers", + |obj: &mut SharedMut<$base::FirstRequest>, headers: HeaderMap| { + if_subgraph! { + $base => { + let _unused = (obj, headers); + Err("cannot mutate originating request on a subgraph".into()) + } else { + obj.with_mut(|request| *request.request.headers_mut() = headers); + Ok(()) + } + } + } + ).register_set( + "headers", + |obj: &mut SharedMut<$base::Response>, headers: HeaderMap| { + if_subgraph! { + $base => { + let _unused = (obj, headers); + Err("cannot mutate originating request on a subgraph".into()) + } else { + obj.with_mut(|response| *response.response.headers_mut() = headers); + Ok(()) + } + } + } + ); + + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + $engine.register_get( + "body", + |obj: &mut SharedMut<$base::ChunkedRequest>| -> Result, Box> { + Ok( obj.with_mut(|request| { request.request.to_vec()})) + } + ); + + $engine.register_set( + "body", + |obj: &mut SharedMut<$base::ChunkedRequest>, body: Vec| { + if_subgraph! { + $base => { + let _unused = (obj, body); + Err("cannot mutate originating request on a subgraph".into()) + } else { + let bytes = Bytes::from(body); + obj.with_mut(|request| request.request = bytes); + Ok(()) + } + } + } + );*/ + + $engine.register_get( + "uri", + |obj: &mut SharedMut<$base::Request>| -> Result> { + Ok(obj.with_mut(|request| request.router_request.uri().clone())) + } + ); + + $engine.register_set( + "uri", + |obj: &mut SharedMut<$base::Request>, uri: Uri| { + if_subgraph! { + $base => { + let _unused = (obj, uri); + Err("cannot mutate originating request on a subgraph".into()) + } else { + obj.with_mut(|request| *request.router_request.uri_mut() = uri); + Ok(()) + } + } + } + ); + )* + }; +} + macro_rules! register_rhai_interface { ($engine: ident, $($base: ident), *) => { $( @@ -1190,6 +1536,8 @@ impl Rhai { tracing::error!(%message, target = %error_main); }); // Add common getter/setters for different types + register_rhai_router_interface!(engine, router); + // Add common getter/setters for different types register_rhai_interface!(engine, supergraph, execution, subgraph); // Since constants in Rhai don't give us the behaviour we expect, let's create some global diff --git a/apollo-router/src/plugins/rhai/execution.rs b/apollo-router/src/plugins/rhai/execution.rs index ea9cfd0d60..a04b00211d 100644 --- a/apollo-router/src/plugins/rhai/execution.rs +++ b/apollo-router/src/plugins/rhai/execution.rs @@ -1,5 +1,65 @@ //! execution module +use std::ops::ControlFlow; + +use tower::BoxError; + +use super::ErrorDetails; +use crate::graphql::Error; pub(crate) use crate::services::execution::*; -pub(crate) type Response = super::engine::RhaiExecutionResponse; +use crate::Context; + +pub(crate) type FirstResponse = super::engine::RhaiExecutionResponse; pub(crate) type DeferredResponse = super::engine::RhaiExecutionDeferredResponse; + +pub(super) fn request_failure( + context: Context, + error_details: ErrorDetails, +) -> Result, BoxError> { + let res = if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build()? + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .context(context) + .status_code(error_details.status) + .build()? + }; + + Ok(ControlFlow::Break(res)) +} + +pub(super) fn response_failure(context: Context, error_details: ErrorDetails) -> Response { + if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build() + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .status_code(error_details.status) + .context(context) + .build() + } + .expect("can't fail to build our error message") +} diff --git a/apollo-router/src/plugins/rhai/mod.rs b/apollo-router/src/plugins/rhai/mod.rs index 5dce61bccb..2f19f9cf32 100644 --- a/apollo-router/src/plugins/rhai/mod.rs +++ b/apollo-router/src/plugins/rhai/mod.rs @@ -45,22 +45,14 @@ use crate::layers::ServiceBuilderExt; use crate::plugin::Plugin; use crate::plugin::PluginInit; use crate::plugins::rhai::engine::OptionDance; -use crate::plugins::rhai::engine::RhaiExecutionDeferredResponse; -use crate::plugins::rhai::engine::RhaiExecutionResponse; -use crate::plugins::rhai::engine::RhaiSupergraphDeferredResponse; -use crate::plugins::rhai::engine::RhaiSupergraphResponse; use crate::register_plugin; -use crate::services::ExecutionRequest; -use crate::services::ExecutionResponse; -use crate::services::SupergraphRequest; -use crate::services::SupergraphResponse; -use crate::Context; mod engine; pub(crate) const RHAI_SPAN_NAME: &str = "rhai_plugin"; mod execution; +mod router; mod subgraph; mod supergraph; @@ -230,6 +222,24 @@ impl Plugin for Rhai { }) } + fn router_service(&self, service: router::BoxService) -> router::BoxService { + const FUNCTION_NAME_SERVICE: &str = "router_service"; + if !self.ast_has_function(FUNCTION_NAME_SERVICE) { + return service; + } + tracing::debug!("router_service function found"); + let shared_service = Arc::new(Mutex::new(Some(service))); + if let Err(error) = self.run_rhai_service( + FUNCTION_NAME_SERVICE, + None, + ServiceStep::Router(shared_service.clone()), + self.block.load().scope.clone(), + ) { + tracing::error!("service callback failed: {error}"); + } + shared_service.take_unwrap() + } + fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { const FUNCTION_NAME_SERVICE: &str = "supergraph_service"; if !self.ast_has_function(FUNCTION_NAME_SERVICE) { @@ -297,6 +307,7 @@ impl Drop for Rhai { #[derive(Clone, Debug)] pub(crate) enum ServiceStep { + Router(SharedMut), Supergraph(SharedMut), Execution(SharedMut), Subgraph(SharedMut), @@ -318,57 +329,15 @@ macro_rules! gen_map_request { ServiceBuilder::new() .instrument(rhai_service_span()) .checkpoint(move |request: $base::Request| { - // Let's define a local function to build an error response - fn failure_message( - context: Context, - error_details: ErrorDetails, - ) -> Result, BoxError> - { - let res = if let Some(body) = error_details.body { - $base::Response::builder() - .extensions(body.extensions) - .errors(body.errors) - .status_code(error_details.status) - .context(context) - .and_data(body.data) - .and_label(body.label) - .and_path(body.path) - .build() - } else { - $base::Response::error_builder() - .errors(vec![Error { - message: error_details.message.unwrap_or_default(), - ..Default::default() - }]) - .context(context) - .status_code(error_details.status) - .build()? - }; - - Ok(ControlFlow::Break(res)) - } let shared_request = Shared::new(Mutex::new(Some(request))); - let result: Result> = if $callback.is_curried() { - $callback.call( - &$rhai_service.engine, - &$rhai_service.ast, - (shared_request.clone(),), - ) - } else { - let mut guard = $rhai_service.scope.lock().unwrap(); - $rhai_service.engine.call_fn( - &mut guard, - &$rhai_service.ast, - $callback.fn_name(), - (shared_request.clone(),), - ) - }; + let result: Result> = + execute(&$rhai_service, &$callback, (shared_request.clone(),)); if let Err(error) = result { let error_details = process_error(error); tracing::error!("map_request callback failed: {error_details:#?}"); let mut guard = shared_request.lock().unwrap(); let request_opt = guard.take(); - return failure_message(request_opt.unwrap().context, error_details); + return $base::request_failure(request_opt.unwrap().context, error_details); } let mut guard = shared_request.lock().unwrap(); let request_opt = guard.take(); @@ -381,49 +350,33 @@ macro_rules! gen_map_request { } // Actually use the checkpoint function so that we can shortcut requests which fail -macro_rules! gen_map_deferred_request { - ($request: ident, $response: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { +macro_rules! gen_map_router_deferred_request { + ($base: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { $borrow.replace(|service| { - fn rhai_service_span() -> impl Fn(&$request) -> tracing::Span + Clone { - move |_request: &$request| { + fn rhai_service_span() -> impl Fn(&$base::Request) -> tracing::Span + Clone { + move |_request: &$base::Request| { tracing::info_span!( RHAI_SPAN_NAME, - "rhai service" = stringify!($request), + "rhai service" = stringify!($base::Request), "otel.kind" = "INTERNAL" ) } } ServiceBuilder::new() .instrument(rhai_service_span()) - .checkpoint(move |request: $request| { - // Let's define a local function to build an error response - fn failure_message( - context: Context, - error_details: ErrorDetails, - ) -> Result, BoxError> { - let res = if let Some(body) = error_details.body { - $response::builder() - .extensions(body.extensions) - .errors(body.errors) - .status_code(error_details.status) - .context(context) - .and_data(body.data) - .and_label(body.label) - .and_path(body.path) - .build()? - } else { - $response::error_builder() - .errors(vec![Error { - message: error_details.message.unwrap_or_default(), - ..Default::default() - }]) - .context(context) - .status_code(error_details.status) - .build()? - }; + .checkpoint( move |chunked_request: $base::Request| { + // we split the request stream into headers+first body chunk, then a stream of chunks + // for which we will implement mapping later + let $base::Request { router_request, context } = chunked_request; + let (parts, stream) = router_request.into_parts(); - Ok(ControlFlow::Break(res)) - } + let request = $base::FirstRequest { + context, + request: http::Request::from_parts( + parts, + (), + ), + }; let shared_request = Shared::new(Mutex::new(Some(request))); let result = execute(&$rhai_service, &$callback, (shared_request.clone(),)); @@ -432,76 +385,97 @@ macro_rules! gen_map_deferred_request { let error_details = process_error(error); let mut guard = shared_request.lock().unwrap(); let request_opt = guard.take(); - return failure_message(request_opt.unwrap().context, error_details); + return $base::request_failure(request_opt.unwrap().context, error_details); } - let mut guard = shared_request.lock().unwrap(); - let request_opt = guard.take(); - Ok(ControlFlow::Continue(request_opt.unwrap())) + + let request_opt = shared_request.lock().unwrap().take(); + + let $base::FirstRequest { context, request } = + request_opt.unwrap(); + let (parts, _body) = http::Request::from(request).into_parts(); + + // Finally, return a response which has a Body that wraps our stream of response chunks. + Ok(ControlFlow::Continue($base::Request { + context, + router_request: http::Request::from_parts(parts, stream), + })) + + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + let ctx = context.clone(); + let rhai_service = $rhai_service.clone(); + let callback = $callback.clone(); + + let mapped_stream = stream + .map_err(BoxError::from) + .and_then(move |chunk| { + let context = ctx.clone(); + let rhai_service = rhai_service.clone(); + let callback = callback.clone(); + async move { + let request = $base::ChunkedRequest { + context, + request: chunk.into(), + }; + let shared_request = Shared::new(Mutex::new(Some(request))); + + let result = execute( + &rhai_service, + &callback, + (shared_request.clone(),), + ); + + if let Err(error) = result { + tracing::error!("map_request callback failed: {error}"); + let error_details = process_error(error); + let error = Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }; + // We don't have a structured response to work with here. Let's + // throw away our response and custom build an error response + let error_response = graphql::Response::builder() + .errors(vec![error]).build(); + return Ok(serde_json::to_vec(&error_response)?.into()); + } + + let request_opt = shared_request.lock().unwrap().take(); + let $base::ChunkedRequest { request, .. } = + request_opt.unwrap(); + Ok(request) + } + }); + + // Finally, return a response which has a Body that wraps our stream of response chunks. + Ok(ControlFlow::Continue($base::Request { + context, + router_request: http::Request::from_parts(parts, hyper::Body::wrap_stream(mapped_stream)), + })) + */ }) .service(service) .boxed() }) }; } + macro_rules! gen_map_response { ($base: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { $borrow.replace(|service| { service .map_response(move |response: $base::Response| { - // Let's define a local function to build an error response - // XXX: This isn't ideal. We already have a response, so ideally we'd - // like to append this error into the existing response. However, - // the significantly different treatment of errors in different - // response types makes this extremely painful. This needs to be - // re-visited at some point post GA. - fn failure_message( - context: Context, - error_details: ErrorDetails, - ) -> $base::Response { - if let Some(body) = error_details.body { - $base::Response::builder() - .extensions(body.extensions) - .errors(body.errors) - .status_code(error_details.status) - .context(context) - .and_data(body.data) - .and_label(body.label) - .and_path(body.path) - .build() - } else { - $base::Response::error_builder() - .errors(vec![Error { - message: error_details.message.unwrap_or_default(), - ..Default::default() - }]) - .status_code(error_details.status) - .context(context) - .build() - .expect("can't fail to build our error message") - } - } let shared_response = Shared::new(Mutex::new(Some(response))); - let result: Result> = if $callback.is_curried() { - $callback.call( - &$rhai_service.engine, - &$rhai_service.ast, - (shared_response.clone(),), - ) - } else { - let mut guard = $rhai_service.scope.lock().unwrap(); - $rhai_service.engine.call_fn( - &mut guard, - &$rhai_service.ast, - $callback.fn_name(), - (shared_response.clone(),), - ) - }; + let result: Result> = + execute(&$rhai_service, &$callback, (shared_response.clone(),)); + if let Err(error) = result { tracing::error!("map_response callback failed: {error}"); let error_details = process_error(error); let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); - return failure_message(response_opt.unwrap().context, error_details); + return $base::response_failure( + response_opt.unwrap().context, + error_details, + ); } let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); @@ -512,46 +486,122 @@ macro_rules! gen_map_response { }; } -macro_rules! gen_map_deferred_response { - ($response: ident, $rhai_response: ident, $rhai_deferred_response: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { +// Even though this macro is only ever used to generate router service handling, I'm leaving it as +// a macro so that the code shape is "similar" to the way in which other services are processed. +// +// I can't easily unify the macros because the router response processing is quite different to +// other service in terms of payload. +macro_rules! gen_map_router_deferred_response { + ($base: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { $borrow.replace(|service| { BoxService::new(service.and_then( - |mapped_response: $response| async move { - // Let's define a local function to build an error response - // XXX: This isn't ideal. We already have a response, so ideally we'd - // like to append this error into the existing response. However, - // the significantly different treatment of errors in different - // response types makes this extremely painful. This needs to be - // re-visited at some point post GA. - fn failure_message( - context: Context, - error_details: ErrorDetails, - ) -> $response { - if let Some(body) = error_details.body { - $response::builder() - .extensions(body.extensions) - .errors(body.errors) - .status_code(error_details.status) - .context(context) - .and_data(body.data) - .and_label(body.label) - .and_path(body.path) - .build() - } else { - $response::error_builder() - .errors(vec![Error { + |mapped_response: $base::Response| async move { + // we split the response stream into headers+first response, then a stream of deferred responses + // for which we will implement mapping later + let $base::Response { response, context } = mapped_response; + let (parts, stream) = response.into_parts(); + + let response = $base::FirstResponse { + context, + response: http::Response::from_parts( + parts, + (), + ) + .into(), + }; + let shared_response = Shared::new(Mutex::new(Some(response))); + + let result = + execute(&$rhai_service, &$callback, (shared_response.clone(),)); + if let Err(error) = result { + tracing::error!("map_response callback failed: {error}"); + let error_details = process_error(error); + let response_opt = shared_response.lock().unwrap().take(); + return Ok($base::response_failure( + response_opt.unwrap().context, + error_details + )); + } + + let response_opt = shared_response.lock().unwrap().take(); + + let $base::FirstResponse { context, response } = + response_opt.unwrap(); + let (parts, _body) = http::Response::from(response).into_parts(); + + + // Finally, return a response which has a Body that wraps our stream of response chunks. + Ok($base::Response { + context, + response: http::Response::from_parts(parts, stream), + }) + + /*TODO: reenable when https://github.com/apollographql/router/issues/3642 is decided + let ctx = context.clone(); + + let mapped_stream = rest + .map_err(BoxError::from) + .and_then(move |deferred_response| { + let rhai_service = $rhai_service.clone(); + let context = ctx.clone(); + let callback = $callback.clone(); + async move { + let response = $base::DeferredResponse { + context, + response: deferred_response.into(), + }; + let shared_response = Shared::new(Mutex::new(Some(response))); + + let result = execute( + &rhai_service, + &callback, + (shared_response.clone(),), + ); + + if let Err(error) = result { + tracing::error!("map_response callback failed: {error}"); + let error_details = process_error(error); + let error = Error { message: error_details.message.unwrap_or_default(), ..Default::default() - }]) - .status_code(error_details.status) - .context(context) - .build() - }.expect("can't fail to build our error message") - } + }; + // We don't have a structured response to work with here. Let's + // throw away our response and custom build an error response + let error_response = graphql::Response::builder() + .errors(vec![error]).build(); + return Ok(serde_json::to_vec(&error_response)?.into()); + } + + let response_opt = shared_response.lock().unwrap().take(); + let $base::DeferredResponse { response, .. } = + response_opt.unwrap(); + Ok(response) + } + }); + + // Create our response stream which consists of the bytes from our first body chained with the + // rest of the responses in our mapped stream. + let final_stream = once(ready(Ok(body))).chain(mapped_stream).boxed(); + + // Finally, return a response which has a Body that wraps our stream of response chunks. + Ok($base::Response { + context, + response: http::Response::from_parts(parts, hyper::Body::wrap_stream(final_stream)), + })*/ + }, + )) + }) + }; +} +macro_rules! gen_map_deferred_response { + ($base: ident, $borrow: ident, $rhai_service: ident, $callback: ident) => { + $borrow.replace(|service| { + BoxService::new(service.and_then( + |mapped_response: $base::Response| async move { // we split the response stream into headers+first response, then a stream of deferred responses // for which we will implement mapping later - let $response { response, context } = mapped_response; + let $base::Response { response, context } = mapped_response; let (parts, stream) = response.into_parts(); let (first, rest) = stream.into_future().await; @@ -562,13 +612,13 @@ macro_rules! gen_map_deferred_response { position: None, body: None }; - return Ok(failure_message( + return Ok($base::response_failure( context, error_details )); } - let response = $rhai_response { + let response = $base::FirstResponse { context, response: http::Response::from_parts( parts, @@ -585,7 +635,7 @@ macro_rules! gen_map_deferred_response { let error_details = process_error(error); let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); - return Ok(failure_message( + return Ok($base::response_failure( response_opt.unwrap().context, error_details )); @@ -593,7 +643,7 @@ macro_rules! gen_map_deferred_response { let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); - let $rhai_response { context, response } = + let $base::FirstResponse { context, response } = response_opt.unwrap(); let (parts, body) = http::Response::from(response).into_parts(); @@ -604,7 +654,7 @@ macro_rules! gen_map_deferred_response { let context = context.clone(); let callback = $callback.clone(); async move { - let response = $rhai_deferred_response { + let response = $base::DeferredResponse { context, response: deferred_response, }; @@ -620,7 +670,7 @@ macro_rules! gen_map_deferred_response { let error_details = process_error(error); let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); - let $rhai_deferred_response { mut response, .. } = response_opt.unwrap(); + let $base::DeferredResponse { mut response, .. } = response_opt.unwrap(); let error = Error { message: error_details.message.unwrap_or_default(), ..Default::default() @@ -631,7 +681,7 @@ macro_rules! gen_map_deferred_response { let mut guard = shared_response.lock().unwrap(); let response_opt = guard.take(); - let $rhai_deferred_response { response, .. } = + let $base::DeferredResponse { response, .. } = response_opt.unwrap(); Some(response) } @@ -642,7 +692,7 @@ macro_rules! gen_map_deferred_response { once(ready(body)).chain(mapped_stream).boxed(), ) .into(); - Ok($response { + Ok($base::Response { context: ctx, response, }) @@ -655,23 +705,14 @@ macro_rules! gen_map_deferred_response { impl ServiceStep { fn map_request(&mut self, rhai_service: RhaiService, callback: FnPtr) { match self { + ServiceStep::Router(service) => { + gen_map_router_deferred_request!(router, service, rhai_service, callback); + } ServiceStep::Supergraph(service) => { - gen_map_deferred_request!( - SupergraphRequest, - SupergraphResponse, - service, - rhai_service, - callback - ); + gen_map_request!(supergraph, service, rhai_service, callback); } ServiceStep::Execution(service) => { - gen_map_deferred_request!( - ExecutionRequest, - ExecutionResponse, - service, - rhai_service, - callback - ); + gen_map_request!(execution, service, rhai_service, callback); } ServiceStep::Subgraph(service) => { gen_map_request!(subgraph, service, rhai_service, callback); @@ -681,25 +722,14 @@ impl ServiceStep { fn map_response(&mut self, rhai_service: RhaiService, callback: FnPtr) { match self { + ServiceStep::Router(service) => { + gen_map_router_deferred_response!(router, service, rhai_service, callback); + } ServiceStep::Supergraph(service) => { - gen_map_deferred_response!( - SupergraphResponse, - RhaiSupergraphResponse, - RhaiSupergraphDeferredResponse, - service, - rhai_service, - callback - ); + gen_map_deferred_response!(supergraph, service, rhai_service, callback); } ServiceStep::Execution(service) => { - gen_map_deferred_response!( - ExecutionResponse, - RhaiExecutionResponse, - RhaiExecutionDeferredResponse, - service, - rhai_service, - callback - ); + gen_map_deferred_response!(execution, service, rhai_service, callback); } ServiceStep::Subgraph(service) => { gen_map_response!(subgraph, service, rhai_service, callback); diff --git a/apollo-router/src/plugins/rhai/router.rs b/apollo-router/src/plugins/rhai/router.rs new file mode 100644 index 0000000000..2a6313daf2 --- /dev/null +++ b/apollo-router/src/plugins/rhai/router.rs @@ -0,0 +1,70 @@ +//! router module + +use std::ops::ControlFlow; + +use tower::BoxError; + +use super::ErrorDetails; +use crate::graphql::Error; +pub(crate) use crate::services::router::*; +use crate::Context; + +pub(crate) type FirstRequest = super::engine::RhaiRouterFirstRequest; +pub(crate) type ChunkedRequest = super::engine::RhaiRouterChunkedRequest; +pub(crate) type FirstResponse = super::engine::RhaiRouterResponse; +pub(crate) type DeferredResponse = super::engine::RhaiRouterChunkedResponse; + +pub(super) fn request_failure( + context: Context, + error_details: ErrorDetails, +) -> Result, BoxError> { + let res = if let Some(body) = error_details.body { + crate::services::router::Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build()? + } else { + crate::services::router::Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .context(context) + .status_code(error_details.status) + .build()? + }; + + Ok(ControlFlow::Break(res)) +} + +pub(super) fn response_failure( + context: Context, + error_details: ErrorDetails, +) -> crate::services::router::Response { + if let Some(body) = error_details.body { + crate::services::router::Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build() + } else { + crate::services::router::Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .status_code(error_details.status) + .context(context) + .build() + } + .expect("can't fail to build our error message") +} diff --git a/apollo-router/src/plugins/rhai/subgraph.rs b/apollo-router/src/plugins/rhai/subgraph.rs index 22da7cc63b..110dce38d5 100644 --- a/apollo-router/src/plugins/rhai/subgraph.rs +++ b/apollo-router/src/plugins/rhai/subgraph.rs @@ -1,3 +1,62 @@ //! subgraph module +use std::ops::ControlFlow; + +use tower::BoxError; + +use super::ErrorDetails; +use crate::graphql::Error; pub(crate) use crate::services::subgraph::*; +use crate::Context; + +pub(super) fn request_failure( + context: Context, + error_details: ErrorDetails, +) -> Result, BoxError> { + let res = if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build() + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .context(context) + .status_code(error_details.status) + .build()? + }; + + Ok(ControlFlow::Break(res)) +} + +pub(super) fn response_failure(context: Context, error_details: ErrorDetails) -> Response { + if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build() + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .status_code(error_details.status) + .context(context) + .build() + .expect("can't fail to build our error message") + } +} diff --git a/apollo-router/src/plugins/rhai/supergraph.rs b/apollo-router/src/plugins/rhai/supergraph.rs index 01cc448b96..9f2905ab62 100644 --- a/apollo-router/src/plugins/rhai/supergraph.rs +++ b/apollo-router/src/plugins/rhai/supergraph.rs @@ -1,5 +1,65 @@ //! supergraph module +use std::ops::ControlFlow; + +use tower::BoxError; + +use super::ErrorDetails; +use crate::graphql::Error; pub(crate) use crate::services::supergraph::*; -pub(crate) type Response = super::engine::RhaiSupergraphResponse; +use crate::Context; + +pub(crate) type FirstResponse = super::engine::RhaiSupergraphResponse; pub(crate) type DeferredResponse = super::engine::RhaiSupergraphDeferredResponse; + +pub(super) fn request_failure( + context: Context, + error_details: ErrorDetails, +) -> Result, BoxError> { + let res = if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build()? + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .context(context) + .status_code(error_details.status) + .build()? + }; + + Ok(ControlFlow::Break(res)) +} + +pub(super) fn response_failure(context: Context, error_details: ErrorDetails) -> Response { + if let Some(body) = error_details.body { + Response::builder() + .extensions(body.extensions) + .errors(body.errors) + .status_code(error_details.status) + .context(context) + .and_data(body.data) + .and_label(body.label) + .and_path(body.path) + .build() + } else { + Response::error_builder() + .errors(vec![Error { + message: error_details.message.unwrap_or_default(), + ..Default::default() + }]) + .status_code(error_details.status) + .context(context) + .build() + } + .expect("can't fail to build our error message") +} diff --git a/apollo-router/src/plugins/rhai/tests.rs b/apollo-router/src/plugins/rhai/tests.rs index a2e5fee18e..bb56ceb654 100644 --- a/apollo-router/src/plugins/rhai/tests.rs +++ b/apollo-router/src/plugins/rhai/tests.rs @@ -21,16 +21,16 @@ use super::process_error; use super::subgraph; use super::PathBuf; use super::Rhai; -use super::RhaiExecutionDeferredResponse; -use super::RhaiExecutionResponse; -use super::RhaiSupergraphDeferredResponse; -use super::RhaiSupergraphResponse; use crate::graphql::Error; use crate::graphql::Request; use crate::http_ext; use crate::plugin::test::MockExecutionService; use crate::plugin::test::MockSupergraphService; use crate::plugin::DynPlugin; +use crate::plugins::rhai::engine::RhaiExecutionDeferredResponse; +use crate::plugins::rhai::engine::RhaiExecutionResponse; +use crate::plugins::rhai::engine::RhaiSupergraphDeferredResponse; +use crate::plugins::rhai::engine::RhaiSupergraphResponse; use crate::services::ExecutionRequest; use crate::services::SubgraphRequest; use crate::services::SupergraphRequest; diff --git a/apollo-router/tests/fixtures/test_callbacks.rhai b/apollo-router/tests/fixtures/test_callbacks.rhai index 340cde9d5e..7ded5601d9 100644 --- a/apollo-router/tests/fixtures/test_callbacks.rhai +++ b/apollo-router/tests/fixtures/test_callbacks.rhai @@ -1,7 +1,7 @@ // This is a test used to make sure each callback is called -fn supergraph_service(service) { - log_info("supergraph_service setup"); +fn router_service(service) { + log_info("router_service setup"); service.map_request(|request| { log_info("from_router_request"); }); @@ -10,6 +10,16 @@ fn supergraph_service(service) { }); } +fn supergraph_service(service) { + log_info("supergraph_service setup"); + service.map_request(|request| { + log_info("from_supergraph_request"); + }); + service.map_response(|response| { + log_info("from_supergraph_response"); + }); +} + fn execution_service(service) { log_info("execution_service setup"); service.map_request(|request| { diff --git a/apollo-router/tests/rhai_tests.rs b/apollo-router/tests/rhai_tests.rs index e1578ae423..d8ff1aecf9 100644 --- a/apollo-router/tests/rhai_tests.rs +++ b/apollo-router/tests/rhai_tests.rs @@ -46,9 +46,12 @@ async fn all_rhai_callbacks_are_invoked() { .unwrap(); dbg!(_response); for expected_log in [ - "supergraph_service setup", + "router_service setup", "from_router_request", "from_router_response", + "supergraph_service setup", + "from_supergraph_request", + "from_supergraph_response", "execution_service setup", "from_execution_request", "from_execution_response", diff --git a/docs/source/customizations/rhai-api.mdx b/docs/source/customizations/rhai-api.mdx index 5496a24da9..83775e96e5 100644 --- a/docs/source/customizations/rhai-api.mdx +++ b/docs/source/customizations/rhai-api.mdx @@ -12,6 +12,7 @@ This article documents symbols and behaviors that are specific to [Rhai customiz Your Rhai script's [main file](./rhai/#the-main-file) hooks into the individual services of the Apollo Router's [request-handling lifecycle](./rhai/#router-request-lifecycle). To do so, it defines whichever combination of the following entry point hooks it requires: ```rhai +fn router_service(service) {} fn supergraph_service(service) {} fn execution_service(service) {} fn subgraph_service(service, subgraph) {} @@ -192,6 +193,31 @@ fn supergraph_service(service) { } ``` +## 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 +fn router_service(service) { + let original = `{"valid":"object"}`; + try { + let encoded = json_decode(original); + // encoded is a Rhai object, with a property (or key) named valid with a String value of "object" + print(`encoded.valid: ${encoded.valid}`); + let and_back = json_encode(encoded); + // and_back will be a string == original. + if and_back != original { + throw "something has gone wrong"; + } + } + catch(err) + { + // log any errors + log_error(`json coding error: ${err}`); + } +} +``` + ## 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. @@ -464,9 +490,10 @@ The following fields are identical in behavior to their `request` counterparts: * [`context`](#requestcontext) * [`headers`](#requestheaders) +* [`body`](#requestbody) * [`body.extensions`](#requestbodyextensions) -Note: Be particularly careful when interacting with headers in a response context. For supergraph_service() and execution_service(), response headers 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 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. +Note: Be particularly careful when interacting with headers in a response context. For router_service(), supergraph_service() and execution_service(), response headers 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 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 if response.is_primary() { diff --git a/docs/source/customizations/rhai.mdx b/docs/source/customizations/rhai.mdx index 157c58a57a..14fe4e62ba 100644 --- a/docs/source/customizations/rhai.mdx +++ b/docs/source/customizations/rhai.mdx @@ -263,8 +263,9 @@ Each hook in your Rhai script's [main file](#main-file) is passed a `service` ob ```mermaid graph LR; client(Client); - client --> supergraph(supergraph_service); + client --> router(router_service); subgraph Apollo Router + router --> supergraph(supergraph_service); supergraph --> execution(execution_service); execution --> subs_a(subgraph_service); execution --> subs_b(subgraph_service); @@ -286,8 +287,9 @@ Each hook in your Rhai script's [main file](#main-file) is passed a `service` ob graph RL; client(Client); subgraph Apollo Router - supergraph(supergraph_service); execution(execution_service); + supergraph(supergraph_service); + router(router_service); subs_a(subgraph_service); subs_b(subgraph_service); end; @@ -298,13 +300,14 @@ Each hook in your Rhai script's [main file](#main-file) is passed a `service` ob subs_a --> execution; subs_b --> execution; execution --> supergraph; - supergraph --> client; + supergraph --> router;; + router --> client; class client,sub_a,sub_b secondary; ``` 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 `response`s. + Afterward, callbacks for `execution_service`, `supergraph_service` and then `router_service` are passed the combined `response` for the client that's assembled from all subgraph `response`s.