Expand description
Plugin HTTP callout transport (DW-167, issue #285).
perform_callout executes ONE proxy_http_call dispatch: the
minimal synchronous HTTP/1.1 client the wasm domain already owns
for registry fetches (super::source) extended with the http://
scheme (callouts usually target internal services) and arbitrary
methods. The wasm runner stays synchronous — the ASYNC boundary is
dataplane::plugin_dispatch, which wraps this function in
spawn_blocking, delivers the response to the instance, and
re-collects the phase outcome (the pause/resume model; see the
adapter and plugin_dispatch docs).
§Guardrails (hard caps, no config surface)
- Timeout: the plugin’s requested timeout (milliseconds, the
unit the Rust SDK’s
dispatch_http_callsends) clamped to[CALLOUT_TIMEOUT_MIN_MS, CALLOUT_TIMEOUT_MAX_MS]= [1ms, 5s]. It is a whole-exchange wall clock: the connect budget and every read are clamped to the remaining time, so a target dripping one byte per read cannot outlive it by resetting a per-read timer. - Body cap:
CALLOUT_BODY_CAP_BYTES(4 MiB) on the response body (Content-Length and received bytes alike). Over-cap is a callout error, not truncation — the plugin must not act on a silently shortened decision payload. - Head cap:
CALLOUT_HEAD_CAP_BYTES(16 KiB), the same bound the registry fetcher uses. - Redirects are NOT followed: a 3xx is delivered to the plugin as data (the plugin decides), the same posture as webhook deliveries and registry fetches.
- SSRF: the gateway
ssrf_filteris applied at connect time against every RESOLVED IP (DNS rebinding mitigation), exactly assource::https_getdoes for registry fetches. - Schemes:
http://andhttps://only; anything else fails validation at the hostcall (BadArgument) before any network work. - Request-head integrity: the request head is built by
interpolating the plugin-supplied method, header names, values,
and
:path— every one of them is validated at the hostcall (BadArgument, fail-closed, no connection attempted): the method and header names must be RFC 7230 tokens, and names, values, and the map:pathmust carry no CR, LF, or NUL. A plugin reflecting client data into a callout therefore cannot smuggle a second request into the gateway-initiated connection (request splitting).perform_calloutre-checks the same grammar as defense in depth (it ispuband callable with a hand-builtCalloutRequest).
§Failure semantics (deliberate deviation from Envoy)
Envoy invokes proxy_on_http_call_response with zeroed counts on a
callout failure (timeout, refused, reset), letting the plugin
branch on the empty maps. dwara instead FAILS THE ROUTE CLOSED: a
callout error is a plugin failure (500 plugin_failed, metric
reason callout_failed; the outcome itself is observable as
dwara_plugin_callouts_total{name,outcome=timeout|error}). The
plugin cannot make a fail-open decision without receiving a
response, and dwara’s plugin posture is fail-closed end to end
(DW-157) — a plugin whose decision input never arrived must not
resume as if it had. Recipes wanting fail-open behavior scope it
explicitly (short timeouts, a fallback verdict implemented without
a callout, or an extension trait instead of a plugin).
Structs§
- Callout
Request - One registered
proxy_http_calldispatch, parsed and validated at the hostcall and performed byperform_callout. - Callout
Response - A completed callout response, ready to deliver to the instance. ANY completed status is delivered — non-2xx is data (the plugin decides what a 403 from its decision service means); only transport failures (timeout/refused/reset/caps/SSRF) are errors.
Enums§
- Callout
Error - Why a callout could not be completed.
Timeoutgets its own metric outcome; every other failure isError.
Constants§
- CALLOUT_
BODY_ CAP_ BYTES - Cap on one callout response body (bytes).
- CALLOUT_
HEAD_ CAP_ BYTES - Cap on the callout response head while locating the blank line (bytes; the same bound the registry fetcher uses).
- CALLOUT_
TIMEOUT_ MAX_ MS - Maximum callout timeout (the plugin’s request is clamped DOWN to this): a per-request edge plugin must never hold a route for long.
- CALLOUT_
TIMEOUT_ MIN_ MS - Minimum callout timeout (the plugin’s request is clamped UP to this): a zero/near-zero timeout would make every callout fail on scheduling jitter alone.
- MAX_
CALLOUT_ ROUNDS - Maximum callout rounds per phase per request (the loop guard,
enforced by
dataplane::plugin_dispatch; declared here so the callout vocabulary lives in one module).
Functions§
- clamp_
timeout_ ms - Clamp the plugin-requested timeout into the documented window.
- decode_
chunked - Minimal chunked transfer decoding with a caller-supplied cap (the
same hardened shape as
source::decode_chunked: 1-16 ASCII hex digits, checked arithmetic, per-chunk and aggregate caps). - is_
head_ safe - Whether
sis free of the bytes that terminate or split the HTTP/1.1 request head: CR, LF, and NUL. Header values and the map:pathride the head verbatim, so any of these bytes is rejected (a value may legally carry obs-text, which this check leaves alone). - is_
rfc7230_ token - Whether
sis an RFC 7230token(thetchargrammar): the method and every header NAME must match it. A non-token would smuggle framing bytes (spaces, CR/LF, control characters) into the interpolated request head — the request-splitting recipe. - parse_
callout_ response - Parse a raw HTTP/1.1 callout response: status line, headers, body
framing (Content-Length, chunked, or read-to-EOF). ANY status is
returned (non-2xx is data); redirects are deliberately not
followed. Public for the integration tests, which pin the framing
edge cases as pure functions (the same posture as
source::parse_response). - perform_
callout - Perform one callout exchange. Synchronous by design (the async
boundary is plugin_dispatch’s
spawn_blocking); reuses the registry fetcher’s transport posture (SSRF at connect, per-read timeouts clamped to the deadline,Connection: close, read to EOF) with thehttp://scheme and arbitrary methods added.