From 8c83cd32542afe6e3d043faf7f22d42fdf048583 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 8 Dec 2025 15:56:05 +0300 Subject: [PATCH 01/19] docs(router): native plugin system API --- .../docs/src/content/router/guides/_meta.ts | 1 + .../guides/extending-the-router/index.mdx | 778 ++++++++++++++++++ 2 files changed, 779 insertions(+) create mode 100644 packages/web/docs/src/content/router/guides/extending-the-router/index.mdx diff --git a/packages/web/docs/src/content/router/guides/_meta.ts b/packages/web/docs/src/content/router/guides/_meta.ts index 61130150e84..4f667027d86 100644 --- a/packages/web/docs/src/content/router/guides/_meta.ts +++ b/packages/web/docs/src/content/router/guides/_meta.ts @@ -2,4 +2,5 @@ export default { 'dynamic-subgraph-routing': 'Dynamic Subgraph Routing', 'header-manipulation': 'Header Manipulation', 'performance-tuning': 'Performance Tuning & Traffic Shaping', + 'extending-the-router': 'Extending the Router', }; diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx new file mode 100644 index 00000000000..5220e12d11c --- /dev/null +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -0,0 +1,778 @@ +--- +title: 'Extending the Router' +--- + +import { Tabs } from '@theguild/components' + +# Extending the Router + +Hive Router is designed to be flexible and extensible, allowing you to customize its behavior to fit +your specific needs. This guide explores various ways to extend the router's functionality, +including custom plugins. + +## Custom Builds with Rust Plugins + +Hive Router is built using Rust, which allows for high performance and safety. One of the powerful +features of Hive Router is the ability to create custom builds with your own Rust plugins. This +enables you to add new capabilities or modify existing ones to better suit your requirements. + +### Development Setup + +## Create a new Rust project + +First, ensure you have the necessary development environment set up for +[Rust 1.91.1 or later](https://rust-lang.org/tools/install/). Then, you need to create a new Rust +project for your custom router; + +```bash +cargo new --bin my_custom_router +cd my_custom_router +``` + +Install `hive-router` as a dependency by adding it to your `Cargo.toml` file: + +```toml +[dependencies] +hive-router = "0.17" +``` + +You can use our example supergraph as a starting point; + +```bash +curl -sSL https://federation-demo.theguild.workers.dev/supergraph.graphql > supergraph.graphql +``` + +Then point to that supergraph in your `router.config.yaml`: + +```yaml filename="router.config.yaml" +supergraph: + source: file + path: ./supergraph.graphql +``` + +Or you can use other ways to provide the supergraph, see +[Supergraph Sources](https://the-guild.dev/graphql/hive/docs/router/supergraph). + +## Create an entrypoint for your custom router + +Next, you need to create an entrypoint for your custom router. This is where you'll initialize the +router and register your plugins. Create a new file `src/main.rs` and add the following code: + +```rust +use hive_router::{PluginRegistry, router_entrypoint}; + +#[ntex::main] +async fn main() -> Result<(), Box> { + /// This is where you can register your custom plugins + let plugin_registry = PluginRegistry::new(); + /// Start the Hive Router with the plugin registry + match router_entrypoint(Some(plugin_registry)).await { + Ok(_) => Ok(()), + Err(err) => { + eprintln!("Failed to start Hive Router:\n {}", err); + + Err(err) + } + } +} +``` + +## Run your custom router + +Finally, you can build and run your custom router using Cargo: + +```bash +cargo run +``` + +## Configure your plugins in `router.config.yaml` + +`plugins` section in your `router.config.yaml` allows you to configure your custom plugins. Here is +an example configuration: + +```yaml filename="router.config.yaml" +plugins: + my_custom_plugin: + option1: value1 + option2: value2 +``` + +### Designing the Plugin + +During this phase of development, you need to learn more about the Hive Router plugin system and how +the hooks are structured. + +#### Hive Router Hooks Lifecycle + +We have the following lifecycle hooks available for plugins: + +```mermaid +flowchart TD + A[on_http_request] --> B[on_graphql_params] + B --> D[on_graphql_parse] + D --> F[on_graphql_validation] + F --> H[on_query_plan] + H --> J[on_execute] + J --> K[on_subgraph_execute] + K --> L[on_subgraph_http_request] + L --> K + K --> J + J --> A +``` + +In addition there is also `on_supergraph_load` hook that is called when the supergraph is loaded or +reloaded. + +#### Hooks Reference + +##### `on_http_request` + +This hook is called immediately after the router receives an HTTP request. It allows you to inspect +or modify the request before any further processing occurs. Remember that, we don't know yet if the +request is a GraphQL request or not. + +On the start of this hook, you can do the following things for example; + +- Implement custom authentication and authorization logic based on HTTP headers, method, path, etc. +- Short-circuit the request by providing a custom response (for example for health checks or + metrics, or custom playgrounds like Apollo Sandbox, Altair etc.) + +On the end of this hook, you can do the following things for example; + +- Header propagation from the incoming request to the final response. +- Custom HTTP Caching headers for your response caching plugin that also uses those headers. +- Handle deserialization for different content types other than JSON + +You can check the following example implementations; + +- `apollo-sandbox` plugin that serves Apollo Sandbox in a custom endpoint using `on_http_request` + hook +- `propagate-status-code` plugin that propagates status codes from subgraphs to clients using the + end phase of `on_http_request` hook by manipulating the final response. + +##### `on_graphql_params` + +This hook is called after the router has determined that the incoming request is a GraphQL request +and it decides to parse the GraphQL parameters (query, variables, operation name, etc.). Here you +can; + +On this hook's start; + +- Handle a specific validation based on the GraphQL parameters +- Inspect or modify the raw body +- Short-circuit the request by providing a custom response (for example for caching) +- Custom request parsing like Multipart requests + +On this hook's end; + +- Persisted Operations or Trusted Documents so that you can get the operation key from the body, and + put the actual `query` string into the GraphQL parameters body. Then the router can continue + processing the request as usual. +- Max Tokens security check that counts the tokens in the operation and rejects the request if it + exceeds a certain limit. +- Any auth logic that relies on `extensions` or other GraphQL parameters instead of HTTP-specific + `headers`. +- A potential response caching plugin that caches based on GraphQL parameters, so that it returns + the response before any further steps like parsing, validation, execution. + +You can check the following example implementations; + +- `forbid-anon-ops` example that checks the parsed `operation_name` in the end payload of this hook + and rejects the operations without operation names. +- `apq` example shows how to implement a simple Automatic Persisted Queries plugin using this hook. + So it takes the hash from the extensions from the parsed body, and looks up the actual query from + a map then puts it into the GraphQL parameters. +- `multipart` plugin that parses the body using `multer`, and holds the file bytes in the context + then fetches then lazily when needed during the subgraph execution. So it shows how to override + parsing logic using this hook. +- `async_auth` plugin that shows how to implement a custom auth logic + +##### `on_graphql_parse` + +This hook is called after the deserialization of the request, and the router has parsed the GraphQL +parameters body expected by GraphQL-over-HTTP spec. But we still need to parse the operation into +AST. + +On the start of this hooks, you can do the following things for example; + +- Some kind of trusted documents implementation that holds not the string representation of the + query, but the parsed AST in a map. So when you get the query string from the GraphQL parameters, + you can look up the AST from the map and put it into the context. Then the router can skip parsing + step. +- Replace or extend the parser for some future RFCs like Fragment Variables etc. + +On the end of this hook, you can do the following things for example; + +- Prevent certain operations from being executed by checked the HTTP headers or other request + properties along with the parsed AST. +- Logging or metrics based on the parsed AST like counting certain fields or directives usage. + +##### `on_graphql_validation` + +This hook is called after the router is ready to validate the operation against the supergraph. In +this stage, we know the supergraph and the operation are ready for validation, then query planning +and execution if valid. + +On the start of this hook, you can do the following things for example; + +- Skip validation for certain operations based on request properties like headers, method, etc. +- Or skip validation for trusted documents/persisted operations since they are trusted +- Custom validation rules like `@oneOf` directive validation etc. +- Custom rules for max depth analysis, rate limiting based on the operation structure +- Preventing the introspection queries based on request properties +- Caching the validation result in some other ways, if cache hit, you can skip validation + +On the end of this hook, you can do the following things for example; + +- Bypassing certain errors based on request properties +- Again caching the validation result in some other ways + +You can check the following example implementations; + +- `root-field-limit` plugin that implements a new `ValidationRule` to limit the number of root + fields in an operation using this hook. +- `one-of` plugin is a more complicated one that combines this hook and `on_execute` hook to + implement custom validation and execution logic for `@oneOf` directive. + +##### `on_query_plan` + +This hook is invoked before the query planner starts the planning process. At this point, we have +the query planner, normalized document, the supergraph, and the public schema and all the parameters +needed for the planning. + +On the start of this hook, you can do the following things for example; + +- Modify the normalized document that is used for planning, or do some validation based on it since + this is the normalized, flattened version of the operation. + +On the end of this hook, you can do the following things for example; + +- Demand Control or Cost Limiting based on the subgraph requests that would be made for the + operation. + +You can check the following example implementations; + +- `root-field-limit` plugin that implements another variant of root field limiting using this hook + besides `on_graphql_validation`. + +##### `on_execute` + +Whenever the variables are coerced in addition to the operation being valid just like other +parameters. This hook is called before the execution starts. At this point, we have the query plan +ready along with the coerced variables. So you can block the operation, manipulate the result, +variables, etc. + +This is different the end of `on_query_plan` hook because we don't have all the parameters ready +like coerced variables, filling the introspection fields that are seperated from the actual planning +and execution etc. + +On the start of this hook, you can do the following things for example; + +- Response Caching based on the query plan or the operation AST together with the coerced variables, + and some auth info from the request. +- Blocking certain operations based on the query plan structure together with the coerced variables. + +On the end of this hook, you can do the following things for example; + +- Add extra metadata to the response based on the execution result like response cache info, tracing + info, some information about the query plan used etc. + +You can check the following example implementations; + +- `one_of` plugin that implements the execution logic for `@oneOf` directive using this hook. + `on_graphql_validation` is not enough by itself because the coerced variables need to be checked + as well. +- `response_caching` plugin that implements a simple response caching mechanism using this hook. + +##### `on_subgraph_execute` + +This hook is called before an execution request is prepared for a subgraph based on the query plan. +At this point, we have the subgraph name, the execution request that would be sent to the subgraph, +and other contextual information. + +But we still don't have the actual "HTTP" request that would be sent to the subgraph. So this is +different than `on_subgraph_http_request` hook. So this is before the serialization to HTTP request. +On the other hand, at the end of this hook, we have deserialized version of the subgraph response, +but not the actual HTTP response. + +On the start of this hook, you can do the following things for example; + +- Mocking the subgraph response based on the execution request and other request properties. +- Demand Control and Cost Limiting based on the execution request and other request properties. +- Handling a custom subgraph auth like HMAC and JWT based auth by manipulating the execution request + headers. +- Custom subgraph execution logic for different transports like protobuf instead of JSON over HTTP. +- APQ for subgraph requests by storing the persisted queries for subgraphs. +- Block some subgraph operations based on some policy. This is the right time before the + serialization. + +On the end of this hook, you can do the following things for example; + +- Any work that needs to be done such as collecting `extensions` data from subgraph responses for + logging, metrics, tracing etc. + +You can check the following example implementations; + +- `subgraph_response_cache` plugin that implements a simple subgraph response caching mechanism + using this hook. +- `context_data` plugin that shows how to pass data between the main request lifecycle and subgraph + execution using this hook. + +##### `on_subgraph_http_request` + +After the subgraph execution request is serialized into an actual HTTP request, this hook is +invoked. At this point, you have access to the full HTTP request that will be sent to the subgraph. + +On the start of this hook, you can do the following things for example; + +- Send custom headers to subgraphs based on the main request properties. +- Some custom HTTP-based subgraph auth mechanisms like AWS Sigv4 that generates signature based on + HTTP properties and the payload +- Change the HTTP method, URL, or other properties based on some logic. +- Choose different endpoints based on the request parameters like region, version, etc. +- HTTP based caching +- Limit the request size based on some policy. +- Replace the default HTTP transport with some custom transport like multipart/form-data for file + uploads. + +On the end of this hook, you can do the following things for example; + +- Header propagation but this time from the subgraph response to the main response. +- Forward cookies from subgraph responses to the main response. +- HTTP Caching's response side based on HTTP response headers like ETag, Cache-Control etc. +- Respecting TTL returned by the subgraph in your response caching plugin. So you can decide on the + final TTL based on subgraph response headers. + +You can check the following example implementations; + +- `propagate-status-code` plugin that propagates status codes from subgraphs to clients using the + end phase of this hook by manipulating the final response. +- `multipart` plugin that overrides the default deserialization logic of Hive Router to handle + `multipart/form-data` requests from the client, and it holds the files in the context for lazy + fetching during subgraph execution. In this hook, it re-uses the files from the context to prepare + the subgraph HTTP request, and replaces the default JSON-based HTTP transport with + `multipart/form-data` when needed. + +#### Short Circuit Responses + +In many of the hooks mentioned above, you have the ability to short-circuit the request processing +by providing a custom response. + +Let's say you want to implement a plugin that returns an early error response if it doesn't have a +specific operation name. So basically this plugin rejects anonymous operations. See the highlighted +code below; + +```rust filename="src/forbid_anon_ops.rs" {33} +#[async_trait::async_trait] +impl RouterPlugin for ForbidAnonymousOperationsPlugin { + async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> { + // After the GraphQL parameters have been parsed, we can check if the operation is anonymous + // So we use `on_end` + payload.on_end(|payload| { + let maybe_operation_name = &payload + .graphql_params + .operation_name + .as_ref(); + + if maybe_operation_name + .is_none_or(|operation_name| operation_name.is_empty()) + { + // let's log the error + tracing::error!("Operation is not allowed!"); + + // Prepare an HTTP 400 response with a GraphQL error message + let body = json!({ + "errors": [ + { + "message": "Anonymous operations are not allowed", + "extensions": { + "code": "ANONYMOUS_OPERATION" + } + } + ] + }); + // Here we short-circuit the request processing by returning an early response + return payload.end_response(HttpResponse { + body: body.to_string().into(), + headers: http::HeaderMap::new(), + status: StatusCode::BAD_REQUEST, + }); + } + // we're good to go! + tracing::info!("operation is allowed!"); + payload.cont() + }) + } +} +``` + +Here we use `end_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom HTTP +response. Then the router will skip all further processing and return this response to the client. + +#### Overriding Default Behavior + +Instead of short-circuiting the entire HTTP request, and returning an early HTTP response, you might +want to override default behavior in certain stages. + +For example, in case of automatic persisted queries, you basically need to manipulate the parsed +request body received from the client. In this case, you need to modify the `query` field in the +`GraphQLParams` struct to put the actual query string into it based on the hash received from the +client. + +In the following example, we implement a simple APQ plugin that uses an in-memory cache to store the +persisted queries. When a request comes in with a hash, it looks up the query from the cache and +puts it into the `GraphQLParams`. If the query is not found, it returns an error response. + +```rust filename="src/apq.rs" {1,2,12-88} +struct APQPlugin { + cache: DashMap, +} + +#[async_trait::async_trait] +impl RouterPlugin for APQPlugin { + async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> { + payload.on_end(|mut payload| { + let persisted_query_ext = payload + .graphql_params + .extensions + .as_ref() + .and_then(|ext| ext.get("persistedQuery")) + .and_then(|pq| pq.as_object()); + if let Some(persisted_query_ext) = persisted_query_ext { + match persisted_query_ext.get(&"version").and_then(|v| v.as_i64()) { + Some(1) => {} + _ => { + let body = json!({ + "errors": [ + { + "message": "Unsupported persisted query version", + "extensions": { + "code": "UNSUPPORTED_PERSISTED_QUERY_VERSION" + } + } + ] + }); + return payload.end_response(HttpResponse { + body: body.to_string().into_bytes().into(), + status: StatusCode::BAD_REQUEST, + headers: http::HeaderMap::new(), + }); + } + } + let sha256_hash = match persisted_query_ext + .get(&"sha256Hash") + .and_then(|h| h.as_str()) + { + Some(h) => h, + None => { + let body = json!({ + "errors": [ + { + "message": "Missing sha256Hash in persisted query", + "extensions": { + "code": "MISSING_PERSISTED_QUERY_HASH" + } + } + ] + }); + return payload.end_response(HttpResponse { + body: body.to_string().into_bytes().into(), + status: StatusCode::BAD_REQUEST, + headers: http::HeaderMap::new(), + }); + } + }; + if let Some(query_param) = &payload.graphql_params.query { + // Store the query in the cache + self.cache + .insert(sha256_hash.to_string(), query_param.to_string()); + } else { + // Try to get the query from the cache + if let Some(cached_query) = self.cache.get(sha256_hash) { + // Update the graphql_params with the cached query + payload.graphql_params.query = Some(cached_query.value().to_string()); + } else { + let body = json!({ + "errors": [ + { + "message": "PersistedQueryNotFound", + "extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ] + }); + return payload.end_response(HttpResponse { + body: body.to_string().into_bytes().into(), + status: StatusCode::NOT_FOUND, + headers: http::HeaderMap::new(), + }); + } + } + } + + payload.cont() + }) + } +} +``` + +#### Context Data Sharing + +Sometimes, you might want to share data between different hooks or stages of the request processing. +Hive Router provides a way to store and retrieve custom data in the request context, allowing you to +pass information between hooks. + +For example, you can store some data in the context during the `on_graphql_params` hook and retrieve +it later in the `on_subgraph_execute` hook. + +```rust filename="src/context_data.rs" {1,2,12-55} +pub struct ContextData { + incoming_data: String, + response_count: u64, +} + +#[async_trait::async_trait] +impl RouterPlugin for ContextDataPlugin { + async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> { + let context_data = ContextData { + incoming_data: "world".to_string(), + response_count: 0, + }; + + payload.context.insert(context_data); + + payload.on_end(|payload| { + let context_data = payload.context.get_mut::(); + if let Some(mut context_data) = context_data { + context_data.response_count += 1; + tracing::info!("subrequest count {}", context_data.response_count); + } + payload.cont() + }) + } + async fn on_subgraph_execute<'exec>( + &'exec self, + mut payload: OnSubgraphExecuteStartHookPayload<'exec>, + ) -> OnSubgraphExecuteStartHookResult<'exec> { + let context_data_entry = payload.context.get_ref::(); + if let Some(ref context_data_entry) = context_data_entry { + tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world! + let new_header_value = format!("Hello {}", context_data_entry.incoming_data); + payload.execution_request.headers.insert( + "x-hello", + http::HeaderValue::from_str(&new_header_value).unwrap(), + ); + } + payload.on_end(|payload: OnSubgraphExecuteEndHookPayload<'exec>| { + let context_data = payload.context.get_mut::(); + if let Some(mut context_data) = context_data { + context_data.response_count += 1; + tracing::info!("subrequest count {}", context_data.response_count); + } + payload.cont() + }) + } +} +``` + +In the example above, we define a `ContextData` struct to hold our custom data. In the +`on_graphql_params` hook, we create an instance of `ContextData` and insert it into the request +context. Later, in the `on_subgraph_execute` hook, we retrieve the `ContextData` from the context +and use its values to modify the subgraph execution request. + +`context` provides a convenient way to share data between different hooks and stages of the request +processing, enabling more complex and stateful plugin behavior. + +`context.insert`, `context.get_ref`, `context.get_mut` methods are used to store and +retrieve data of type `T` in the request context. + +- `insert(&self, data: T)` - Inserts data of type `T` into the context. +- `get_ref(&self) -> Option<&T>` - Retrieves a reference to the data of type `T` from the + context. +- `get_mut(&mut self) -> Option<&mut T>` - Retrieves a mutable reference to the data of type `T` + from the context. + +#### Configuration of Plugins + +Plugins can be configured via the `router.config.yaml` file. Each plugin can have its own entry +under `plugins` section, where you can specify various options and settings specific to that plugin. + +The configuration `struct` should be `serde` compliant, so that it can be deserialized from the YAML +file. + +Let's say we have a custom auth logic that checks the expected header values from a file +dynamically. + +```rust + +pub struct AllowClientIdFromFilePlugin { + header_key: String, + allowed_ids_path: PathBuf, +} + +#[async_trait::async_trait] +impl RouterPlugin for AllowClientIdFromFilePlugin { + // Whenever it is a GraphQL request, + // We don't use on_http_request here because we want to run this only when it is a GraphQL request + async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> { + let header = payload.router_http_request.headers.get(&self.header_key); + match header { + Some(client_id) => { + let client_id_str = client_id.to_str(); + match client_id_str { + Ok(client_id) => { + let allowed_clients: Vec = serde_json::from_str( + std::fs::read_to_string(self.allowed_ids_path.clone()) + .unwrap() + .as_str(), + ) + .unwrap(); + + if !allowed_clients.contains(&client_id.to_string()) { + // Prepare an HTTP 403 response with a GraphQL error message + let body = json!( + { + "errors": [ + { + "message": "client-id is not allowed", + "extensions": { + "code": "UNAUTHORIZED_CLIENT_ID" + } + } + ] + } + ); + return payload.end_response(HttpResponse { + body: sonic_rs::to_vec(&body).unwrap_or_default().into(), + headers: http::HeaderMap::new(), + status: http::StatusCode::FORBIDDEN, + }); + } + } + Err(_not_a_string_error) => { + let message = format!("'{}' value is not a string", &self.header_key); + tracing::error!(message); + let body = json!( + { + "errors": [ + { + "message": message, + "extensions": { + "code": "BAD_CLIENT_ID" + } + } + ] + } + ); + return payload.end_response(HttpResponse { + body: sonic_rs::to_vec(&body).unwrap_or_default().into(), + headers: http::HeaderMap::new(), + status: http::StatusCode::BAD_REQUEST, + }); + } + } + } + None => { + let message = format!("Missing '{}' header", &self.header_key); + tracing::error!(message); + let body = json!( + { + "errors": [ + { + "message": message, + "extensions": { + "code": "AUTH_ERROR" + } + } + ] + } + ); + return payload.end_response(HttpResponse { + body: sonic_rs::to_vec(&body).unwrap_or_default().into(), + headers: http::HeaderMap::new(), + status: http::StatusCode::UNAUTHORIZED, + }); + } + } + payload.cont() + } +} +``` + +So we can have a configuration struct like below; + +```rust filename="src/dynamic_auth.rs" +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct AllowClientIdConfig { + pub enabled: bool, + pub header: String, + pub path: String, +} +``` + +Then attach it to the plugin struct; + +```rust filename="src/dynamic_auth.rs" +impl RouterPluginWithConfig for AllowClientIdFromFilePlugin { + type Config = AllowClientIdConfig; + fn plugin_name() -> &'static str { + "allow_client_id_from_file" + } + fn from_config(config: AllowClientIdConfig) -> Option { + if config.enabled { + Some(AllowClientIdFromFilePlugin { + header_key: config.header, + allowed_ids_path: PathBuf::from(config.path), + }) + } else { + None + } + } +} +``` + +`plugin_name` method should return the name of the plugin as it appears in the `router.config.yaml` +file. The `from_config` method is responsible for creating an instance of the plugin from the +provided configuration. If `from_config` returns `None`, the plugin will not be registered. + +With this setup, you can now configure the `allow_client_id_from_file` plugin in your +`router.config.yaml` file like this: + +```yaml filename="router.config.yaml" +plugins: + allow_client_id_from_file: + enabled: true + header: 'x-client-id' +``` + +#### Registration of Plugins + +Finally, to use your custom plugin, you need to register it with the `PluginRegistry` in your +`main.rs` file. + +```rust filename="src/main.rs" {9-14} + let mut plugin_registry = PluginRegistry::new(); + // Register your custom plugin + plugin_registry.register_plugin::(); +``` + +Then pass the `plugin_registry` to the `router_entrypoint` function as shown earlier. + +```rust filename="src/main.rs" {15} + match router_entrypoint(Some(plugin_registry)).await { +``` From 4bc6bf9a28072a40b959ed491fe0d4a98d50c24b Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 8 Dec 2025 16:20:58 +0300 Subject: [PATCH 02/19] Go --- .../guides/extending-the-router/index.mdx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 5220e12d11c..0f61c1a04cf 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -18,7 +18,7 @@ enables you to add new capabilities or modify existing ones to better suit your ### Development Setup -## Create a new Rust project +#### Create a new Rust project First, ensure you have the necessary development environment set up for [Rust 1.91.1 or later](https://rust-lang.org/tools/install/). Then, you need to create a new Rust @@ -53,7 +53,7 @@ supergraph: Or you can use other ways to provide the supergraph, see [Supergraph Sources](https://the-guild.dev/graphql/hive/docs/router/supergraph). -## Create an entrypoint for your custom router +#### Create an entrypoint for your custom router Next, you need to create an entrypoint for your custom router. This is where you'll initialize the router and register your plugins. Create a new file `src/main.rs` and add the following code: @@ -77,7 +77,7 @@ async fn main() -> Result<(), Box> { } ``` -## Run your custom router +#### Run your custom router Finally, you can build and run your custom router using Cargo: @@ -85,7 +85,7 @@ Finally, you can build and run your custom router using Cargo: cargo run ``` -## Configure your plugins in `router.config.yaml` +#### Configure your plugins in `router.config.yaml` `plugins` section in your `router.config.yaml` allows you to configure your custom plugins. Here is an example configuration: @@ -97,7 +97,7 @@ plugins: option2: value2 ``` -### Designing the Plugin +### Implementation of Plugins During this phase of development, you need to learn more about the Hive Router plugin system and how the hooks are structured. @@ -353,7 +353,12 @@ You can check the following example implementations; the subgraph HTTP request, and replaces the default JSON-based HTTP transport with `multipart/form-data` when needed. -#### Short Circuit Responses +#### Use Cases for Plugin Implementation + +Those hooks provide a lot of flexibility to implement various kinds of plugins. Here are some +example use cases and how you can implement them using the hooks mentioned above. + +##### Short Circuit Responses In many of the hooks mentioned above, you have the ability to short-circuit the request processing by providing a custom response. @@ -412,7 +417,7 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { Here we use `end_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom HTTP response. Then the router will skip all further processing and return this response to the client. -#### Overriding Default Behavior +##### Overriding Default Behavior Instead of short-circuiting the entire HTTP request, and returning an early HTTP response, you might want to override default behavior in certain stages. @@ -523,7 +528,7 @@ impl RouterPlugin for APQPlugin { } ``` -#### Context Data Sharing +##### Context Data Sharing Sometimes, you might want to share data between different hooks or stages of the request processing. Hive Router provides a way to store and retrieve custom data in the request context, allowing you to @@ -602,7 +607,7 @@ retrieve data of type `T` in the request context. - `get_mut(&mut self) -> Option<&mut T>` - Retrieves a mutable reference to the data of type `T` from the context. -#### Configuration of Plugins +##### Configuration of Plugins Plugins can be configured via the `router.config.yaml` file. Each plugin can have its own entry under `plugins` section, where you can specify various options and settings specific to that plugin. @@ -760,7 +765,7 @@ plugins: header: 'x-client-id' ``` -#### Registration of Plugins +##### Registration of Plugins Finally, to use your custom plugin, you need to register it with the `PluginRegistry` in your `main.rs` file. From ea927457ca1710d96d965af6f65d2ace4e66a78b Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 8 Dec 2025 17:50:41 +0300 Subject: [PATCH 03/19] Better --- .../guides/extending-the-router/index.mdx | 109 +++++++++++++----- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 0f61c1a04cf..1d5ea6298c9 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -16,9 +16,7 @@ Hive Router is built using Rust, which allows for high performance and safety. O features of Hive Router is the ability to create custom builds with your own Rust plugins. This enables you to add new capabilities or modify existing ones to better suit your requirements. -### Development Setup - -#### Create a new Rust project +### Create a new Rust project First, ensure you have the necessary development environment set up for [Rust 1.91.1 or later](https://rust-lang.org/tools/install/). Then, you need to create a new Rust @@ -53,7 +51,7 @@ supergraph: Or you can use other ways to provide the supergraph, see [Supergraph Sources](https://the-guild.dev/graphql/hive/docs/router/supergraph). -#### Create an entrypoint for your custom router +### Create an entrypoint for your custom router Next, you need to create an entrypoint for your custom router. This is where you'll initialize the router and register your plugins. Create a new file `src/main.rs` and add the following code: @@ -77,7 +75,7 @@ async fn main() -> Result<(), Box> { } ``` -#### Run your custom router +### Run your custom router Finally, you can build and run your custom router using Cargo: @@ -85,7 +83,7 @@ Finally, you can build and run your custom router using Cargo: cargo run ``` -#### Configure your plugins in `router.config.yaml` +### Configure your plugins in `router.config.yaml` `plugins` section in your `router.config.yaml` allows you to configure your custom plugins. Here is an example configuration: @@ -97,12 +95,10 @@ plugins: option2: value2 ``` -### Implementation of Plugins - During this phase of development, you need to learn more about the Hive Router plugin system and how the hooks are structured. -#### Hive Router Hooks Lifecycle +## Hive Router Hooks Lifecycle We have the following lifecycle hooks available for plugins: @@ -123,9 +119,7 @@ flowchart TD In addition there is also `on_supergraph_load` hook that is called when the supergraph is loaded or reloaded. -#### Hooks Reference - -##### `on_http_request` +### `on_http_request` This hook is called immediately after the router receives an HTTP request. It allows you to inspect or modify the request before any further processing occurs. Remember that, we don't know yet if the @@ -150,7 +144,7 @@ You can check the following example implementations; - `propagate-status-code` plugin that propagates status codes from subgraphs to clients using the end phase of `on_http_request` hook by manipulating the final response. -##### `on_graphql_params` +### `on_graphql_params` This hook is called after the router has determined that the incoming request is a GraphQL request and it decides to parse the GraphQL parameters (query, variables, operation name, etc.). Here you @@ -187,7 +181,7 @@ You can check the following example implementations; parsing logic using this hook. - `async_auth` plugin that shows how to implement a custom auth logic -##### `on_graphql_parse` +### `on_graphql_parse` This hook is called after the deserialization of the request, and the router has parsed the GraphQL parameters body expected by GraphQL-over-HTTP spec. But we still need to parse the operation into @@ -207,7 +201,7 @@ On the end of this hook, you can do the following things for example; properties along with the parsed AST. - Logging or metrics based on the parsed AST like counting certain fields or directives usage. -##### `on_graphql_validation` +### `on_graphql_validation` This hook is called after the router is ready to validate the operation against the supergraph. In this stage, we know the supergraph and the operation are ready for validation, then query planning @@ -234,7 +228,7 @@ You can check the following example implementations; - `one-of` plugin is a more complicated one that combines this hook and `on_execute` hook to implement custom validation and execution logic for `@oneOf` directive. -##### `on_query_plan` +### `on_query_plan` This hook is invoked before the query planner starts the planning process. At this point, we have the query planner, normalized document, the supergraph, and the public schema and all the parameters @@ -255,7 +249,7 @@ You can check the following example implementations; - `root-field-limit` plugin that implements another variant of root field limiting using this hook besides `on_graphql_validation`. -##### `on_execute` +### `on_execute` Whenever the variables are coerced in addition to the operation being valid just like other parameters. This hook is called before the execution starts. At this point, we have the query plan @@ -284,7 +278,7 @@ You can check the following example implementations; as well. - `response_caching` plugin that implements a simple response caching mechanism using this hook. -##### `on_subgraph_execute` +### `on_subgraph_execute` This hook is called before an execution request is prepared for a subgraph based on the query plan. At this point, we have the subgraph name, the execution request that would be sent to the subgraph, @@ -318,7 +312,7 @@ You can check the following example implementations; - `context_data` plugin that shows how to pass data between the main request lifecycle and subgraph execution using this hook. -##### `on_subgraph_http_request` +### `on_subgraph_http_request` After the subgraph execution request is serialized into an actual HTTP request, this hook is invoked. At this point, you have access to the full HTTP request that will be sent to the subgraph. @@ -353,12 +347,29 @@ You can check the following example implementations; the subgraph HTTP request, and replaces the default JSON-based HTTP transport with `multipart/form-data` when needed. -#### Use Cases for Plugin Implementation +### `on_supergraph_load` + +This hook is called whenever the supergraph is loaded or reloaded. This can happen at startup or +when the supergraph configuration changes. + +You can do the following things for example; + +- Precalculate some data based on the supergraph schema that would be used later during request + processing such as TTL calculation based on `@cacheControl` directives for response caching + plugin. +- In addition to above, you can also precalculate cost analysis data for cost limiting plugins. +- You can also refresh the state of the plugin based on the supergraph changes, for example the + caching plugins can clear their caches. + +You can check the following example implementations; -Those hooks provide a lot of flexibility to implement various kinds of plugins. Here are some -example use cases and how you can implement them using the hooks mentioned above. +- `one_of` plugin that precalculates the `@oneOf` directive locations in the supergraph schema for + faster access during request processing. So it avoids traversing the schema for each request, and + refreshes the state whenever the supergraph is reloaded. +- `response_cache` plugin that precalculates the TTL information based on `@cacheControl` directives + in the supergraph schema for response caching. -##### Short Circuit Responses +## Short Circuit Responses In many of the hooks mentioned above, you have the ability to short-circuit the request processing by providing a custom response. @@ -417,7 +428,7 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { Here we use `end_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom HTTP response. Then the router will skip all further processing and return this response to the client. -##### Overriding Default Behavior +## Overriding Default Behavior Instead of short-circuiting the entire HTTP request, and returning an early HTTP response, you might want to override default behavior in certain stages. @@ -528,7 +539,7 @@ impl RouterPlugin for APQPlugin { } ``` -##### Context Data Sharing +## Context Data Sharing Sometimes, you might want to share data between different hooks or stages of the request processing. Hive Router provides a way to store and retrieve custom data in the request context, allowing you to @@ -607,7 +618,53 @@ retrieve data of type `T` in the request context. - `get_mut(&mut self) -> Option<&mut T>` - Retrieves a mutable reference to the data of type `T` from the context. -##### Configuration of Plugins +## Refresh State on Supergraph Reload + +Plugins can refresh their internal state whenever the supergraph is reloaded. This is useful for +plugins that depend on the supergraph schema or configuration. + +The following code is from `response_cache` example plugin refreshes `ttl_per_type` map whenever the +supergraph is reloaded by visiting the schema and looking for `@cacheControl` directives. + +```rust +pub struct ResponseCachePlugin { + /// ... + ttl_per_type: DashMap, +} + + +/// ... + fn on_supergraph_reload<'a>( + &'a self, + payload: OnSupergraphLoadStartHookPayload, + ) -> OnSupergraphLoadStartHookResult<'a> { + // Visit the schema and update ttl_per_type based on some directive + payload.new_ast.definitions.iter().for_each(|def| { + if let graphql_parser::schema::Definition::TypeDefinition(type_def) = def { + if let graphql_parser::schema::TypeDefinition::Object(obj_type) = type_def { + for directive in &obj_type.directives { + if directive.name == "cacheControl" { + for arg in &directive.arguments { + if arg.0 == "maxAge" { + if let graphql_parser::query::Value::Int(max_age) = &arg.1 { + if let Some(max_age) = max_age.as_i64() { + self.ttl_per_type + .insert(obj_type.name.clone(), max_age as u64); + } + } + } + } + } + } + } + } + }); + + payload.cont() + } +``` + +## Configuration of Plugins Plugins can be configured via the `router.config.yaml` file. Each plugin can have its own entry under `plugins` section, where you can specify various options and settings specific to that plugin. From 8f889b2131549c4a26252e94428c6e4deda5393b Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 8 Dec 2025 17:50:58 +0300 Subject: [PATCH 04/19] Lets go --- .../src/content/router/guides/extending-the-router/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 1d5ea6298c9..8e4d846052c 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -822,7 +822,7 @@ plugins: header: 'x-client-id' ``` -##### Registration of Plugins +## Registration of Plugins Finally, to use your custom plugin, you need to register it with the `PluginRegistry` in your `main.rs` file. From 30f93a50e6e2ed53b5e2062e028bf5e871b16844 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 8 Dec 2025 18:25:31 +0300 Subject: [PATCH 05/19] .. --- .../src/content/router/guides/extending-the-router/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 8e4d846052c..d766286b881 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -98,7 +98,7 @@ plugins: During this phase of development, you need to learn more about the Hive Router plugin system and how the hooks are structured. -## Hive Router Hooks Lifecycle +## Hooks Lifecycle We have the following lifecycle hooks available for plugins: From 0b7fa8ea8375b31ef96cc6ae51ee89faafd81daf Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 16:38:17 +0300 Subject: [PATCH 06/19] Update docs --- .../guides/extending-the-router/index.mdx | 268 +++++++++++------- 1 file changed, 167 insertions(+), 101 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index d766286b881..b26586ce1f1 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -369,6 +369,21 @@ You can check the following example implementations; - `response_cache` plugin that precalculates the TTL information based on `@cacheControl` directives in the supergraph schema for response caching. +### `on_shutdown` + +This hook is called when the router is shutting down. If your plugin holds resources that need to be +cleaned up, you can implement this method to perform the necessary cleanup. + +You can do the following things for example; + +- Close database connections or file handles. +- Flush any pending logs or metrics. +- Perform any other necessary cleanup tasks to ensure a graceful shutdown. + +You can check the following example implementations; + +- `usage_reporting` plugin that flushes any pending usage reports to the server before shutdown. + ## Short Circuit Responses In many of the hooks mentioned above, you have the ability to short-circuit the request processing @@ -411,11 +426,12 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { ] }); // Here we short-circuit the request processing by returning an early response - return payload.end_response(HttpResponse { - body: body.to_string().into(), - headers: http::HeaderMap::new(), - status: StatusCode::BAD_REQUEST, - }); + return payload.end_response( + Response::with_body( + StatusCode::BAD_REQUEST, + body.to_string().into(), + ) + ); } // we're good to go! tracing::info!("operation is allowed!"); @@ -426,7 +442,46 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { ``` Here we use `end_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom HTTP -response. Then the router will skip all further processing and return this response to the client. +response. But there are some helper methods to make it easier to create early responses. + +### Helpers for early responses + +To make it easier to create early responses, Hive Router provides some helper methods on the hook +payloads. + +- `end_response_body(T: Serialize)` - This method allows you to provide a response body that is + serializable using `serde`. It automatically serializes the body to JSON and sets the appropriate + headers. So if we want to write less code for the above example, we can do the following; + +```rust filename="src/forbid_anon_ops.rs" {33-52} + let body = json!({ + "errors": [ + { + "message": "Anonymous operations are not allowed", + "extensions": { + "code": "ANONYMOUS_OPERATION" + } + } + ] + }); + // Here we short-circuit the request processing by returning an early response + return payload.end_response_body( + StatusCode::BAD_REQUEST, + &body, + ); +``` + +- `end_graphql_error(error: GraphQLError, status: StatusCode)` - This method allows you to provide a + `GraphQLError` directly, and it constructs the appropriate GraphQL error response for you. + +```rust filename="src/forbid_anon_ops.rs" {33-52} + let graphql_error = GraphQLError::from_message_and_code("Anonymous operations are not allowed", "ANONYMOUS_OPERATION"); + // Here we short-circuit the request processing by returning an early response + return payload.end_graphql_error( + graphql_error, + StatusCode::BAD_REQUEST, + ); +``` ## Overriding Default Behavior @@ -464,21 +519,13 @@ impl RouterPlugin for APQPlugin { match persisted_query_ext.get(&"version").and_then(|v| v.as_i64()) { Some(1) => {} _ => { - let body = json!({ - "errors": [ - { - "message": "Unsupported persisted query version", - "extensions": { - "code": "UNSUPPORTED_PERSISTED_QUERY_VERSION" - } - } - ] - }); - return payload.end_response(HttpResponse { - body: body.to_string().into_bytes().into(), - status: StatusCode::BAD_REQUEST, - headers: http::HeaderMap::new(), - }); + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + "Unsupported persisted query version", + "UNSUPPORTED_PERSISTED_QUERY_VERSION", + ), + StatusCode::BAD_REQUEST, + ); } } let sha256_hash = match persisted_query_ext @@ -487,21 +534,13 @@ impl RouterPlugin for APQPlugin { { Some(h) => h, None => { - let body = json!({ - "errors": [ - { - "message": "Missing sha256Hash in persisted query", - "extensions": { - "code": "MISSING_PERSISTED_QUERY_HASH" - } - } - ] - }); - return payload.end_response(HttpResponse { - body: body.to_string().into_bytes().into(), - status: StatusCode::BAD_REQUEST, - headers: http::HeaderMap::new(), - }); + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + "Missing sha256Hash in persisted query", + "MISSING_PERSISTED_QUERY_HASH", + ), + StatusCode::BAD_REQUEST, + ); } }; if let Some(query_param) = &payload.graphql_params.query { @@ -514,21 +553,13 @@ impl RouterPlugin for APQPlugin { // Update the graphql_params with the cached query payload.graphql_params.query = Some(cached_query.value().to_string()); } else { - let body = json!({ - "errors": [ - { - "message": "PersistedQueryNotFound", - "extensions": { - "code": "PERSISTED_QUERY_NOT_FOUND" - } - } - ] - }); - return payload.end_response(HttpResponse { - body: body.to_string().into_bytes().into(), - status: StatusCode::NOT_FOUND, - headers: http::HeaderMap::new(), - }); + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + "Persisted query not found", + "PERSISTED_QUERY_NOT_FOUND", + ), + StatusCode::NOT_FOUND, + ); } } } @@ -676,7 +707,6 @@ Let's say we have a custom auth logic that checks the expected header values fro dynamically. ```rust - pub struct AllowClientIdFromFilePlugin { header_key: String, allowed_ids_path: PathBuf, @@ -705,68 +735,38 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { if !allowed_clients.contains(&client_id.to_string()) { // Prepare an HTTP 403 response with a GraphQL error message - let body = json!( - { - "errors": [ - { - "message": "client-id is not allowed", - "extensions": { - "code": "UNAUTHORIZED_CLIENT_ID" - } - } - ] - } + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + "client-id is not allowed", + "UNAUTHORIZED_CLIENT_ID", + ), + http::StatusCode::FORBIDDEN, ); - return payload.end_response(HttpResponse { - body: sonic_rs::to_vec(&body).unwrap_or_default().into(), - headers: http::HeaderMap::new(), - status: http::StatusCode::FORBIDDEN, - }); } } Err(_not_a_string_error) => { let message = format!("'{}' value is not a string", &self.header_key); tracing::error!(message); - let body = json!( - { - "errors": [ - { - "message": message, - "extensions": { - "code": "BAD_CLIENT_ID" - } - } - ] - } + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + &message, + "BAD_CLIENT_ID", + ), + http::StatusCode::BAD_REQUEST, ); - return payload.end_response(HttpResponse { - body: sonic_rs::to_vec(&body).unwrap_or_default().into(), - headers: http::HeaderMap::new(), - status: http::StatusCode::BAD_REQUEST, - }); } } } None => { let message = format!("Missing '{}' header", &self.header_key); tracing::error!(message); - let body = json!( - { - "errors": [ - { - "message": message, - "extensions": { - "code": "AUTH_ERROR" - } - } - ] - } + return payload.end_graphql_error( + GraphQLError::from_message_and_code( + &message, + "MISSING_CLIENT_ID", + ), + http::StatusCode::UNAUTHORIZED, ); - return payload.end_response(HttpResponse { - body: sonic_rs::to_vec(&body).unwrap_or_default().into(), - headers: http::HeaderMap::new(), - status: http::StatusCode::UNAUTHORIZED, - }); } } payload.cont() @@ -790,7 +790,7 @@ pub struct AllowClientIdConfig { Then attach it to the plugin struct; ```rust filename="src/dynamic_auth.rs" -impl RouterPluginWithConfig for AllowClientIdFromFilePlugin { +impl RouterPlugin for AllowClientIdFromFilePlugin { type Config = AllowClientIdConfig; fn plugin_name() -> &'static str { "allow_client_id_from_file" @@ -838,3 +838,69 @@ Then pass the `plugin_registry` to the `router_entrypoint` function as shown ear ```rust filename="src/main.rs" {15} match router_entrypoint(Some(plugin_registry)).await { ``` + +## Cleanup State on Shutdown + +If your plugin holds resources that need to be cleaned up, you can implement the `on_shutdown` +method to perform the necessary cleanup. + +For example, if your plugin opens a database connection or a file handle, you should close them +gracefully in the `on_shutdown` method. + +```rust filename="usage_reporting.rs" +struct UsageReportingPlugin { + endpoint: String, + reports: Mutex>, +} + +#[async_trait::async_trait] +impl RouterPlugin for UsageReportingPlugin { + type Config = UsageReportingPluginConfig; + fn plugin_name() -> &'static str { + "usage_reporting" + } + fn from_config(config: UsageReportingPluginConfig) -> Option { + if config.enabled { + Some(UsageReportingPlugin { + endpoint: config.endpoint, + reports: Default::default(), + }) + } else { + None + } + } + async fn on_execute<'exec>( + &'exec self, + payload: OnExecuteStartHookPayload<'exec>, + ) -> OnExecuteStartHookResult<'exec> { + self.reports.lock().await.push(UsageReport { + query: payload.operation_for_plan.to_string(), + operation_name: payload.operation_for_plan.name.clone(), + }); + tracing::trace!( + "Pushed usage report for operation: {:?}", + payload.operation_for_plan.name + ); + payload.cont() + } + // This method is called when the router is shutting down + async fn on_shutdown<'exec>(&'exec self) { + println!("Disposing UsageReportingPlugin and sending usage report"); + // Here you would gather and send the usage report + let reports = self.reports.lock().await; + match reqwest::Client::new() + .post(&self.endpoint) + .json(reports.as_slice()) + .send() + .await + { + Ok(response) => { + tracing::trace!("Usage report sent successfully: {:?}", response); + } + Err(e) => { + tracing::trace!("Failed to send usage report: {:?}", e); + } + } + } +} +``` From ee71354054e3a4cf11602693b87666cb46df51ee Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 16:44:10 +0300 Subject: [PATCH 07/19] Correct --- .../content/router/guides/extending-the-router/index.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index b26586ce1f1..554cd3f3315 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -449,9 +449,10 @@ response. But there are some helper methods to make it easier to create early re To make it easier to create early responses, Hive Router provides some helper methods on the hook payloads. -- `end_response_body(T: Serialize)` - This method allows you to provide a response body that is - serializable using `serde`. It automatically serializes the body to JSON and sets the appropriate - headers. So if we want to write less code for the above example, we can do the following; +- `end_response_body(body: Serialize, status_code: StatusCode)` - This method allows you to provide + a response body that is serializable using `serde`. It automatically serializes the body to JSON + and sets the appropriate headers. So if we want to write less code for the above example, we can + do the following; ```rust filename="src/forbid_anon_ops.rs" {33-52} let body = json!({ From a04eec820a8fc1622e13951ba1aed7872c9b14c4 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 16:44:24 +0300 Subject: [PATCH 08/19] Go --- .../src/content/router/guides/extending-the-router/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 554cd3f3315..c8b0dee308b 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -468,7 +468,7 @@ payloads. // Here we short-circuit the request processing by returning an early response return payload.end_response_body( StatusCode::BAD_REQUEST, - &body, + body, ); ``` From 3accc4bc48723bf88adb5b2ae6f597996dd3ccd4 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 16:44:35 +0300 Subject: [PATCH 09/19] Correct --- .../src/content/router/guides/extending-the-router/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index c8b0dee308b..3894fa542ea 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -467,8 +467,8 @@ payloads. }); // Here we short-circuit the request processing by returning an early response return payload.end_response_body( - StatusCode::BAD_REQUEST, body, + StatusCode::BAD_REQUEST, ); ``` From 6dabb87cda6a168305091a89b162d872fb1f4d81 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 18:16:57 +0300 Subject: [PATCH 10/19] Update --- .../guides/extending-the-router/index.mdx | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 3894fa542ea..7b4a625d70e 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -661,7 +661,7 @@ supergraph is reloaded by visiting the schema and looking for `@cacheControl` di ```rust pub struct ResponseCachePlugin { /// ... - ttl_per_type: DashMap, + ttl_per_type: ArcSwap>, } @@ -670,6 +670,8 @@ pub struct ResponseCachePlugin { &'a self, payload: OnSupergraphLoadStartHookPayload, ) -> OnSupergraphLoadStartHookResult<'a> { + // Create a new map + let ttl_per_type = HashMap::new(); // Visit the schema and update ttl_per_type based on some directive payload.new_ast.definitions.iter().for_each(|def| { if let graphql_parser::schema::Definition::TypeDefinition(type_def) = def { @@ -680,7 +682,7 @@ pub struct ResponseCachePlugin { if arg.0 == "maxAge" { if let graphql_parser::query::Value::Int(max_age) = &arg.1 { if let Some(max_age) = max_age.as_i64() { - self.ttl_per_type + ttl_per_type .insert(obj_type.name.clone(), max_age as u64); } } @@ -691,11 +693,72 @@ pub struct ResponseCachePlugin { } } }); + self.ttl_per_type.store(ttl_per_type.into()) payload.cont() } ``` +> Notice that we use `ArcSwap` here, because the supergraph reload can happen at any time, so we +> want to make sure we use the same `ttl_per_type` map during request processing. + +How do we use this `ttl_per_type` map during request processing? + +```rust + async fn on_execute<'exec>( + &'exec self, + payload: OnExecuteStartHookPayload<'exec>, + ) -> OnExecuteStartHookResult<'exec> { + /** + * ... + */ + payload.on_end(|payload| { + /** + * ... + */ + if let Ok(serialized) = sonic_rs::to_vec(&payload.data) { + // Here we load the ttl_per_type map once + let ttl_per_type = self.ttl_per_type.load(); + trace!("Caching response for key: {}", key); + // Decide on the ttl somehow + // Get the type names + let mut max_ttl = 0; + + // Imagine this code is traversing the response data to find type names + if let Some(obj) = payload.data.as_object() { + if let Some(typename) = obj + .iter() + .position(|(k, _)| k == &TYPENAME_FIELD_NAME) + .and_then(|idx| obj[idx].1.as_str()) + { + if let Some(ttl) = ttl_per_type.get(typename) { + max_ttl = max_ttl.max(*ttl); + } + } + } + + // If no ttl found, default + if max_ttl == 0 { + max_ttl = self.default_ttl_seconds; + } + trace!("Using TTL of {} seconds for key: {}", max_ttl, key); + + // Insert the ttl into extensions for client awareness + payload.add_extension("response_cache_ttl", max_ttl); + + // Set the cache with the decided ttl + let result = conn.set_ex::<&str, Vec, ()>(&key, serialized, max_ttl); + if let Err(err) = result { + trace!("Failed to set cache for key {}: {}", key, err); + } else { + trace!("Cached response for key: {} with TTL: {}", key, max_ttl); + } + } + payload.cont() + }) + } +``` + ## Configuration of Plugins Plugins can be configured via the `router.config.yaml` file. Each plugin can have its own entry From d472f5311e49567bfc953f9e23134a0aa9a3c5bb Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 20:25:39 +0300 Subject: [PATCH 11/19] Lets go --- .../guides/extending-the-router/index.mdx | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 7b4a625d70e..0697f3654ff 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -426,7 +426,7 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { ] }); // Here we short-circuit the request processing by returning an early response - return payload.end_response( + return payload.end_with_response( Response::with_body( StatusCode::BAD_REQUEST, body.to_string().into(), @@ -435,24 +435,24 @@ impl RouterPlugin for ForbidAnonymousOperationsPlugin { } // we're good to go! tracing::info!("operation is allowed!"); - payload.cont() + payload.proceed() }) } } ``` -Here we use `end_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom HTTP -response. But there are some helper methods to make it easier to create early responses. +Here we use `end_with_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom +HTTP response. But there are some helper methods to make it easier to create early responses. ### Helpers for early responses To make it easier to create early responses, Hive Router provides some helper methods on the hook payloads. -- `end_response_body(body: Serialize, status_code: StatusCode)` - This method allows you to provide - a response body that is serializable using `serde`. It automatically serializes the body to JSON - and sets the appropriate headers. So if we want to write less code for the above example, we can - do the following; +- `end_with_response_body(body: Serialize, status_code: StatusCode)` - This method allows you to + provide a response body that is serializable using `serde`. It automatically serializes the body + to JSON and sets the appropriate headers. So if we want to write less code for the above example, + we can do the following; ```rust filename="src/forbid_anon_ops.rs" {33-52} let body = json!({ @@ -466,19 +466,20 @@ payloads. ] }); // Here we short-circuit the request processing by returning an early response - return payload.end_response_body( + return payload.end_with_response_body( body, StatusCode::BAD_REQUEST, ); ``` -- `end_graphql_error(error: GraphQLError, status: StatusCode)` - This method allows you to provide a - `GraphQLError` directly, and it constructs the appropriate GraphQL error response for you. +- `end_with_graphql_error(error: GraphQLError, status: StatusCode)` - This method allows you to + provide a `GraphQLError` directly, and it constructs the appropriate GraphQL error response for + you. ```rust filename="src/forbid_anon_ops.rs" {33-52} let graphql_error = GraphQLError::from_message_and_code("Anonymous operations are not allowed", "ANONYMOUS_OPERATION"); // Here we short-circuit the request processing by returning an early response - return payload.end_graphql_error( + return payload.end_with_graphql_error( graphql_error, StatusCode::BAD_REQUEST, ); @@ -520,7 +521,7 @@ impl RouterPlugin for APQPlugin { match persisted_query_ext.get(&"version").and_then(|v| v.as_i64()) { Some(1) => {} _ => { - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( "Unsupported persisted query version", "UNSUPPORTED_PERSISTED_QUERY_VERSION", @@ -535,7 +536,7 @@ impl RouterPlugin for APQPlugin { { Some(h) => h, None => { - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( "Missing sha256Hash in persisted query", "MISSING_PERSISTED_QUERY_HASH", @@ -554,7 +555,7 @@ impl RouterPlugin for APQPlugin { // Update the graphql_params with the cached query payload.graphql_params.query = Some(cached_query.value().to_string()); } else { - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( "Persisted query not found", "PERSISTED_QUERY_NOT_FOUND", @@ -565,7 +566,7 @@ impl RouterPlugin for APQPlugin { } } - payload.cont() + payload.proceed() }) } } @@ -597,22 +598,22 @@ impl RouterPlugin for ContextDataPlugin { response_count: 0, }; - payload.context.insert(context_data); + payload.proceedext.insert(context_data); payload.on_end(|payload| { - let context_data = payload.context.get_mut::(); + let context_data = payload.proceedext.get_mut::(); if let Some(mut context_data) = context_data { context_data.response_count += 1; tracing::info!("subrequest count {}", context_data.response_count); } - payload.cont() + payload.proceed() }) } async fn on_subgraph_execute<'exec>( &'exec self, mut payload: OnSubgraphExecuteStartHookPayload<'exec>, ) -> OnSubgraphExecuteStartHookResult<'exec> { - let context_data_entry = payload.context.get_ref::(); + let context_data_entry = payload.proceedext.get_ref::(); if let Some(ref context_data_entry) = context_data_entry { tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world! let new_header_value = format!("Hello {}", context_data_entry.incoming_data); @@ -622,12 +623,12 @@ impl RouterPlugin for ContextDataPlugin { ); } payload.on_end(|payload: OnSubgraphExecuteEndHookPayload<'exec>| { - let context_data = payload.context.get_mut::(); + let context_data = payload.proceedext.get_mut::(); if let Some(mut context_data) = context_data { context_data.response_count += 1; tracing::info!("subrequest count {}", context_data.response_count); } - payload.cont() + payload.proceed() }) } } @@ -695,7 +696,7 @@ pub struct ResponseCachePlugin { }); self.ttl_per_type.store(ttl_per_type.into()) - payload.cont() + payload.proceed() } ``` @@ -754,7 +755,7 @@ How do we use this `ttl_per_type` map during request processing? trace!("Cached response for key: {} with TTL: {}", key, max_ttl); } } - payload.cont() + payload.proceed() }) } ``` @@ -799,7 +800,7 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { if !allowed_clients.contains(&client_id.to_string()) { // Prepare an HTTP 403 response with a GraphQL error message - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( "client-id is not allowed", "UNAUTHORIZED_CLIENT_ID", @@ -811,7 +812,7 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { Err(_not_a_string_error) => { let message = format!("'{}' value is not a string", &self.header_key); tracing::error!(message); - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( &message, "BAD_CLIENT_ID", @@ -824,7 +825,7 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { None => { let message = format!("Missing '{}' header", &self.header_key); tracing::error!(message); - return payload.end_graphql_error( + return payload.end_with_graphql_error( GraphQLError::from_message_and_code( &message, "MISSING_CLIENT_ID", @@ -833,7 +834,7 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { ); } } - payload.cont() + payload.proceed() } } ``` @@ -945,7 +946,7 @@ impl RouterPlugin for UsageReportingPlugin { "Pushed usage report for operation: {:?}", payload.operation_for_plan.name ); - payload.cont() + payload.proceed() } // This method is called when the router is shutting down async fn on_shutdown<'exec>(&'exec self) { From 2a3a4c384317d3b13d6c81032d962fd760d18eb5 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 7 Jan 2026 20:26:41 +0300 Subject: [PATCH 12/19] Lets go --- .../src/content/router/guides/extending-the-router/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 0697f3654ff..965df309f40 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -57,10 +57,10 @@ Next, you need to create an entrypoint for your custom router. This is where you router and register your plugins. Create a new file `src/main.rs` and add the following code: ```rust -use hive_router::{PluginRegistry, router_entrypoint}; +use hive_router::{PluginRegistry, router_entrypoint, BoxError}; #[ntex::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), BoxError> { /// This is where you can register your custom plugins let plugin_registry = PluginRegistry::new(); /// Start the Hive Router with the plugin registry From dfd18f8f8d869fdd2a2882bc31551fa582c70550 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 15 Jan 2026 17:55:31 +0300 Subject: [PATCH 13/19] Fix context var --- .../content/router/guides/extending-the-router/index.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 965df309f40..c107a09eb72 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -598,10 +598,10 @@ impl RouterPlugin for ContextDataPlugin { response_count: 0, }; - payload.proceedext.insert(context_data); + payload.context.insert(context_data); payload.on_end(|payload| { - let context_data = payload.proceedext.get_mut::(); + let context_data = payload.context.get_mut::(); if let Some(mut context_data) = context_data { context_data.response_count += 1; tracing::info!("subrequest count {}", context_data.response_count); @@ -613,7 +613,8 @@ impl RouterPlugin for ContextDataPlugin { &'exec self, mut payload: OnSubgraphExecuteStartHookPayload<'exec>, ) -> OnSubgraphExecuteStartHookResult<'exec> { - let context_data_entry = payload.proceedext.get_ref::(); + // Get immutable reference from the context + let context_data_entry = payload.context.get_ref::(); if let Some(ref context_data_entry) = context_data_entry { tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world! let new_header_value = format!("Hello {}", context_data_entry.incoming_data); @@ -623,7 +624,7 @@ impl RouterPlugin for ContextDataPlugin { ); } payload.on_end(|payload: OnSubgraphExecuteEndHookPayload<'exec>| { - let context_data = payload.proceedext.get_mut::(); + let context_data = payload.context.get_mut::(); if let Some(mut context_data) = context_data { context_data.response_count += 1; tracing::info!("subrequest count {}", context_data.response_count); From 944711997d7c503273461f9cce3db5932fd3ba57 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 15:07:47 +0300 Subject: [PATCH 14/19] Update docs --- .../guides/extending-the-router/index.mdx | 695 ++++++++---------- 1 file changed, 311 insertions(+), 384 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index c107a09eb72..808c7e78d4b 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -10,7 +10,7 @@ Hive Router is designed to be flexible and extensible, allowing you to customize your specific needs. This guide explores various ways to extend the router's functionality, including custom plugins. -## Custom Builds with Rust Plugins +## Getting Started Hive Router is built using Rust, which allows for high performance and safety. One of the powerful features of Hive Router is the ability to create custom builds with your own Rust plugins. This @@ -27,13 +27,41 @@ cargo new --bin my_custom_router cd my_custom_router ``` -Install `hive-router` as a dependency by adding it to your `Cargo.toml` file: +### Installing Dependencies + +Install `hive-router` and `serde` as a dependency by adding it to your `Cargo.toml` file: ```toml [dependencies] -hive-router = "0.17" +hive-router = "0.0.39" +serde = "1" +``` + +### Create an entrypoint for your custom router + +Next, you need to create an entrypoint for your custom router. This is where you'll initialize the +router and register your plugins. Create a new file `src/main.rs` and add the following code: + +```rust +use hive_router::{ + error::RouterInitError, init_rustls_crypto_provider, router_entrypoint, DefaultGlobalAllocator, + PluginRegistry, +}; + +#[global_allocator] +static GLOBAL: DefaultGlobalAllocator = DefaultGlobalAllocator; + +#[ntex::main] +async fn main() -> Result<(), RouterInitError> { + init_rustls_crypto_provider(); + + /// Start the Hive Router with the plugin registry + router_entrypoint(PluginRegistry::new()).await +} ``` +### Provide an example supergraph + You can use our example supergraph as a starting point; ```bash @@ -51,36 +79,68 @@ supergraph: Or you can use other ways to provide the supergraph, see [Supergraph Sources](https://the-guild.dev/graphql/hive/docs/router/supergraph). -### Create an entrypoint for your custom router +### Create a custom plugin -Next, you need to create an entrypoint for your custom router. This is where you'll initialize the -router and register your plugins. Create a new file `src/main.rs` and add the following code: +Now you can create a custom plugin by implementing the `RouterPlugin` trait. For example, let's say +you want to create a plugin that logs the incoming GraphQL operations. You can create a new file +`src/plugin.rs` and add the following code: ```rust -use hive_router::{PluginRegistry, router_entrypoint, BoxError}; - -#[ntex::main] -async fn main() -> Result<(), BoxError> { - /// This is where you can register your custom plugins - let plugin_registry = PluginRegistry::new(); - /// Start the Hive Router with the plugin registry - match router_entrypoint(Some(plugin_registry)).await { - Ok(_) => Ok(()), - Err(err) => { - eprintln!("Failed to start Hive Router:\n {}", err); - - Err(err) - } +use hive_router::{ + async_trait, + plugins::{ + hooks::{ + on_graphql_params::{OnGraphQLParamsStartHookPayload, OnGraphQLParamsStartHookResult}, on_plugin_init::{OnPluginInitPayload, OnPluginInitResult} + }, + plugin_trait::{RouterPlugin, StartHookPayload}, + }, +}; + +#[derive(Default)] +pub struct MyPlugin; + +#[async_trait] +impl RouterPlugin for MyPlugin { + type Config = (); + fn plugin_name() -> &'static str { + "my_plugin" + } + fn on_plugin_init(payload: OnPluginInitPayload) -> OnPluginInitResult { + payload.initialize_plugin_with_defaults() + } + async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> { + tracing::info!("Received GraphQL operation: {:?}", payload.graphql_params.query); + payload.proceed() } } + ``` -### Run your custom router +Then register your plugin in the `main.rs`: -Finally, you can build and run your custom router using Cargo: +```diff +use hive_router::{ + error::RouterInitError, init_rustls_crypto_provider, ntex, router_entrypoint, + DefaultGlobalAllocator, PluginRegistry, +}; ++ mod plugin; ++ use plugin::MyPlugin; -```bash -cargo run +#[global_allocator] +static GLOBAL: DefaultGlobalAllocator = DefaultGlobalAllocator; + +#[hive_router::main] +async fn main() -> Result<(), RouterInitError> { + init_rustls_crypto_provider(); + + router_entrypoint( + PluginRegistry::new() ++ .register::() + ).await +} ``` ### Configure your plugins in `router.config.yaml` @@ -90,14 +150,21 @@ an example configuration: ```yaml filename="router.config.yaml" plugins: - my_custom_plugin: - option1: value1 - option2: value2 + my_plugin: + enabled: true ``` During this phase of development, you need to learn more about the Hive Router plugin system and how the hooks are structured. +### Run your custom router + +Finally, you can build and run your custom router using Cargo: + +```bash +cargo run +``` + ## Hooks Lifecycle We have the following lifecycle hooks available for plugins: @@ -139,10 +206,11 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `apollo-sandbox` plugin that serves Apollo Sandbox in a custom endpoint using `on_http_request` - hook -- `propagate-status-code` plugin that propagates status codes from subgraphs to clients using the - end phase of `on_http_request` hook by manipulating the final response. +- [`apollo_sandbox`](https://github.com/graphql-hive/router/tree/main/plugin_examples/apollo_sandbox) + plugin that serves Apollo Sandbox in a custom endpoint using `on_http_request` hook +- [`propagate_status_code`](https://github.com/graphql-hive/router/tree/main/plugin_examples/propagate_status_code) + plugin that propagates status codes from subgraphs to clients using the end phase of + `on_http_request` hook by manipulating the final response ### `on_graphql_params` @@ -171,15 +239,19 @@ On this hook's end; You can check the following example implementations; -- `forbid-anon-ops` example that checks the parsed `operation_name` in the end payload of this hook - and rejects the operations without operation names. -- `apq` example shows how to implement a simple Automatic Persisted Queries plugin using this hook. - So it takes the hash from the extensions from the parsed body, and looks up the actual query from - a map then puts it into the GraphQL parameters. -- `multipart` plugin that parses the body using `multer`, and holds the file bytes in the context - then fetches then lazily when needed during the subgraph execution. So it shows how to override - parsing logic using this hook. -- `async_auth` plugin that shows how to implement a custom auth logic +- [`forbid_anonymous_operations`](https://github.com/graphql-hive/router/tree/main/plugin_examples/forbid_anonymous_operations) + example that checks the parsed `operation_name` in the end payload of this hook and rejects the + operations without operation names. +- [`apq`](https://github.com/graphql-hive/router/tree/main/plugin_examples/apq) example shows how to + implement a simple Automatic Persisted Queries plugin using this hook. So it takes the hash from + the extensions from the parsed body, and looks up the actual query from a map then puts it into + the GraphQL parameters. +- [`multipart`](https://github.com/graphql-hive/router/tree/main/plugin_examples/multipart) plugin + that parses the body using `multer`, and holds the file bytes in the context then fetches then + lazily when needed during the subgraph execution. So it shows how to override parsing logic using + this hook. +- [`async_auth`](https://github.com/graphql-hive/router/tree/main/plugin_examples/async_auth) plugin + that shows how to implement a custom auth logic ### `on_graphql_parse` @@ -223,10 +295,12 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `root-field-limit` plugin that implements a new `ValidationRule` to limit the number of root - fields in an operation using this hook. -- `one-of` plugin is a more complicated one that combines this hook and `on_execute` hook to - implement custom validation and execution logic for `@oneOf` directive. +- [`root_field_limit`](https://github.com/graphql-hive/router/tree/main/plugin_examples/root_field_limit) + plugin that implements a new `ValidationRule` to limit the number of root fields in an operation + using this hook. +- [`one_of`](https://github.com/graphql-hive/router/tree/main/plugin_examples/one_of) plugin is a + more complicated one that combines this hook and `on_execute` hook to implement custom validation + and execution logic for `@oneOf` directive. ### `on_query_plan` @@ -246,8 +320,9 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `root-field-limit` plugin that implements another variant of root field limiting using this hook - besides `on_graphql_validation`. +- [`root_field_limit`](https://github.com/graphql-hive/router/tree/main/plugin_examples/root_field_limit) + plugin that implements another variant of root field limiting using this hook besides + `on_graphql_validation`. ### `on_execute` @@ -273,10 +348,11 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `one_of` plugin that implements the execution logic for `@oneOf` directive using this hook. - `on_graphql_validation` is not enough by itself because the coerced variables need to be checked - as well. -- `response_caching` plugin that implements a simple response caching mechanism using this hook. +- [`one_of`](https://github.com/graphql-hive/router/tree/main/plugin_examples/one_of) plugin that + implements the execution logic for `@oneOf` directive using this hook. `on_graphql_validation` is + not enough by itself because the coerced variables need to be checked as well. +- [`response_cache`](https://github.com/graphql-hive/router/tree/main/plugin_examples/response_cache) + plugin that implements a simple response caching mechanism using this hook. ### `on_subgraph_execute` @@ -307,10 +383,11 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `subgraph_response_cache` plugin that implements a simple subgraph response caching mechanism - using this hook. -- `context_data` plugin that shows how to pass data between the main request lifecycle and subgraph - execution using this hook. +- [`subgraph_response_cache`](https://github.com/graphql-hive/router/tree/main/plugin_examples/subgraph_response_cache) + plugin that implements a simple subgraph response caching mechanism using this hook. +- [`context_data`](https://github.com/graphql-hive/router/tree/main/plugin_examples/context_data) + plugin that shows how to pass data between the main request lifecycle and subgraph execution using + this hook. ### `on_subgraph_http_request` @@ -339,13 +416,15 @@ On the end of this hook, you can do the following things for example; You can check the following example implementations; -- `propagate-status-code` plugin that propagates status codes from subgraphs to clients using the - end phase of this hook by manipulating the final response. -- `multipart` plugin that overrides the default deserialization logic of Hive Router to handle - `multipart/form-data` requests from the client, and it holds the files in the context for lazy - fetching during subgraph execution. In this hook, it re-uses the files from the context to prepare - the subgraph HTTP request, and replaces the default JSON-based HTTP transport with - `multipart/form-data` when needed. +- [`propagate_status_code`](https://github.com/graphql-hive/router/tree/main/plugin_examples/propagate_status_code) + plugin that propagates status codes from subgraphs to clients using the end phase of this hook by + manipulating the final response. +- [`multipart`](https://github.com/graphql-hive/router/tree/main/plugin_examples/multipart) plugin + that overrides the default deserialization logic of Hive Router to handle `multipart/form-data` + requests from the client, and it holds the files in the context for lazy fetching during subgraph + execution. In this hook, it re-uses the files from the context to prepare the subgraph HTTP + request, and replaces the default JSON-based HTTP transport with `multipart/form-data` when + needed. ### `on_supergraph_load` @@ -363,11 +442,13 @@ You can do the following things for example; You can check the following example implementations; -- `one_of` plugin that precalculates the `@oneOf` directive locations in the supergraph schema for - faster access during request processing. So it avoids traversing the schema for each request, and - refreshes the state whenever the supergraph is reloaded. -- `response_cache` plugin that precalculates the TTL information based on `@cacheControl` directives - in the supergraph schema for response caching. +- [`one_of`](https://github.com/graphql-hive/router/tree/main/plugin_examples/one_of) plugin that + precalculates the `@oneOf` directive locations in the supergraph schema for faster access during + request processing. So it avoids traversing the schema for each request, and refreshes the state + whenever the supergraph is reloaded. +- [`response_cache`](https://github.com/graphql-hive/router/tree/main/plugin_examples/response_cache) + plugin that precalculates the TTL information based on `@cacheControl` directives in the + supergraph schema for response caching. ### `on_shutdown` @@ -382,67 +463,31 @@ You can do the following things for example; You can check the following example implementations; -- `usage_reporting` plugin that flushes any pending usage reports to the server before shutdown. +- [`usage_reporting`](https://github.com/graphql-hive/router/tree/main/plugin_examples/usage_reporting) + plugin that flushes any pending usage reports to the server before shutdown. ## Short Circuit Responses In many of the hooks mentioned above, you have the ability to short-circuit the request processing by providing a custom response. -Let's say you want to implement a plugin that returns an early error response if it doesn't have a -specific operation name. So basically this plugin rejects anonymous operations. See the highlighted -code below; - -```rust filename="src/forbid_anon_ops.rs" {33} -#[async_trait::async_trait] -impl RouterPlugin for ForbidAnonymousOperationsPlugin { - async fn on_graphql_params<'exec>( - &'exec self, - payload: OnGraphQLParamsStartHookPayload<'exec>, - ) -> OnGraphQLParamsStartHookResult<'exec> { - // After the GraphQL parameters have been parsed, we can check if the operation is anonymous - // So we use `on_end` - payload.on_end(|payload| { - let maybe_operation_name = &payload - .graphql_params - .operation_name - .as_ref(); - - if maybe_operation_name - .is_none_or(|operation_name| operation_name.is_empty()) - { - // let's log the error - tracing::error!("Operation is not allowed!"); +Let's say you want to implement a plugin that returns an early error response in some cases; - // Prepare an HTTP 400 response with a GraphQL error message - let body = json!({ - "errors": [ - { - "message": "Anonymous operations are not allowed", - "extensions": { - "code": "ANONYMOUS_OPERATION" - } - } - ] - }); - // Here we short-circuit the request processing by returning an early response - return payload.end_with_response( - Response::with_body( - StatusCode::BAD_REQUEST, - body.to_string().into(), - ) - ); - } - // we're good to go! - tracing::info!("operation is allowed!"); - payload.proceed() - }) - } -} +```rust +return payload.end_with_response( + Response::with_body( + StatusCode::BAD_REQUEST, + body.to_string().into(), + ) +); ``` -Here we use `end_with_response` method on the `OnGraphQLParamsEndHookPayload` to provide a custom -HTTP response. But there are some helper methods to make it easier to create early responses. +Here we use `end_with_response` method on the `payload` to provide a custom HTTP response. But there +are some helper methods to make it easier to create early responses. + +You can see +[forbid_anonymous_operations](https://github.com/graphql-hive/router/blob/main/plugin_examples/forbid_anonymous_operations/src/plugin.rs#L40) +example to see a full example. ### Helpers for early responses @@ -495,15 +540,7 @@ request body received from the client. In this case, you need to modify the `que `GraphQLParams` struct to put the actual query string into it based on the hash received from the client. -In the following example, we implement a simple APQ plugin that uses an in-memory cache to store the -persisted queries. When a request comes in with a hash, it looks up the query from the cache and -puts it into the `GraphQLParams`. If the query is not found, it returns an error response. - -```rust filename="src/apq.rs" {1,2,12-88} -struct APQPlugin { - cache: DashMap, -} - +```rust #[async_trait::async_trait] impl RouterPlugin for APQPlugin { async fn on_graphql_params<'exec>( @@ -517,54 +554,10 @@ impl RouterPlugin for APQPlugin { .as_ref() .and_then(|ext| ext.get("persistedQuery")) .and_then(|pq| pq.as_object()); - if let Some(persisted_query_ext) = persisted_query_ext { - match persisted_query_ext.get(&"version").and_then(|v| v.as_i64()) { - Some(1) => {} - _ => { - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - "Unsupported persisted query version", - "UNSUPPORTED_PERSISTED_QUERY_VERSION", - ), - StatusCode::BAD_REQUEST, - ); - } - } - let sha256_hash = match persisted_query_ext - .get(&"sha256Hash") - .and_then(|h| h.as_str()) - { - Some(h) => h, - None => { - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - "Missing sha256Hash in persisted query", - "MISSING_PERSISTED_QUERY_HASH", - ), - StatusCode::BAD_REQUEST, - ); - } - }; - if let Some(query_param) = &payload.graphql_params.query { - // Store the query in the cache - self.cache - .insert(sha256_hash.to_string(), query_param.to_string()); - } else { - // Try to get the query from the cache - if let Some(cached_query) = self.cache.get(sha256_hash) { - // Update the graphql_params with the cached query - payload.graphql_params.query = Some(cached_query.value().to_string()); - } else { - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - "Persisted query not found", - "PERSISTED_QUERY_NOT_FOUND", - ), - StatusCode::NOT_FOUND, - ); - } - } - } + + // Get the original query string from the map using the hash from the extensions + let query = get_persisted_query(persisted_query_ext); + payload.graphql_params.query = Some(query); payload.proceed() }) @@ -572,6 +565,8 @@ impl RouterPlugin for APQPlugin { } ``` +[See `apq` example for a full implementation](https://github.com/graphql-hive/router/blob/main/plugin_examples/apq/src/plugin.rs#L29) + ## Context Data Sharing Sometimes, you might want to share data between different hooks or stages of the request processing. @@ -584,7 +579,6 @@ it later in the `on_subgraph_execute` hook. ```rust filename="src/context_data.rs" {1,2,12-55} pub struct ContextData { incoming_data: String, - response_count: u64, } #[async_trait::async_trait] @@ -595,19 +589,9 @@ impl RouterPlugin for ContextDataPlugin { ) -> OnGraphQLParamsStartHookResult<'exec> { let context_data = ContextData { incoming_data: "world".to_string(), - response_count: 0, }; payload.context.insert(context_data); - - payload.on_end(|payload| { - let context_data = payload.context.get_mut::(); - if let Some(mut context_data) = context_data { - context_data.response_count += 1; - tracing::info!("subrequest count {}", context_data.response_count); - } - payload.proceed() - }) } async fn on_subgraph_execute<'exec>( &'exec self, @@ -618,23 +602,18 @@ impl RouterPlugin for ContextDataPlugin { if let Some(ref context_data_entry) = context_data_entry { tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world! let new_header_value = format!("Hello {}", context_data_entry.incoming_data); + // Add a new header to the subgraph execution request payload.execution_request.headers.insert( "x-hello", http::HeaderValue::from_str(&new_header_value).unwrap(), ); } - payload.on_end(|payload: OnSubgraphExecuteEndHookPayload<'exec>| { - let context_data = payload.context.get_mut::(); - if let Some(mut context_data) = context_data { - context_data.response_count += 1; - tracing::info!("subrequest count {}", context_data.response_count); - } - payload.proceed() - }) } } ``` +[See `context_data` example for a full implementation](https://github.com/graphql-hive/router/blob/main/plugin_examples/context_data/src/plugin.rs) + In the example above, we define a `ContextData` struct to hold our custom data. In the `on_graphql_params` hook, we create an instance of `ContextData` and insert it into the request context. Later, in the `on_subgraph_execute` hook, we retrieve the `ContextData` from the context @@ -675,26 +654,7 @@ pub struct ResponseCachePlugin { // Create a new map let ttl_per_type = HashMap::new(); // Visit the schema and update ttl_per_type based on some directive - payload.new_ast.definitions.iter().for_each(|def| { - if let graphql_parser::schema::Definition::TypeDefinition(type_def) = def { - if let graphql_parser::schema::TypeDefinition::Object(obj_type) = type_def { - for directive in &obj_type.directives { - if directive.name == "cacheControl" { - for arg in &directive.arguments { - if arg.0 == "maxAge" { - if let graphql_parser::query::Value::Int(max_age) = &arg.1 { - if let Some(max_age) = max_age.as_i64() { - ttl_per_type - .insert(obj_type.name.clone(), max_age as u64); - } - } - } - } - } - } - } - } - }); + /** .. */ self.ttl_per_type.store(ttl_per_type.into()) payload.proceed() @@ -721,46 +681,19 @@ How do we use this `ttl_per_type` map during request processing? if let Ok(serialized) = sonic_rs::to_vec(&payload.data) { // Here we load the ttl_per_type map once let ttl_per_type = self.ttl_per_type.load(); - trace!("Caching response for key: {}", key); - // Decide on the ttl somehow - // Get the type names - let mut max_ttl = 0; - - // Imagine this code is traversing the response data to find type names - if let Some(obj) = payload.data.as_object() { - if let Some(typename) = obj - .iter() - .position(|(k, _)| k == &TYPENAME_FIELD_NAME) - .and_then(|idx| obj[idx].1.as_str()) - { - if let Some(ttl) = ttl_per_type.get(typename) { - max_ttl = max_ttl.max(*ttl); - } - } - } - - // If no ttl found, default - if max_ttl == 0 { - max_ttl = self.default_ttl_seconds; - } - trace!("Using TTL of {} seconds for key: {}", max_ttl, key); - - // Insert the ttl into extensions for client awareness - payload.add_extension("response_cache_ttl", max_ttl); - - // Set the cache with the decided ttl - let result = conn.set_ex::<&str, Vec, ()>(&key, serialized, max_ttl); - if let Err(err) = result { - trace!("Failed to set cache for key {}: {}", key, err); - } else { - trace!("Cached response for key: {} with TTL: {}", key, max_ttl); - } + /** + * ... + */ } - payload.proceed() + /** + * ... + */ }) } ``` +[See the full example here](https://github.com/graphql-hive/router/tree/main/plugin_examples/response_cache) + ## Configuration of Plugins Plugins can be configured via the `router.config.yaml` file. Each plugin can have its own entry @@ -769,77 +702,6 @@ under `plugins` section, where you can specify various options and settings spec The configuration `struct` should be `serde` compliant, so that it can be deserialized from the YAML file. -Let's say we have a custom auth logic that checks the expected header values from a file -dynamically. - -```rust -pub struct AllowClientIdFromFilePlugin { - header_key: String, - allowed_ids_path: PathBuf, -} - -#[async_trait::async_trait] -impl RouterPlugin for AllowClientIdFromFilePlugin { - // Whenever it is a GraphQL request, - // We don't use on_http_request here because we want to run this only when it is a GraphQL request - async fn on_graphql_params<'exec>( - &'exec self, - payload: OnGraphQLParamsStartHookPayload<'exec>, - ) -> OnGraphQLParamsStartHookResult<'exec> { - let header = payload.router_http_request.headers.get(&self.header_key); - match header { - Some(client_id) => { - let client_id_str = client_id.to_str(); - match client_id_str { - Ok(client_id) => { - let allowed_clients: Vec = serde_json::from_str( - std::fs::read_to_string(self.allowed_ids_path.clone()) - .unwrap() - .as_str(), - ) - .unwrap(); - - if !allowed_clients.contains(&client_id.to_string()) { - // Prepare an HTTP 403 response with a GraphQL error message - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - "client-id is not allowed", - "UNAUTHORIZED_CLIENT_ID", - ), - http::StatusCode::FORBIDDEN, - ); - } - } - Err(_not_a_string_error) => { - let message = format!("'{}' value is not a string", &self.header_key); - tracing::error!(message); - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - &message, - "BAD_CLIENT_ID", - ), - http::StatusCode::BAD_REQUEST, - ); - } - } - } - None => { - let message = format!("Missing '{}' header", &self.header_key); - tracing::error!(message); - return payload.end_with_graphql_error( - GraphQLError::from_message_and_code( - &message, - "MISSING_CLIENT_ID", - ), - http::StatusCode::UNAUTHORIZED, - ); - } - } - payload.proceed() - } -} -``` - So we can have a configuration struct like below; ```rust filename="src/dynamic_auth.rs" @@ -847,7 +709,6 @@ use serde::Deserialize; #[derive(Deserialize)] pub struct AllowClientIdConfig { - pub enabled: bool, pub header: String, pub path: String, } @@ -861,22 +722,21 @@ impl RouterPlugin for AllowClientIdFromFilePlugin { fn plugin_name() -> &'static str { "allow_client_id_from_file" } - fn from_config(config: AllowClientIdConfig) -> Option { - if config.enabled { - Some(AllowClientIdFromFilePlugin { - header_key: config.header, - allowed_ids_path: PathBuf::from(config.path), - }) - } else { - None - } + fn on_plugin_init(payload: OnPluginInitPayload) -> OnPluginInitResult { + // Payload config method will deserialize the config section + // for this plugin from the router.config.yaml file into the AllowClientIdConfig struct + let config = payload.config()?; + payload.initialize_plugin(Self { + header_key: config.header, + allowed_ids_path: PathBuf::from(config.path), + }) } } ``` `plugin_name` method should return the name of the plugin as it appears in the `router.config.yaml` -file. The `from_config` method is responsible for creating an instance of the plugin from the -provided configuration. If `from_config` returns `None`, the plugin will not be registered. +file. The `on_plugin_init` hook is responsible for creating an instance of the plugin from the +provided configuration. With this setup, you can now configure the `allow_client_id_from_file` plugin in your `router.config.yaml` file like this: @@ -885,24 +745,29 @@ With this setup, you can now configure the `allow_client_id_from_file` plugin in plugins: allow_client_id_from_file: enabled: true - header: 'x-client-id' + config: + header: 'x-client-id' ``` +[See the full `async_auth` example here](https://github.com/graphql-hive/router/tree/main/plugin_examples/async_auth) + ## Registration of Plugins Finally, to use your custom plugin, you need to register it with the `PluginRegistry` in your `main.rs` file. ```rust filename="src/main.rs" {9-14} - let mut plugin_registry = PluginRegistry::new(); - // Register your custom plugin - plugin_registry.register_plugin::(); + PluginRegistry::new() + .register_plugin::() ``` Then pass the `plugin_registry` to the `router_entrypoint` function as shown earlier. ```rust filename="src/main.rs" {15} - match router_entrypoint(Some(plugin_registry)).await { + router_entrypoint( + PluginRegistry::new() + .register_plugin::() + ).await ``` ## Cleanup State on Shutdown @@ -914,59 +779,121 @@ For example, if your plugin opens a database connection or a file handle, you sh gracefully in the `on_shutdown` method. ```rust filename="usage_reporting.rs" -struct UsageReportingPlugin { - endpoint: String, - reports: Mutex>, -} - #[async_trait::async_trait] impl RouterPlugin for UsageReportingPlugin { - type Config = UsageReportingPluginConfig; - fn plugin_name() -> &'static str { - "usage_reporting" - } - fn from_config(config: UsageReportingPluginConfig) -> Option { - if config.enabled { - Some(UsageReportingPlugin { - endpoint: config.endpoint, - reports: Default::default(), - }) - } else { - None - } - } - async fn on_execute<'exec>( - &'exec self, - payload: OnExecuteStartHookPayload<'exec>, - ) -> OnExecuteStartHookResult<'exec> { - self.reports.lock().await.push(UsageReport { - query: payload.operation_for_plan.to_string(), - operation_name: payload.operation_for_plan.name.clone(), - }); - tracing::trace!( - "Pushed usage report for operation: {:?}", - payload.operation_for_plan.name - ); - payload.proceed() - } - // This method is called when the router is shutting down + /** .. */ async fn on_shutdown<'exec>(&'exec self) { println!("Disposing UsageReportingPlugin and sending usage report"); // Here you would gather and send the usage report let reports = self.reports.lock().await; - match reqwest::Client::new() - .post(&self.endpoint) - .json(reports.as_slice()) - .send() - .await - { - Ok(response) => { - tracing::trace!("Usage report sent successfully: {:?}", response); - } - Err(e) => { - tracing::trace!("Failed to send usage report: {:?}", e); - } - } + // Send the requests here using your preferred HTTP client + /** .. */ + } +} +``` + +[See the full example here](https://github.com/graphql-hive/router/tree/main/plugin_examples/usage_reporting) + +## Understanding the hotpath + +When creating custom plugins, it's important to understand the hotpath of the request processing. +The hotpath refers to the critical path that affects the performance of the router. When +implementing the request lifecycle hooks like `on_graphql_params`, `on_graphql_validation`, +`on_query_plan`, `on_execute`, etc., you should be mindful of the performance implications of your +code. Avoid doing heavy computations or blocking operations in these hooks, as they can +significantly impact the latency of your GraphQL requests. + +```rust +#[async_trait::async_trait] +impl RouterPlugin for MyPlugin { + async fn on_subgraph_execute<'exec>( + &'exec self, + payload: OnSubgraphExecuteStartHookPayload<'exec>, + ) -> OnSubgraphExecuteStartHookResult<'exec> { + // Any heavy computations here would block the subgraph execution and increase latency + // if the operation is complex or if there are many subgraph requests. + payload.proceed() } } ``` + +## Build and distribute the custom build + +Finally, after you have implemented your custom router with the necessary plugins, you can build and +distribute it. You can build a binary using Cargo and share it with others or deploy it in +production. + +### Executable single binary with Cargo + +After you have implemented your custom router with the necessary plugins, you can build a +distributable binary using Cargo. This will allow you to deploy your custom router in production or +share it with others when necessary. + +Make sure the `Cargo.toml` file has the correct configuration for building a binary, and the name of +the binary is named as desired; + +```toml +[package] +name = "my-plugin-example" +version = "0.0.1" +edition = "2021" +license = "MIT" + +[lib] + +[[bin]] +name = "hive_router_with_my_plugin" # Name of the binary +path = "src/main.rs" + +[dependencies] +hive-router = "0.0.39" +serde = "1" +``` + +```bash +cargo build --release +``` + +This command will create an optimized binary in the `target/release` directory. You can then run the +binary directly: + +```bash +./target/release/hive_router_with_my_plugin +``` + +### Containerize with Docker + +To containerize your custom router, you can create a `Dockerfile` in the root of your project. Here +is an example `Dockerfile` that builds and runs your custom router: + +```dockerfile +# Use the official Rust image as the base image +FROM rust:latest + +# Set the working directory inside the container +WORKDIR /app + +# Copy the entire project into the container +COPY . . + +# Build the project in release mode +RUN cargo build --release + +# Expose the port that the router will run on +EXPOSE 4000 + +# Run the custom router binary +CMD ["./target/release/hive_router_with_my_plugin"] +``` + +You can then build the Docker image using the following command: + +```bash +docker build -t my-custom-router . +``` + +After the image is built, you can run a container from it: + +```bash +docker run -p 4000:4000 my-custom-router +``` From 1f27d3bf116343ce1154e1dccfc669ec3554a5ad Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 17:43:19 +0300 Subject: [PATCH 15/19] .. --- .../guides/extending-the-router/index.mdx | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 808c7e78d4b..93028b5f134 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -2,7 +2,7 @@ title: 'Extending the Router' --- -import { Tabs } from '@theguild/components' +import { Callout, Tabs } from '@theguild/components' # Extending the Router @@ -804,19 +804,44 @@ code. Avoid doing heavy computations or blocking operations in these hooks, as t significantly impact the latency of your GraphQL requests. ```rust -#[async_trait::async_trait] -impl RouterPlugin for MyPlugin { - async fn on_subgraph_execute<'exec>( +// We use thread-safe `DashMap` here to cache the schema with feature flags applied +// So that we can avoid visiting the schema for each request, and instead cache the result based on some cache key +// We keep it in `Arc` to avoid cloning the schema for each request, since the schema can be quite large +pub struct FeatureFlagsPlugin { + schema_with_flags_cache: DashMap>, +} + +#[async_trait] +impl RouterPlugin for FeatureFlagsPlugin { + async fn on_graphql_validation<'exec>( &'exec self, - payload: OnSubgraphExecuteStartHookPayload<'exec>, - ) -> OnSubgraphExecuteStartHookResult<'exec> { - // Any heavy computations here would block the subgraph execution and increase latency - // if the operation is complex or if there are many subgraph requests. - payload.proceed() + payload: OnGraphQLValidationStartHookPayload<'exec>, + ) -> OnGraphQLValidationStartHookResult<'exec> { + // Generate a cache key based on the request properties, for example using the headers + let cache_key = generate_cache_key_from_header(&payload.router_http_request); + + // Do not repeat the expensive schema transformation for each request, instead cache the result based on the cache key + let cached_schema = self + .schema_with_flags_cache + .entry(cache_key) + .or_insert_with(|| { + let visitor = FeatureFlagsVisitor { feature_flags }; + + visitor + .visit_schema_document(SchemaDocument::clone(&payload.schema.document), &mut ()) + .unwrap() + .into() + }) + .value() + .clone(); + + payload.with_schema(cached_schema).proceed() } } ``` +[See the full of `feature_flags` example](https://github.com/graphql-hive/router/tree/main/plugin_examples/feature_flags) + ## Build and distribute the custom build Finally, after you have implemented your custom router with the necessary plugins, you can build and @@ -832,7 +857,7 @@ share it with others when necessary. Make sure the `Cargo.toml` file has the correct configuration for building a binary, and the name of the binary is named as desired; -```toml +```toml filename="Cargo.toml" [package] name = "my-plugin-example" version = "0.0.1" @@ -866,7 +891,7 @@ binary directly: To containerize your custom router, you can create a `Dockerfile` in the root of your project. Here is an example `Dockerfile` that builds and runs your custom router: -```dockerfile +```dockerfile name="Dockerfile" # Use the official Rust image as the base image FROM rust:latest From 46eef8d7fcc845bffa9e42ee3ddd4275adcc605b Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 18:00:15 +0300 Subject: [PATCH 16/19] Update --- .../guides/extending-the-router/index.mdx | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 93028b5f134..83948567791 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -2,7 +2,7 @@ title: 'Extending the Router' --- -import { Callout, Tabs } from '@theguild/components' +import { Tabs } from '@theguild/components' # Extending the Router @@ -212,6 +212,15 @@ You can check the following example implementations; plugin that propagates status codes from subgraphs to clients using the end phase of `on_http_request` hook by manipulating the final response +```rust +fn on_http_request<'exec>( + &'exec self, + payload: OnHttpRequestHookPayload<'exec>, + ) -> OnHttpRequestHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/main/lib/executor/src/plugins/plugin_trait.rs#L162) + ### `on_graphql_params` This hook is called after the router has determined that the incoming request is a GraphQL request @@ -253,6 +262,15 @@ You can check the following example implementations; - [`async_auth`](https://github.com/graphql-hive/router/tree/main/plugin_examples/async_auth) plugin that shows how to implement a custom auth logic +```rust +async fn on_graphql_params<'exec>( + &'exec self, + payload: OnGraphQLParamsStartHookPayload<'exec>, + ) -> OnGraphQLParamsStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L176C5-L179C41) + ### `on_graphql_parse` This hook is called after the deserialization of the request, and the router has parsed the GraphQL @@ -273,6 +291,15 @@ On the end of this hook, you can do the following things for example; properties along with the parsed AST. - Logging or metrics based on the parsed AST like counting certain fields or directives usage. +```rust +async fn on_graphql_parse<'exec>( + &'exec self, + payload: OnGraphQLParseStartHookPayload<'exec>, + ) -> OnGraphQLParseHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L169C5-L172C46) + ### `on_graphql_validation` This hook is called after the router is ready to validate the operation against the supergraph. In @@ -302,6 +329,15 @@ You can check the following example implementations; more complicated one that combines this hook and `on_execute` hook to implement custom validation and execution logic for `@oneOf` directive. +```rust +async fn on_graphql_validation<'exec>( + &'exec self, + payload: OnGraphQLValidationStartHookPayload<'exec>, + ) -> OnGraphQLValidationStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L183C5-L188C6) + ### `on_query_plan` This hook is invoked before the query planner starts the planning process. At this point, we have @@ -324,6 +360,15 @@ You can check the following example implementations; plugin that implements another variant of root field limiting using this hook besides `on_graphql_validation`. +```rust +async fn on_query_plan<'exec>( + &'exec self, + payload: OnQueryPlanStartHookPayload<'exec>, + ) -> OnQueryPlanStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L190) + ### `on_execute` Whenever the variables are coerced in addition to the operation being valid just like other @@ -354,6 +399,15 @@ You can check the following example implementations; - [`response_cache`](https://github.com/graphql-hive/router/tree/main/plugin_examples/response_cache) plugin that implements a simple response caching mechanism using this hook. +```rust +async fn on_execute<'exec>( + &'exec self, + payload: OnExecuteStartHookPayload<'exec>, + ) -> OnExecuteStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L197) + ### `on_subgraph_execute` This hook is called before an execution request is prepared for a subgraph based on the query plan. @@ -389,6 +443,15 @@ You can check the following example implementations; plugin that shows how to pass data between the main request lifecycle and subgraph execution using this hook. +```rust +async fn on_subgraph_execute<'exec>( + &'exec self, + payload: OnSubgraphExecuteStartHookPayload<'exec>, + ) -> OnSubgraphExecuteStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L204) + ### `on_subgraph_http_request` After the subgraph execution request is serialized into an actual HTTP request, this hook is @@ -426,6 +489,15 @@ You can check the following example implementations; request, and replaces the default JSON-based HTTP transport with `multipart/form-data` when needed. +```rust +async fn on_subgraph_http_request<'exec>( + &'exec self, + payload: OnSubgraphHttpRequestStartHookPayload<'exec>, + ) -> OnSubgraphHttpRequestStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L211) + ### `on_supergraph_load` This hook is called whenever the supergraph is loaded or reloaded. This can happen at startup or @@ -450,6 +522,15 @@ You can check the following example implementations; plugin that precalculates the TTL information based on `@cacheControl` directives in the supergraph schema for response caching. +```rust +fn on_supergraph_reload<'exec>( + &'exec self, + start_payload: OnSupergraphLoadStartHookPayload, + ) -> OnSupergraphLoadStartHookResult<'exec> +``` + +[See the definition of the hook](https://github.com/graphql-hive/router/blob/plugin-system/lib/executor/src/plugins/plugin_trait.rs#L218C5-L221C48) + ### `on_shutdown` This hook is called when the router is shutting down. If your plugin holds resources that need to be @@ -466,6 +547,22 @@ You can check the following example implementations; - [`usage_reporting`](https://github.com/graphql-hive/router/tree/main/plugin_examples/usage_reporting) plugin that flushes any pending usage reports to the server before shutdown. +```rust +async fn on_shutdown<'exec>(&'exec self) +``` + +### `on_graphql_error` + +This hook is called whenever a GraphQL error is about to be sent to the client. This allows you to +inspect or modify the error before it is sent. You can use this hook to implement custom error +handling logic, such as masking certain error details, adding additional information to the error +response, or even transforming the error into a different format. + +You can check the following example implementation; + +- [`error_mapping`](https://github.com/graphql-hive/router/tree/main/plugin_examples/error_mapping) + plugin that maps certain error messages to custom error codes and formats using this hook. + ## Short Circuit Responses In many of the hooks mentioned above, you have the ability to short-circuit the request processing From 2695d92cd2e9e22a7e5ab6f036b152038e2aab04 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 20:36:29 +0300 Subject: [PATCH 17/19] No need for highlighting --- .../src/content/router/guides/extending-the-router/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index 83948567791..a5cf1c2dd27 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -673,7 +673,7 @@ pass information between hooks. For example, you can store some data in the context during the `on_graphql_params` hook and retrieve it later in the `on_subgraph_execute` hook. -```rust filename="src/context_data.rs" {1,2,12-55} +```rust filename="src/context_data.rs" pub struct ContextData { incoming_data: String, } From edb622fb15cb2262483f25c8eb95cf75a1b883dc Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 20:37:08 +0300 Subject: [PATCH 18/19] Simplify --- .../content/router/guides/extending-the-router/index.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index a5cf1c2dd27..bd2946fa251 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -698,12 +698,6 @@ impl RouterPlugin for ContextDataPlugin { let context_data_entry = payload.context.get_ref::(); if let Some(ref context_data_entry) = context_data_entry { tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world! - let new_header_value = format!("Hello {}", context_data_entry.incoming_data); - // Add a new header to the subgraph execution request - payload.execution_request.headers.insert( - "x-hello", - http::HeaderValue::from_str(&new_header_value).unwrap(), - ); } } } From f7c919f785f3aac7a801ec4a9e0146768e4bef69 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 17 Feb 2026 20:37:22 +0300 Subject: [PATCH 19/19] Simplify --- .../src/content/router/guides/extending-the-router/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx index bd2946fa251..a5a0cd5ca9c 100644 --- a/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx +++ b/packages/web/docs/src/content/router/guides/extending-the-router/index.mdx @@ -675,7 +675,7 @@ it later in the `on_subgraph_execute` hook. ```rust filename="src/context_data.rs" pub struct ContextData { - incoming_data: String, + incoming_data: &'static str, } #[async_trait::async_trait] @@ -685,7 +685,7 @@ impl RouterPlugin for ContextDataPlugin { payload: OnGraphQLParamsStartHookPayload<'exec>, ) -> OnGraphQLParamsStartHookResult<'exec> { let context_data = ContextData { - incoming_data: "world".to_string(), + incoming_data: "world", }; payload.context.insert(context_data);