Envoy Gateway HTTP Routing in Practice: Host, Path, Header, and Traffic Splitting

6 min read

The first two posts gave you the "what is this thing" foundation. This one gets into what you'll actually be editing day-to-day: HTTPRoute.

Think of HTTPRoute as a routing script for incoming traffic — except instead of writing Nginx rules or memorizing controller annotations, you're using Kubernetes-native resources.

One thing to get clear first: this post only covers HTTPRoute, because it handles HTTP/HTTPS semantics. For gRPC, you'd lean toward GRPCRoute; for raw TCP, TCPRoute; for TLS passthrough, TLSRoute. Don't force everything into HTTPRoute — the YAML starts giving off a "making do" vibe.

The Most Common Routing: Host and Path

Here's the most typical example:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

What this means:

  • Only Host: app.example.com will match this route
  • Path must start with /api
  • Traffic is forwarded to api-service:8080

If you remove hostnames, the route becomes "path-only, any domain."

What matches Can Match

matches is one of the most central concepts in HTTPRoute. Think of it as "what conditions trigger this rule."

Common match conditions:

  • path
  • headers
  • queryParams
  • method

Example:

matches:
  - method: GET
    path:
      type: PathPrefix
      value: /api
    headers:
      - name: version
        value: v2

Meaning:

  • Only GET requests
  • Path starts with /api
  • Request must include header version: v2

This pattern is ideal for API version trials or staged rollouts to specific users.

One Route, Multiple Rules

A single HTTPRoute can route different paths to different services:

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /api
    backendRefs:
      - name: api-service
        port: 8080
  - matches:
      - path:
          type: PathPrefix
          value: /admin
    backendRefs:
      - name: admin-service
        port: 8080

This is great for splitting sub-paths under one domain — for example:

  • /api goes to the API service
  • /admin goes to the admin backend

The routing logic is immediately visible. Unlike some legacy config files where you read halfway through and start doubting your own existence.

Header, Method, and Query as Conditions

HTTPRoute isn't limited to path matching — it can also match on request headers, methods, and query params.

For example, only route requests with version: v2 to the new service:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: versioned-route
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
          headers:
            - name: version
              value: v2
      backendRefs:
        - name: api-v2
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-v1
          port: 8080

This pattern works well for:

  • Canary releases
  • Letting specific customers try new versions early
  • Manual validation of new features

Compared to cutting over an entire new domain, header-based routing is more granular and easier to experiment with.

filters: Process Requests Before Forwarding

HTTPRoute doesn't just "match and forward" — it can also apply transformations before or after forwarding. That's what filters are for.

A common use case is adding headers:

rules:
  - filters:
      - type: RequestHeaderModifier
        requestHeaderModifier:
          add:
            - name: x-env
              value: prod
    backendRefs:
      - name: api-service
        port: 8080

This is useful for:

  • Injecting tracing metadata before forwarding to the backend
  • Adding fixed headers at the ingress layer
  • Request/response transformations

That said, filters are powerful but not a free pass to make the ingress layer a giant middleware blob. Too much logic piled onto routes will make debugging feel like it needs its own therapy session.

Weighted Traffic Splitting: The Most Common Canary Pattern

To send 90% of traffic to the old version and 10% to the new, use weight:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: canary-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - backendRefs:
        - name: api-v1
          port: 8080
          weight: 90
        - name: api-v2
          port: 8080
          weight: 10

This is basic canary / traffic splitting.

You don't need a full service mesh just to run a traffic experiment. For many API services, this is more than enough — and it's a lot cleaner to configure.

💡 weight is relative, not required to sum to 100. 9 and 1 express the same ratio as 90 and 10.

timeouts: Don't Let Requests Hang Forever

HTTPRoute supports timeout config at the rule level. This matters — if the ingress layer has no limits, bad requests sit around indefinitely like a rude customer who won't leave.

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /reports
    timeouts:
      request: 10s
      backendRequest: 2s
    backendRefs:
      - name: report-service
        port: 8080

Understanding the two fields:

  • request: Maximum total wait time for the client request
  • backendRequest: Maximum wait for a single request to the backend

Note that backendRequest shouldn't exceed request — the logic would be like saying "the whole trip takes 10 minutes, but one bus leg can wait 20 minutes." The universe gets confused.

Two Common Nuances

1. parentRefs Can Target a Specific Listener

If a Gateway has multiple listeners, you can bind to just one:

parentRefs:
  - name: eg
    sectionName: http

This means the route only attaches to the http listener. In multi-listener setups, this is far more predictable than attaching to everything.

2. No matches = Match Everything

If a rule has no matches, it defaults to matching / — essentially a catch-all.

This is convenient, but it can silently swallow what you thought were more specific rules. Don't skip it out of laziness and then spend an hour wondering why your precise rules aren't firing.

Verifying That Routing Works

After configuring routes, validate with curl:

curl -H "Host: app.example.com" http://$GATEWAY_HOST/api/users
curl -H "Host: app.example.com" -H "version: v2" http://$GATEWAY_HOST/api/users

To check HTTPRoute status:

kubectl get httproute app-route -o yaml

Pay particular attention to:

  • status.parents
  • The Accepted condition

Sometimes the YAML looks correct, but the Gateway rejected it. That's like submitting a job application and assuming you got the job because you heard nothing back.

One-Line Summary

The core of HTTPRoute is "match first, then forward." Once you have host, path, header, and weight covered, you can handle about 80% of day-to-day HTTP ingress requirements.

Next Step

Next post tackles something you'll hit in every production environment: HTTPS, certificates, TLS termination, and security: 👉 TLS and Security