Header-Based Routing for Spatial Domains

Header-based routing is the cheapest deterministic way to steer a spatial request to its owning domain without parsing the payload or trusting DNS. When each request advertises its domain, coordinate reference system, and tile matrix set in explicit headers, an Istio VirtualService can match those values and hand the request to exactly one backend — every time, for the same inputs. This operation lives under Cross-Domain Routing Strategies, part of the Federated Ownership & Routing Architecture: it implements the synchronous steering leg with header match blocks and a guaranteed default route, so that a request missing its domain header never silently lands on the wrong owner. Once a domain is migrating, this same match surface pairs with Weighted Traffic Shifting for Spatial Domain Migration to split a matched slice across old and new owners.

Figure — A request’s domain, CRS, and tile-matrix headers are matched in order; an unmatched request falls through to the default route rather than a guessed backend.

Header match order for spatial domain routing An ingress request carrying x-spatial-domain, x-crs, and tile matrix set headers is evaluated against ordered match rules. If the domain header equals terrain, it routes to the terrain domain service. If it equals hydrology, it routes to the hydrology domain service. If no rule matches, the request falls through to a default route that returns a 404 with diagnostics rather than steering to an arbitrary backend. Ingress request x-spatial-domain, x-crs Match 1: domain exact = terrain terrain service over mTLS Match 2: domain exact = hydrology hydrology service over mTLS Default route 404 with diagnostics

Prerequisites

Requirement Value / Constraint
Service mesh Istio 1.20+ with sidecar injection enabled on the spatial namespace
CLI tools istioctl 1.20+, kubectl, curl 7.80+ (for -H header injection)
CRS assumption Vector routes advertise x-crs: EPSG:4326; tiled raster routes may advertise x-crs: EPSG:3857 with a matching tile matrix set
Header contract x-spatial-domain (owning domain id), x-crs (CRS URN or EPSG code), x-tile-matrix-set (registered set name for tiled routes)
Access roles mesh-routing-admin to apply VirtualService; GIS Data Steward to register a domain id in the routing catalog
Env vars MESH_HOST=spatial.mesh.enterprise.internal, GATEWAY=mesh-ingress-gateway

Header matching is a pure function of the request headers, so applying the same VirtualService twice yields the same routing table — the operation is idempotent and safe to re-apply from CI. Keep the header names lowercase; Istio compares HTTP/2 header keys case-insensitively but your catalog and diagnostics should be canonical.

Step-by-Step Implementation

1. Route on the owning-domain header with an exact match

Bind the ingress gateway and add one match per registered domain. Order matters: Istio evaluates rules top-down and takes the first match, so the most specific rules come first and the default falls through last.

yaml
# spatial-domain-routing.yaml — apply with: kubectl apply -f
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: spatial-domain-router
  namespace: spatial
spec:
  hosts:
    - "spatial.mesh.enterprise.internal"
  gateways:
    - mesh-ingress-gateway
  http:
    - name: terrain-domain
      match:
        - headers:
            x-spatial-domain:
              exact: "terrain"          # owning domain id from the routing catalog
      route:
        - destination:
            host: terrain.spatial.svc.cluster.local
            port:
              number: 8080

Verify the matched route resolves to the terrain backend by injecting the header explicitly:

bash
curl -sD - -o /dev/null \
  -H "x-spatial-domain: terrain" \
  "https://${MESH_HOST}/v1/features/collections" | grep -Ei "HTTP/|x-envoy-upstream"

2. Add CRS and tile-matrix-set constraints to the match

A domain can own several products at different CRS or tiling schemes. Combine header matches so a request for web-mercator tiles cannot land on the EPSG:4326 vector backend. All headers in one match block are ANDed.

yaml
    - name: terrain-tiles-3857
      match:
        - headers:
            x-spatial-domain:
              exact: "terrain"
            x-crs:
              exact: "EPSG:3857"
            x-tile-matrix-set:
              exact: "WebMercatorQuad"   # registered set name
      route:
        - destination:
            host: terrain-tiles.spatial.svc.cluster.local
            port:
              number: 8080

Confirm that only the fully-specified header triple reaches the tile backend, and that a CRS mismatch does not:

bash
# Should hit the tile backend (200).
curl -so /dev/null -w "%{http_code}\n" \
  -H "x-spatial-domain: terrain" -H "x-crs: EPSG:3857" \
  -H "x-tile-matrix-set: WebMercatorQuad" \
  "https://${MESH_HOST}/v1/tiles/1/0/0"

