Skip to content

Gateway route matchers

A common and annoying mismatch: the OpenAPI document describes the service’s own paths (/users/{id}), but you actually have to call it through a gateway that mounts it somewhere else (https://gw.example.com/api/v1/users/{id}).

Editing every URL by hand defeats the point of a live spec binding. A route matcher solves it by rewriting URLs at send time.

Create a route matcher and paste the gateway’s own configuration. Three kinds are supported:

KindWhat to paste
kongdeck / declarative YAML
nginxnginx.conf
envoyEnvoy YAML or JSON

The config is stored inline in the matcher’s YAML under .tinspec/route-matchers/, so it is committed with the project and reviewable.

Two scopes:

  • To an API provider — the usual case. In the provider’s settings, open the Network tab and pick the matcher, the upstream it corresponds to, and the gateway base URL. Every endpoint from that spec is then rewritten.
  • To a single request — the request editor’s Gateway tab, for a one-off.

A request’s own binding wins over the provider’s.

A matcher applies wherever the request is sent from — directly from the editor, as a step in a chain, as a single-node “test this step now”, as a request’s pre-run chain, and inside a pipeline. All of them load the same matchers from .tinspec/route-matchers/, so a request cannot go to the gateway one way and to the raw service URL another.

routeMatcher:
matcher: edge-kong
upstream: users-service
gatewayUrl: "{{gatewayUrl}}"

Leave upstream out and Tinspec matches automatically by comparing the spec’s server URL to the routes’ upstreams. Put gatewayUrl in an environment so the same project works against your local gateway and staging.

The provider’s Network tab shows a live preview table: each spec path next to the gateway URL it will actually be sent to, with a running count (“38/41 endpoints rewritten”) and every unmatched endpoint flagged with the reason.

Read that table. An endpoint the gateway does not route is a real finding — either the route is missing or the spec documents something that is not exposed.

When the config does not express a mapping — or expresses it in a way the parser cannot follow — add an explicit override:

overrides:
- upstream: users-service
from: /healthz # the backend path
to: /api/v1/healthz # the gateway-facing path

The common reason a route is unfollowable is a regex inbound match. Matching is done by inverting the gateway’s forward transform — given the backend path, produce the inbound path that would reach it — and a prefix or exact match inverts cleanly while an arbitrary regex does not. Simple regexes that are really prefixes in disguise are normalised; anything else is skipped, its endpoints show as unmatched, and an override is how you state the mapping by hand.

The Network tab is also where a provider’s send settings go: timeout, redirect policy, TLS (client certificate, CA bundle, OS trust store), and the proxy for every request under that provider. “This whole API is behind the corporate proxy, with this client certificate” becomes one statement instead of a per-request chore.

They resolve in four layers, most specific first — the request’s own Settings tab, the collection or folder’s defaults, this API-provider layer, and the app’s global settings as the floor (timeout and redirects under Settings → Sending, proxy and TLS under Settings → Network). A field this layer does not set is not “default”: it shows the value that will apply and where it comes from — “30 s — from provider billing-staging” — computed by the engine that does the sending rather than re-derived in the UI.

Proxy and TLS are deliberately offered on a folder, a provider and global settings, but not on an individual request: they describe a network, not a call. Secret material stays a reference — tls.clientKeyRef, proxy.auth.passwordRef — so a committed file never carries the value.

A gateway can also transcode gRPC to JSON — Envoy’s grpc_json_transcoder being the common case. Mark the matcher as transcoding; for Envoy configs this is auto-detected and the toggle just reflects it.

Then a unary gRPC method carrying a google.api.http annotation is sent as the mapped HTTP call rather than as native gRPC. The gRPC editor shows a chip naming the path it will use, so the decision is visible rather than surprising.

Streaming methods and methods without the annotation fall back to native gRPC.

  • The provider preview table shows the native gRPC retarget for a transcoding matcher — transcoded preview rows are not rendered. Transcoding needs the compiled descriptors, which the send path has and the preview does not.
  • The provider and collection send-settings layers do not apply inside a chain. A chain run is given your auth providers and your route matchers, but not the settings layers, so only a request’s own settings block applies. A suite behind a provider-configured proxy therefore works in the editor and fails in a chain. Put the setting on the request itself until this is wired.
  • There is no gRPC settings editor. Message-size limits and compression are carried in the file format and resolved by the engine, but nothing in the app edits them yet.
  • A proxy is not applied to gRPC. Rather than silently bypassing it, the engine refuses the send and names the proxy and the layer that configured it, so the fix — a noProxy entry for that host, or clearing the proxy for that request — is obvious.
routeMatchers:
- name: edge-kong
kind: kong
config: |
services:
- name: users-service
url: http://users:8000
routes:
- name: users
paths: [/api/v1]
strip_path: true
overrides:
- upstream: users-service
from: /healthz
to: /api/v1/healthz
transcodes: false