HttpWardContext Extensions: Middleware Data Sharing Guide¶
Overview¶
HttpWardContext now includes an extensions field (ExtensionsMap) that allows middleware to store and retrieve arbitrary data during request processing. This enables middleware to:
- Create and store data (e.g., analysis results, parsed tokens)
- Share that data with downstream middleware
- Retrieve and use data from upstream middleware
Architecture¶
ExtensionsMap¶
A thread-safe, cloneable storage mechanism using type-erased values:
pub struct ExtensionsMap {
inner: Arc<RwLock<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
}
Key Features: - ✅ Thread-safe (RwLock + Arc) - ✅ Type-safe access via generics - ✅ Cloneable (cheap clone due to Arc) - ✅ No runtime panics on type mismatch (returns Option) - ✅ Works across async boundaries
Usage Patterns¶
1. Basic Storage and Retrieval¶
use httpward_core::core::HttpWardContext;
async fn middleware_1(ctx: &HttpWardContext) {
// Store arbitrary data
ctx.extensions.insert("user_id", 12345u64);
ctx.extensions.insert("token", "abc123xyz".to_string());
}
async fn middleware_2(ctx: &HttpWardContext) {
// Retrieve data with type safety
if let Some(user_id) = ctx.extensions.get::<u64>("user_id") {
println!("User ID: {}", user_id);
}
}
2. Storing Complex Types¶
#[derive(Clone, Debug)]
pub struct AnalysisResult {
pub is_bot: bool,
pub risk_score: u32,
}
// In middleware 1
let result = AnalysisResult {
is_bot: false,
risk_score: 25,
};
ctx.extensions.insert("analysis", result);
// In middleware 2
if let Some(result) = ctx.extensions.get::<AnalysisResult>("analysis") {
if result.risk_score > 50 {
println!("High risk request!");
}
}
3. Multiple Middleware Coordination¶
// Middleware A: Extract fingerprint
async fn fingerprint_mw(ctx: &HttpWardContext) {
let fp = calculate_fingerprint();
ctx.extensions.insert("fingerprint", fp);
}
// Middleware B: Enrich with geolocation
async fn geo_mw(ctx: &HttpWardContext) {
if let Some(fp) = ctx.extensions.get::<String>("fingerprint") {
let location = lookup_location(&fp);
ctx.extensions.insert("location", location);
}
}
// Middleware C: Make security decision
async fn security_mw(ctx: &HttpWardContext) {
match (
ctx.extensions.get::<String>("fingerprint"),
ctx.extensions.get::<Location>("location"),
) {
(Some(fp), Some(loc)) => {
// Make decision based on both signals
}
_ => {}
}
}
API Reference¶
Insert Data¶
pub fn insert<T: Any + Send + Sync + 'static>(&self, key: impl Into<String>, value: T)
Stores a value with the given key. The value can be any type that implements Send + Sync + 'static.
Example:
ctx.extensions.insert("user_id", 42u64);
ctx.extensions.insert("claims", jwt_claims);
Get Data¶
pub fn get<T: Any + Send + Sync + 'static>(&self, key: &str) -> Option<Arc<T>>
Retrieves a value by key. Returns None if: - The key doesn't exist - The stored type doesn't match the requested type T
Example:
if let Some(user_id) = ctx.extensions.get::<u64>("user_id") {
println!("ID: {}", user_id);
}
Contains Key¶
pub fn contains_key(&self, key: &str) -> bool
Checks if a key exists in the extensions map.
Example:
if ctx.extensions.contains_key("analysis_result") {
// Process further
}
Remove Data¶
pub fn remove(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>>
Removes and returns a value by key.
Example:
if let Some(data) = ctx.extensions.remove("temporary_data") {
// Use and discard
}
Clear All¶
pub fn clear(&self)
Removes all stored data.
Example:
ctx.extensions.clear();
Length and Empty Check¶
pub fn len(&self) -> usize
pub fn is_empty(&self) -> bool
Example:
if ctx.extensions.is_empty() {
println!("No extensions set");
}
Best Practices¶
1. Use Meaningful Keys¶
// ✅ Good
ctx.extensions.insert("jwt_claims", claims);
ctx.extensions.insert("user_analysis", analysis);
// ❌ Avoid
ctx.extensions.insert("data1", claims);
ctx.extensions.insert("tmp", analysis);
2. Document Expected Keys¶
Create constants or enums for extension keys:
pub mod extension_keys {
pub const JWT_CLAIMS: &str = "jwt_claims";
pub const USER_ANALYSIS: &str = "user_analysis";
pub const IP_GEOLOCATION: &str = "ip_geolocation";
}
// Usage
ctx.extensions.insert(extension_keys::JWT_CLAIMS, claims);
if let Some(claims) = ctx.extensions.get::<JwtClaims>(extension_keys::JWT_CLAIMS) {
// ...
}
3. Handle Type Mismatches Gracefully¶
// ✅ Good: Handle both cases
match (
ctx.extensions.get::<UserAnalysis>("analysis"),
ctx.extensions.get::<JwtClaims>("claims"),
) {
(Some(analysis), Some(claims)) => {
// Use both
}
(Some(analysis), None) => {
// Only analysis available
}
(None, Some(claims)) => {
// Only claims available
}
(None, None) => {
// No data
}
}
4. Clone Values When Needed¶
Since get() returns Arc<T>, cloning the Arc is cheap:
let claims = ctx.extensions.get::<JwtClaims>("claims");
// For owned value, clone the inner data if needed:
if let Some(claims_arc) = claims {
let claims_owned = (*claims_arc).clone(); // Clone inner data
// Now you have owned JwtClaims
}
5. Middleware Ordering¶
Ensure middleware that produces data runs before middleware that consumes it:
// builder
.add_layer(FingerprinterMiddleware) // Produces "fingerprint"
.add_layer(EnricherMiddleware) // Consumes "fingerprint"
.add_layer(SecurityDecisionMiddleware) // Uses enriched data
Performance Considerations¶
Thread Safety¶
- Uses
parking_lot::RwLockfor better performance thanstd::sync::RwLock - Multiple readers can access simultaneously
- Cloning the
ExtensionsMapis cheap (just increments Arc refcount)
Memory¶
- Data is stored in Arc, so each value is heap-allocated once
- Cheap cloning of maps between middleware
- No unnecessary copying
Locking¶
insert()andremove()take write locks (brief)get()takes read locks (non-blocking for concurrent readers)clear()takes write lock (expensive for large maps)
Serialization Patterns¶
If you need to serialize/deserialize across boundaries:
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SerializableData {
pub field1: String,
pub field2: u64,
}
// Store
let data = SerializableData { field1: "test".into(), field2: 42 };
ctx.extensions.insert("serializable", data);
// Retrieve and serialize to JSON
if let Some(data) = ctx.extensions.get::<SerializableData>("serializable") {
let json = serde_json::to_string(&*data)?;
println!("Serialized: {}", json);
}
Testing¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_middleware_coordination() {
let ctx = HttpWardContext::new(
"127.0.0.1:8080".parse().unwrap(),
Arc::new(server_instance),
);
// Simulate middleware 1
ctx.extensions.insert("step1", "completed".to_string());
// Simulate middleware 2
assert_eq!(
ctx.extensions.get::<String>("step1").map(|s| (*s).clone()),
Some("completed".to_string())
);
}
}
Examples¶
See extensions_example.rs for complete working examples of: - Storing UserAnalysis results - Storing JWT claims - Multi-middleware coordination - Type safety guarantees
Troubleshooting¶
Extension Not Found¶
// Check if it was actually stored
if !ctx.extensions.contains_key("my_data") {
eprintln!("Data not stored!");
}
Type Mismatch¶
// Verify the type you're using matches what was stored
// get::<WrongType>(...) will return None
// Use a debugger or logging to verify types
ctx.extensions.insert("value", 42u64);
assert_eq!(ctx.extensions.get::<u32>("value"), None); // Type mismatch
assert_eq!(ctx.extensions.get::<u64>("value"), Some(Arc::new(42u64))); // Correct
Middleware Ordering¶
// Middleware B won't find data if Middleware A hasn't run yet
// Ensure correct ordering in builder:
builder
.add_layer(ProducerMiddleware) // Must come first
.add_layer(ConsumerMiddleware) // Uses data from producer
Migration Guide¶
If you previously passed data through request headers or query parameters, you can now use extensions:
Before¶
// Had to add custom headers
request.headers_mut().insert("X-Analysis", HeaderValue::from_static("..."));
// Other middleware had to parse headers
After¶
// Store directly in context
ctx.extensions.insert("analysis", analysis_result);
// Other middleware can access directly
if let Some(analysis) = ctx.extensions.get::<AnalysisResult>("analysis") {
// Use directly
}