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.
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.
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.
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
Exactunless 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
Related
- Kubernetes Ingress & Gateway API — the Gateway, listener and attachment model these rules live inside.
- Path & Header-Based Routing — the same matching concepts outside Kubernetes, with match-cost analysis.
- Migrating from Ingress-NGINX to Gateway API — where these semantics bite during an annotation translation.