Skip to main content

bitwarden_core/client/
tracing_middleware.rs

1//! Middleware for logging HTTP request/response details at TRACE level.
2//!
3//! Logs request method, URL, headers, request body, and response status and headers.
4//! All output is at TRACE level so it is silent unless the subscriber is configured
5//! to capture TRACE spans for this crate.
6//!
7//! Each request is wrapped in an `http_request` span with a unique `request_id`,
8//! making it easy to correlate request/response log lines when multiple requests
9//! are in flight.
10//!
11//! Sensitive headers (`Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`)
12//! are redacted in the log output.
13//!
14//! # Warning
15//!
16//! At TRACE level this will include request body payloads.
17//! Only enable TRACE logging in development environments.
18
19use std::{
20    str::from_utf8,
21    sync::atomic::{AtomicU32, Ordering},
22};
23
24use http::Extensions;
25use reqwest::{Request, Response};
26use reqwest_middleware::{Middleware, Next, Result};
27use tracing::{Instrument, trace};
28
29const MAX_REQUEST_BODY_LOG_SIZE: usize = 100 * 1024; // 100 KB
30
31const REDACTED_HEADERS: &[&str] = &[
32    "authorization",
33    "cookie",
34    "set-cookie",
35    "proxy-authorization",
36];
37
38/// Reqwest middleware that logs HTTP request and response details via `tracing` at TRACE level.
39pub struct ReqwestTracingMiddleware;
40
41#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
42#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
43impl Middleware for ReqwestTracingMiddleware {
44    async fn handle(
45        &self,
46        req: Request,
47        extensions: &mut Extensions,
48        next: Next<'_>,
49    ) -> Result<Response> {
50        if !tracing::enabled!(tracing::Level::TRACE) {
51            return next.run(req, extensions).await;
52        }
53
54        static REQUEST_ID: AtomicU32 = AtomicU32::new(1);
55        let request_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
56        let span = tracing::span!(tracing::Level::TRACE, "http_request", request_id);
57
58        async move {
59            trace!(
60                method = %req.method(),
61                url = %req.url(),
62                "HTTP request"
63            );
64
65            for (name, value) in req.headers() {
66                if is_sensitive(name) {
67                    trace!(name = %name, value = "<redacted>", "HTTP request header");
68                } else {
69                    trace!(name = %name, value = ?value, "HTTP request header");
70                }
71            }
72
73            if let Some(body) = req.body().and_then(|b| b.as_bytes()) {
74                if body.len() > MAX_REQUEST_BODY_LOG_SIZE {
75                    trace!(size = body.len(), "HTTP request body (truncated)");
76                } else if let Ok(text) = from_utf8(body) {
77                    trace!(body = %text, "HTTP request body");
78                } else {
79                    trace!(size = body.len(), "HTTP request body (binary)");
80                }
81            }
82
83            let response = next.run(req, extensions).await?;
84
85            trace!(
86                status = %response.status(),
87                "HTTP response"
88            );
89
90            for (name, value) in response.headers() {
91                if is_sensitive(name) {
92                    trace!(name = %name, value = "<redacted>", "HTTP response header");
93                } else {
94                    trace!(name = %name, value = ?value, "HTTP response header");
95                }
96            }
97
98            Ok(response)
99        }
100        .instrument(span)
101        .await
102    }
103}
104
105fn is_sensitive(name: &http::HeaderName) -> bool {
106    REDACTED_HEADERS.iter().any(|h| name.as_str() == *h)
107}