Setting the file. One moment.
Skill 07 · Apollo Router Plugin Creator
Subchapter 7.3
references/service-hooks.mdMarkdown7 KBView on GitHub
Intercepts at HTTP level before GraphQL parsing. Access raw HTTP request/response.
fn router_service(&self, service: router::BoxService) -> router::BoxService {
if !self.configuration.enabled {
return service;
}
ServiceBuilder::new()
.map_request(|mut request: router::Request| {
// Access/modify headers
let auth = request.router_request.headers().get("authorization");
request.router_request.headers_mut().insert("x-custom", "value".parse().unwrap());
// Add to context for later hooks
request.context.insert("my_key", value).unwrap();
request
})
.map_response(|mut response: router::Response| {
// Modify response headers
response.response.headers_mut().insert("cache-control", "no-store".parse().unwrap());
response
})
.service(service)
.boxed()
}Request types:
request.router_request - The HTTP requestrequest.router_request.headers() - Request headersrequest.router_request.headers_mut() - Mutable headersrequest.context - Shared contextIntercepts at GraphQL level after parsing. Access query, variables, extensions.
fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService {
if !self.configuration.enabled {
return service;
}
ServiceBuilder::new()
.checkpoint(move |request: supergraph::Request| {
// Check introspection
if let Ok(Some(true)) = request.context.get::<_, bool>(IS_INTROSPECTION_QUERY) {
return Ok(ControlFlow::Break(
supergraph::Response::error_builder()
.error(Error::builder().message("Forbidden").extension_code("FORBIDDEN").build())
.status_code(StatusCode::OK)
.context(request.context)
.build()
.unwrap()
));
}
Ok(ControlFlow::Continue(request))
})
.map_response(|response: supergraph::Response| {
// Transform response stream
response.map_stream(|mut graphql_response| {
graphql_response.extensions.insert("custom", json!({"key": "value"}).into());
graphql_response
})
})
.service(service)
.boxed()
}Request types:
request.supergraph_request - The GraphQL requestrequest.supergraph_request.headers() - Request headersrequest.query_plan - The query plan (in execution_service)request.context - Shared contextResponse transformation:
// For streaming responses
response.map_stream(|mut graphql_response| {
// Modify each response chunk
graphql_response
})Intercepts after query planning, before subgraph calls. Access to the query plan.
fn execution_service(&self, service: execution::BoxService) -> execution::BoxService {
if !self.configuration.enabled {
return service;
}
ServiceBuilder::new()
.checkpoint(move |request: execution::Request| {
// Access query plan
if request.query_plan.contains_mutations() {
return Ok(ControlFlow::Break(
execution::Response::error_builder()
.error(
Error::builder()
.message("Mutations are forbidden")
.extension_code("MUTATION_FORBIDDEN")
.build()
)
.status_code(StatusCode::BAD_REQUEST)
.context(request.context)
.build()
.unwrap()
));
}
Ok(ControlFlow::Continue(request))
})
.service(service)
.boxed()
}Request types:
request.query_plan - The generated query planrequest.query_plan.contains_mutations() - Check if plan has mutationsrequest.context - Shared contextUse cases:
Intercepts per-subgraph calls. Receives subgraph name as parameter.
fn subgraph_service(&self, name: &str, service: subgraph::BoxService) -> subgraph::BoxService {
if !self.configuration.enabled {
return service;
}
let subgraph_name = name.to_string();
let config = self.configuration.clone();
ServiceBuilder::new()
.map_request(move |mut request: subgraph::Request| {
// Add headers to subgraph request
request.subgraph_request.headers_mut().insert("x-subgraph", subgraph_name.parse().unwrap());
request
})
.map_response(move |mut response: subgraph::Response| {
// Read subgraph response extensions
let extensions = &response.response.body().extensions;
// Collect headers from subgraph
let cookies = response.response.headers().get_all("set-cookie");
response
})
.service(service)
.boxed()
}Request types:
request.subgraph_request - The HTTP request to subgraphresponse.response.body() - The GraphQL response bodyresponse.response.body().extensions - Response extensionsPer-subgraph configuration:
fn subgraph_service(&self, name: &str, service: subgraph::BoxService) -> subgraph::BoxService {
// Check if this subgraph has specific config
let subgraph_config = self.configuration.subgraphs.get(name);
if let Some(config) = subgraph_config {
// Apply subgraph-specific logic
}
service
}Use checkpoint_async for async operations:
fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService {
let client = self.http_client.clone();
ServiceBuilder::new()
.checkpoint_async(move |request: supergraph::Request| {
let client = client.clone();
async move {
// Async validation
let result = client.validate(&request).await;
if result.is_err() {
return Ok(ControlFlow::Break(error_response()));
}
Ok(ControlFlow::Continue(request))
}
})
.service(service)
.boxed()
}Use buffered() when cloning the service:
ServiceBuilder::new()
.buffered()
.checkpoint_async(...)
.service(service)
.boxed()supergraph::Response::error_builder()
.error(
Error::builder()
.message("Error message")
.extension_code("ERROR_CODE")
.build()
)
.status_code(StatusCode::OK)
.context(request.context)
.build()
.unwrap()supergraph::Response::error_builder()
.error(Error::builder().message("Forbidden").build())
.status_code(StatusCode::FORBIDDEN)
.context(request.context)
.build()
.unwrap()router::Response::error_builder()
.error(Error::builder().message("Not Found").build())
.status_code(StatusCode::NOT_FOUND)
.context(request.context)
.build()
.unwrap()