Header Manipulation
When you're running a federated GraphQL setup, the router is the perfect place to handle HTTP headers consistently across all your subgraphs. Hive Router lets you add, remove, and modify headers as requests flow through your system.
This guide shows you practical ways to use header manipulation in real-world scenarios. For the
complete configuration reference, see headers configuration.
How Header Rules Work
Understanding a few key concepts will help you configure headers effectively:
Execution Order: Global rules (in the all block) run first, then subgraph-specific rules. This
lets you set defaults and then override them for specific subgraphs.
Request vs Response:
requestrules modify headers going from the router to your subgraphsresponserules modify headers going from the router back to the client
Rule Chaining: Rules within a block run in order, so you can chain transformations together.
Passing Client Headers to Subgraphs
The most common use case is forwarding headers from client requests to your subgraphs.
headers:
all:
request:
# Forward the Authorization header to all subgraphs
- propagate:
named: Authorization
# Also forward custom headers
- propagate:
named: X-User-ID
# Forward with a default if missing
- propagate:
named: X-Trace-ID
default: "router-generated-trace"This forwards Authorization and X-User-ID headers if they exist, and always sends X-Trace-ID
(creating one if the client didn't provide it).
Adding Custom Headers
Sometimes you need to add headers that weren't in the original request.
headers:
all:
request:
# Add a header with a fixed value
- insert:
name: X-Environment
value: production
# Add request timestamp
- insert:
name: X-Request-Time
expression: ".timestamp"Removing Sensitive Headers
You might want to strip certain headers before they reach specific subgraphs.
headers:
# By default, pass internal headers to all subgraphs
all:
request:
- propagate:
named: X-Internal-User-ID
- propagate:
named: X-Session-Token
# But remove them for the public-facing products service
subgraphs:
products:
request:
- remove:
named: X-Internal-User-ID
- remove:
named: X-Session-TokenModifying Header Values
You can also transform header values on the fly using expressions.
headers:
all:
request:
# Remove existing auth header and add normalized version
- remove:
named: Authorization
- insert:
name: Authorization
expression: "Bearer " + replace(replace(.request.headers.authorization, "Basic ", ""), "Bearer ", "")As you can see, this example uses the replace function to ensure the Authorization header is
always in Bearer <token> format. It's a function of VRL (Vector Remap Language) that Hive Router
supports for advanced transformations. List of available functions can be found in the
VRL documentation.
Managing Response Headers
Control what headers get sent back to clients.
headers:
all:
response:
# Add security headers to all responses
- insert:
name: X-Content-Type-Options
value: nosniff
- insert:
name: X-Frame-Options
value: DENY
# Forward Cache-Control; the router merges values from all subgraphs
- propagate:
named: Cache-Control
algorithm: appendCache-Control is handled specially: instead of picking one subgraph's value, the router merges the
values from every subgraph using a restrictive, most-conservative policy. See
Restrictive Cache-Control Merging below for the full details.
Restrictive Cache-Control Merging
In a federated graph, a single client response is assembled from many subgraph responses. Each
subgraph may return its own Cache-Control header. The question is: what Cache-Control should the
router send back to the client?
Picking one subgraph's value (or simply concatenating them) is dangerous. A public subgraph could
silently override a private or no-cache subgraph, and the router would happily cache a response
that was never meant to be cached. Worse, a mutation or an errored response could end up cached just
because one subgraph in the request had caching configured.
To prevent this, Hive Router merges Cache-Control across all subgraph responses using the most
conservative directive. The safest behavior is the default, and it is enforced - there is no way
to opt into the unsafe behavior.
Why it is always on
Making safe merging opt-in means an unaware developer gets unsafe behavior by default. Caching bugs are silent and easy to miss: nothing errors, responses just get served stale or leak across users. By the time you notice, private data may already have been cached and served to the wrong client.
Anyone propagating Cache-Control across subgraphs wants restrictive merging - there is no realistic
scenario where you would prefer a public subgraph to override a private one. So Hive Router opts
everyone in by design: the merge runs automatically whenever you propagate Cache-Control, and you
cannot turn it off while still propagating the header.
The merge algorithm
Given the Cache-Control headers from N subgraph responses, the router computes the result like
this:
- Any
no-store,no-cache, orprivatedirective in any subgraph short-circuits the result tono-store, no-cache. The most restrictive directive always wins. - Otherwise
max-ageis the minimum of all presentmax-agevalues. Subgraphs without amax-ageare ignored. publicis emitted only when every subgraph ispublic. If a single subgraph omitspublic, isprivate, or does not emit aCache-Controlheader at all, the mergedCache-Controlwill not contain thepublicdirective. The remaining directives still follow the merge rules above.must-revalidateis emitted if any subgraph sets it.
On top of that, the router forces no-store, no-cache, must-revalidate regardless of what the
subgraphs returned when any of the following is true:
- A subgraph executor error occurred (network failure, bad status, etc.).
- A subgraph response contained a GraphQL-level error (a non-empty
errorsarray). - The operation is a mutation.
Finally, the Cache-Control header is removed entirely when no subgraph sent a valid
Cache-Control value - for example non-UTF-8 bytes, or an empty or whitespace-only string that
parses to nothing.
Enabling the merge
The merge activates as soon as you propagate Cache-Control from your subgraph responses. Use
algorithm: append so every subgraph's value is kept and fed into the merge.
headers:
all:
response:
# Forward subgraph Cache-Control; append keeps every value for the merge
- propagate:
named: cache-control
algorithm: append
# Default emitted unless a subgraph provides one
default: "public, max-age=180"Because a subgraph that emits no Cache-Control header strips public from the merged result,
the default is your safety net. If you are not sure whether every subgraph will emit a
Cache-Control header, set a default (like public, max-age=180 above) - it is applied for any
subgraph that does not provide one, so the merge can still produce a public result. If you skip the
default, you must make sure all subgraphs emit a public Cache-Control header, otherwise the
merged response will never be public. The merge itself still works either way - you only lose the
public directive; every other directive (max-age, must-revalidate, etc.) is still merged
normally.
If you do not propagate Cache-Control, the merge is inactive and the header is not forwarded.
This is the safer default: you have to explicitly not propagate to disable merging, rather than
having to remember to enable safety.
Propagating Cache-Control with any algorithm other than append is rejected at compile time. Propagating
it across subgraphs without the restrictive merge would be unsafe, so the router will not let you do it.
Pinning or dropping a subgraph's contribution
Because the merge runs over whatever each subgraph contributes, you can use the regular insert and
remove rules to shape a specific subgraph's value before it enters the merge.
Pin a subgraph to a fixed value regardless of what it actually returns:
headers:
all:
response:
- propagate:
named: cache-control
algorithm: append
subgraphs:
pricing:
response:
# Pin this subgraph regardless of what it returns
- insert:
name: cache-control
value: "no-cache"Drop a subgraph from the merge entirely so it never influences the client's Cache-Control:
headers:
all:
response:
- propagate:
named: cache-control
algorithm: append
subgraphs:
analytics:
response:
# This subgraph's Cache-Control never reaches the merge
- remove:
named: cache-controlYou get the full power of header rules to augment caching - there is no separate config option, because the existing primitives already cover everything while keeping the restrictive merge always on.
Conditional Header Rules
Apply different rules based on request properties, using expressions.
headers:
all:
request:
# Add API version info
- insert:
name: X-API-Version
expression: |
if contains(.request.headers.accept || "", "application/vnd.api+json;version=2") {
"v2"
} else {
"v1"
}The above example checks the Accept header to determine which API version the client expects and
sets the X-API-Version header accordingly.
Best Practices
- Security first: Be careful about which headers you forward to external services. Remove sensitive headers from requests going to third-party APIs.
- Performance matters: Avoid complex header transformations in high-traffic scenarios. Simple propagation is fastest.
- Be explicit: Use descriptive header names and add comments to your configuration to explain business logic.
- Test thoroughly: Header manipulation can affect authentication, caching, and other critical behaviors. Test your rules with realistic traffic patterns.
- Monitor in production: Watch for header-related errors in your logs, especially authentication failures that might indicate misconfigured forwarding rules.
Remember that header manipulation happens on every request, so keep your rules efficient and well-tested.