Hive RouterGuides

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:

  • request rules modify headers going from the router to your subgraphs
  • response rules 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.

router.config.yaml
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.

router.config.yaml
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.

router.config.yaml
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-Token

Modifying Header Values

You can also transform header values on the fly using expressions.

router.config.yaml
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.

router.config.yaml
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: append

Cache-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:

  1. Any no-store, no-cache, or private directive in any subgraph short-circuits the result to no-store, no-cache. The most restrictive directive always wins.
  2. Otherwise max-age is the minimum of all present max-age values. Subgraphs without a max-age are ignored.
  3. public is emitted only when every subgraph is public. If a single subgraph omits public, is private, or does not emit a Cache-Control header at all, the merged Cache-Control will not contain the public directive. The remaining directives still follow the merge rules above.
  4. must-revalidate is 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 errors array).
  • 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.

router.config.yaml
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:

router.config.yaml
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:

router.config.yaml
headers:
  all:
    response:
      - propagate:
          named: cache-control
          algorithm: append
  subgraphs:
    analytics:
      response:
        # This subgraph's Cache-Control never reaches the merge
        - remove:
            named: cache-control

You 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.

router.config.yaml
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.