Module callout

Module callout 

Source
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_call sends) 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_filter is applied at connect time against every RESOLVED IP (DNS rebinding mitigation), exactly as source::https_get does for registry fetches.
  • Schemes: http:// and https:// 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 :path must 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_callout re-checks the same grammar as defense in depth (it is pub and callable with a hand-built CalloutRequest).

§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§

CalloutRequest
One registered proxy_http_call dispatch, parsed and validated at the hostcall and performed by perform_callout.
CalloutResponse
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§

CalloutError
Why a callout could not be completed. Timeout gets its own metric outcome; every other failure is Error.

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 s is free of the bytes that terminate or split the HTTP/1.1 request head: CR, LF, and NUL. Header values and the map :path ride 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 s is an RFC 7230 token (the tchar grammar): 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 the http:// scheme and arbitrary methods added.