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}