HTTPRoute Path Rewrites and Header Matching

Path rewriting is where most Gateway API migrations first meet a behaviour they did not expect, because the typed rewrite filter is deliberately narrower than the regular-expression rewrite it replaces. Header matching has the opposite problem: it is simple enough that people reach for it without thinking about what a caller can set. This page covers the exact semantics of both, with the cases that produce a working-looking route that sends the wrong path upstream.

Prerequisite concepts

This builds on the resource model in Kubernetes Ingress and Gateway API and on the general matching concepts in path and header-based routing, which covers precedence and the cost of each match type independently of Kubernetes.

What a rewrite actually replaces

ReplacePrefixMatch replaces exactly the prefix that the rule matched — not a fixed number of segments, and not a pattern. The consequence is that the rewritten path depends on the matches block, so editing a match silently changes every rewrite under it.

The matched prefix is the part that disappears With a PathPrefix match on /api/orders and a ReplacePrefixMatch of /v2, three example requests show the matched prefix being swapped for the replacement while the remainder of the path is preserved exactly, including a trailing slash and an empty remainder. match /api/orders → replace with /v2 request sent upstream /api/orders /8891 /v2/8891 /api/orders / /v2/ — the trailing slash survives /api/orders /v2 — no trailing slash added The last two rows are the ones that break upstreams: many frameworks treat /v2 and /v2/ as different routes, and the gateway faithfully forwards whichever the caller sent.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders-rewrite
  namespace: orders-prod
spec:
  parentRefs:
    - { name: edge, namespace: gateway-infra, sectionName: https }
  hostnames: ["orders.api.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api/orders }
      filters:
        - type: URLRewrite
          urlRewrite:
            hostname: orders.internal        # rewrite Host as well as path
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /v2
      backendRefs:
        - { name: orders, port: 8080 }

The other rewrite mode, ReplaceFullPath, discards the request path entirely and substitutes a fixed one. It is the right choice for collapsing a family of legacy URLs onto a single handler and the wrong choice everywhere else, because it silently throws away resource identifiers.

Matching on headers

A header match is an AND with the path match in the same matches entry, and separate entries in the list are ORed. That distinction produces two very different routes from nearly identical YAML.

  rules:
    # AND: path prefix AND both headers must match
    - matches:
        - path: { type: PathPrefix, value: /v2/orders }
          headers:
            - { name: x-api-version, value: "2" }
            - { name: x-channel, type: Exact, value: mobile }
      backendRefs: [{ name: orders-mobile, port: 8080 }]

    # OR: either of these two conditions selects the same backend
    - matches:
        - path: { type: PathPrefix, value: /v2/orders }
        - path: { type: PathPrefix, value: /orders }
      backendRefs: [{ name: orders, port: 8080 }]

Header matching supports Exact and RegularExpression. Exact is the default and is what you should use: a regular expression here is evaluated against a value the caller controls, with the cost characteristics described in regex vs prefix route matching performance. Where an implementation supports it, QueryParamMatch behaves the same way and carries the same caution.

Precedence when several rules match

Implementations do not evaluate rules top to bottom. The specification defines an ordering, and understanding it is what stops a rule from appearing dead.

The tiebreak ladder, in the order the specification applies it An exact path match beats a prefix match. A longer prefix beats a shorter one. More header matches beat fewer. If rules are still tied, the older resource by creation timestamp wins, and within one resource the earlier rule in the list wins. 1. exact path match beats prefix match 2. longer prefix beats shorter prefix — /v2/orders/8891 over /v2 3. more header or query matches beats fewer 4. older resource by creation timestamp — invisible in a manifest diff Rule 4 is deterministic and unreviewable: two teams writing overlapping routes get an outcome decided by which one applied first. Avoid overlap rather than relying on it, and assert the winner in a test.

Decision matrix

Goal Use
Strip a routing prefix before the upstream sees it ReplacePrefixMatch
Collapse many legacy URLs onto one handler ReplaceFullPath
Send one client segment to a different backend header match with Exact
Serve two path shapes from one backend two entries in one matches list
Require several conditions together one entry with several fields
Change the Host seen by the upstream urlRewrite.hostname

Gotchas and failure signals

A rewrite whose replacement is / turns /api/orders/8891 into /8891 and /api/orders into /, which usually means the upstream’s index handler answers requests that should have been a resource lookup. The symptom is a 200 with the wrong body rather than an error.

Header names are case-insensitive but values are not. x-channel: Mobile does not match value: mobile, and nothing reports this — the request simply falls through to a less specific rule.

Matching on a header the caller controls is an authorisation decision. If x-internal: true selects an internal backend, anyone can send it. Strip such headers with a RequestHeaderModifier on the way in, and route on verified identity as described in routing by API key vs JWT claims.

Filters run in a fixed order, not the order you list them. Header modification, redirects and rewrites each have a defined position in the chain, so listing a rewrite before a header modifier does not make it run first.

Filters run in specification order, not manifest order Request header modification happens first, then any redirect, which terminates the request, then the URL rewrite, then request mirroring, then forwarding to the backend. Writing the rewrite above the header modifier in the YAML does not change when either runs. RequestHeader Modifier RequestRedirect terminates here URLRewrite RequestMirror backend A redirect ends the request, so anything to its right never runs on a redirected call. This is why a header added for the upstream still appears on a mirrored request, and why a rewrite cannot be used to influence a redirect target — the redirect has already been decided by the time the rewrite would run. Only one filter of each type is permitted per rule, which removes the question of what two rewrites would mean.

Validation

  • Every rewrite tested with a trailing slash, without one, and with an empty remainder
  • A request that should 404 still 404s after the rewrite, at the upstream rather than at the gateway
  • Header matches use Exact unless a regular expression is genuinely required
  • Any header used for routing is stripped from inbound requests before matching, or verified
  • Overlapping rules eliminated, or the intended winner asserted in a test

FAQ

What is the difference between ReplacePrefixMatch and ReplaceFullPath?

ReplacePrefixMatch swaps the prefix that the rule matched and keeps the remainder of the path intact, so /api/orders/8891 with a match on /api/orders and a replacement of /v2 becomes /v2/8891. ReplaceFullPath discards the entire request path and substitutes a fixed value, which is right for collapsing legacy URLs onto one handler and wrong anywhere the path carries a resource identifier.

Do multiple entries in the matches list AND or OR together?

Entries in the matches list are ORed; fields inside one entry are ANDed. So one entry with a path and two headers requires all three to match, while three separate entries mean any one of them selects the rule. This is the single most common source of a route that matches far more traffic than intended.

Can I match on a header the client sets?

You can, and that makes it an authorisation decision rather than a routing detail. Anything a caller can set, a caller can forge, so either strip the header at the edge with a RequestHeaderModifier and re-add it from verified identity, or route on a claim from a validated token instead.

Which rule wins when several match?

Exact paths beat prefixes, longer prefixes beat shorter ones, more header matches beat fewer, and if everything is still tied the older resource wins by creation timestamp. That last tiebreak is deterministic but impossible to see in a manifest diff, so overlapping rules are best eliminated rather than reasoned about.


Parent: Kubernetes Ingress & Gateway API