Skip to main content

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}