Module transforms

Module transforms 

Source
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, and apply_security_headers never 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_body are 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 most max_bytes — enforced against a declared Content-Length up 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§

RequestBodyOutcome
The outcome of the request body-transform step (DW-028).
RequestBodyTransformError
Why a REQUEST body transform refused or failed (DW-028). The proxy maps each arm to its client answer: RouteLimit keeps the route limit’s 413 envelope (the route’s cap, not the transform’s, bound the body); SignatureMismatch keeps the HMAC family’s 401 (the client’s own signature bound a digest the body did not match); TooLarge/InvalidJson/Unresolved answer 413/400/400 in the transform’s own envelope.
ResponseBodyTransformError
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), then remove — see HeaderOps for the rationale. Every name/value was validated representable at config publish; the if let Ok skips 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 None when 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; the if let Ok skips are the same generation-tear backstop posture as everywhere in the tail.
is_json_body
Does this body’s Content-Type make it a JSON body-transform candidate (DW-028)? (application/json and the application/ <*+json> family, parameters ignored — see config::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::policy warn 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-Type header (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 None check), 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.