Lesson 34 of 66 · 20 min read · Updated

HTTPRoute Matching and Filters — Path, Header, Query and Method Routing

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:

  1. The structure of an HTTPRoute rule: matches, filters and backendRefs
  2. Path matching with Exact, PathPrefix and RegularExpression
  3. Header, query parameter and method matching
  4. How conditions combine: AND inside a match, OR between matches
  5. Precedence: which rule wins when several match the same request
  6. Filters: RequestHeaderModifier, ResponseHeaderModifier, URLRewrite, RequestRedirect and RequestMirror
  7. Request timeouts with request and backendRequest

Technical Requirements

  1. The minikube cluster with Envoy Gateway v1.9.2 and the storefront-gateway Gateway (GatewayClass eg, listener http on port 80) in the storefront namespace.
  2. The kiosk-api:1.0 Deployment and its ClusterIP Service kiosk-api (port 80 → 8080).
  3. Docker and the .NET 10 SDK to build a second sample API, orders-api.
  4. kubectl and curl. 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:

PartQuestion it answersIf omitted
matchesWhich requests does this rule take?One match: PathPrefix /, so every request
filtersWhat happens to the request (or response) on the way?Nothing is changed
backendRefsWhere does the request go?Nowhere. Only valid with a filter that answers by itself, such as RequestRedirect
rules:
- matches: # WHICH requests
- path:
type: PathPrefix
value: /api/orders
filters: # WHAT to change
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: x-matched-rule
value: orders
backendRefs: # WHERE to send them
- name: orders-api
port: 80

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

TypeMatches /api/ordersMatches /api/orders/42Matches /api/ordersarchiveSupport
Exact with /api/ordersYesNoNoCore
PathPrefix with /api/ordersYesYesNoCore
RegularExpression with ^/api/orders/[0-9]+$NoYesNoImplementation-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:

matches:
- path:
type: PathPrefix
value: /api/orders
headers:
- name: x-api-version
value: "2"
queryParams:
- name: debug
value: "true"
method: GET
FieldDefault typeSupport
headersExact; header names are case-insensitiveCore (Exact), implementation-specific (RegularExpression)
queryParamsExact; names are case-sensitiveExtended (Exact), implementation-specific (RegularExpression)
methodOne HTTP method, such as GET or POSTExtended

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:

  1. Everything inside one match must be true. Path AND headers AND query parameters AND method.
  2. The list of matches is ORed. The rule is taken if any single match is satisfied.
matches:
- path: # match 1
type: Exact
value: /status
- queryParams: # match 2
- name: debug
value: "true"

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:

  1. An Exact path match.
  2. Otherwise the PathPrefix match with the most characters.
  3. Then a method match.
  4. Then the largest number of header matches.
  5. 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:

FilterWhat it doesSupport
RequestHeaderModifierset, add or remove request headers before they reach the backendCore
ResponseHeaderModifierSame, for the response on its way backExtended
RequestRedirectAnswers with a 3xx and a new Location; the backend is never calledCore
URLRewriteChanges the host name or path the backend sees; the client never noticesExtended
RequestMirrorSends a copy of the request to another backend and ignores its responseExtended
CORSAnswers CORS preflights and adds the CORS response headersExtended
ExtensionRefPoints at an implementation's own filter resourceImplementation-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:

RequestRedirectURLRewrite
Who sees the new URLThe client, in the Location headerOnly the backend
Extra round tripYesNo
Typical useOld URLs moved permanently, HTTP to HTTPSPublic path /api/orders mapped to backend path /orders
Status code301 or 302 (Core); 303, 307, 308 (Extended); default 302None, 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:

FieldCoversSupport
requestThe whole request as the client experiences it, including retriesExtended, Standard channel since v1.2
backendRequestOne attempt from the Gateway to a backendExtended, 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.

kubectl get httproute storefront-routes -n storefront -o yaml

Check that a route was accepted

Accepted=False with a reason such as UnsupportedValue means the Gateway refused part of the route.

kubectl get httproute storefront-routes -n storefront -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'

Send a request with a custom header

-H sets any header. The Host header selects the route's host name.

curl -s -H "Host: storefront.local" -H "x-api-version: 2" http://localhost:8888/api/orders

Send a request with a different method

-X changes the method. -d adds a body and implies POST.

curl -s -X POST -H "Host: storefront.local" -d '{}' http://localhost:8888/api/orders

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.

curl -s -i -H "Host: storefront.local" http://localhost:8888/api/orders

Watch a mirror target receive traffic

Follow the logs of the shadow Deployment while you send requests to the primary.

kubectl logs -n storefront deploy/orders-api-shadow -f

Ask the API server which fields exist

kubectl explain reads the installed CRD, so it shows exactly what your Gateway API version supports.

