Expand description
Request/response transforms and security-header injection (DW-028, feature analysis 4.12 + the 5-Security header-injection row).
§The streaming split (the issue’s done-when)
“Streaming is preserved unless a transform explicitly buffers.” The implementation splits exactly there:
apply_header_ops,apply_query_ops, andapply_security_headersnever touch a body. A route carrying only these forwards and streams byte-identically to a route with no transforms block at all.transform_request_body/transform_response_bodyare the ONE explicitly buffering path: they apply only when the route configured a JSON body transform AND the body declares itself JSON AND it is non-empty, and they buffer at mostmax_bytes— enforced against a declaredContent-Lengthup front and against the live stream frame by frame. Over-cap fails CLOSED (413 request-side, 502 response-side): passing the body through untransformed would be fail-open in exactly the masking direction DW-029 builds on this machinery for.
§Where each piece runs (the documented order)
Request side, inside the proxy action on the FORWARD path: DW-010
path rewrite, then apply_query_ops (the rewrite re-attaches the
original query verbatim, this block is the only thing that may
change it), then hop-by-hop stripping and the gateway’s trusted
headers (X-Forwarded-*, X-Consumer-*), then apply_header_ops
— ops see the near-final forwarded request, which is also why they
MAY remove the trust headers (the operator owns the upstream’s
contract; see config::transforms), and finally the body transform
BEFORE retry buffering, so a retried attempt replays the
TRANSFORMED bytes.
Response side, in the decoration tail: FIELD MASKING first (DW-029,
mask_response_body — before anything else can so much as read
the body: once the sentinel replaces a secret, the original bytes
exist nowhere in the gateway, so no later stage — operator
transforms, the compression codec — can resurrect or re-emit them),
then the body transform (compression then encodes the transformed
bytes and its eligibility check sees the final content type), then
header ops, then the existing compression -> versioning -> CORS
stages, then apply_security_headers (last policy stamp before
rate headers), then rate headers.
Response HEADER and BODY transforms apply to action responses only;
security headers apply to every route-matched response including
gateway short-circuits (see config::transforms::SecurityHeaders
for the asymmetry with deprecation stamps). MASKING applies to
PROXY action responses only: the leak surface it guards is the
upstream’s output — gateway-authored bodies (redirect, respond) are
operator config bytes with no upstream data to redact, and bodiless
statuses (1xx/101/204/304) have nothing to leak; both pass.
§Masking’s inverted gate posture (DW-029)
The DW-028 body transform PASSES THROUGH what it cannot handle
(encoded or non-JSON bodies) — a convenience transform skipping
itself is harmless. Masking inverts every one of those gates into a
502 REFUSAL: the gateway cannot prove the configured fields absent
from bytes it cannot parse, and for a redaction policy a skipped
pass IS the leak (the module docs of config::transforms carry the
full argument). A route that configures masking pins its proxied
responses to the contract “identity-encoded JSON within the cap,
with every configured pointer present” — an upstream that violates
it answers 502 with a generic envelope, and one dwara::policy
warn event names the refusal class server-side. Every successful
mask emits one dwara::policy info event (the audit trail the
issue’s done-when asks for: route, consumer, count, request-id).
§Why header/query ops are lenient where JSON pointers are strict
A header may legitimately be absent from a given request; renaming or removing an absent header is a no-op, not an error. A JSON pointer that does not resolve is a DATA-SHAPE violation against a contract the operator pinned — and in the remove direction a silent miss is the leak the policy exists to prevent. Leniency and strictness each sit where their failure mode says they belong.
§Framing after a body transform
Both body transforms rewrite Content-Length to the transformed
body’s exact length (the buffered bytes ARE the body; any stale
declared length would misframe the hop). Framing headers are
meanwhile rejected from header ops by validation
(config::transforms::is_forbidden_*_header), so exactly one
component — this module — ever writes them.
Enums§
- Request
Body Outcome - The outcome of the request body-transform step (DW-028).
- Request
Body Transform Error - Why a REQUEST body transform refused or failed (DW-028). The proxy
maps each arm to its client answer:
RouteLimitkeeps the route limit’s 413 envelope (the route’s cap, not the transform’s, bound the body);SignatureMismatchkeeps the HMAC family’s 401 (the client’s own signature bound a digest the body did not match);TooLarge/InvalidJson/Unresolvedanswer 413/400/400 in the transform’s own envelope. - Response
Body Transform Error - Why a RESPONSE body transform failed (DW-028); every arm answers 502 — the upstream violated the contract the transform pinned (the client never sees upstream detail).
Functions§
- apply_
header_ ops - Apply header manipulation ops in the frozen order (DW-028):
set(replace-all),add(append),rename(relabel every value), thenremove— seeHeaderOpsfor the rationale. Every name/value was validated representable at config publish; theif let Okskips are generation-tear backstops (never a panic, never a 500), the same posture as the respond-action headers. - apply_
query_ ops - Apply query manipulation ops to a request URI (DW-028). Returns the
rebuilt URI, or
Nonewhen nothing changed (the caller keeps the original — its bytes are already correct). Original pairs are carried VERBATIM (no decode/re-encode round trip: a client’s exact percent-encoding spelling survives); only pairs a named op touches are encoded, by the shared percent-encoder below. - apply_
security_ headers - Stamp a route’s security-header policy onto a response (DW-028).
Every present field REPLACES any upstream-sent value: the gateway
is the source of truth for its edge policy (the same rule as the
deprecation and
X-RateLimit-*headers). Values are static or validation-checked representable; theif let Okskips are the same generation-tear backstop posture as everywhere in the tail. - is_
json_ body - Does this body’s
Content-Typemake it a JSON body-transform candidate (DW-028)? (application/jsonand theapplication/ <*+json>family, parameters ignored — seeconfig::transforms::is_json_media_type.) - mask_
response_ body - The RESPONSE field masking pass (DW-029), the decoration tail’s
first stage and the security floor of the response path. Gates, in
order — every one of them FAILS CLOSED (the inverted posture; see
the module docs): bodiless statuses pass (nothing to leak), then a
content-encoded body, a non-JSON content type, a declared length
over the cap, an over-cap or dying stream, unparseable JSON, and a
pointer miss each answer 502 with a generic envelope plus one
dwara::policywarn event naming the refusal class server-side. The gateway’s OWN compression (DW-027) runs later in the tail, so it can never trip the encoding gate — only upstream-pre-encoded responses do. - media_
type - The lowercase media type of a
Content-Typeheader (parameters stripped), when the header is present and textual. - transform_
request_ body - The REQUEST body transform (DW-028): gate, buffer (capped), parse,
apply ops, serialize. Gates in order, cheapest first — no compiled
policy (the caller’s
Nonecheck), then here: non-JSON content type (bodies of other types stream through untouched), an already-encoded body (the transform does not decode), a declared empty body (nothing to transform — bodiless methods pass), and a declared length over the cap (413 without reading a byte). Only then does anything buffer. - transform_
response_ body - The RESPONSE body transform (DW-028), the decoration tail’s first stage: gate, buffer (capped), parse, apply ops, serialize. Gates: informational/101/204/304 statuses (no body to shape), non-JSON content type, and an already-encoded body all pass through UNTOUCHED — SSE and streamed downloads on a transformed route keep streaming, the core guarantee of this feature. A declared length over the cap fails 502 without reading.