Skip to main content

plecto_host/
filter.rs

1//! The public per-request filter API: [`LoadedFilter`].
2
3use std::time::{Duration, Instant, SystemTime};
4
5use crate::observe;
6use crate::pool::{HookResult, LoadedInner, TrustedPool};
7use crate::runtime::{WasmtimeInstance, WasmtimeRuntime};
8use crate::{
9    Hook, HttpRequest, HttpResponse, Isolation, LogLine, RequestBodyDecision, RequestDecision,
10    RequestTrace, ResponseDecision, RunError, SpanOutcome,
11};
12
13/// A loaded filter, ready to run per request. Trusted filters reuse instances from a
14/// `TrustedPool` (checked out per request, ADR 000012); untrusted filters instantiate fresh
15/// each request.
16///
17/// A trap leaves the guest's linear memory undefined, so the host discards that instance and a
18/// later checkout rebuilds + re-inits one (self-heal, ADR 000006), with a pool-wide cooldown
19/// bounding re-init storms (review f000003 #5). The `Option` is the isolation discriminator —
20/// `None` means untrusted (fresh instance per request).
21pub struct LoadedFilter {
22    pub(crate) inner: LoadedInner<WasmtimeRuntime>,
23    pub(crate) trusted: Option<TrustedPool<WasmtimeInstance>>,
24}
25
26impl LoadedFilter {
27    pub fn isolation(&self) -> Isolation {
28        self.inner.isolation
29    }
30
31    /// Whether this filter reads the request body — i.e. it exports `on-request-body` (world
32    /// `filter-body`). The fast path buffers the body ONLY for a route with at least one such
33    /// filter; a route of header-only filters keeps the zero-copy streaming path (ADR 000038 /
34    /// ADR 000005 mechanism 2). Detected from the component's exports at load, so it is sound
35    /// (fail-closed): a filter cannot read the body without declaring it in the contract.
36    pub fn reads_body(&self) -> bool {
37        self.inner.runtime.body_export.is_some()
38    }
39
40    /// Run the request-side hook under the request's trace context (`trace`, ADR 000009). The
41    /// host times the call and emits one span — parented by `trace`, carrying the outcome and
42    /// the filter's host-log lines as events — to its `TelemetrySink`. Returns the typed
43    /// decision plus those log lines (the direct-access form), or a `RunError` the caller MUST
44    /// fail-closed on (deadline / trap / instantiation — never a pass-through to upstream).
45    pub fn on_request(
46        &self,
47        req: &HttpRequest,
48        trace: &RequestTrace,
49    ) -> std::result::Result<(RequestDecision, Vec<LogLine>), RunError> {
50        if !self.inner.sink.enabled() {
51            return self.run_on_request(req).map_err(|(err, _)| err);
52        }
53        let start = SystemTime::now();
54        let elapsed = Instant::now();
55        let result = self.run_on_request(req);
56        let outcome = match &result {
57            Ok((decision, _)) => SpanOutcome::from(decision),
58            Err((err, _)) => SpanOutcome::from(err),
59        };
60        self.emit_span(
61            trace,
62            Hook::OnRequest,
63            outcome,
64            start,
65            elapsed.elapsed(),
66            &result,
67        );
68        result.map_err(|(err, _)| err)
69    }
70
71    fn run_on_request(&self, req: &HttpRequest) -> HookResult<RequestDecision> {
72        self.inner.run_hook(self.trusted.as_ref(), |inst| {
73            self.inner.runtime.call_on_request(inst, req)
74        })
75    }
76
77    /// Run the request-side BODY hook (buffer-then-decide, ADR 000025). The host hands the filter
78    /// the fully-buffered request body; the filter returns the (possibly transformed) body to
79    /// continue, or a `short-circuit` response (synthesised before upstream is reached). Same
80    /// fail-closed contract and span emission as `on_request`.
81    pub fn on_request_body(
82        &self,
83        body: &[u8],
84        trace: &RequestTrace,
85    ) -> std::result::Result<(RequestBodyDecision, Vec<LogLine>), RunError> {
86        if !self.inner.sink.enabled() {
87            return self.run_on_request_body(body).map_err(|(err, _)| err);
88        }
89        let start = SystemTime::now();
90        let elapsed = Instant::now();
91        let result = self.run_on_request_body(body);
92        let outcome = match &result {
93            Ok((RequestBodyDecision::Continue(_), _)) => SpanOutcome::Continue,
94            Ok((RequestBodyDecision::ShortCircuit(_), _)) => SpanOutcome::ShortCircuit,
95            Err((err, _)) => SpanOutcome::from(err),
96        };
97        self.emit_span(
98            trace,
99            Hook::OnRequestBody,
100            outcome,
101            start,
102            elapsed.elapsed(),
103            &result,
104        );
105        result.map_err(|(err, _)| err)
106    }
107
108    fn run_on_request_body(&self, body: &[u8]) -> HookResult<RequestBodyDecision> {
109        // Header-only filter: no `on-request-body` export, so the body never enters guest memory.
110        // The fast path already skips buffering (`reads_body()` is false); this is the defensive
111        // floor — pass the body through unchanged without instantiating anything.
112        if self.inner.runtime.body_export.is_none() {
113            return Ok((RequestBodyDecision::Continue(body.to_vec()), Vec::new()));
114        }
115        self.inner.run_hook(self.trusted.as_ref(), |inst| {
116            self.inner.runtime.call_body_hook(inst, body)
117        })
118    }
119
120    /// Build and emit the span for one filter execution (ADR 000009). The filter's host-log
121    /// lines become span events whether the call succeeded (`Ok`) or trapped (`Err`) — a `RunError`
122    /// still carries whatever logs (including fat-guest stdio, ADR 000063) were recovered from the
123    /// failed instance before it was discarded, so a trapping guest's own diagnostic output (e.g.
124    /// a panic message) still shows up in this span. Errors never abort emission — telemetry is
125    /// best-effort and out of the fail-closed path.
126    fn emit_span<T>(
127        &self,
128        trace: &RequestTrace,
129        hook: Hook,
130        outcome: SpanOutcome,
131        start: SystemTime,
132        duration: Duration,
133        result: &HookResult<T>,
134    ) {
135        let logs: &[LogLine] = match result {
136            Ok((_, logs)) => logs,
137            Err((_, logs)) => logs,
138        };
139        let span = observe::build_filter_span(
140            trace,
141            &self.inner.filter_id,
142            self.inner.isolation,
143            hook,
144            outcome,
145            start,
146            duration,
147            logs,
148        );
149        self.inner.sink.export(&span);
150    }
151
152    /// Run the response-side hook for one response. Same fail-closed contract as `on_request`.
153    /// `req` is the as-forwarded request snapshot (ADR 000073) — the request as it left the
154    /// request-side chain — passed to a 0.3 guest as `on-response`'s first parameter and
155    /// dropped by the 0.1 / 0.2 adapters.
156    pub fn on_response(
157        &self,
158        req: &HttpRequest,
159        resp: &HttpResponse,
160        trace: &RequestTrace,
161    ) -> std::result::Result<(ResponseDecision, Vec<LogLine>), RunError> {
162        if !self.inner.sink.enabled() {
163            return self.run_on_response(req, resp).map_err(|(err, _)| err);
164        }
165        let start = SystemTime::now();
166        let elapsed = Instant::now();
167        let result = self.run_on_response(req, resp);
168        let outcome = match &result {
169            Ok((decision, _)) => SpanOutcome::from(decision),
170            Err((err, _)) => SpanOutcome::from(err),
171        };
172        self.emit_span(
173            trace,
174            Hook::OnResponse,
175            outcome,
176            start,
177            elapsed.elapsed(),
178            &result,
179        );
180        result.map_err(|(err, _)| err)
181    }
182
183    fn run_on_response(
184        &self,
185        req: &HttpRequest,
186        resp: &HttpResponse,
187    ) -> HookResult<ResponseDecision> {
188        self.inner.run_hook(self.trusted.as_ref(), |inst| {
189            self.inner.runtime.call_on_response(inst, req, resp)
190        })
191    }
192}