oxihttp_client/middleware.rs
1//! Lightweight middleware/interceptor chain for the HTTP client.
2//!
3//! Provides a `ClientMiddleware` trait that lets callers observe and instrument
4//! each request/response round-trip without the complex type-erasure required
5//! by a full `tower::Service` composition.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use oxihttp_client::{Client, middleware::{ClientMiddleware, LoggingMiddleware}};
11//!
12//! let client = Client::builder()
13//! .with_middleware(LoggingMiddleware::new("my-client"))
14//! .build()
15//! .expect("client build");
16//! ```
17
18use std::sync::Arc;
19use std::time::Duration;
20
21use http::{HeaderMap, Method, Uri};
22
23// ---------------------------------------------------------------------------
24// Context types
25// ---------------------------------------------------------------------------
26
27/// Request context passed to `ClientMiddleware::before_request`.
28///
29/// Contains read-only references to the parts of the outgoing request that
30/// are cheaply observable without buffering the body.
31pub struct RequestContext<'a> {
32 /// HTTP method of the outgoing request.
33 pub method: &'a Method,
34 /// Target URI of the outgoing request.
35 pub uri: &'a Uri,
36 /// Headers of the outgoing request.
37 pub headers: &'a HeaderMap,
38}
39
40/// Response context passed to `ClientMiddleware::after_response`.
41pub struct ResponseContext {
42 /// Final HTTP status code of the response (after any redirects/retries).
43 pub status: http::StatusCode,
44 /// Wall-clock duration of the entire send operation.
45 pub elapsed: Duration,
46}
47
48// ---------------------------------------------------------------------------
49// Trait
50// ---------------------------------------------------------------------------
51
52/// A request/response interceptor that can observe (but not modify) each
53/// HTTP round-trip.
54///
55/// Both callbacks receive a context struct describing the relevant phase.
56/// The `before_request` hook fires once per `send()` call, immediately before
57/// the first attempt. The `after_response` hook fires once after the final
58/// successful response (or after the last failed attempt returns an error,
59/// though in error cases `after_response` is *not* called — see
60/// `ClientMiddleware::after_error` for that).
61pub trait ClientMiddleware: Send + Sync {
62 /// Called immediately before the first network attempt.
63 fn before_request(&self, ctx: &RequestContext<'_>);
64
65 /// Called after a successful response is obtained (including after retries).
66 fn after_response(&self, ctx: &ResponseContext);
67}
68
69// ---------------------------------------------------------------------------
70// Concrete middleware
71// ---------------------------------------------------------------------------
72
73/// Logging middleware: emits one line to `stderr` before each request and one
74/// line after each response.
75///
76/// # Example
77///
78/// ```rust,no_run
79/// use oxihttp_client::middleware::LoggingMiddleware;
80///
81/// let mw = LoggingMiddleware::new("api-client");
82/// ```
83#[derive(Debug)]
84pub struct LoggingMiddleware {
85 name: String,
86}
87
88impl LoggingMiddleware {
89 /// Create a new `LoggingMiddleware` with the given label.
90 ///
91 /// The label is included in every log line so that multiple clients can
92 /// be distinguished in the same log stream.
93 pub fn new(name: impl Into<String>) -> Self {
94 Self { name: name.into() }
95 }
96}
97
98impl ClientMiddleware for LoggingMiddleware {
99 fn before_request(&self, ctx: &RequestContext<'_>) {
100 eprintln!("[{}] → {} {}", self.name, ctx.method, ctx.uri);
101 }
102
103 fn after_response(&self, ctx: &ResponseContext) {
104 eprintln!(
105 "[{}] ← {} ({:.1}ms)",
106 self.name,
107 ctx.status,
108 ctx.elapsed.as_secs_f64() * 1000.0
109 );
110 }
111}
112
113/// Timing middleware: invokes a user-supplied callback with the elapsed
114/// duration after each successful response.
115///
116/// Useful for recording latency metrics without coupling to any particular
117/// metrics framework.
118///
119/// # Example
120///
121/// ```rust,no_run
122/// use std::sync::Arc;
123/// use std::sync::Mutex;
124/// use oxihttp_client::middleware::TimingMiddleware;
125///
126/// let recorded = Arc::new(Mutex::new(Vec::new()));
127/// let r = Arc::clone(&recorded);
128/// let mw = TimingMiddleware::new(move |d| r.lock().expect("lock").push(d));
129/// ```
130pub struct TimingMiddleware {
131 callback: Arc<dyn Fn(Duration) + Send + Sync>,
132}
133
134impl TimingMiddleware {
135 /// Create a new `TimingMiddleware` with the given callback.
136 ///
137 /// The callback receives the wall-clock elapsed time of the full
138 /// `send()` call (including redirect/retry overhead).
139 pub fn new<F: Fn(Duration) + Send + Sync + 'static>(f: F) -> Self {
140 Self {
141 callback: Arc::new(f),
142 }
143 }
144}
145
146impl std::fmt::Debug for TimingMiddleware {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("TimingMiddleware").finish_non_exhaustive()
149 }
150}
151
152impl ClientMiddleware for TimingMiddleware {
153 fn before_request(&self, _ctx: &RequestContext<'_>) {}
154
155 fn after_response(&self, ctx: &ResponseContext) {
156 (self.callback)(ctx.elapsed);
157 }
158}