kubectl explain httproute.spec.rules.filters.requestMirror

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:

  1. Public path /api/orders/... maps to the backend path /orders/....
  2. Clients that send x-api-version: 2 are routed by a separate rule, so the team can later point them at a new version.
  3. POST requests to /api/orders are handled by their own rule with a stricter timeout.
  4. The old URL /shop/orders/... redirects permanently to /api/orders/....
  5. Any request with ?debug=true, or a call to /status, goes to orders-api with a debug tag, so it echoes what it received.
  6. Every GET to the orders API is mirrored to orders-api-shadow.
  7. 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

dotnet new web -n OrdersApi
cd OrdersApi

Replace Program.cs. One catch-all endpoint echoes the request, and an optional delayMs query parameter simulates a slow backend.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string service = Environment.GetEnvironmentVariable("SERVICE_NAME") ?? "orders-api";
string podName = Environment.GetEnvironmentVariable("POD_NAME") ?? Environment.MachineName;
string[] hiddenPrefixes = ["x-forwarded-", "x-envoy-", "x-request-id"];

app.Map("/{**path}", async (HttpContext context) =>
{
if (int.TryParse(context.Request.Query["delayMs"], out var delayMs))
{
await Task.Delay(delayMs);
}

var headers = context.Request.Headers
.Where(h => h.Key.StartsWith("x-", StringComparison.OrdinalIgnoreCase))
.Where(h => !hiddenPrefixes.Any(p => h.Key.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
.ToDictionary(h => h.Key.ToLowerInvariant(), h => h.Value.ToString());

app.Logger.LogInformation("{Service} received {Method} {Path}{Query} host={Host}",
service, context.Request.Method, context.Request.Path, context.Request.QueryString, context.Request.Host);

return Results.Ok(new
{
service,
servedByPod = podName,
method = context.Request.Method,
path = context.Request.Path.Value,
query = context.Request.QueryString.Value,
headers
});
});

app.Run();

Add a Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrdersApi.csproj -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "OrdersApi.dll"]

Build it against minikube's Docker daemon, as in the earlier articles, so the cluster can use the image without a registry:

eval $(minikube docker-env)
docker build -t orders-api:1.0 .

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.

apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: storefront
spec:
replicas: 2
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: orders-api
image: orders-api:1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
---
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: storefront
spec:
selector:
app: orders-api
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api-shadow
namespace: storefront
spec:
replicas: 1
selector:
matchLabels:
app: orders-api-shadow
template:
metadata:
labels:
app: orders-api-shadow
spec:
containers:
- name: orders-api
image: orders-api:1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: SERVICE_NAME
value: orders-api-shadow
---
apiVersion: v1
kind: Service
metadata:
name: orders-api-shadow
namespace: storefront
spec:
selector:
app: orders-api-shadow
ports:
- name: http
port: 80
targetPort: 8080
kubectl apply -f 1-orders-api.yaml
kubectl get pods -n storefront -l 'app in (orders-api, orders-api-shadow)'
NAME READY STATUS RESTARTS AGE
orders-api-5f8d9c6b7-4kq2m 1/1 Running 0 15s
orders-api-5f8d9c6b7-x7p9t 1/1 Running 0 15s
orders-api-shadow-7c9b8d5f4-n2v6w 1/1 Running 0 15s

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:

kubectl delete httproute --all -n storefront

Read the route rule by rule; the comments say which requirement each one implements.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: storefront-routes
namespace: storefront
spec:
parentRefs:
- name: storefront-gateway
hostnames:
- storefront.local
rules:
# 1. Catch-all: everything nobody else wants goes to kiosk-api.
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: kiosk-api
port: 80

# 2. The orders API: rewrite /api/orders -> /orders, mirror, tag the response.
- matches:
- path:
type: PathPrefix
value: /api/orders
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: x-matched-rule
value: orders
remove:
- x-internal-debug
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /orders
- type: ResponseHeaderModifier
responseHeaderModifier:
add:
- name: x-served-via
value: storefront-gateway
- type: RequestMirror
requestMirror:
backendRef:
name: orders-api-shadow
port: 80
timeouts:
request: 10s
backendRequest: 5s
backendRefs:
- name: orders-api
port: 80

# 3. Same path, but only for clients that ask for version 2.
- matches:
- path:
type: PathPrefix
value: /api/orders
headers:
- name: x-api-version
value: "2"
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: x-matched-rule
value: orders-v2
backendRefs:
- name: orders-api
port: 80

# 4. Writes get their own rule and a much shorter timeout.
- matches:
- path:
type: PathPrefix
value: /api/orders
method: POST
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: x-matched-rule
value: orders-write
timeouts:
request: 3s
backendRequest: 2s
backendRefs:
- name: orders-api
port: 80

# 5. Old URLs move permanently to the new path.
- matches:
- path:
type: PathPrefix
value: /shop/orders
filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /api/orders
statusCode: 301

# 6. /status OR ?debug=true goes to orders-api with a debug tag.
- matches:
- path:
type: Exact
value: /status
- queryParams:
- name: debug
value: "true"
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: x-matched-rule
value: debug
backendRefs:
- name: orders-api
port: 80
kubectl apply -f 2-storefront-routes.yaml
kubectl get httproute storefront-routes -n storefront -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
Accepted=True (Accepted)
ResolvedRefs=True (ResolvedRefs)

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:

export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system \
--selector=gateway.envoyproxy.io/owning-gateway-namespace=storefront,gateway.envoyproxy.io/owning-gateway-name=storefront-gateway \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n envoy-gateway-system port-forward service/$ENVOY_SERVICE 8888:80

Step 4 — Prefix Matching and the Rewrite

curl -s -H "Host: storefront.local" http://localhost:8888/api/orders/42
{"service":"orders-api","servedByPod":"orders-api-5f8d9c6b7-4kq2m","method":"GET","path":"/orders/42","query":"","headers":{"x-matched-rule":"orders"}}

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:

curl -s -i -H "Host: storefront.local" http://localhost:8888/api/ordersarchive
HTTP/1.1 404 Not Found
content-length: 0
server: Kestrel

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

curl -s -H "Host: storefront.local" -H "x-api-version: 2" http://localhost:8888/api/orders/42
{"service":"orders-api",...,"path":"/api/orders/42","query":"","headers":{"x-api-version":"2","x-matched-rule":"orders-v2"}}

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

curl -s -X POST -H "Host: storefront.local" -d '{}' http://localhost:8888/api/orders
{"service":"orders-api",...,"method":"POST","path":"/api/orders","query":"","headers":{"x-matched-rule":"orders-write"}}

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:

curl -s -X POST -H "Host: storefront.local" -H "x-api-version: 2" -d '{}' http://localhost:8888/api/orders
{"service":"orders-api",...,"method":"POST","path":"/api/orders","query":"","headers":{"x-api-version":"2","x-matched-rule":"orders-v2"}}

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:

curl -s -H "Host: storefront.local" http://localhost:8888/status
curl -s -H "Host: storefront.local" "http://localhost:8888/products?debug=true"
{"service":"orders-api",...,"path":"/status","query":"","headers":{"x-matched-rule":"debug"}}
{"service":"orders-api",...,"path":"/products","query":"?debug=true","headers":{"x-matched-rule":"debug"}}

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

curl -s -H "Host: storefront.local" "http://localhost:8888/api/orders?debug=true"
{"service":"orders-api",...,"path":"/orders","query":"?debug=true","headers":{"x-matched-rule":"orders"}}

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:

curl -s -i -H "Host: storefront.local" -H "x-internal-debug: on" -H "x-tenant: north" http://localhost:8888/api/orders
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-served-via: storefront-gateway
...
{"service":"orders-api",...,"headers":{"x-tenant":"north","x-matched-rule":"orders"}}

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:

curl -s -H "Host: storefront.local" -H "x-matched-rule: admin" http://localhost:8888/api/orders

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

curl -s -i -H "Host: storefront.local" "http://localhost:8888/shop/orders/42?page=1"
HTTP/1.1 301 Moved Permanently
location: http://storefront.local/api/orders/42?page=1

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:

kubectl logs -n storefront deploy/orders-api-shadow -f

Send a few requests through rule 2:

for i in 1 2 3; do curl -s -o /dev/null -H "Host: storefront.local" http://localhost:8888/api/orders/$i; done
info: OrdersApi[0]
orders-api-shadow received GET /orders/1 host=storefront.local
info: OrdersApi[0]
orders-api-shadow received GET /orders/2 host=storefront.local
info: OrdersApi[0]
orders-api-shadow received GET /orders/3 host=storefront.local

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:

time curl -s -i -X POST -H "Host: storefront.local" -d '{}' "http://localhost:8888/api/orders?delayMs=3000"
HTTP/1.1 504 Gateway Timeout
content-length: 24
content-type: text/plain

upstream request timeout
real 0m2.04s

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:

curl -s -H "Host: storefront.local" "http://localhost:8888/api/orders?delayMs=3000"

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.

kubectl delete httproute storefront-routes -n storefront
kubectl delete -f 1-orders-api.yaml

Summary

  1. An HTTPRoute rule has matches (which requests), filters (what to change) and backendRefs (where to send them). Without matches, a rule takes everything under /.
  2. PathPrefix matches whole path segments: /api/orders matches /api/orders/42 but not /api/ordersarchive. RegularExpression is implementation-specific.
  3. Conditions inside one match are ANDed. The list of matches is ORed. One misplaced dash changes the meaning.
  4. 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.
  5. Filters belong to the winning rule only. A more specific rule without a rewrite means no rewrite.
  6. RequestHeaderModifier is Core; use set for headers your backend trusts so clients cannot spoof them. ResponseHeaderModifier, URLRewrite and RequestMirror are Extended.
  7. A redirect changes the URL the client sees. A rewrite changes only what the backend receives.
  8. Mirroring sends real traffic to a second backend and discards its response. Mirror only into isolated services.
  9. timeouts.request and timeouts.backendRequest turn 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.