Lesson 34 of 66 · 20 min read · Updated
HTTPRoute Matching and Filters — Path, Header, Query and Method Routing
On this page
In the previous article you put HTTPS in front of kiosk-api and redirected plain HTTP to it. Every request still went to the same backend. The route had one rule, and that rule matched everything under /.
Real gateways do much more than that. One host name usually fronts several services, and the Gateway decides where each request goes by looking at the path, the headers, the query string and the method. On the way through it often changes the request: it adds a header, strips a path prefix, sends a copy to a test service, or refuses to wait for a slow backend. In Ingress most of this needed controller-specific annotations. In Gateway API it is part of the HTTPRoute specification.
We will cover the following topics:
- The structure of an HTTPRoute rule: matches, filters and backendRefs
- Path matching with
Exact,PathPrefixandRegularExpression - Header, query parameter and method matching
- How conditions combine: AND inside a match, OR between matches
- Precedence: which rule wins when several match the same request
- Filters:
RequestHeaderModifier,ResponseHeaderModifier,URLRewrite,RequestRedirectandRequestMirror - Request timeouts with
requestandbackendRequest
Technical Requirements
- The minikube cluster with Envoy Gateway v1.9.2 and the
storefront-gatewayGateway (GatewayClasseg, listenerhttpon port 80) in thestorefrontnamespace. - The
kiosk-api:1.0Deployment and its ClusterIP Servicekiosk-api(port 80 → 8080). - Docker and the .NET 10 SDK to build a second sample API,
orders-api. kubectlandcurl. If you finished the previous article with HTTPS still enabled, the examples work the same on port 8443 with--resolve; this article uses plain HTTP to keep the commands short.
Core Concepts
Anatomy of an HTTPRoute Rule
An HTTPRoute has a list of rules. Each rule has three parts:
| Part | Question it answers | If omitted |
|---|---|---|
matches | Which requests does this rule take? | One match: PathPrefix /, so every request |
filters | What happens to the request (or response) on the way? | Nothing is changed |
backendRefs | Where does the request go? | Nowhere. Only valid with a filter that answers by itself, such as RequestRedirect |
All rules of all HTTPRoutes attached to the same listener and host name are merged into one routing table. For every request exactly one rule wins.
Path Matching
| Type | Matches /api/orders | Matches /api/orders/42 | Matches /api/ordersarchive | Support |
|---|---|---|---|---|
Exact with /api/orders | Yes | No | No | Core |
PathPrefix with /api/orders | Yes | Yes | No | Core |
RegularExpression with ^/api/orders/[0-9]+$ | No | Yes | No | Implementation-specific |
Two details matter here. First, PathPrefix matches whole path segments, not characters. /api/orders matches /api/orders/42 but not /api/ordersarchive. Many Ingress controllers got this wrong for years; Gateway API defines it precisely. A trailing slash in the prefix is ignored, so /api/orders/ behaves like /api/orders.
Second, RegularExpression is implementation-specific. Envoy Gateway uses Envoy's RE2 dialect, other implementations may use PCRE or POSIX, and how a regex rule ranks against other rules is also left to the implementation. Use it when you have to, and re-test it if you ever change implementation.
Header, Query Parameter and Method Matching
A match can also inspect the request beyond the path:
| Field | Default type | Support |
|---|---|---|
headers | Exact; header names are case-insensitive | Core (Exact), implementation-specific (RegularExpression) |
queryParams | Exact; names are case-sensitive | Extended (Exact), implementation-specific (RegularExpression) |
method | One HTTP method, such as GET or POST | Extended |
Extended means the feature is defined by the specification and conformance-tested, but an implementation may choose not to support it. Envoy Gateway supports all of them. When you choose another implementation, check its conformance report for HTTPRouteQueryParamMatching and HTTPRouteMethodMatching.
AND Inside a Match, OR Between Matches
This is the rule that causes the most confusion, so it is worth stating plainly:
- Everything inside one match must be true. Path AND headers AND query parameters AND method.
- The list of matches is ORed. The rule is taken if any single match is satisfied.
This rule takes /status OR any request with ?debug=true. If you put both conditions into the same list item, it would only take /status?debug=true. The YAML differs by one dash.
Precedence: Which Rule Wins
When several rules match one request, Gateway API decides with a fixed order. Across all rules on all routes for the same host name, the winner is the match with:
- An
Exactpath match. - Otherwise the
PathPrefixmatch with the most characters. - Then a method match.
- Then the largest number of header matches.
- Then the largest number of query parameter matches.
If there is still a tie between routes, the oldest HTTPRoute wins, then the one first in alphabetical order of namespace/name. Inside a single HTTPRoute, the first matching rule in list order wins.
Treat this list as the specification, and test ties that cross categories on your implementation. In our test with Envoy Gateway v1.9.2, a rule with one header match won over a rule with a method match on the same prefix, the opposite of the specified order (Step 6 shows it). Design your rules so that one request is never claimed by a header rule and a method rule at the same time, and the question does not arise.
The practical consequence: the order in which you write rules almost never matters. A catch-all PathPrefix / rule can sit at the top of the file and it still loses to /api/orders, because the longer prefix wins. A rule with a header condition beats the same path without one. Specificity decides, not position.
Filters
Filters run after a rule has been chosen. Each has a support level:
| Filter | What it does | Support |
|---|---|---|
RequestHeaderModifier | set, add or remove request headers before they reach the backend | Core |
ResponseHeaderModifier | Same, for the response on its way back | Extended |
RequestRedirect | Answers with a 3xx and a new Location; the backend is never called | Core |
URLRewrite | Changes the host name or path the backend sees; the client never notices | Extended |
RequestMirror | Sends a copy of the request to another backend and ignores its response | Extended |
CORS | Answers CORS preflights and adds the CORS response headers | Extended |
ExtensionRef | Points at an implementation's own filter resource | Implementation-specific |
set overwrites a header, add appends a value to whatever is already there, and remove deletes it. RequestRedirect and URLRewrite cannot be used in the same rule: one sends the client elsewhere, the other changes the request silently, and doing both makes no sense.
Redirect vs Rewrite
These two are easy to mix up:
| RequestRedirect | URLRewrite | |
|---|---|---|
| Who sees the new URL | The client, in the Location header | Only the backend |
| Extra round trip | Yes | No |
| Typical use | Old URLs moved permanently, HTTP to HTTPS | Public path /api/orders mapped to backend path /orders |
| Status code | 301 or 302 (Core); 303, 307, 308 (Extended); default 302 | None, the backend answers |
Both accept a path modifier of two types. ReplaceFullPath replaces the whole path. ReplacePrefixMatch replaces only the part matched by the rule's PathPrefix, so /api/orders/42 with prefix /api/orders and replacement /orders becomes /orders/42. ReplacePrefixMatch is only valid when the rule has exactly one match and it is a PathPrefix; the API server rejects the route otherwise.
Mirroring
RequestMirror sends a copy of each matched request to a second backend. The response of the copy is thrown away, and the client only ever sees the primary backend's answer. That makes it a safe way to test a new version against real traffic. You can mirror a share of requests with percent (0 to 100) or fraction; without either, every request is copied. Older Envoy releases marked the copies by appending -shadow to the Host header; current Envoy, including the one shipped with Envoy Gateway v1.9.2, sends the original Host unchanged. The shadow service therefore cannot tell a mirrored request from a real one by looking at it, which is one more reason to run it in isolation.
Mirrored traffic is real traffic. A mirrored POST /orders creates a second order if the shadow service writes to the same database. Mirror into something that is isolated or read-only.
Timeouts
A rule can carry two timeouts:
| Field | Covers | Support |
|---|---|---|
request | The whole request as the client experiences it, including retries | Extended, Standard channel since v1.2 |
backendRequest | One attempt from the Gateway to a backend | Extended, Standard channel since v1.2 |
backendRequest must not be greater than request. 0s disables a timeout. When a timeout fires, Envoy answers 504 Gateway Timeout and the client gets a fast, honest failure instead of a hung connection.
Hands-On: Kubernetes Commands
Show every rule of a route
kubectl get hides the rules; -o yaml shows them exactly as the API server stored them, including defaults it filled in.
Check that a route was accepted
Accepted=False with a reason such as UnsupportedValue means the Gateway refused part of the route.
Send a request with a custom header
-H sets any header. The Host header selects the route's host name.
Send a request with a different method
-X changes the method. -d adds a body and implies POST.
Show only the response headers
-I sends a HEAD request; -i includes headers with a normal request. -i is safer, because a route with a method: GET match does not take HEAD requests.
Watch a mirror target receive traffic
Follow the logs of the shadow Deployment while you send requests to the primary.
Ask the API server which fields exist
kubectl explain reads the installed CRD, so it shows exactly what your Gateway API version supports.
Step-by-Step Example
The Scenario
The storefront now has a second service. orders-api owns everything under /api/orders, and kiosk-api keeps serving everything else on storefront.local. The requirements:
- Public path
/api/orders/...maps to the backend path/orders/.... - Clients that send
x-api-version: 2are routed by a separate rule, so the team can later point them at a new version. POSTrequests to/api/ordersare handled by their own rule with a stricter timeout.- The old URL
/shop/orders/...redirects permanently to/api/orders/.... - Any request with
?debug=true, or a call to/status, goes toorders-apiwith a debug tag, so it echoes what it received. - Every
GETto the orders API is mirrored toorders-api-shadow. - Every response says which gateway served it.
To make each decision visible, every rule sets a request header named x-matched-rule, and orders-api echoes back the method, path, query and x- headers it received.
Step 1 — Create orders-api
Replace Program.cs. One catch-all endpoint echoes the request, and an optional delayMs query parameter simulates a slow backend.
Add a Dockerfile:
Build it against minikube's Docker daemon, as in the earlier articles, so the cluster can use the image without a registry:
On Docker Desktop's built-in kind cluster, skip minikube docker-env: a plain docker build is enough, because the cluster nodes can use images from Docker Desktop's image store. On a plain kind cluster, run kind load docker-image orders-api:1.0 after the build. If you rebuild the image later, give it a new tag: nodes that already cached orders-api:1.0 keep running the old copy under imagePullPolicy: IfNotPresent.
Step 2 — Deploy orders-api and Its Shadow
The shadow runs the same image. Only SERVICE_NAME differs, so its responses and logs are easy to recognise.
Step 3 — Write the Route
One HTTPRoute holds every rule for storefront.local. Remove any routes left over from earlier articles first, so nothing else competes for the same host name:
Read the route rule by rule; the comments say which requirement each one implements.
Notice that the catch-all is rule 1, at the top. It does not swallow the other rules, because precedence is decided by specificity, not by position.
Start the port-forward in a second terminal if it is not already running:
Step 4 — Prefix Matching and the Rewrite
The client asked for /api/orders/42; the backend received /orders/42. ReplacePrefixMatch swapped only the matched prefix and kept the rest.
Now prove that prefixes match whole segments:
/api/ordersarchive does not start with the segment /api/orders, so rule 2 does not apply and the catch-all sends it to kiosk-api. kiosk-api only maps / and /healthz, so it answers 404 itself. The server: Kestrel header proves that a .NET backend answered. When Envoy finds no route at all, its own 404 has no server header, and orders-api would have echoed the request as JSON.
Step 5 — Header Matching and Precedence
Send the same path with the version header:
Rules 2 and 3 both match, with the same prefix. Rule 3 has one header match and rule 2 has none, so rule 3 wins. Notice what that means: rule 3 has no URLRewrite, so the backend now sees the original /api/orders/42. Filters belong to a rule, not to a path. When a more specific rule wins, the filters of the less specific rule do not run.
Step 6 — Method Matching
Rule 4 wins over rule 2 because a method match ranks higher than no method match.
What if a POST also carries x-api-version: 2? Rule 3 has a header match, rule 4 has a method match. The specification puts method before headers, so rule 4 should win. Check what your implementation actually does:
Envoy Gateway v1.9.2 picked rule 3, the header rule. The write request lost its 3-second timeout without any warning. This is exactly the kind of cross-category tie you should not leave to the implementation. If writes must always get their own timeout, add method: POST to a copy of the header match too, or keep version routing and write routing on different paths.
Step 7 — OR Between Matches
Rule 6 has two matches in its list, so either one is enough:
/status is an Exact match, the strongest kind. /products?debug=true competes with the catch-all on the prefix /; both have the same prefix length, but rule 6 has one query parameter match, so it wins.
Now try /api/orders?debug=true:
Rule 2 wins. Its prefix /api/orders is longer than rule 6's implicit /, and path length is checked before query parameters. If that is not what you want, add the query parameter to a match that also has the longer path.
Step 8 — Header Modifiers
Rule 2 removes x-internal-debug before the request reaches the backend, and adds x-served-via to the response:
x-tenant passed through untouched, x-internal-debug never arrived, and the response carries a header the application never set. This is how you keep internal headers from leaking in either direction without touching application code.
Also try to spoof the rule header yourself:
The backend still sees "x-matched-rule":"orders", because set overwrites. Rule 6 uses add instead, which appends to any value the client sent. Use set for any header your backend trusts.
Step 9 — The Redirect
The backend was never called. The client receives a new URL, follows it, and from then on uses the new path. Compare with Step 4, where the rewrite was invisible to the client.
Step 10 — Mirroring
Follow the shadow's logs in a third terminal:
Send a few requests through rule 2:
The shadow received every request, already rewritten to /orders/..., with the original host name. The client only saw responses from orders-api. If the shadow is slow or crashes, the client does not notice. To mirror only a sample, add percent: 10 to requestMirror.
Step 11 — Timeouts
Rule 4 allows a backend 2 seconds. Ask orders-api to take 3:
The Gateway gave up after 2 seconds and returned 504. Send the same delay as a GET, which goes through rule 2 with its 5-second budget:
This one succeeds after 3 seconds. Same backend, same delay, different rule, different outcome.
Step 12 — Clean Up
Remove the route and both orders Deployments. kiosk-api stays; the next article builds on it.
Summary
- An HTTPRoute rule has matches (which requests), filters (what to change) and backendRefs (where to send them). Without matches, a rule takes everything under
/. PathPrefixmatches whole path segments:/api/ordersmatches/api/orders/42but not/api/ordersarchive.RegularExpressionis implementation-specific.- Conditions inside one match are ANDed. The list of matches is ORed. One misplaced dash changes the meaning.
- Precedence is decided by specificity: Exact path, longest prefix, method, number of header matches, number of query matches, then the oldest route. Rule order in the file almost never matters. Test ties that mix a header rule and a method rule; Envoy Gateway v1.9.2 favoured the header rule.
- Filters belong to the winning rule only. A more specific rule without a rewrite means no rewrite.
RequestHeaderModifieris Core; usesetfor headers your backend trusts so clients cannot spoof them.ResponseHeaderModifier,URLRewriteandRequestMirrorare Extended.- A redirect changes the URL the client sees. A rewrite changes only what the backend receives.
- Mirroring sends real traffic to a second backend and discards its response. Mirror only into isolated services.
timeouts.requestandtimeouts.backendRequestturn a hung backend into a fast 504.
In the next article we use backendRefs weights to move traffic gradually from one version of kiosk-api to another, and build canary and blue/green releases entirely at the Gateway.