3. Guarantee a deterministic default route

Never let an unmatched request pick a backend by accident. The final rule has no match block, so it catches everything that fell through and returns a structured 404 rather than steering to a default backend that does not own the data.

yaml
    - name: default-no-domain
      route:
        - destination:
            host: routing-diagnostics.spatial.svc.cluster.local
            port:
              number: 8080
      headers:
        response:
          add:
            x-routing-outcome: "no-domain-match"   # diagnostic for the runbook

Verify that a request with no domain header falls through to diagnostics, not a real domain:

bash
curl -sD - -o /dev/null \
  "https://${MESH_HOST}/v1/features/collections" \
  | grep -Ei "HTTP/|x-routing-outcome"

4. Freeze the routing table under version control

Apply the manifest through GitOps so the live boundary is reproducible, then use istioctl to prove the effective route order matches the committed file.

bash
kubectl apply -f spatial-domain-routing.yaml
istioctl proxy-config routes deploy/istio-ingressgateway \
  --name http.8080 -o json | jq '.[].virtualHosts[].routes[].match.headers'

Configuration Reference

Field Layer Required Purpose / Constraint
hosts VirtualService Yes The mesh hostname the gateway serves; must match the ingress Gateway
gateways VirtualService Yes Binds the route table to mesh-ingress-gateway; omit for mesh-internal only
http[].match.headers.x-spatial-domain Match Yes Exact owning-domain id; the primary steering key
http[].match.headers.x-crs Match Tiled/multi-CRS EPSG:4326 for vector, EPSG:3857 for web-mercator tiles
http[].match.headers.x-tile-matrix-set Match Tiled routes Registered set name, e.g. WebMercatorQuad; blocks CRS/tiling mismatch
route[].destination.host Route Yes Fully-qualified in-cluster service of the owning domain
Rule order VirtualService Yes First-match wins; specific rules first, unmatched default last
Default rule Route Yes The match-less final rule returning 404 with x-routing-outcome

Common Failure Modes & Fixes

  • Symptom: every request lands on the first domain regardless of header. Root cause: the default (match-less) rule was placed above the specific rules, so it wins for everything. Fix: reorder so all match rules precede the default; re-apply and re-run the step 4 istioctl proxy-config routes check.
  • Symptom: tile requests reach the vector backend and return EPSG:4326 geometry. Root cause: the tile rule matched only x-spatial-domain and ignored x-crs/x-tile-matrix-set. Fix: add the CRS and tile-matrix headers to the same match block so all three are ANDed, per step 2.
  • Symptom: header match never fires even with the correct -H value. Root cause: an upstream proxy stripped or lower-cased the header, or the rule used prefix where exact was intended. Fix: confirm propagation with curl -sD - and switch the match type to exact; register the header in the ingress allow-list.
  • Symptom: unmatched requests hang instead of returning 404. Root cause: no default rule exists, so Envoy has no route and holds the connection. Fix: append the default-no-domain rule from step 3 and re-apply.

FAQ

Why match on a header instead of the URL path or hostname?

Path and host encode the product, not the owner. A spatial product can migrate between owning domains without its public URL changing, and a single domain can serve many paths. Matching x-spatial-domain keeps ownership routing orthogonal to the API surface, which is exactly what makes a weighted migration possible without rewriting client URLs.

Is header matching deterministic and idempotent?

Yes. Given the same request headers, Istio’s first-match evaluation always selects the same route, and applying the same VirtualService produces the same table. That determinism is why this pattern is safe to drive from GitOps and safe to retry — no request is ever routed by chance.

How do I stop a client from spoofing x-spatial-domain to reach another domain?

Header matching decides where, not whether. Pair it with contract and identity checks at the same edge so a forged domain header still fails policy — see OPA vs Envoy WASM for Spatial Contract Validation for the validation leg. The x-spatial-domain value should be asserted against the JWT’s spatial scope claims, not trusted blindly.

What CRS header value should vector routes carry?

Vector exchange advertises x-crs: EPSG:4326 (lon/lat). Reserve EPSG:3857 for tiled raster and always pair it with x-tile-matrix-set: WebMercatorQuad so a mismatched CRS/tiling pair cannot match a route and instead falls through to the default 404.