Skip to main content

cratestack_axum/headers/
client_ip_context.rs

1use std::convert::Infallible;
2use std::net::SocketAddr;
3
4use axum::extract::{ConnectInfo, FromRequestParts};
5use axum::http::request::Parts;
6
7use crate::trusted_proxy::TrustedProxyConfig;
8
9/// The trusted-proxy configuration (if an `Extension<TrustedProxyConfig>`
10/// was applied to the router), the verified socket peer (if the router
11/// is served via `into_make_service_with_connect_info`), and a clone of
12/// the request's full `http::Extensions` map, bundled into a single axum
13/// extractor so every generated dispatch fn threads one new parameter
14/// instead of several (#415).
15///
16/// The `extensions` field (added for the `AuthProvider::authenticate`
17/// extensions plumbing, request_context — see `cratestack_core::
18/// RequestContext::extensions`'s doc) is threaded through exactly this
19/// struct rather than as a brand-new parameter: `ClientIpContext` is
20/// already the one extractor every REST/RPC dispatch fn in the
21/// generated code accepts, so reusing it means every transport picks the
22/// new field up for free instead of needing its own separate threading
23/// (and its own separate chance to be forgotten).
24///
25/// **Cost, and why it can't be avoided by borrowing instead of cloning:**
26/// `extensions.clone()` runs unconditionally on every request, on every
27/// transport, whether or not the installed `AuthProvider` ever reads
28/// `RequestContext::extensions`. This can't be sidestepped by threading a
29/// `&'a http::Extensions` instead: `FromRequestParts::from_request_parts`
30/// returns an owned `Self` with no lifetime tied to its `&mut Parts`
31/// argument (axum-core 0.5.6's trait signature — `Self: Sized`, no
32/// borrow), and by the time a generated dispatch fn runs, the `Parts`
33/// this extractor was called from no longer exists as a distinct value
34/// for anything to borrow from — axum's tuple-extractor machinery
35/// recombines it with the body for the next extractor in the parameter
36/// list. Getting a genuine `&'a Extensions` would require every generated
37/// handler to take one `axum::extract::Request` parameter and hand-roll
38/// every other extraction (headers, path, query, body) that today comes
39/// free from typed extractors — a rewrite of the entire handler-codegen
40/// surface, not a local fix here. This is exactly the same constraint
41/// `HeaderMap`'s own axum-core extractor is already under (`Ok(parts.
42/// headers.clone())` — `axum-core-0.5.6/src/extract/request_parts.rs`),
43/// which every generated dispatch fn already accepts unconditionally, so
44/// this field's cost is the same class the framework already pays, not a
45/// new one. Benchmarked (`tests_extensions_clone_cost.rs`, run via
46/// `cargo test -p cratestack-axum -- --ignored --nocapture
47/// extensions_clone_cost`) against a realistic served-router extensions
48/// map (`ConnectInfo<SocketAddr>` + a 3-entry-allowlist
49/// `TrustedProxyConfig`): **roughly 30-150ns/clone in a `--release` build**
50/// (200,000-iteration loop, several runs on a loaded dev machine), the
51/// same order of magnitude as — and never measured meaningfully above —
52/// the `HeaderMap` clone (also ~30-150ns across the same runs) every
53/// generated dispatch fn already pays unconditionally for a representative
54/// 4-header set. Both are noise next to a real request's network/DB round
55/// trip (microseconds-to-milliseconds). Debug builds measure ~265ns for
56/// both (same relationship, uniformly slower), so this isn't a
57/// debug-vs-release artifact. Most axum-ecosystem extensions (`ConnectInfo`, `MatchedPath`,
58/// a `tracing::Span`) are `Copy` or `Arc`-backed and cheap to clone,
59/// but `http::Extensions::clone()` is a deep clone of the typemap: a
60/// consumer who inserts a large non-`Arc`-backed value into extensions
61/// (a big `Vec`/`String`/owned buffer, say) now pays that clone's real
62/// cost on every single request, not just when read — size your own
63/// extension values accordingly, or wrap them in `Arc` before inserting.
64///
65/// A hand-written `FromRequestParts` impl rather than `Option<Extension<T>>`/
66/// `Option<ConnectInfo<T>>` extractor parameters: axum 0.8 only extends its
67/// blanket `Option<T>: FromRequestParts` impl to types implementing the
68/// separate `OptionalFromRequestParts` trait, which neither `Extension<T>`
69/// nor `ConnectInfo<T>` implements — so those two, wrapped in `Option`,
70/// are not valid extractor parameter types on this axum version. Reading
71/// `Parts::extensions` directly (via the infallible `Extensions` extractor
72/// axum-core itself provides) sidesteps that entirely and never fails.
73#[derive(Clone, Debug, Default)]
74pub struct ClientIpContext {
75    pub trusted_proxy: Option<TrustedProxyConfig>,
76    pub peer: Option<SocketAddr>,
77    pub extensions: http::Extensions,
78}
79
80impl ClientIpContext {
81    /// Build directly from a raw `http::Extensions` map — the shared
82    /// construction path used both by non-axum test harnesses that build
83    /// requests by hand and by the `FromRequestParts` impl below, which
84    /// delegates here rather than duplicating the field-by-field
85    /// extraction logic.
86    pub fn from_extensions(extensions: &http::Extensions) -> Self {
87        Self {
88            trusted_proxy: extensions.get::<TrustedProxyConfig>().cloned(),
89            peer: extensions
90                .get::<ConnectInfo<SocketAddr>>()
91                .map(|ConnectInfo(addr)| *addr),
92            extensions: extensions.clone(),
93        }
94    }
95}
96
97impl<S> FromRequestParts<S> for ClientIpContext
98where
99    S: Send + Sync,
100{
101    type Rejection = Infallible;
102
103    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
104        Ok(Self::from_extensions(&parts.extensions))
105    }
106}