What are the trade-offs between http::Extensions::get and get_mut for accessing request-scoped data?
get returns an immutable reference to stored extension data while get_mut returns a mutable referenceβchoosing between them depends on whether you need to modify the stored value and whether you can obtain mutable access to the Extensions map. The trade-off spans ownership semantics, borrow checker constraints, and architectural patterns for middleware and request handling. Understanding these methods helps you design request-processing pipelines that safely share state.
The Extensions Type
use http::Extensions;
fn basic_usage() {
let mut extensions = Extensions::new();
// Extensions is a type map: TypeId -> Box<dyn Any>
// Store values by type
extensions.insert(42u32);
extensions.insert("config".to_string());
extensions.insert(vec![1, 2, 3]);
// Retrieve by type
let num: Option<&u32> = extensions.get();
let config: Option<&String> = extensions.get();
let vec: Option<&Vec<i32>> = extensions.get();
println!("num: {:?}", num); // Some(&42)
println!("config: {:?}", config); // Some(&"config")
println!("vec: {:?}", vec); // Some(&[1, 2, 3])
// Mutable access requires get_mut
if let Some(vec) = extensions.get_mut::<Vec<i32>>() {
vec.push(4);
}
println!("vec after mutation: {:?}", extensions.get::<Vec<i32>>());
}Extensions stores type-keyed values; get retrieves immutable references, get_mut retrieves mutable references.
The Signature Difference
use http::Extensions;
// Signature comparison:
impl Extensions {
// get: immutable access
pub fn get<T: 'static>(&self) -> Option<&T>
// get_mut: mutable access
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T>
}
// The key difference: self vs &mut self
fn signature_implications() {
let mut extensions = Extensions::new();
extensions.insert(vec![1, 2, 3]);
// get: requires &self
// Can borrow immutably while holding other references
let ext_ref = &extensions;
let vec: Option<&Vec<i32>> = ext_ref.get();
// ext_ref is still valid
// get_mut: requires &mut self
// Must have exclusive mutable access
let ext_mut_ref = &mut extensions;
let vec: Option<&mut Vec<i32>> = ext_mut_ref.get_mut();
// ext_mut_ref is borrowed while vec exists
}
// This means:
// - get works with shared references
// - get_mut requires exclusive mutable access to Extensionsget takes &self; get_mut takes &mut selfβthis determines where each can be used.
Borrow Checker Implications
use http::Extensions;
fn borrow_patterns() {
let mut extensions = Extensions::new();
extensions.insert(10u32);
extensions.insert(20u32);
// Multiple immutable gets are fine
let a: Option<&u32> = extensions.get();
let b: Option<&u32> = extensions.get();
// Both references coexist
// Cannot get_mut while immutable references exist
// let c = extensions.get_mut::<u32>(); // Error: cannot borrow mutably
// Drop immutable refs first
drop(a);
drop(b);
// Now mutable access is allowed
if let Some(num) = extensions.get_mut::<u32>() {
*num += 5;
}
// Cannot have multiple mutable references
// let x = extensions.get_mut::<u32>();
// let y = extensions.get_mut::<u32>(); // Error: cannot borrow twice
// Pattern: get, modify, done
if let Some(num) = extensions.get_mut::<u32>() {
*num += 1;
}
// Mutable borrow ends here
}
fn simultaneous_access() {
let mut extensions = Extensions::new();
extensions.insert(1u32);
extensions.insert(2i32); // Different types
// Different types can be accessed simultaneously with get
let u32_ref: Option<&u32> = extensions.get();
let i32_ref: Option<&i32> = extensions.get();
// Both valid - different types but same Extensions reference
// With get_mut, still limited by &mut self
// Cannot get_mut for different types simultaneously
// Even though stored values don't overlap
// The &mut self requirement is the constraint, not the stored values
}get allows multiple concurrent reads; get_mut requires exclusive access to the entire Extensions map.
Request Handling Context
use http::{Request, Extensions};
fn request_context() {
// In web frameworks, Extensions often lives on Request
let mut request = Request::new(());
request.extensions_mut().insert(UserId(42));
request.extensions_mut().insert(Session::new());
// Middleware typically has &mut Request
fn middleware<B>(req: &mut Request<B>) {
// Can use get_mut because we have &mut
if let Some(user_id) = req.extensions_mut().get_mut::<UserId>() {
user_id.0 += 1; // Modify in place
}
}
// Handlers might only have &Request
fn handler<B>(req: &Request<B>) {
// Can only use get - no mutable access
let user_id: Option<&UserId> = req.extensions().get();
// Cannot modify user_id
// If modification needed, must have been done earlier
// or requires different architecture
}
}
#[derive(Debug, Clone)]
struct UserId(u64);
#[derive(Debug)]
struct Session {
id: String,
}
impl Session {
fn new() -> Self {
Session { id: "abc123".to_string() }
}
}In web frameworks, the request lifecycle determines whether get or get_mut is available.
Middleware Patterns
use http::{Request, Response};
use http_body::Body;
// Pattern 1: Read-only middleware
fn read_only_middleware<B>(req: &Request<B>) {
// Only needs to read, use get
if let Some(session) = req.extensions().get::<Session>() {
println!("Session: {:?}", session.id);
}
}
// Pattern 2: Modifying middleware
fn modifying_middleware<B>(req: &mut Request<B>) {
// Needs to modify stored data, use get_mut
if let Some(session) = req.extensions_mut().get_mut::<Session>() {
session.id.push_str("_modified");
}
}
// Pattern 3: Conditional modification
fn conditional_middleware<B>(req: &mut Request<B>) {
// First check with get (no modification needed)
if req.extensions().get::<Session>().is_some() {
// Session exists, now we might modify
if let Some(session) = req.extensions_mut().get_mut::<Session>() {
session.id.push_str("_active");
}
}
// Note: This pattern re-borrows; not optimal
}
// Pattern 4: Insert-or-modify
fn insert_or_modify<B>(req: &mut Request<B>) {
// Try to get existing
if let Some(counter) = req.extensions_mut().get_mut::<RequestCount>() {
counter.0 += 1;
} else {
// Insert new if doesn't exist
req.extensions_mut().insert(RequestCount(1));
}
// Or use entry API pattern (Extensions doesn't have entry, but we can simulate)
let count = req.extensions_mut().get_mut::<RequestCount>()
.map(|c| { c.0 += 1; c.0 })
.unwrap_or_else(|| {
req.extensions_mut().insert(RequestCount(1));
1
});
}
#[derive(Debug)]
struct RequestCount(u32);Middleware design must account for when get vs get_mut is appropriate.
Interior Mutability Workarounds
use http::Extensions;
use std::cell::RefCell;
use std::sync::Arc;
use parking_lot::Mutex;
// Problem: Need to modify without &mut Extensions
// Solution: Store mutable data inside RefCell/Mutex
fn interior_mutability() {
let mut extensions = Extensions::new();
// Store mutable data inside RefCell
extensions.insert(RefCell::new(vec![1, 2, 3]));
// Now get (immutable) allows modification
if let Some(vec_cell) = extensions.get::<RefCell<Vec<i32>>>() {
vec_cell.borrow_mut().push(4); // Mutate through RefCell
}
// This works even though we only have &self!
println!("Vec: {:?}", extensions.get::<RefCell<Vec<i32>>>().unwrap().borrow());
}
fn thread_safe_mutation() {
let mut extensions = Extensions::new();
// For thread-safe mutation, use Mutex
extensions.insert(Arc::new(Mutex::new(Counter(0))));
// get provides &Arc<Mutex<Counter>>
// Mutex provides interior mutability
if let Some(counter) = extensions.get::<Arc<Mutex<Counter>>>() {
counter.lock().0 += 1;
}
// Thread-safe even with shared reference
}
#[derive(Debug)]
struct Counter(u32);
// This pattern is common in frameworks:
// - Store Arc<Mutex<T>> in Extensions
// - Use get() everywhere
// - Lock when modification neededInterior mutability (RefCell, Mutex) allows mutation through get when get_mut isn't available.
Real-World Example: Rate Limiting
use http::{Request, Extensions};
use std::time::Instant;
#[derive(Debug)]
struct RateLimitState {
request_count: u32,
last_request: Instant,
window_start: Instant,
}
impl RateLimitState {
fn new() -> Self {
RateLimitState {
request_count: 0,
last_request: Instant::now(),
window_start: Instant::now(),
}
}
fn check_and_increment(&mut self, max_requests: u32) -> bool {
let now = Instant::now();
// Reset window if needed
if now.duration_since(self.window_start).as_secs() >= 60 {
self.window_start = now;
self.request_count = 0;
}
self.last_request = now;
if self.request_count < max_requests {
self.request_count += 1;
true
} else {
false
}
}
}
// Middleware that modifies rate limit state
fn rate_limit_middleware<B>(req: &mut Request<B>) -> Result<(), &'static str> {
// Use get_mut because we need to modify
if let Some(state) = req.extensions_mut().get_mut::<RateLimitState>() {
if !state.check_and_increment(100) {
return Err("Rate limit exceeded");
}
Ok(())
} else {
// No rate limit state - create one
req.extensions_mut().insert(RateLimitState::new());
Ok(())
}
}
// Alternative: Interior mutability approach
fn rate_limit_middleware_refcell<B>(req: &Request<B>) -> Result<(), &'static str> {
// Only need &Request, not &mut Request
use std::cell::RefCell;
if let Some(state_cell) = req.extensions().get::<RefCell<RateLimitState>>() {
let mut state = state_cell.borrow_mut();
if !state.check_and_increment(100) {
return Err("Rate limit exceeded");
}
}
Ok(())
}Rate limiting demonstrates when get_mut is needed versus when interior mutability suffices.
Performance Considerations
use http::Extensions;
fn performance_comparison() {
let mut extensions = Extensions::new();
for i in 0..100 {
extensions.insert(format!("key_{}", i));
}
// get: O(1) hash lookup, returns reference
let start = std::time::Instant::now();
for _ in 0..1000 {
let _ = extensions.get::<String>();
}
println!("get: {:?}", start.elapsed());
// get_mut: O(1) hash lookup, returns mutable reference
let start = std::time::Instant::now();
for _ in 0..1000 {
let _ = extensions.get_mut::<String>();
}
println!("get_mut: {:?}", start.elapsed());
// Performance is essentially the same
// Both are O(1) HashMap lookups
// The real cost difference is in application architecture:
// - get allows shared references (more flexible)
// - get_mut requires exclusive mutable access (less flexible)
// Interior mutability adds overhead:
use std::cell::RefCell;
extensions.insert(RefCell::new(0u32));
let start = std::time::Instant::now();
for _ in 0..1000 {
if let Some(cell) = extensions.get::<RefCell<u32>>() {
cell.borrow_mut() += 1; // Runtime borrow check
}
}
println!("RefCell approach: {:?}", start.elapsed());
// RefCell has runtime borrow checking overhead
// But allows mutation through shared reference
}get and get_mut have identical lookup performance; the real trade-off is API flexibility.
API Design Patterns
use http::Extensions;
// Pattern 1: Builder-style insertion, read-only retrieval
fn builder_pattern() {
let mut extensions = Extensions::new();
// Build phase: &mut Extensions
extensions.insert(Config::default());
extensions.insert(User::new("alice"));
// Use phase: &Extensions (no modifications)
let extensions = extensions; // Freeze
process_request(&extensions);
}
fn process_request(extensions: &Extensions) {
// Only get is available here
let user: Option<&User> = extensions.get();
let config: Option<&Config> = extensions.get();
// Cannot modify - read-only phase
}
// Pattern 2: Mutable throughout
fn always_mutable(extensions: &mut Extensions) {
// Full flexibility
if let Some(user) = extensions.get_mut::<User>() {
user.name = "bob".to_string();
}
// But caller must have &mut Extensions
// May not work in all contexts
}
// Pattern 3: Interior mutability for shared modification
use std::sync::Arc;
use parking_lot::RwLock;
struct RequestContext {
user: Arc<RwLock<Option<User>>>,
config: Arc<RwLock<Config>>,
}
fn shared_modification() {
let mut extensions = Extensions::new();
extensions.insert(RequestContext {
user: Arc::new(RwLock::new(Some(User::new("alice")))),
config: Arc::new(RwLock::new(Config::default())),
});
// Now get works everywhere with modification capability
fn modify_user(extensions: &Extensions) {
if let Some(ctx) = extensions.get::<RequestContext>() {
let mut user = ctx.user.write();
*user = Some(User::new("modified"));
}
}
}
#[derive(Debug)]
struct User {
name: String,
}
impl User {
fn new(name: &str) -> Self {
User { name: name.to_string() }
}
}
#[derive(Debug, Default)]
struct Config;Choose patterns based on when mutation is needed and what references are available.
Error Handling
use http::Extensions;
fn error_handling() {
let mut extensions = Extensions::new();
extensions.insert(42u32);
// get returns Option
match extensions.get::<u32>() {
Some(num) => println!("Found: {}", num),
None => println!("Not found"),
}
// get_mut returns Option
match extensions.get_mut::<u32>() {
Some(num) => *num += 1,
None => println!("Not found"),
}
// Type mismatch returns None, not error
let not_found: Option<&String> = extensions.get();
assert!(not_found.is_none());
// Cannot use unwrap patterns that might panic
// let num = extensions.get::<String>().unwrap(); // Panic!
// Safe patterns:
if let Some(num) = extensions.get_mut::<u32>() {
*num += 1;
} else {
// Handle missing value
extensions.insert(0u32);
}
// Or use default
let num = extensions.get::<u32>().copied().unwrap_or(0);
}Both methods return Option; handle the None case when types don't match or values aren't inserted.
Synthesis
Quick reference:
use http::Extensions;
// Method signatures:
impl Extensions {
fn get<T: 'static>(&self) -> Option<&T>;
fn get_mut<T: 'static>(&mut self) -> Option<&mut T>;
}
// Decision matrix:
// ββββββββββββββββββββββββββββββββββββββ¬βββββββββββ¬ββββββββββββββββββββββ
// β Scenario β Use β Reason β
// ββββββββββββββββββββββββββββββββββββββΌβββββββββββΌββββββββββββββββββββββ€
// β Read extension data β get β Immutable access β
// β Have &Request (not &mut) β get β Only option β
// β Handler in web framework β get β Often immutable β
// β Multiple concurrent reads β get β Allows sharing β
// β Modify extension data β get_mut β Mutable access β
// β Have &mut Request β get_mut β Available β
// β Middleware modifying state β get_mut β Needed for mutation β
// β Increment counters, update state β get_mut β Mutation required β
// β Need mutation but only have &ref β RefCell β Interior mutability β
// β Thread-safe mutation β Arc<Mut> β Sync interior mut. β
// ββββββββββββββββββββββββββββββββββββββ΄βββββββββββ΄ββββββββββββββββββββββ
// Trade-offs:
// get:
// + Works with shared references
// + Allows multiple concurrent readers
// + Simpler ownership story
// - Cannot modify values
// - Requires interior mutability wrapper for mutation
// get_mut:
// + Direct mutation of values
// + No runtime borrow checking
// - Requires &mut Extensions
// - Exclusive access during lifetime of reference
// - Cannot coexist with other references
// Common patterns:
// 1. Read-only handlers: get with &Request
// 2. Modifying middleware: get_mut with &mut Request
// 3. Shared state: Arc<Mutex<T>> + get
// 4. Thread-local state: RefCell<T> + get
// Anti-patterns:
// β Trying to use get_mut with only &Request
// β Holding get_mut reference while doing other operations
// β Storing direct references to extension data (lifetime issues)
// Best practices:
// β
Use get by default, get_mut when mutation required
// β
Design middleware to have &mut Request when modification needed
// β
Use interior mutability for state shared across handlers
// β
Check Option result rather than unwrappingKey insight: The trade-off between get and get_mut reflects Rust's ownership model applied to request-scoped storage. get is the safe defaultβit works with shared references, enables concurrent reads, and fits cleanly into read-only contexts like handlers. get_mut provides the mutation capability but requires exclusive mutable access, restricting where it can be used. The tension arises when you need mutation but only have shared access: this is where interior mutability patterns (RefCell, Mutex) bridge the gap, letting you store RefCell<T> and mutate through get by borrowing the RefCell. The choice isn't just about the immediate operationβit shapes your entire request processing architecture, determining whether middleware and handlers need mutable access to the request and how state flows through the system.
