trillium_client/client_handler.rs
1use crate::{Conn, ConnExt, Result};
2use std::{
3 any::Any,
4 borrow::Cow,
5 fmt::{self, Debug, Formatter},
6 future::Future,
7 pin::Pin,
8 sync::Arc,
9};
10
11/// Client middleware extension point.
12///
13/// [`ClientHandler`] is the composition primitive for trillium-client middleware. It mirrors the
14/// server-side [`trillium::Handler`] in spirit — handlers compose into tuples, halting on the conn
15/// short-circuits the chain — but differs in shape because the client has different ownership
16/// semantics: the conn is user-owned, so handlers take `&mut Conn` rather than owned `Conn`, and
17/// they return `Result<()>` because client execution can fail outright (TLS handshake, refresh
18/// token, signing, etc.).
19///
20/// [`trillium::Handler`]: https://docs.trillium.rs/trillium/trait.Handler.html
21///
22/// ## Lifecycle
23///
24/// Awaiting a [`Conn`] runs handlers in three steps:
25///
26/// 1. **Forward pass — `run`.** Each handler runs in declared order. A handler may mutate the
27/// request, short-circuit by [calling `Conn::halt`] + populating synthetic response state (cache
28/// hit, mocked response), or fail. If any handler halts, subsequent `run` methods are skipped.
29/// 2. **Network round-trip.** Skipped if the conn is halted.
30/// 3. **Reverse pass — `after_response`.** Each handler's `after_response` runs in *reverse* order,
31/// *regardless of halt status*. This mirrors `trillium::Handler::before_send` and lets handlers
32/// that observe the response record cache hits and short-circuited responses, not just
33/// transport-backed ones.
34///
35/// [calling `Conn::halt`]: crate::Conn::halt
36///
37/// ## Re-execution
38///
39/// Handlers that need to re-issue a request (follow-redirects, retry, auth-refresh) build a fresh
40/// `Conn` from `conn.client()` in `after_response`, configure it (filtered headers, replayed body,
41/// handler-internal state), and queue it via
42/// [`ConnExt::set_followup`][crate::ConnExt::set_followup]. The
43/// [`IntoFuture for &mut Conn`][std::future::IntoFuture] loop picks the follow-up up after the
44/// current cycle's `after_response` has fully unwound: it recycles the current response body,
45/// swaps the follow-up into place, and runs another full `(run → network → after_response)`
46/// cycle on it.
47///
48/// ## Handler-author affordances on `Conn`
49///
50/// Lifecycle-driving methods — queue a follow-up, stash or recover the transport-level error —
51/// live on the [`ConnExt`][crate::ConnExt] extension trait rather than directly on
52/// [`Conn`]. Bring them into scope with `use trillium_client::ConnExt;`. The split is
53/// intentional: those operations are meaningful only from inside a handler, and keeping them off
54/// `Conn`'s inherent surface stops them from appearing in IDE completion for user code that holds
55/// a `Conn` directly.
56///
57/// ## Type erasure
58///
59/// Implementors write [`ClientHandler`] using native `async fn` syntax. The crate type-erases
60/// handlers internally for storage on `Client`; [`Client::with_handler`] accepts any
61/// `impl ClientHandler`, and [`Client::downcast_handler`] is the way to recover the concrete type
62/// from a `Client` that has one installed.
63///
64/// [`Client::with_handler`]: crate::Client::with_handler
65/// [`Client::downcast_handler`]: crate::Client::downcast_handler
66pub trait ClientHandler: Send + Sync + 'static {
67 /// Forward-pass hook, called before the network round-trip in declared order.
68 ///
69 /// A handler can mutate the request, halt to short-circuit, or fail. The default
70 /// implementation is a no-op.
71 fn run(&self, conn: &mut Conn) -> impl Future<Output = Result<()>> + Send {
72 let _ = conn;
73 async { Ok(()) }
74 }
75
76 /// Reverse-pass hook, called after the network round-trip (or after a halt-skipped network
77 /// call) in *reverse* declared order. Always runs regardless of halt status or transport
78 /// error.
79 ///
80 /// A handler can observe the response, mutate it before passing it to upstream handlers,
81 /// recover from a transport-level error, or fail.
82 ///
83 /// **Transport errors.** If the network call failed (connect refused, TLS handshake error,
84 /// malformed HTTP frame, timeout), the framework stashes the error on the conn and runs
85 /// `after_response` anyway. A handler that recovers from an error should:
86 /// 1. Inspect [`conn.error()`][crate::ConnExt::error] to detect the failure.
87 /// 2. Populate response state synthetically (`set_status`, `response_headers_mut`,
88 /// `set_response_body`) or enqueue a new followup conn.
89 /// 3. Call [`conn.take_error()`][crate::ConnExt::take_error] to clear the error so the awaited
90 /// conn returns `Ok`.
91 ///
92 /// The `error` / `take_error` / `set_error` methods live on the
93 /// [`ConnExt`][crate::ConnExt] extension trait — `use
94 /// trillium_client::ConnExt;` to bring them into scope.
95 ///
96 /// If no handler clears the error, it propagates as `Err` from the awaited conn.
97 ///
98 /// The default implementation is a no-op.
99 fn after_response(&self, conn: &mut Conn) -> impl Future<Output = Result<()>> + Send {
100 let _ = conn;
101 async { Ok(()) }
102 }
103
104 /// Human-readable name for logging/debugging. Defaults to the type name.
105 fn name(&self) -> Cow<'static, str> {
106 std::any::type_name::<Self>().into()
107 }
108}
109
110/// Object-safe twin of [`ClientHandler`] used for internal type erasure. Users implement
111/// [`ClientHandler`] with native `async fn`; the blanket impl below adapts it.
112pub(crate) trait ObjectSafeClientHandler: Any + Send + Sync + 'static {
113 fn run<'a>(
114 &'a self,
115 conn: &'a mut Conn,
116 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
117 fn after_response<'a>(
118 &'a self,
119 conn: &'a mut Conn,
120 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
121 fn name(&self) -> Cow<'static, str>;
122 fn as_any(&self) -> &dyn Any;
123}
124
125impl<H: ClientHandler> ObjectSafeClientHandler for H {
126 fn run<'a>(
127 &'a self,
128 conn: &'a mut Conn,
129 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
130 Box::pin(ClientHandler::run(self, conn))
131 }
132
133 fn after_response<'a>(
134 &'a self,
135 conn: &'a mut Conn,
136 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
137 Box::pin(ClientHandler::after_response(self, conn))
138 }
139
140 fn name(&self) -> Cow<'static, str> {
141 ClientHandler::name(self)
142 }
143
144 fn as_any(&self) -> &dyn Any {
145 self
146 }
147}
148
149/// Internal `Arc`-shared, type-erased [`ClientHandler`]. Stored on a `Client` and cloned onto
150/// each conn it builds. Not exposed publicly — `Client::with_handler` accepts
151/// `impl ClientHandler` and `Client::downcast_handler` recovers the concrete type.
152#[derive(Clone)]
153pub(crate) struct ArcedClientHandler(Arc<dyn ObjectSafeClientHandler>);
154
155impl Debug for ArcedClientHandler {
156 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
157 f.debug_tuple("ArcedClientHandler")
158 .field(&self.0.name())
159 .finish()
160 }
161}
162
163impl ArcedClientHandler {
164 pub(crate) fn new(handler: impl ClientHandler) -> Self {
165 Self(Arc::new(handler))
166 }
167
168 pub(crate) fn downcast_ref<T: Any + 'static>(&self) -> Option<&T> {
169 self.0.as_any().downcast_ref()
170 }
171}
172
173impl ClientHandler for ArcedClientHandler {
174 async fn run(&self, conn: &mut Conn) -> Result<()> {
175 self.0.run(conn).await
176 }
177
178 async fn after_response(&self, conn: &mut Conn) -> Result<()> {
179 self.0.after_response(conn).await
180 }
181
182 fn name(&self) -> Cow<'static, str> {
183 self.0.name()
184 }
185}
186
187impl ClientHandler for () {}
188
189impl<H: ClientHandler> ClientHandler for Option<H> {
190 async fn run(&self, conn: &mut Conn) -> Result<()> {
191 if let Some(h) = self {
192 h.run(conn).await?;
193 }
194 Ok(())
195 }
196
197 async fn after_response(&self, conn: &mut Conn) -> Result<()> {
198 if let Some(h) = self {
199 h.after_response(conn).await?;
200 }
201 Ok(())
202 }
203
204 fn name(&self) -> Cow<'static, str> {
205 match self {
206 Some(h) => h.name(),
207 None => "None".into(),
208 }
209 }
210}
211
212macro_rules! reverse_after_response {
213 ($conn:ident, $name:ident) => {
214 log::trace!("after_response {}", $name.name());
215 $name.after_response($conn).await?;
216 };
217 ($conn:ident, $name:ident $($rest:ident)+) => {
218 reverse_after_response!($conn, $($rest)+);
219 log::trace!("after_response {}", $name.name());
220 $name.after_response($conn).await?;
221 };
222}
223
224macro_rules! impl_client_handler_tuple {
225 ($($name:ident)+) => {
226 impl<$($name: ClientHandler),+> ClientHandler for ($($name,)+) {
227 #[allow(non_snake_case)]
228 async fn run(&self, conn: &mut Conn) -> Result<()> {
229 let ($(ref $name,)+) = *self;
230 $(
231 log::trace!("running {}", $name.name());
232 $name.run(conn).await?;
233 if conn.is_halted() {
234 return Ok(());
235 }
236 )+
237 Ok(())
238 }
239
240 #[allow(non_snake_case)]
241 async fn after_response(&self, conn: &mut Conn) -> Result<()> {
242 let ($(ref $name,)+) = *self;
243 reverse_after_response!(conn, $($name)+);
244 Ok(())
245 }
246
247 #[allow(non_snake_case)]
248 fn name(&self) -> Cow<'static, str> {
249 let ($(ref $name,)+) = *self;
250 format!(concat!("(\n", $(
251 concat!(" {",stringify!($name) ,":},\n")
252 ),*, ")"), $($name = ($name).name()),*).into()
253 }
254 }
255 };
256}
257
258impl_client_handler_tuple! { A }
259impl_client_handler_tuple! { A B }
260impl_client_handler_tuple! { A B C }
261impl_client_handler_tuple! { A B C D }
262impl_client_handler_tuple! { A B C D E }
263impl_client_handler_tuple! { A B C D E F }
264impl_client_handler_tuple! { A B C D E F G }
265impl_client_handler_tuple! { A B C D E F G H }
266impl_client_handler_tuple! { A B C D E F G H I }
267impl_client_handler_tuple! { A B C D E F G H I J }
268impl_client_handler_tuple! { A B C D E F G H I J K }
269impl_client_handler_tuple! { A B C D E F G H I J K L }
270impl_client_handler_tuple! { A B C D E F G H I J K L M }
271impl_client_handler_tuple! { A B C D E F G H I J K L M N }
272impl_client_handler_tuple! { A B C D E F G H I J K L M N O }