Expand description
Request/response transformation grammar (DW-028): the config shapes for header/query manipulation and JSON-pointer body transforms, the shared JSON-Pointer grammar validation and the runtime agree on, and the security-header injection policy (feature analysis 4.12 and the 5-Security “security header injection” row).
This is config-contract grammar in the same sense as net.rs (the
IP/CIDR vocabulary) and versioning.rs (the HTTP-date and media-type
vocabularies): validation (snapshot::validate) and the runtime
(dataplane::transforms) must agree on ONE parsing of these strings,
so the grammar lives in config, the lowest consuming domain.
§The streaming contract (the issue’s done-when)
“Streaming is preserved unless a transform explicitly buffers.” The surface splits exactly along that line:
- Header and query ops, and security headers, never touch a body: streaming responses (SSE, upgrades, chunked downloads) pass with zero buffering, exactly as before.
- A JSON body transform is the one EXPLICITLY BUFFERING transform:
it opts the route in, applies only to JSON bodies (content-type
gated), and buffers at most
max_bytes— a hard cap enforced both against a declaredContent-Lengthand against the live stream. Over-cap bodies fail CLOSED (413 request-side, 502 response-side), never “transform what fit” and never silently pass through: a skipped policy transform is a fail-open data leak in the masking direction (DW-029 builds on this machinery).
§Pointer semantics (why strict)
A set/remove whose JSON Pointer does not resolve at runtime is an
ERROR, not a skip. Pointer misses are schema drift, and in the
redaction direction (remove the secret field) a silent miss is
exactly the leak the policy exists to prevent. Strictness costs
operators a 400/502 on drifted contracts; leniency would cost them
their data. The error names the offending pointer server-side only
(the client envelope carries a generic message).
§Security headers vs the decoration tail
Security headers are an EDGE property of every response the route
emits — action responses and gateway short-circuits alike (a 401
without X-Content-Type-Options: nosniff is a real, if minor, gap;
contrast deprecation stamps, which announce API lifecycle and are
deliberately absent from short-circuits). They REPLACE any
upstream-sent values: the gateway is the source of truth for its
edge policy, the same rule as the deprecation and X-RateLimit-*
headers. They apply last in the response decoration tail, after
operator transforms: an operator who needs per-route-exception
behavior omits the field here and sets it via transforms.
§Response field masking (DW-029) — the security sharp edge
Masking redacts JSON fields on the way OUT, per consumer group
(the mass-assignment/data-leak guard). It reuses the buffering and
pointer machinery above with ONE posture change: every gate that the
DW-028 body transform treats as pass-through (an already-encoded
body, a non-JSON content type) is, for masking, a REJECTION — the
gateway cannot prove fields absent from bytes it cannot parse, and a
skipped masking policy is exactly the leak the policy exists to
prevent. Over-cap, invalid JSON, and unresolved pointers fail the
same way (502), the strictness the pointer rules above already
argue for. Masking runs FIRST in the response decoration tail —
before the DW-028 body transform and before compression (DW-027) —
so no later stage can resurrect a redacted value: once the sentinel
replaces the secret, the original bytes exist nowhere in the
gateway.
Structs§
- Body
Transform - A body transform (DW-028). The
jsonvariant is the whole of v1: any other body transform family (templating, XML) would be a new variant here — the wrapper exists so adding one does not reshape the config surface. - Compiled
Json Transform - Snapshot-compiled form of a
JsonBodyTransform(DW-028): every op’s pointer parsed once at snapshot-compile time; the request path applies tokens, never re-parsing config strings. Validation guarantees every pointer parses, so compilation drops nothing on any publishable config; aNone-producing parse is skipped with the same unreachable-skip contract as the CORS/compression compilations. - Compiled
Masking - Snapshot-compiled form of a
Maskingpolicy (DW-029): every pointer parsed once at snapshot-compile time. The effective pointer set is resolved per request (the floor plus the authenticated consumer’s groups — the union rule), which is why groups stay a map here rather than being flattened at compile time. - Header
Ops - Header manipulation ops (DW-028). Applied in ONE deterministic
order regardless of YAML key order:
set(replace all values of the name with one value), thenadd(append one value, keeping any existing values), thenrename(move every value offromtoto), thenremove(drop every value of the name) — soremovecan clean up what earlier ops placed, andrenamesees both upstream and just-set values. Maps iterate in sorted key order (BTreeMap), making multi-entry application deterministic. - Json
Body Transform - JSON-pointer body transform (DW-028): RFC 6901 pointers against a
parsed JSON document, applied in listed order (each op sees the
previous op’s result). THIS IS THE EXPLICITLY BUFFERING TRANSFORM —
the only place in the transforms surface that reads a body. Hard
rules, enforced by
dataplane::transforms: - Json
Pointer - A parsed RFC 6901 JSON pointer (DW-028): the compiled form the
runtime applies and validation checks, so the two can never
disagree on what
/items/0means. Built once at snapshot compile; the raw config string never reaches the request path (the same precompute contract as the CORS/compression/deprecation tables). - Masking
- Response field masking policy (DW-029, feature analysis 5-Security
“response field masking”): RFC 6901 pointers whose values are
replaced with the fixed
MASKED_VALUEsentinel on the route’s responses, before any other body-handling stage. The mass-assignment/data-leak guard: a field named here NEVER reaches the client, whatever the upstream put in it. - Query
Ops - Query-string manipulation ops (DW-028), applied to the forwarded
request’s query. The original query is split into pairs WITHOUT
decoding (each pair’s key and value stay byte-verbatim, so untouched
parameters — including their exact percent-encoding — survive); only
pairs a named op touches are re-encoded (new values are
percent-encoded by the gateway). Ops apply in the same deterministic
order as header ops:
set,add,rename,remove. - Request
Transforms - Request-side transforms (DW-028).
- Response
Transforms - Response-side transforms (DW-028), applied to the route’s action responses (proxy, redirect, respond) — not to gateway short-circuits (limits, authn/authz, rate limits, maintenance, sheds), which describe the request, not the upstream’s output.
- Security
Headers - Security-header injection policy (DW-028, feature analysis 5-Security “security header injection”). Each field that is present injects (REPLACING any upstream-sent value — the gateway is the source of truth at its edge) exactly one standard hardening header on every response the route emits, including gateway short-circuits (401/403/413/429/503, CORS preflights): unlike deprecation stamps, these harden every byte the edge sends, and a browser parsing an error page deserves the same guarantees as one parsing a 200.
- Transforms
- Route-scoped request and response transforms (DW-028, feature analysis 4.12). Absent (the default): the gateway forwards bytes and headers untouched. Presence opts the route’s traffic into the declared manipulations — nothing here applies to any other route.
Enums§
- Compiled
Json Op - One compiled op.
- Frame
Options - The
X-Frame-Optionsvalue (DW-028). - JsonOp
- One JSON-pointer operation (DW-028).
setwrites any JSON value at the pointer (creating the final key in an object parent that must already exist; replacing an existing element in an array parent);removedeletes the addressed value. The ROOT pointer ("") is valid forset(whole-document replacement) and rejected forremoveby validation (a body with no document is not a state this transform can produce). - Json
Transform Error - Why a compiled transform failed on a document (DW-028). The offending pointer rides along for the server-side log; the client envelope stays generic.
Constants§
- MASKED_
VALUE - The value a masked field is replaced with (DW-029): the JSON string
"***"(the feature analysis row’sJSON paths -> ***). FIXED, not configurable — the sentinel is a fixed marker the client can rely on (a value the gateway itself chose and documents) and identical on every route, so nothing about a masked response depends on per-route config a client cannot see. One ambiguity is inherent and documented: source data that is literally"***"at a masked position is indistinguishable from the sentinel — treat every"***"at a configured pointer as masked (the operator docs carry the same caveat). Operators who need a different shape on a specific route combine masking with a DW-028 response transform (which runs after masking and sees the sentinel).
Functions§
- is_
forbidden_ request_ header - Header names no REQUEST-side header op may touch (DW-028). Framing
(
content-length,transfer-encoding) is rebuilt by the forward pipeline from the actual body — an op that forced a disagreeing value would be a request-smuggling primitive aimed at the upstream, the exact class the pre-parse sniff exists to stop on the inbound side. The rest is the hop-by-hop class (RFC 9110 section 7.6.1 plus the legacy keep-alive/proxy spellings): the gateway strips and, for upgrades, re-addsconnection/upgradeitself.hostis not the operator’s either — the gateway names the origin it dials (seeUpstreamHandle::send_with_hash_key). - is_
forbidden_ response_ header - Header names no RESPONSE-side header op may touch (DW-028): the
framing/hop-by-hop class plus
content-encoding, which only the compression pipeline may manage — an op that stripped it without decoding would corrupt the body, and one that added it would misdescribe bytes it did not encode. - is_
json_ media_ type - Is
media_type(the part before any;, already lowercased) a JSON media type this gateway will body-transform (DW-028)? Exactly theapplication/jsontype and the structured-syntax suffix familyapplication/<*+json>(anapplication/vnd.acme+jsonbody IS a JSON body); parameters are the caller’s business (stripped before this check). No other type family carries the+jsonsuffix in practice, and widening totext/json-style one-offs buys nothing.