Loading page…
Rust walkthroughs
Loading page…
http::Extensions enable type-safe storage of arbitrary request/response data?The http::Extensions type provides a type-safe mechanism for storing arbitrary data alongside HTTP requests and responses without modifying their structure. It's implemented as a type map where each value is stored with its type as the key, allowing multiple types to coexist in a single container. This enables middleware, handlers, and application code to attach custom data to requests and responses that travels through the entire request lifecycle—essential for carrying authentication information, request IDs, metrics, or any application-specific context without creating wrapper types or extending the base HTTP types.
use http::Extensions;
fn basic_extensions() {
let mut extensions = Extensions::new();
// Insert values of different types
extensions.insert(42i32);
extensions.insert("hello".to_string());
extensions.insert(vec![1, 2, 3]);
// Retrieve by type
let num: Option<&i32> = extensions.get();
assert_eq!(num, Some(&42));
let text: Option<&String> = extensions.get();
assert_eq!(text, Some(&"hello".to_string()));
let vec: Option<&Vec<i32>> = extensions.get();
assert_eq!(vec, Some(&vec![1, 2, 3]));
}Each type acts as its own key, allowing type-safe retrieval without string keys or casting.
use http::Extensions;
use std::any::TypeId;
fn type_as_key() {
// Extensions uses TypeId internally
// Each type can have at most one value stored
let mut extensions = Extensions::new();
// Insert i32
extensions.insert(10i32);
// Insert another i32 - replaces the first
extensions.insert(20i32);
assert_eq!(extensions.get::<i32>(), Some(&20));
// Different types coexist
extensions.insert(30i64); // i64 is different type than i32
assert_eq!(extensions.get::<i32>(), Some(&20));
assert_eq!(extensions.get::<i64>(), Some(&30));
// Types must match exactly
// extensions.get::<u32>() would return None
assert_eq!(extensions.get::<u32>(), None);
}Each type can have exactly one value; inserting the same type replaces the previous value.
use http::{Request, Response, Extensions};
fn request_response_extensions() {
// Request has extensions built-in
let mut request: Request<()> = Request::new(());
// Access extensions
request.extensions_mut().insert("request_id_123");
// Later, in middleware or handler
let request_id: Option<&&str> = request.extensions().get();
println!("Request ID: {:?}", request_id);
// Response also has extensions
let mut response: Response<()> = Response::new(());
response.extensions_mut().insert("processed: true");
// Extensions travel with the response
let status: Option<&&str> = response.extensions().get();
println!("Response status: {:?}", status);
}Both Request and Response include Extensions fields for arbitrary data.
use http::Extensions;
use std::time::Instant;
// Define custom types for extension data
#[derive(Debug, Clone)]
struct RequestId(String);
#[derive(Debug)]
struct Authentication {
user_id: u64,
roles: Vec<String>,
}
#[derive(Debug)]
struct Timing {
start: Instant,
}
fn custom_types() {
let mut extensions = Extensions::new();
// Store custom types
extensions.insert(RequestId("req-123".to_string()));
extensions.insert(Authentication {
user_id: 42,
roles: vec!["admin".to_string(), "user".to_string()],
});
extensions.insert(Timing {
start: Instant::now(),
});
// Retrieve by custom type
if let Some(auth) = extensions.get::<Authentication>() {
println!("User ID: {}", auth.user_id);
println!("Roles: {:?}", auth.roles);
}
if let Some(request_id) = extensions.get::<RequestId>() {
println!("Request ID: {}", request_id.0);
}
}Custom wrapper types provide clear semantics and avoid type collisions.
use http::Extensions;
// Problem: multiple modules might use String for different purposes
// String and String collide - only one String value allowed
fn collision_problem() {
let mut extensions = Extensions::new();
// Module A stores a "status" string
extensions.insert("active".to_string());
// Module B tries to store "priority" string
extensions.insert("high".to_string());
// Module A's value was overwritten!
assert_eq!(extensions.get::<String>(), Some(&"high".to_string()));
}
// Solution: use wrapper types
#[derive(Debug)]
struct Status(String);
#[derive(Debug)]
struct Priority(String);
fn collision_solution() {
let mut extensions = Extensions::new();
extensions.insert(Status("active".to_string()));
extensions.insert(Priority("high".to_string()));
// Both coexist
assert_eq!(extensions.get::<Status>().map(|s| &s.0), Some(&"active".to_string()));
assert_eq!(extensions.get::<Priority>().map(|p| &p.0), Some(&"high".to_string()));
}Wrapper types prevent accidental collisions between modules using the same base type.
use http::{Request, Response, Extensions};
use std::time::Instant;
struct Metrics {
request_start: Instant,
request_id: String,
}
// Middleware adds metrics
async fn metrics_middleware<B>(mut request: Request<B>) -> Request<B> {
let request_id = format!("req-{}", uuid::Uuid::new_v4());
request.extensions_mut().insert(Metrics {
request_start: Instant::now(),
request_id: request_id.clone(),
});
request
}
// Handler uses metrics
async fn handler<B>(request: Request<B>) -> Response<String> {
// Retrieve metrics added by middleware
let metrics = request.extensions().get::<Metrics>();
let request_id = metrics
.map(|m| m.request_id.as_str())
.unwrap_or("unknown");
Response::builder()
.status(200)
.body(format!("Request {} processed", request_id))
.unwrap()
}
// Post-processing middleware reads timing
async fn timing_middleware<B>(response: &mut Response<B>) {
if let Some(metrics) = response.extensions().get::<Metrics>() {
let elapsed = metrics.request_start.elapsed();
println!("Request {} took {:?}", metrics.request_id, elapsed);
}
}Extensions enable data flow between middleware layers without parameter passing.
use http::{Request, Extensions};
struct Counter {
requests: u64,
}
fn mutable_access() {
let mut extensions = Extensions::new();
extensions.insert(Counter { requests: 0 });
// get_mut returns mutable reference
if let Some(counter) = extensions.get_mut::<Counter>() {
counter.requests += 1;
}
// Multiple mutations
for _ in 0..5 {
if let Some(counter) = extensions.get_mut::<Counter>() {
counter.requests += 1;
}
}
assert_eq!(extensions.get::<Counter>().unwrap().requests, 6);
}get_mut allows in-place modification of stored values.
use http::Extensions;
fn removing_values() {
let mut extensions = Extensions::new();
extensions.insert(42i32);
extensions.insert("hello".to_string());
// Remove by type
let removed: Option<i32> = extensions.remove();
assert_eq!(removed, Some(42));
// Value is gone
assert_eq!(extensions.get::<i32>(), None);
// Other types remain
assert_eq!(extensions.get::<String>(), Some(&"hello".to_string()));
// Remove non-existent returns None
let not_found: Option<u64> = extensions.remove();
assert_eq!(not_found, None);
}remove extracts and returns the value, cleaning up the entry.
use http::Extensions;
fn checking_existence() {
let mut extensions = Extensions::new();
// contains checks if type exists
assert!(!extensions.contains::<i32>());
extensions.insert(42i32);
assert!(extensions.contains::<i32>());
assert!(!extensions.contains::<String>());
// get is None if not present
assert!(extensions.get::<String>().is_none());
}contains provides a quick existence check without retrieving the value.
use http::Extensions;
struct Config {
debug: bool,
timeout_ms: u64,
}
impl Default for Config {
fn default() -> Self {
Config {
debug: false,
timeout_ms: 5000,
}
}
}
fn get_or_default(extensions: &mut Extensions) -> &Config {
// Insert default if not present
if !extensions.contains::<Config>() {
extensions.insert(Config::default());
}
extensions.get::<Config>().unwrap()
}
fn get_or_insert_with(extensions: &mut Extensions) -> &Config {
// Manual or_insert_with pattern
if extensions.get::<Config>().is_none() {
extensions.insert(Config::default());
}
extensions.get::<Config>().unwrap()
}Extensions doesn't have or_insert_with, so manual patterns are needed.
use http::Extensions;
fn clear_extensions() {
let mut extensions = Extensions::new();
extensions.insert(1i32);
extensions.insert("text".to_string());
extensions.insert(vec![1, 2, 3]);
// Remove all extensions
extensions.clear();
assert!(extensions.get::<i32>().is_none());
assert!(extensions.get::<String>().is_none());
assert!(extensions.get::<Vec<i32>>().is_none());
// Extensions is empty
assert!(extensions.is_empty());
}clear removes all values regardless of type.
use http::{Request, Response};
async fn request_lifecycle() {
// 1. Initial request
let mut request: Request<()> = Request::new(());
// 2. Middleware adds authentication
request.extensions_mut().insert(Auth {
user_id: 42,
authenticated: true,
});
// 3. Routing middleware adds route info
request.extensions_mut().insert(Route {
path: "/api/users".to_string(),
method: "GET".to_string(),
});
// 4. Handler can access all middleware data
let auth = request.extensions().get::<Auth>();
let route = request.extensions().get::<Route>();
// 5. Handler creates response
let mut response = Response::new("".to_string());
// 6. Copy relevant data to response extensions
if let Some(auth) = request.extensions().get::<Auth>() {
response.extensions_mut().insert(auth.clone());
}
// 7. Post-processing middleware can use response extensions
if let Some(auth) = response.extensions().get::<Auth>() {
println!("Response for user {}", auth.user_id);
}
}
#[derive(Clone)]
struct Auth {
user_id: u64,
authenticated: bool,
}
struct Route {
path: String,
method: String,
}Extensions travel through the entire request/response lifecycle.
use http::Extensions;
use std::sync::Arc;
// Extensions itself is not Sync
// But you can store Sync types inside
#[derive(Debug)]
struct SharedState {
counter: std::sync::atomic::AtomicU64,
}
fn thread_safety() {
let mut extensions = Extensions::new();
// Arc allows sharing across threads
let shared = Arc::new(SharedState {
counter: std::sync::atomic::AtomicU64::new(0),
});
extensions.insert(shared.clone());
// Extensions can be accessed from multiple threads
// if the stored types are Sync
std::thread::spawn(move || {
// In another thread, we'd need mutable access
// which Extensions doesn't provide concurrently
});
}Extensions is Send + Sync when stored values are Send + Sync.
use http::Extensions;
use std::mem;
fn performance_characteristics() {
// Extensions is essentially a HashMap<TypeId, Box<dyn Any>>
let extensions = Extensions::new();
// Size on stack is small (just the HashMap pointer)
println!("Extensions size: {} bytes", mem::size_of::<Extensions>());
// Each insertion allocates on heap for the Box<dyn Any>
// Lookup is O(1) average case (HashMap)
// No iteration API - only type-specific get/insert
// This is intentional: type-safety means no runtime discovery
}Extensions uses HashMap<TypeId, Box<dyn Any>> internally.
use http::{Request, Response, Extensions};
// Axum-style extension extraction
trait FromRequest: Sized {
fn from_request(request: &Request<()>) -> Option<&Self>;
}
impl FromRequest for String {
fn from_request(request: &Request<()>) -> Option<&Self> {
request.extensions().get::<String>()
}
}
// Simulated Axum handler
async fn user_handler(request: Request<()>) -> Response<String> {
// Extensions are available in handler
let user_id = request.extensions().get::<UserId>();
match user_id {
Some(id) => Response::new(format!("User: {}", id.0)),
None => Response::builder()
.status(401)
.body("Unauthorized".to_string())
.unwrap(),
}
}
struct UserId(u64);
// Hyper service wrapper
async fn service_layer(request: Request<()>) -> Response<String> {
// Add context before routing
let mut request = request;
request.extensions_mut().insert(RequestContext {
started: std::time::Instant::now(),
});
// Route to handler
let response = user_handler(request).await;
// Post-process
// Response could have its own extensions
response
}
struct RequestContext {
started: std::time::Instant,
}Web frameworks use Extensions for request-scoped context without parameter threading.
use http::Extensions;
use std::time::{Duration, Instant};
// Pattern 1: Request timing
struct RequestTimer {
start: Instant,
}
impl RequestTimer {
fn new() -> Self {
RequestTimer { start: Instant::now() }
}
fn elapsed(&self) -> Duration {
self.start.elapsed()
}
}
// Pattern 2: Request ID tracking
struct RequestId(String);
impl RequestId {
fn new() -> Self {
RequestId(format!("req-{}", uuid::Uuid::new_v4()))
}
fn as_str(&self) -> &str {
&self.0
}
}
// Pattern 3: Feature flags via extensions
struct FeatureFlags {
experimental_api: bool,
rate_limiting: bool,
}
// Pattern 4: Database connection per request
struct DatabaseConnection(/* connection details */);
// Pattern 5: User session
struct Session {
user_id: Option<u64>,
preferences: std::collections::HashMap<String, String>,
}
fn initialize_request(extensions: &mut Extensions) {
extensions.insert(RequestTimer::new());
extensions.insert(RequestId::new());
extensions.insert(FeatureFlags {
experimental_api: true,
rate_limiting: false,
});
}Common extension types serve cross-cutting concerns like timing, IDs, and configuration.
use http::{Request, Extensions};
use std::collections::HashMap;
// Alternative 1: HashMap<String, Box<dyn Any>>
// - String keys can collide
// - Requires downcasting
// - No compile-time type safety
// Alternative 2: Custom Request struct
struct MyRequest<B> {
inner: Request<B>,
user_id: Option<u64>,
request_id: Option<String>,
// Must add fields for each new use case
}
// Alternative 3: Extensions (what we have)
// - Type-safe by using TypeId as key
// - Extensible without modification
// - No field proliferation
fn comparison() {
let mut request: Request<()> = Request::new(());
// Extensions: add any type, no struct modification
request.extensions_mut().insert(42u64);
request.extensions_mut().insert("context".to_string());
request.extensions_mut().insert(vec![1, 2, 3]);
// Type-safe retrieval
let num: Option<&u64> = request.extensions().get();
let text: Option<&String> = request.extensions().get();
}Extensions provides extensibility without struct modification.
use http::{Request, Response, StatusCode, Extensions};
use std::sync::Arc;
#[derive(Clone)]
struct User {
id: u64,
username: String,
roles: Vec<String>,
}
struct AuthMiddleware;
impl AuthMiddleware {
fn authenticate<B>(request: &mut Request<B>) -> Result<(), StatusCode> {
// Extract auth header
let auth_header = request
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok());
match auth_header {
Some(header) if header.starts_with("Bearer ") => {
let token = &header[7..];
// Validate token (simplified)
let user = Self::validate_token(token)?;
request.extensions_mut().insert(user);
Ok(())
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
fn validate_token(token: &str) -> Result<User, StatusCode> {
// Simplified validation
if token == "valid_token" {
Ok(User {
id: 42,
username: "alice".to_string(),
roles: vec!["user".to_string(), "admin".to_string()],
})
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
}
// Handler requiring authentication
async fn protected_handler<B>(request: Request<B>) -> Response<String> {
// Get user from extensions (added by middleware)
match request.extensions().get::<User>() {
Some(user) => {
Response::new(format!("Hello, {}!", user.username))
}
None => {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("Auth middleware not run".to_string())
.unwrap()
}
}
}
// Role-based access control
fn require_role<B>(request: &Request<B>, role: &str) -> Result<(), StatusCode> {
match request.extensions().get::<User>() {
Some(user) if user.roles.iter().any(|r| r == role) => Ok(()),
Some(_) => Err(StatusCode::FORBIDDEN),
None => Err(StatusCode::UNAUTHORIZED),
}
}Authentication context flows through middleware to handlers via extensions.
use http::{Request, Response, Extensions};
use std::time::Instant;
#[derive(Clone)]
struct TraceContext {
trace_id: String,
span_id: String,
parent_span_id: Option<String>,
start_time: Instant,
}
impl TraceContext {
fn new() -> Self {
TraceContext {
trace_id: format!("trace-{}", uuid::Uuid::new_v4()),
span_id: format!("span-{}", uuid::Uuid::new_v4()),
parent_span_id: None,
start_time: Instant::now(),
}
}
fn child(&self) -> Self {
TraceContext {
trace_id: self.trace_id.clone(),
span_id: format!("span-{}", uuid::Uuid::new_v4()),
parent_span_id: Some(self.span_id.clone()),
start_time: Instant::now(),
}
}
}
fn tracing_middleware<B>(mut request: Request<B>) -> Request<B> {
request.extensions_mut().insert(TraceContext::new());
request
}
fn log_with_context<B>(request: &Request<B>, message: &str) {
if let Some(trace) = request.extensions().get::<TraceContext>() {
let elapsed = trace.start_time.elapsed();
println!("[{}][{}] {} ({}µs)", trace.trace_id, trace.span_id, message, elapsed.as_micros());
}
}
// Propagate trace to response
fn propagate_trace<B>(request: &Request<()>, response: &mut Response<B>) {
if let Some(trace) = request.extensions().get::<TraceContext>() {
response.extensions_mut().insert(trace.clone());
// Could also add trace headers
}
}Tracing context propagates across service boundaries via extensions.
Core operations:
| Method | Purpose | Return Type |
|--------|---------|-------------|
| insert<T>(&mut self, T) | Store a value | Option<T> (previous) |
| get<T>(&self) | Retrieve reference | Option<&T> |
| get_mut<T>(&mut self) | Retrieve mutable reference | Option<&mut T> |
| remove<T>(&mut self) | Extract value | Option<T> |
| contains<T>(&self) | Check existence | bool |
| clear(&mut self) | Remove all | () |
Design characteristics:
| Property | Implication |
|----------|-------------|
| Type as key | One value per type |
| TypeId internally | O(1) lookup |
| Box<dyn Any> storage | Heap allocation per value |
| Send + Sync conditional | Thread-safe when values are |
| No iteration API | Compile-time type safety |
Best practices:
| Practice | Reason | |----------|--------| | Wrapper types | Avoid collisions | | Newtype pattern | Clear semantics | | Clone for context | Request extensions copied to response | | Middleware for insertion | Consistent initialization | | Option handling | Graceful degradation if missing |
Key insight: http::Extensions solves the challenge of attaching arbitrary data to HTTP requests and responses without modifying their types. It's implemented as a type map using TypeId as keys, allowing each type to have exactly one stored value. This is essential for middleware architectures where data like authentication state, request IDs, timing information, and application context needs to flow through the request lifecycle without threading parameters through every function. The type-safe API prevents runtime key collisions (unlike string-keyed maps) and the Send + Sync bounds allow sharing across thread boundaries when the stored values support it. Use wrapper types (newtype pattern) to prevent collisions when multiple modules might use the same base type—struct RequestId(String) instead of storing raw String. Extensions are cleared when the request/response is dropped, making them ideal for request-scoped data that shouldn't persist beyond the current request.