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 /// Mark this conn as eligible for an upgrade.
170 ///
171 /// This will not send a request body, and instead will leave the Conn in a state that can be
172 /// converted into an Upgrade after execution.
173 fn upgrade(self) -> Self;
174
175 /// Whether this conn is armed for an upgrade. See [`ConnExt::upgrade`].
176 fn is_upgrade(&self) -> bool;
177}
178
179impl ConnExt for Conn {
180 fn set_followup(&mut self, conn: Conn) -> &mut Self {
181 self.followup = Some(Box::new(conn));
182 self
183 }
184
185 fn followup(&self) -> Option<&Conn> {
186 self.followup.as_deref()
187 }
188
189 fn take_followup(&mut self) -> Option<Conn> {
190 self.followup.take().map(|b| *b)
191 }
192
193 fn error(&self) -> Option<&Error> {
194 self.error.as_ref()
195 }
196
197 fn set_error(&mut self, error: Error) -> &mut Self {
198 self.error = Some(error);
199 self
200 }
201
202 fn take_error(&mut self) -> Option<Error> {
203 self.error.take()
204 }
205
206 fn halt(&mut self) -> &mut Self {
207 self.halted = true;
208 self
209 }
210
211 fn set_halted(&mut self, halted: bool) -> &mut Self {
212 self.halted = halted;
213 self
214 }
215
216 fn is_halted(&self) -> bool {
217 self.halted
218 }
219
220 fn set_response_body(&mut self, body: impl Into<Body>) -> &mut Self {
221 let body: Body = body.into().without_chunked_framing();
222 if let Some(len) = body.len() {
223 self.response_headers_mut()
224 .insert(KnownHeaderName::ContentLength, len.to_string())
225 .remove(KnownHeaderName::TransferEncoding);
226 } else {
227 self.response_headers_mut()
228 .remove(KnownHeaderName::ContentLength);
229 if self.http_version == Version::Http1_1 {
230 self.response_headers_mut()
231 .insert(KnownHeaderName::TransferEncoding, "chunked");
232 }
233 }
234 // Recycle whatever body was here — once the override is installed, the transport
235 // (if any) won't be read from again.
236 drop(self.take_response_body());
237 self.body_override = Some(body);
238 self
239 }
240
241 fn with_response_body(mut self, body: impl Into<Body>) -> Self {
242 self.set_response_body(body);
243 self
244 }
245
246 fn set_status(&mut self, status: Status) -> &mut Self {
247 self.status = Some(status);
248 self
249 }
250
251 fn with_status(mut self, status: Status) -> Self {
252 self.status = Some(status);
253 self
254 }
255
256 fn response_headers_mut(&mut self) -> &mut Headers {
257 &mut self.response_headers
258 }
259
260 fn set_response_headers(&mut self, response_headers: Headers) -> &mut Self {
261 self.response_headers = response_headers;
262 self
263 }
264
265 fn response_trailers_mut(&mut self) -> Option<&mut Headers> {
266 self.response_trailers.as_mut()
267 }
268
269 fn set_response_trailers(&mut self, response_trailers: Headers) -> &mut Self {
270 self.response_trailers = Some(response_trailers);
271 self
272 }
273
274 fn upgrade(mut self) -> Self {
275 self.upgrade = true;
276 self
277 }
278
279 fn is_upgrade(&self) -> bool {
280 self.upgrade
281 }
282}