Plugin System
Overview
Hive Router provides a plugin system that allows developers to extend its functionality with custom plugins. Plugins can be used to add new features, modify existing behavior, or integrate with external services.
Just like the Hive Router itself, custom plugins are written in Rust, which allows for high performance and safety. To ease the development process, Hive Router is also published to Crates.io as a single package hive-router that contains everything you need to focus on building your plugin, without having to configure or rebuild other parts of the service.
This page documents the API, hooks, and different implementation options that are available for plugin developers. If you are looking for a guide on how to write, test and distribute a custom plugin, please refer to Extending the Router guide.
hive-router Crate
The hive-router crate provides everything a plugin developer needs to implement a custom plugin.
To simplify the dependency tree and versioning, this crate also re-exports internal Router dependencies, traits, and structs, to simplify plugin development and keep the Router core version up to date.
Refer to the crate’s lib.rs entrypoint to see what is exported and available for extending the Router.
trait RouterPlugin
The primary interface for the plugin system is RouterPlugin. To implement a custom plugin, you’ll need to impl the RouterPlugin for your custom plugin’s struct. Due to the async nature of the plugin system, the #[async_trait] attribute is also required.
use hive_router::plugins::plugin_trait::RouterPlugin;
use hive_router::async_trait;
struct MyPlugin;
#[async_trait]
impl RouterPlugin for MyPlugin {
type Config = ();
fn plugin_name() -> &'static str {
"my_plugin"
}
}The plugin above will be instantiated using the following Router YAML config:
plugins:
my_plugin:
enabled: trueA plugin can implement one or more hooks, and leverage the following concepts:
Configuration
The RouterPlugin trait accepts a generic type parameter for plugin configuration.
The configuration type must implement DeserializeOwned + Sync, as the Router is responsible for loading and deserializing the configuration from the Router config file.
The on_plugin_init hook provides access to the plugin’s configuration.
use hive_router::plugins::plugin_trait::RouterPlugin;
use hive_router::async_trait;
use serde::Deserialize;
struct MyPlugin;
#[derive(Deserialize)]
struct MyPluginConfig {
some_field: bool
}
#[async_trait]
impl RouterPlugin for MyPlugin {
type Config = MyPluginConfig;
fn plugin_name() -> &'static str {
"my_plugin"
}
fn on_plugin_init(payload: OnPluginInitPayload<Self>) -> OnPluginInitResult<Self> {
payload.initialize_plugin(Self { some_field: payload.config()?.some_field })
}
}The plugin above will be instantiated using the following Router YAML config:
plugins:
my_plugin:
enabled: true
config:
some_field: trueHooks
A plugin can implement one or more hooks to participate in different stages of the Router execution and lifecycle.
Hooks allow plugins to participate in the control flow and change the behavior of the router by returning different types from the hook function.
As a plugin developer, you can hook into the router execution flow in the following areas:
- HTTP request handling
- GraphQL operation lookup and extraction from the HTTP request
- GraphQL operation parsing
- GraphQL operation validation
- Federation query planner
- Query plan execution
- Subgraph request execution
- HTTP calls to subgraph
- Supergraph reload notification
The following chart describes the execution order and available hooks. Each hook can have a start/end phase, where you can control different parts of the execution flow.
Shared State
A plugin can maintain state directly on its struct and access it using self in hooks. The state must be thread-safe, as it may be shared across multiple threads handling concurrent HTTP requests. Please refer to Rust’s Interior Mutability guide for more information about how to implement a mutable state.
use hive_router::plugins::plugin_trait::RouterPlugin;
use hive_router::async_trait;
struct MyPlugin {
state: Mutex<Vec<String>>,
}
#[async_trait]
impl RouterPlugin for MyPlugin {
type Config = ();
fn plugin_name() -> &'static str {
"my_plugin"
}
// In hooks, you can use `self.state`
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
// For the sake of the demo, this plugin only stores the paths for all requests
self.state.lock().unwrap().push(payload.request.uri().path().to_string());
payload.proceed()
}
}Context Data Sharing
In cases where you need to share data between hooks, you can use a custom context struct. Define a custom struct to hold the data you would like to share between hooks, and use the hooks’ payload to access the context field.
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.
pub struct ContextData {
incoming_data: &'static str,
}
#[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",
};
payload.context.insert(context_data);
}
async fn on_subgraph_execute<'exec>(
&'exec self,
mut payload: OnSubgraphExecuteStartHookPayload<'exec>,
) -> OnSubgraphExecuteStartHookResult<'exec> {
// Get immutable reference from the context
let context_data_entry = payload.context.get_ref::<ContextData>();
if let Some(ref context_data_entry) = context_data_entry {
tracing::info!("hello {}", context_data_entry.incoming_data); // Hello world!
}
}
}The payload.context API is simple:
insert<T>(&self, data: T)- Inserts data of typeTinto the context.get_ref<T>(&self) -> Option<&T>- Retrieves a reference to the data of typeTfrom the context.get_mut<T>(&mut self) -> Option<&mut T>- Retrieves a mutable reference to the data of typeTfrom the context.
Plugin Lifecycle Hooks
on_plugin_init
This hook is called exactly once during Router initialization, when the plugin is first loaded.
You can use this hook to access the plugin configuration, create the initial state of the plugin, and register background tasks to be handled by the router.
fn on_plugin_init(payload: OnPluginInitPayload<Self>) -> OnPluginInitResult<Self>Access the plugin configuration
Use payload.config() to read the plugin’s configuration from the Router YAML. The ? propagates initialization errors back to the router.
fn on_plugin_init(payload: OnPluginInitPayload<Self>) -> OnPluginInitResult<Self> {
let config = payload.config()?;
payload.initialize_plugin_with_defaults()
}Customize plugin instance
Build the plugin’s initial state from the parsed configuration before the router starts serving requests.
fn on_plugin_init(payload: OnPluginInitPayload<Self>) -> OnPluginInitResult<Self> {
payload.initialize_plugin(Self {
my_thing: payload.config()?.my_config_flag,
})
}Register background tasks
Spawn long-running work tied to the router lifecycle. The CancellationToken is signaled when the router shuts down.
use hive_router::background_tasks::BackgroundTask;
#[async_trait]
impl BackgroundTask for MyBackgroundTask {
fn id(&self) -> &str {
"my_task"
}
async fn run(&self, token: CancellationToken) {
// Check token.is_cancelled() to handle graceful shutdown
}
}
fn on_plugin_init(payload: OnPluginInitPayload<Self>) -> OnPluginInitResult<Self> {
payload.bg_tasks_manager.register_task(MyBackgroundTask::new());
payload.initialize_plugin_with_defaults()
}Payload API Reference
Plugin Examples
Request Lifecycle Hooks
on_http_request
This hook is called immediately after the router receives an HTTP request. It allows you to inspect or modify the HTTP request before any further processing occurs. At this point, the Router doesn’t know yet if the request is a GraphQL request or not.
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>
) -> OnHttpRequestHookResult<'req>The
'reqlifetime parameter represents the lifetime of the HTTP request. It is used to ensure that the payload and result are valid for the duration of the request.
With this hook, you can add custom authentication and authorization logic based on HTTP headers, method, path, and similar request properties. You can also implement custom restrictions, HTTP validations, short-circuit responses, or serve static assets.
Usage Examples
Reject unauthenticated requests
Check for required HTTP headers in the start phase and short-circuit with an error before any GraphQL processing begins.
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
if payload.router_http_request.headers().get("authorization").is_none() {
return payload.end_with_graphql_error(
GraphQLError::from_message_and_code("Unauthorized", "UNAUTHORIZED"),
StatusCode::UNAUTHORIZED,
);
}
payload.proceed()
}Add a response header
Use on_end with map_response to attach custom headers to every outgoing HTTP response.
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
payload.on_end(|payload| {
payload.map_response(|mut response| {
response.response_mut().headers_mut().insert(
"x-served-by",
"hive-router".parse().unwrap(),
);
response
}).proceed()
})
}Override the HTTP response status
Use map_response in on_end to rewrite the status code on the outgoing response — for example, to normalize all successful responses to 200 OK.
use hive_router::http::StatusCode;
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
payload.on_end(|end_payload| {
end_payload.map_response(|mut response| {
*response.response_mut().status_mut() = StatusCode::OK;
response
})
})
}Propagate subgraph status codes
Read context data written by a downstream hook (e.g., on_subgraph_http_request) and apply it to the final HTTP response status.
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
payload.on_end(|payload| {
let ctx = payload.context.get_ref::<PropagateStatusCodeCtx>();
if let Some(ctx) = ctx {
return payload.map_response(|mut response| {
*response.response_mut().status_mut() = ctx.status_code;
response
}).proceed();
}
payload.proceed()
})
}Payload API Reference
Plugin Examples
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.).
This hook allows you to customize how GraphQL request parameters are detected and extracted. You can also prevent execution of the GraphQL request if certain conditions are met.
async fn on_graphql_params<'exec>(
&'exec self,
payload: OnGraphQLParamsStartHookPayload<'exec>
) -> OnGraphQLParamsStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Read extracted GraphQL parameters
Use the on_end callback to access the fully extracted GraphQL parameters — query, operation_name, variables, and extensions are all available here.
async fn on_graphql_params<'exec>(
&'exec self,
payload: OnGraphQLParamsStartHookPayload<'exec>,
) -> OnGraphQLParamsStartHookResult<'exec> {
payload.on_end(|end_payload| {
let maybe_operation_name = &end_payload.graphql_params.operation_name.as_ref();
let maybe_query = &end_payload.graphql_params.query.as_ref();
let variables = &end_payload.graphql_params.variables;
end_payload.proceed()
})
}Modify extracted GraphQL parameters
Mutate the extracted GraphQL parameters inside on_end to override the query, operation name, or variables before execution continues.
async fn on_graphql_params<'exec>(
&'exec self,
payload: OnGraphQLParamsStartHookPayload<'exec>,
) -> OnGraphQLParamsStartHookResult<'exec> {
payload.on_end(|mut end_payload| {
end_payload.graphql_params.query = Some("query { overridingTheOperation }".to_string());
end_payload.proceed()
})
}Reject anonymous operations
Return a GraphQL error from on_end to short-circuit the request when a condition on the parsed parameters is not met.
async fn on_graphql_params<'exec>(
&'exec self,
payload: OnGraphQLParamsStartHookPayload<'exec>,
) -> OnGraphQLParamsStartHookResult<'exec> {
payload.on_end(|payload| {
if payload.graphql_params.operation_name.is_none() {
return payload.end_with_graphql_error(
GraphQLError::from_message_and_code(
"Anonymous operations are not allowed",
"ANONYMOUS_OPERATION",
),
StatusCode::BAD_REQUEST,
);
}
payload.proceed()
})
}Resolve a query from extensions (Persisted Queries)
Read from extensions in on_end and rewrite graphql_params.query to implement patterns like Automatic Persisted Queries (APQ).
async fn on_graphql_params<'exec>(
&'exec self,
payload: OnGraphQLParamsStartHookPayload<'exec>,
) -> OnGraphQLParamsStartHookResult<'exec> {
payload.on_end(|mut payload| {
let hash = payload
.graphql_params
.extensions
.as_ref()
.and_then(|ext| ext.get("persistedQuery"))
.and_then(|pq| pq.as_object())
.and_then(|obj| obj.get(&"sha256Hash"))
.and_then(|h| h.as_str());
if let Some(hash) = hash {
if let Some(cached_query) = self.cache.get(hash) {
payload.graphql_params.query = Some(cached_query.value().to_string());
} else {
return payload.end_with_graphql_error(
GraphQLError::from_message_and_code(
"PersistedQueryNotFound",
"PERSISTED_QUERY_NOT_FOUND",
),
StatusCode::BAD_REQUEST,
);
}
}
payload.proceed()
})
}Payload API Reference
Plugin Examples
on_graphql_parse
This hook is called after the deserialization of the request, and before the router has parsed the GraphQL parameters body expected by GraphQL-over-HTTP spec.
async fn on_graphql_parse<'exec>(
&'exec self,
payload: OnGraphQLParseStartHookPayload<'exec>,
) -> OnGraphQLParseHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Read the raw query string
The start phase has access to the raw GraphQLParams, including the unparsed query string, before the router parses it into a document.
async fn on_graphql_parse<'exec>(
&'exec self,
payload: OnGraphQLParseStartHookPayload<'exec>,
) -> OnGraphQLParseHookResult<'exec> {
let raw_query = &payload.graphql_params.query;
payload.proceed()
}Access the parsed document (AST)
Use on_end to access the fully parsed Document AST after the router has completed parsing the operation.
async fn on_graphql_parse<'exec>(
&'exec self,
payload: OnGraphQLParseStartHookPayload<'exec>,
) -> OnGraphQLParseHookResult<'exec> {
payload.on_end(|end_payload| {
let document = &end_payload.document;
end_payload.proceed()
})
}Payload API Reference
on_graphql_validation
This hook is called during the GraphQL validation phase of the Router. At this stage, the GraphQL operation has been extracted and parsed from the incoming request, and validation rules are being applied against the GraphQL schema.
Plugins can use this hook to add custom validation rules, skip validation entirely, or modify the operation prior to validation.
async fn on_graphql_validation<'exec>(
&'exec self,
payload: OnGraphQLValidationStartHookPayload<'exec>,
) -> OnGraphQLValidationStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Add a custom validation rule
Implement ValidationRule and register it with payload.with_validation_rule() to enforce custom constraints on every incoming operation.
impl ValidationRule for MyCustomValidationRule {
fn error_code<'a>(&self) -> &'a str {
"MAX_ALIASES_EXCEEDED"
}
fn validate(
&self,
ctx: &mut OperationVisitorContext<'_>,
error_collector: &mut ValidationErrorContext,
) {
// ...
}
}
async fn on_graphql_validation<'exec>(
&'exec self,
payload: OnGraphQLValidationStartHookPayload<'exec>,
) -> OnGraphQLValidationStartHookResult<'exec> {
payload.with_validation_rule(MyCustomValidationRule::new()).proceed()
}Payload API Reference
Plugin Examples
on_query_plan
This hook is invoked during the Federation query planning process. At this stage, the Router already extracted, parsed and validated the GraphQL operation, and it is ready to plan how to execute the operation across subgraphs.
This hook gives plugins access to the Router’s internal query planning process, allowing them to modify the operation being planned or inspect and modify the resulting plan.
async fn on_query_plan<'exec>(
&'exec self,
payload: OnQueryPlanStartHookPayload<'exec>,
) -> OnQueryPlanStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Inspect the query plan
Use on_end to access the constructed QueryPlan after the planner has completed. This runs after operation parsing and validation.
async fn on_query_plan<'exec>(
&'exec self,
payload: OnQueryPlanStartHookPayload<'exec>,
) -> OnQueryPlanStartHookResult<'exec> {
payload.on_end(|end_payload| {
let query_plan = &end_payload.query_plan;
end_payload.proceed()
})
}Reject based on query plan content
Inspect the plan in on_end and short-circuit with an error if the operation exceeds a policy — for example, too many root fields.
async fn on_query_plan<'exec>(
&'exec self,
payload: OnQueryPlanStartHookPayload<'exec>,
) -> OnQueryPlanStartHookResult<'exec> {
payload.on_end(|end_payload| {
let root_field_count = count_root_fields(&end_payload.query_plan);
if root_field_count > self.max_root_fields {
return end_payload.end_with_graphql_error(
GraphQLError::from_message_and_code(
"Query exceeds the maximum number of root fields",
"TOO_MANY_ROOT_FIELDS",
),
StatusCode::PAYLOAD_TOO_LARGE,
);
}
end_payload.proceed()
})
}Payload API Reference
Plugin Examples
on_execute
This hook is invoked during the Federation query execution process. At this stage, the Router already extracted, parsed and validated the GraphQL operation. Also, a query plan has been constructed and is ready to be executed.
Plugins can use this hook to observe the execution process or implement solutions such as response caching that bypass it entirely.
async fn on_execute<'exec>(
&'exec self,
payload: OnExecuteStartHookPayload<'exec>,
) -> OnExecuteStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Return a cached response
Bypass the execution pipeline entirely and return a pre-built response — useful for response caching, where a cached result can be served without querying any subgraphs.
async fn on_execute<'exec>(
&'exec self,
payload: OnExecuteStartHookPayload<'exec>,
) -> OnExecuteStartHookResult<'exec> {
if let Some(cached) = self.cache.get(&cache_key) {
return payload.end_with_response(cached);
}
payload.proceed()
}Inspect the GraphQL response
Use on_end to read the executed data and errors before they are serialized and sent to the client — useful for caching the result or collecting metrics.
async fn on_execute<'exec>(
&'exec self,
payload: OnExecuteStartHookPayload<'exec>,
) -> OnExecuteStartHookResult<'exec> {
payload.on_end(|end_payload| {
let data = &end_payload.data;
let errors = &end_payload.errors;
end_payload.proceed()
})
}Capture the operation for usage reporting
Read operation_for_plan in the start phase to record the query string and operation name for telemetry or analytics before execution proceeds.
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(),
});
payload.proceed()
}Payload API Reference
Plugin Examples
on_subgraph_execute
This hook is called during the preparation process of every subgraph request, based on the query plan. At this stage, the subgraph name, the execution request to be sent, and other contextual information are available.
Plugins can use this hook to control the planning process of subgraph requests, replace or modify a subgraph response, pass custom data to subgraphs, and handle custom execution transports.
Unlike the on_subgraph_http_request hook, this hook does not yet have the complete HTTP request to be sent to the subgraph.
async fn on_subgraph_execute<'exec>(
&'exec self,
payload: OnSubgraphExecuteStartHookPayload<'exec>,
) -> OnSubgraphExecuteStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Add a header to the subgraph request
Mutate payload.execution_request.headers to inject custom headers into every request sent to a subgraph before it is dispatched.
async fn on_subgraph_execute<'exec>(
&'exec self,
mut payload: OnSubgraphExecuteStartHookPayload<'exec>,
) -> OnSubgraphExecuteStartHookResult<'exec> {
payload.execution_request.headers.insert(
"x-internal-token",
http::HeaderValue::from_static("secret"),
);
payload.on_end(|payload| payload.proceed())
}Filter by subgraph name
Use payload.subgraph_name to apply logic selectively to specific subgraphs.
async fn on_subgraph_execute<'exec>(
&'exec self,
payload: OnSubgraphExecuteStartHookPayload<'exec>,
) -> OnSubgraphExecuteStartHookResult<'exec> {
if payload.subgraph_name == "products" {
tracing::info!("Dispatching request to the products subgraph");
}
payload.on_end(|payload| payload.proceed())
}Count subgraph calls per request
Use context to track how many subgraph calls were made during a single GraphQL request and log the total in on_end.
async fn on_subgraph_execute<'exec>(
&'exec self,
payload: OnSubgraphExecuteStartHookPayload<'exec>,
) -> OnSubgraphExecuteStartHookResult<'exec> {
payload.on_end(|payload| {
let count = payload.context.get_mut::<SubgraphCallCount>();
if let Some(mut count) = count {
count.value += 1;
}
payload.proceed()
})
}Payload API Reference
Plugin Examples
on_subgraph_http_request
This hook is called during the actual HTTP call made to the subgraph. In this hook, you have access to the full HTTP request that will be sent to the subgraph.
Plugins using this hook can extend or modify the HTTP request, HTTP headers, path and other aspects of communicating with the subgraph.
You can also use this hook to capture or forward data returned from subgraphs.
async fn on_subgraph_http_request<'exec>(
&'exec self,
payload: OnSubgraphHttpRequestStartHookPayload<'exec>,
) -> OnSubgraphHttpRequestStartHookResult<'exec>The
'execlifetime parameter represents the lifetime of the GraphQL execution. It is used to ensure that the payload and result are valid for the duration of the GraphQL execution.
Usage Examples
Track the subgraph response status
Use on_end to capture the HTTP response status from each subgraph call and store it in context for use in a later hook (e.g., on_http_request).
async fn on_subgraph_http_request<'exec>(
&'exec self,
payload: OnSubgraphHttpRequestHookPayload<'exec>,
) -> OnSubgraphHttpRequestHookResult<'exec> {
payload.on_end(|payload| {
let status = payload.response.status;
if self.watched_codes.contains(&status) {
payload.context.insert(SubgraphStatus { code: status });
}
payload.proceed()
})
}Replace the subgraph HTTP request
Bypass the default HTTP transport entirely and send a custom request.
async fn on_subgraph_http_request<'exec>(
&'exec self,
payload: OnSubgraphHttpRequestHookPayload<'exec>,
) -> OnSubgraphHttpRequestHookResult<'exec> {
let response = self.client
.post(payload.endpoint.to_string())
.body(payload.body.clone())
.send()
.await
.unwrap();
payload.end_with_response(SubgraphHttpResponse {
status: response.status(),
headers: response.headers().clone().into(),
body: response.bytes().await.unwrap(),
})
}Payload API Reference
Plugin Examples
on_graphql_error
This hook is called whenever a GraphQL error is about to be sent to the client during plan execution. Use it to inspect or modify the error before it reaches the client.
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 transforming the error into a different format.
fn on_graphql_error(
&self,
payload: OnGraphQLErrorHookPayload
) -> OnGraphQLErrorHookResult;Usage Examples
Collect errors for monitoring
Intercept errors before they reach the client and record them for analytics, alerting, or structured logging.
fn on_graphql_error(&self, mut payload: OnGraphQLErrorHookPayload) -> OnGraphQLErrorHookResult {
payload.collected_errors.push(payload.error.message.clone());
payload.proceed()
}Mask sensitive error details
Sanitize error output by replacing the message and clearing fields like path before the error is sent to the client.
fn on_graphql_error(&self, mut payload: OnGraphQLErrorHookPayload) -> OnGraphQLErrorHookResult {
payload.error.message = "An internal error occurred".to_string();
payload.error.path = None;
payload.proceed()
}Remap error codes and HTTP status
Map known error codes to different codes or HTTP status codes based on plugin configuration — useful for normalizing errors from subgraphs.
fn on_graphql_error(&self, mut payload: OnGraphQLErrorHookPayload) -> OnGraphQLErrorHookResult {
if let Some(code) = &payload.error.extensions.code {
if let Some(mapping) = self.config.get(code) {
if let Some(new_code) = &mapping.code {
payload.error.extensions.code = Some(new_code.clone());
}
if let Some(status) = mapping.status_code {
if let Ok(status_code) = StatusCode::from_u16(status) {
payload.status_code = status_code;
}
}
}
}
payload.proceed()
}Payload API Reference
Plugin Examples
Router Lifecycle
on_shutdown
This hook is called when the router is shutting down.
Plugins can use this hook to implement cleanup and flush logic.
async fn on_shutdown<'exec>(&'exec self)Usage Examples
Flush buffered data before exit
Collect and send any in-memory data (e.g., usage reports or metrics) to an external service before the router process exits.
async fn on_shutdown<'exec>(&'exec self) {
let reports = self.reports.lock().await;
let _ = reqwest::Client::new()
.post(&self.endpoint)
.json(reports.as_slice())
.send()
.await;
}Plugin Examples
on_supergraph_load
This hook is called whenever the supergraph is loaded or reloaded. This occurs at startup and any time the supergraph is reloaded.
Plugins can use this hook to clear or reset caches and state that depends on the schema structure.
This hook is guaranteed to be called at least once for a healthy router instance during startup. If the Router is configured to poll for schema updates, it will be called each time the schema is reloaded.
fn on_supergraph_reload<'exec>(
&'exec self,
start_payload: OnSupergraphLoadStartHookPayload,
) -> OnSupergraphLoadStartHookResult<'exec>Usage Examples
Scan the schema for directives
Inspect the incoming new_ast to extract schema metadata — for example, collecting all types annotated with a custom directive and caching them for use in later hooks.
fn on_supergraph_reload<'exec>(
&'exec self,
start_payload: OnSupergraphLoadStartHookPayload,
) -> OnSupergraphLoadStartHookResult<'exec> {
let tagged_types: Vec<String> = start_payload.new_ast
.definitions
.iter()
.filter(|def| has_directive(def, "myDirective"))
.filter_map(|def| type_name(def))
.collect();
self.tagged_types.store(Arc::new(tagged_types));
start_payload.proceed()
}Invalidate caches on schema reload
Clear any schema-dependent state whenever the supergraph is reloaded, so stale entries don’t leak across schema versions.
fn on_supergraph_reload<'exec>(
&'exec self,
start_payload: OnSupergraphLoadStartHookPayload,
) -> OnSupergraphLoadStartHookResult<'exec> {
self.cache.clear();
tracing::info!("Supergraph reloaded — cache invalidated");
start_payload.proceed()
}Payload API Reference
Plugin Examples
Control Flow
Short-Circuit Responses
In many of the hooks mentioned above, you can short-circuit the request processing by providing a custom response.
For example, to return an early error response in certain conditions:
return payload.end_with_response(
Response::with_body(
StatusCode::BAD_REQUEST,
body.to_string().into(),
)
);The end_with_response method on payload provides a custom HTTP response. Several helper methods are also available to simplify creating 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_with_response_body(body: Serialize, status_code: StatusCode)- Accepts aserde-serializable body, automatically serializes it to JSON, and sets the appropriate headers. To simplify the above example:
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_body(
body,
StatusCode::BAD_REQUEST,
);end_with_graphql_error(error: GraphQLError, status: StatusCode)- Accepts aGraphQLErrordirectly and constructs the appropriate GraphQL error response.
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_with_graphql_error(
graphql_error,
StatusCode::BAD_REQUEST,
);Overriding Default Behavior
Rather than short-circuiting the entire request and returning an early response, you may want to override the default behavior at a specific stage.
For example, when implementing automatic persisted queries (APQ), you need to replace the query field in the GraphQLParams struct with the actual query string, resolved from the hash provided by the client.
#[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());
// 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()
})
}
}Integrations
OpenTelemetry Traces
Hive Router supports OpenTelemetry at its core, and uses the tracing crate for collecting, processing, and exporting OpenTelemetry trace information.
Use the tracing API to create spans and enrich the exported trace data with custom spans:
use tracing::{debug};
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
// Create and enter the span; the guard finalizes it when the function returns
let my_span = span!(Level::INFO, "my_plugin_on_http_request");
let _guard = my_span.enter();
payload.proceed()
}Logging
Hive Router uses the tracing crate for its internal logging. Logs emitted from plugins are correlated with the request being processed.
Use the built-in macros (debug!(...), info!(...), warn!(...), and error!(...)) to emit log lines, which will appear based on the Router’s logger configuration.
use tracing::{debug};
fn on_http_request<'req>(
&'req self,
payload: OnHttpRequestHookPayload<'req>,
) -> OnHttpRequestHookResult<'req> {
debug!("on_http_request is called now!");
payload.proceed()
}