Skip to main content

trillium_client/
conn_handler_ext.rs

1use crate::Conn;
2use trillium_http::{Body, Error, Headers, KnownHeaderName, Status, Version};
3
4/// The extension trait handler authors use to drive the [`ClientHandler`] lifecycle.
5///
6/// [`ClientHandler`]: crate::ClientHandler
7///
8/// These methods govern flow within the handler chain — queue a follow-up request for the
9/// [`IntoFuture for &mut Conn`][std::future::IntoFuture] loop to re-execute, or stash /
10/// inspect / recover the transport-level error that runs through `after_response`. They
11/// are meaningful only from inside a [`ClientHandler`] implementation: external user code
12/// holding a [`Conn`] has no reason to call them. A queued follow-up is picked up only by
13/// the handler-chain loop; an externally-installed error just turns into an `Err` on the
14/// next `.await`.
15///
16/// Bring the methods into scope with `use trillium_client::ConnExt;`. The split
17/// from [`Conn`]'s inherent methods is intentional — these affordances live on a trait
18/// so handler authors opt into them explicitly and user code holding a `Conn` directly
19/// doesn't see them in IDE completion.
20pub trait ConnExt {
21    /// Queue a follow-up [`Conn`] to be executed after the current cycle's
22    /// `after_response` chain has fully unwound.
23    ///
24    /// The follow-up is picked up by the [`IntoFuture for &mut Conn`][std::future::IntoFuture]
25    /// loop, which drains and recycles the current conn's response body, then runs a fresh
26    /// `(run → network → after_response)` cycle on the follow-up. After the loop finishes,
27    /// the user's conn handle holds the *terminal* response — the same shape they see
28    /// after a redirect chain.
29    ///
30    /// Setting a follow-up while one is already queued replaces the previous one
31    /// (last-writer-wins). Handlers that want to be polite about not clobbering a
32    /// follow-up queued by an earlier handler can peek via [`ConnExt::followup`]
33    /// or take via [`ConnExt::take_followup`] first.
34    ///
35    /// An unrecovered error stash on the conn (see [`ConnExt::error`] and
36    /// [`ConnExt::take_error`]) wins over a queued follow-up: when the current cycle ends
37    /// with `Err`, the queued follow-up is discarded and the error propagates. Recovery
38    /// handlers that want the follow-up to run anyway (retry-on-error, stale-if-error
39    /// cache) must call `take_error()` inside `after_response` before queuing.
40    fn set_followup(&mut self, conn: Conn) -> &mut Self;
41
42    /// Borrow the queued follow-up [`Conn`], if any, without consuming it.
43    ///
44    /// Returns `None` when no follow-up has been installed. Useful for "polite"
45    /// composition — a handler that wants to avoid clobbering a follow-up queued by an
46    /// earlier handler in the chain can check this before calling
47    /// [`ConnExt::set_followup`].
48    fn followup(&self) -> Option<&Conn>;
49
50    /// Detach the queued follow-up [`Conn`], if any.
51    ///
52    /// Pairs with [`ConnExt::set_followup`] for handlers that want to revoke or
53    /// inspect a follow-up queued by an earlier handler in the chain — e.g. take,
54    /// mutate, and re-queue, or take and discard outright.
55    fn take_followup(&mut self) -> Option<Conn>;
56
57    /// Borrow the transport-level error stashed on this conn, if any.
58    ///
59    /// During a handler chain's `after_response` pass, this is `Some` when the network
60    /// round-trip failed (connect refused, TLS handshake error, malformed HTTP frame,
61    /// timeout, etc.). Observer handlers (logger, metrics) use this to record failures;
62    /// recovery handlers (stale-if-error cache, retry-with-fallback) use it as the
63    /// trigger to synthesize a fallback response and clear the error via
64    /// [`ConnExt::take_error`].
65    fn error(&self) -> Option<&Error>;
66
67    /// Install a transport-level error on this conn.
68    ///
69    /// Mostly internal — the framework stashes round-trip errors here automatically so
70    /// the handler chain's `after_response` runs and can recover. Handler-authored use
71    /// is rare and usually means "synthesize a failure mode for a downstream recovery
72    /// handler to observe."
73    fn set_error(&mut self, error: Error) -> &mut Self;
74
75    /// Take the transport-level error stashed on this conn, leaving `None` in its place.
76    ///
77    /// This is the recovery path: a handler that wants to convert a transport failure
78    /// into a synthetic success response (stale-if-error cache, retry-with-fallback)
79    /// calls this inside `after_response` to clear the stash before populating the
80    /// response state synthetically. If no handler clears the error, it propagates as
81    /// `Err` from the awaited conn.
82    fn take_error(&mut self) -> Option<Error>;
83
84    /// Mark this conn halted, skipping the network round-trip in the current cycle.
85    ///
86    /// Use this in combination with synthetic response state ([`ConnExt::set_status`],
87    /// [`ConnExt::response_headers_mut`], [`ConnExt::set_response_body`]) when a handler
88    /// wants to fully synthesize a response — cache hits, mocked responses, or
89    /// circuit-breaker short-circuits. The halt flag is internal to the handler chain and
90    /// is cleared on egress, so the user's conn handle never observes residual halt state
91    /// after the awaited conn returns.
92    fn halt(&mut self) -> &mut Self;
93
94    /// Set the halt flag explicitly.
95    ///
96    /// Same semantics as [`ConnExt::halt`] for the affirmative case. The explicit
97    /// setter exists for the rare handler that wants to un-halt a conn another handler in
98    /// the chain has halted.
99    fn set_halted(&mut self, halted: bool) -> &mut Self;
100
101    /// Whether this conn is halted within the current cycle.
102    ///
103    /// `after_response` handlers can use this to differentiate "synthetic response" from
104    /// "transport-backed response" — e.g. a logger or metrics handler that wants to record
105    /// cache hits distinctly from network-backed responses.
106    fn is_halted(&self) -> bool;
107
108    /// Install an override response body, replacing whatever transport-backed body would
109    /// otherwise be read from the network.
110    ///
111    /// Used by handlers that synthesize responses — cache hits, mocked responses,
112    /// stale-if-error fallbacks. Typically combined with [`ConnExt::set_status`],
113    /// [`ConnExt::response_headers_mut`], and [`ConnExt::halt`] to construct a complete
114    /// synthetic response.
115    ///
116    /// Accepts anything convertible to a [`Body`], so common patterns work directly:
117    ///
118    /// ```ignore
119    /// conn.set_response_body("hello");
120    /// conn.set_response_body(vec![1, 2, 3]);
121    /// conn.set_response_body(Body::new_streaming(file_reader, Some(file_size)));
122    /// ```
123    ///
124    /// Encoding for [`ResponseBody::read_string`] is determined by the response headers'
125    /// Content-Type, just like a transport-backed body — set the appropriate header before
126    /// or after this call as needed. The user-set `max_len` is enforced for override bodies
127    /// as well as transport-backed ones.
128    ///
129    /// [`ResponseBody::read_string`]: crate::ResponseBody::read_string
130    fn set_response_body(&mut self, body: impl Into<Body>) -> &mut Self;
131
132    /// Owned chainable variant of [`ConnExt::set_response_body`].
133    #[must_use]
134    fn with_response_body(self, body: impl Into<Body>) -> Self
135    where
136        Self: Sized;
137
138    /// Set the response status — handler-author synthesis.
139    ///
140    /// Setting a status on a conn that's about to be sent has no meaningful effect: the
141    /// status reflects what the server returned. The only sensible uses are inside a
142    /// handler synthesizing a response (cache hit, mocked response, stale-if-error
143    /// fallback) — pair with [`ConnExt::set_response_body`],
144    /// [`ConnExt::response_headers_mut`], and [`ConnExt::halt`].
145    fn set_status(&mut self, status: Status) -> &mut Self;
146
147    /// Owned chainable variant of [`ConnExt::set_status`].
148    #[must_use]
149    fn with_status(self, status: Status) -> Self
150    where
151        Self: Sized;
152
153    /// Mutably borrow the response headers — handler-author synthesis.
154    ///
155    /// The read-only [`Conn::response_headers`] accessor stays inherent for user code that
156    /// wants to inspect what the server returned. Mutating those headers only makes sense
157    /// from inside a handler synthesizing a response.
158    fn response_headers_mut(&mut self) -> &mut Headers;
159
160    /// Replace the response headers wholesale — handler-author synthesis.
161    fn set_response_headers(&mut self, response_headers: Headers) -> &mut Self;
162
163    /// Mutably borrow the response trailers, if any — handler-author synthesis.
164    fn response_trailers_mut(&mut self) -> Option<&mut Headers>;
165
166    /// Install response trailers — handler-author synthesis.
167    fn set_response_trailers(&mut self, response_trailers: Headers) -> &mut Self;
168}
169
170impl ConnExt for Conn {
171    fn set_followup(&mut self, conn: Conn) -> &mut Self {
172        self.followup = Some(Box::new(conn));
173        self
174    }
175
176    fn followup(&self) -> Option<&Conn> {
177        self.followup.as_deref()
178    }
179
180    fn take_followup(&mut self) -> Option<Conn> {
181        self.followup.take().map(|b| *b)
182    }
183
184    fn error(&self) -> Option<&Error> {
185        self.error.as_ref()
186    }
187
188    fn set_error(&mut self, error: Error) -> &mut Self {
189        self.error = Some(error);
190        self
191    }
192
193    fn take_error(&mut self) -> Option<Error> {
194        self.error.take()
195    }
196
197    fn halt(&mut self) -> &mut Self {
198        self.halted = true;
199        self
200    }
201
202    fn set_halted(&mut self, halted: bool) -> &mut Self {
203        self.halted = halted;
204        self
205    }
206
207    fn is_halted(&self) -> bool {
208        self.halted
209    }
210
211    fn set_response_body(&mut self, body: impl Into<Body>) -> &mut Self {
212        let body: Body = body.into().without_chunked_framing();
213        if let Some(len) = body.len() {
214            self.response_headers_mut()
215                .insert(KnownHeaderName::ContentLength, len.to_string())
216                .remove(KnownHeaderName::TransferEncoding);
217        } else {
218            self.response_headers_mut()
219                .remove(KnownHeaderName::ContentLength);
220            if self.http_version == Version::Http1_1 {
221                self.response_headers_mut()
222                    .insert(KnownHeaderName::TransferEncoding, "chunked");
223            }
224        }
225        // Recycle whatever body was here — once the override is installed, the transport
226        // (if any) won't be read from again.
227        drop(self.take_response_body());
228        self.body_override = Some(body);
229        self
230    }
231
232    fn with_response_body(mut self, body: impl Into<Body>) -> Self {
233        self.set_response_body(body);
234        self
235    }
236
237    fn set_status(&mut self, status: Status) -> &mut Self {
238        self.status = Some(status);
239        self
240    }
241
242    fn with_status(mut self, status: Status) -> Self {
243        self.status = Some(status);
244        self
245    }
246
247    fn response_headers_mut(&mut self) -> &mut Headers {
248        &mut self.response_headers
249    }
250
251    fn set_response_headers(&mut self, response_headers: Headers) -> &mut Self {
252        self.response_headers = response_headers;
253        self
254    }
255
256    fn response_trailers_mut(&mut self) -> Option<&mut Headers> {
257        self.response_trailers.as_mut()
258    }
259
260    fn set_response_trailers(&mut self, response_trailers: Headers) -> &mut Self {
261        self.response_trailers = Some(response_trailers);
262        self
263    }
264}