Expand description
API versioning aids (DW-048): the Accept media-type criterion of route matching, and the Deprecation/Sunset response-header automation.
§What is code here vs already expressible (the honest split)
The router (DW-010) already covers the common versioning shapes, and this module deliberately does NOT re-implement any of them:
- Path-segment versions (
/v1/users,/v2/users): plain path routes with the canonical precedence (exact > regex > prefix; longest prefix wins), plusrewrite.replace_prefixto strip the version segment toward a version-agnostic upstream. Nothing new was needed. - Version-header constraints (
X-API-Version: "2"): the exact header matcher (match.headers) already expresses “this route additionally requires that header value”. Nothing new was needed. - Version-in-query (
?version=2):match.querycovers it.
Two things were genuinely missing, and are the whole of the code:
- Accept media-type selection (
accept_matches): the exact header matcher requires byte-equality of the ENTIRE header value, soAccept: application/vnd.acme.v2+json, application/json;q=0.8cannot select a version.match.acceptnames one media type; the route applies when any Accept entry names that type/subtype (case-insensitive), ignoring parameters and q-values — including q=0, which RFC 9110 section 12.5.1 defines asnot acceptablebut which does not affect selection in v1 (a documented design decision: naming the version selects it, however reluctantly). The configured value is normalized (padding, case) once at snapshot compile and the compiled form is what the criterion compares, so the raw config string never reaches the hot path. Wildcard entries and a missing Accept never match: version selection requires the client to NAME the version, so unconstrained clients fall through to the unversioned default route. - Deprecation/Sunset automation (
decorate): theroutes[].deprecationconfig block emitting the RFC headers (seeconfig::Deprecationfor the frozen semantics).
§The v1 same-path limitation (documented, deliberately not fixed)
Non-path criteria are applied AFTER path resolution and a criteria
miss does NOT fall through to another candidate (the frozen DW-010
model; duplicate exact templates are additionally rejected at
compile). Consequence: multiple versions of ONE path cannot be
selected by header or Accept in v1 — a version family uses distinct
paths (/v1/, /v2/, optionally constrained further by
match.headers/match.accept), or a single route serving one
version. Candidate iteration across same-path routes is a router
model change, out of scope for this aid; noted for the future.
§Caching correctness
A response whose route was selected by match.accept varies with
the request’s Accept, so the decoration tail merges
Vary: Accept into every response of such a route (the same
reasoning as CORS’s Vary: Origin), composing with compression’s
Vary: Accept-Encoding.
Functions§
- accept_
matches - Does the request’s
Acceptheader namemedia_type(the compiled, NORMALIZEDtype/subtypeofmatch.accept—RouteTable::accept_media_type, never the raw config string)? Any comma-separated entry of ANYAcceptheader line matches, case-insensitively, with parameters (;q=..., media-type parameters) and surrounding whitespace ignored. Wildcard entries (*/*,type/*) never match a specific type, and noAcceptheader at all never matches — both deliberately (see the module docs). - decorate
- Stamp a route’s deprecation policy onto a response (DW-048).
DeprecationandSunsetREPLACE any upstream-sent values (the gateway is the source of truth for the policy it is configured to emit — the same rule as theX-RateLimit-*headers);Linkis APPENDED (a list header: upstream links must survive). Unbuildable values are skipped, never panic — validation has already rejected them for publishable configs, so this is only a generation-tear backstop, the same posture as the respond-action headers.