Chapter 08: Middleware & Request Context
This chapter explores how the Pierre Fitness Platform uses Axum middleware to extract authentication, tenant context, rate limiting information, and tracing data from HTTP requests before routing to handlers. You’ll learn about middleware composition, request ID generation, CORS configuration, and PII-safe logging.
Middleware Stack Overview
The Pierre platform uses a layered middleware stack that processes every HTTP request before it reaches handlers:
┌────────────────────────────────────────────────────────────┐
│ HTTP Request │
└───────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ CORS Middleware │ ← Allow cross-origin requests
│ (OPTIONS preflight) │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Request ID Middleware │ ← Generate UUID for tracing
│ x-request-id: ... │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Tracing Middleware │ ← Create span with metadata
│ RequestContext │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Auth Middleware │ ← Validate JWT/API key
│ Extract user_id │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Tenant Middleware │ ← Extract tenant context
│ TenantContext │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Rate Limit Middleware │ ← Check usage limits
│ Add X-RateLimit-* │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Route Handler │ ← Business logic
│ Process request │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Response │ ← Add security headers
│ x-request-id: ... │
└──────────────────────────┘
Source: src/middleware/mod.rs:1-77
#![allow(unused)]
fn main() {
// ABOUTME: HTTP middleware for request tracing, authentication, and context propagation
// ABOUTME: Provides request ID generation, span creation, and tenant context for structured logging
/// Authentication middleware for MCP and API requests
pub mod auth;
/// CORS middleware configuration
pub mod cors;
/// Rate limiting middleware and utilities
pub mod rate_limiting;
/// PII redaction and sensitive data masking
pub mod redaction;
/// Request ID generation and propagation
pub mod request_id;
/// Request tracing and context propagation
pub mod tracing;
// Authentication middleware
/// MCP authentication middleware
pub use auth::McpAuthMiddleware;
// CORS middleware
/// Setup CORS layer for HTTP endpoints
pub use cors::setup_cors;
// Rate limiting middleware and utilities
/// Check rate limit and send error response
pub use rate_limiting::check_rate_limit_and_respond;
/// Create rate limit error
pub use rate_limiting::create_rate_limit_error;
/// Create rate limit headers
pub use rate_limiting::create_rate_limit_headers;
/// Rate limit headers module
pub use rate_limiting::headers;
// PII-safe logging and redaction
/// Mask email addresses for logging
pub use redaction::mask_email;
/// Redact sensitive HTTP headers
pub use redaction::redact_headers;
/// Redact JSON fields by pattern
pub use redaction::redact_json_fields;
/// Redact token patterns from strings
pub use redaction::redact_token_patterns;
/// Bounded tenant label for tracing
pub use redaction::BoundedTenantLabel;
/// Bounded user label for tracing
pub use redaction::BoundedUserLabel;
/// Redaction configuration
pub use redaction::RedactionConfig;
/// Redaction features toggle
pub use redaction::RedactionFeatures;
// Request ID middleware
/// Request ID middleware function
pub use request_id::request_id_middleware;
/// Request ID extractor
pub use request_id::RequestId;
// Request tracing and context management
/// Create database operation span
pub use tracing::create_database_span;
/// Create MCP operation span
pub use tracing::create_mcp_span;
/// Create HTTP request span
pub use tracing::create_request_span;
/// Request context for tracing
pub use tracing::RequestContext;
}
Rust Idiom: Re-exporting with pub use
The middleware/mod.rs file acts as a facade, re-exporting commonly used types from submodules. This allows handlers to use crate::middleware::RequestId instead of use crate::middleware::request_id::RequestId, reducing coupling to internal module organization.
Request ID Generation
Every HTTP request receives a unique identifier for distributed tracing and log correlation:
Source: src/middleware/request_id.rs:39-61
#![allow(unused)]
fn main() {
/// Request ID middleware that generates and propagates correlation IDs
///
/// This middleware:
/// 1. Generates a unique UUID v4 for each request
/// 2. Adds the request ID to request extensions for handler access
/// 3. Records the request ID in the current tracing span
/// 4. Includes the request ID in the response header
pub async fn request_id_middleware(mut req: Request, next: Next) -> Response {
// Generate unique request ID
let request_id = Uuid::new_v4().to_string();
// Record request ID in current tracing span
let span = Span::current();
span.record("request_id", &request_id);
// Add to request extensions for handler access
req.extensions_mut().insert(RequestId(request_id.clone()));
// Process request
let mut response = next.run(req).await;
// Add request ID to response header
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
response
.headers_mut()
.insert(REQUEST_ID_HEADER, header_value);
}
response
}
}
Flow:
- Generate: Create UUID v4 for globally unique ID
- Record: Add to current tracing span for structured logs
- Extend: Store in request extensions for handler access
- Process: Call next middleware/handler with
next.run(req) - Respond: Include
x-request-idheader in response
Rust Idiom: Request extensions for typed data
Axum’s req.extensions_mut().insert(RequestId(...)) provides type-safe request-scoped storage. Handlers can extract RequestId using:
#![allow(unused)]
fn main() {
async fn handler(Extension(request_id): Extension<RequestId>) -> String {
format!("Request ID: {}", request_id.0)
}
}
The type system ensures you can’t accidentally insert or extract the wrong type.
Requestid Extractor
Source: src/middleware/request_id.rs:75-90
#![allow(unused)]
fn main() {
/// Request ID extractor for use in handlers
///
/// This can be extracted in any Axum handler to access the request ID
/// generated by the middleware.
#[derive(Debug, Clone)]
pub struct RequestId(pub String);
impl RequestId {
/// Get the request ID as a string slice
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RequestId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
}
Newtype pattern: Wrapping String in RequestId provides:
- Type safety: Can’t confuse request ID with other strings
- Display trait: Use
{request_id}in format strings - Documentation: Self-documenting API (function signature says “I need a RequestId”)
Request Context and Tracing
The RequestContext struct flows through the entire request lifecycle, accumulating metadata:
Source: src/middleware/tracing.rs:10-67
#![allow(unused)]
fn main() {
/// Request context that flows through the entire request lifecycle
#[derive(Debug, Clone)]
pub struct RequestContext {
/// Unique identifier for this request
pub request_id: String,
/// Authenticated user ID (if available)
pub user_id: Option<Uuid>,
/// Tenant ID for multi-tenancy (if available)
pub tenant_id: Option<Uuid>,
/// Authentication method used (e.g., "Bearer", "ApiKey")
pub auth_method: Option<String>,
}
impl RequestContext {
/// Create new request context with generated request ID
#[must_use]
pub fn new() -> Self {
Self {
request_id: format!("req_{}", Uuid::new_v4().simple()),
user_id: None,
tenant_id: None,
auth_method: None,
}
}
/// Update context with authentication information
#[must_use]
pub fn with_auth(mut self, user_id: Uuid, auth_method: String) -> Self {
self.user_id = Some(user_id);
self.tenant_id = Some(user_id); // For now, user_id serves as tenant_id
self.auth_method = Some(auth_method);
self
}
/// Record context in current tracing span
pub fn record_in_span(&self) {
let span = Span::current();
span.record("request_id", &self.request_id);
if let Some(user_id) = &self.user_id {
span.record("user_id", user_id.to_string());
}
if let Some(tenant_id) = &self.tenant_id {
span.record("tenant_id", tenant_id.to_string());
}
if let Some(auth_method) = &self.auth_method {
span.record("auth_method", auth_method);
}
}
}
}
Builder pattern: The with_auth method allows chaining:
#![allow(unused)]
fn main() {
let context = RequestContext::new()
.with_auth(user_id, "Bearer".into());
}
Span recording: The record_in_span method populates tracing fields declared as Empty:
#![allow(unused)]
fn main() {
let span = tracing::info_span!("request", user_id = tracing::field::Empty);
context.record_in_span(); // Now span has user_id field
}
Span Creation Utilities
The platform provides helpers for creating tracing spans with pre-configured fields:
Source: src/middleware/tracing.rs:69-110
#![allow(unused)]
fn main() {
/// Create a tracing span for HTTP requests
pub fn create_request_span(method: &str, path: &str) -> tracing::Span {
tracing::info_span!(
"http_request",
method = %method,
path = %path,
request_id = tracing::field::Empty,
user_id = tracing::field::Empty,
tenant_id = tracing::field::Empty,
auth_method = tracing::field::Empty,
status_code = tracing::field::Empty,
duration_ms = tracing::field::Empty,
)
}
/// Create a tracing span for MCP operations
pub fn create_mcp_span(operation: &str) -> tracing::Span {
tracing::info_span!(
"mcp_operation",
operation = %operation,
request_id = tracing::field::Empty,
user_id = tracing::field::Empty,
tenant_id = tracing::field::Empty,
tool_name = tracing::field::Empty,
duration_ms = tracing::field::Empty,
success = tracing::field::Empty,
)
}
/// Create a tracing span for database operations
pub fn create_database_span(operation: &str, table: &str) -> tracing::Span {
tracing::debug_span!(
"database_operation",
operation = %operation,
table = %table,
request_id = tracing::field::Empty,
user_id = tracing::field::Empty,
tenant_id = tracing::field::Empty,
duration_ms = tracing::field::Empty,
rows_affected = tracing::field::Empty,
)
}
}
Usage pattern:
#![allow(unused)]
fn main() {
async fn handle_request() -> Result<Response> {
let span = create_request_span("POST", "/api/activities");
let _guard = span.enter();
// All logs within this scope include span fields
tracing::info!("Processing activity request");
// Later: record additional fields
Span::current().record("status_code", 200);
Span::current().record("duration_ms", 42);
Ok(response)
}
}
CORS Configuration
The platform configures Cross-Origin Resource Sharing (CORS) for web client access:
Source: src/middleware/cors.rs:40-96
#![allow(unused)]
fn main() {
/// Configure CORS settings for the MCP server
///
/// Configures cross-origin requests based on `CORS_ALLOWED_ORIGINS` environment variable.
/// Supports both wildcard ("*") for development and specific origin lists for production.
pub fn setup_cors(config: &crate::config::environment::ServerConfig) -> CorsLayer {
// Parse allowed origins from configuration
let allow_origin =
if config.cors.allowed_origins.is_empty() || config.cors.allowed_origins == "*" {
// Development mode: allow any origin
AllowOrigin::any()
} else {
// Production mode: parse comma-separated origin list
let origins: Vec<HeaderValue> = config
.cors
.allowed_origins
.split(',')
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
HeaderValue::from_str(trimmed).ok()
}
})
.collect();
if origins.is_empty() {
// Fallback to any if parsing failed
AllowOrigin::any()
} else {
AllowOrigin::list(origins)
}
};
CorsLayer::new()
.allow_origin(allow_origin)
.allow_headers([
HeaderName::from_static("content-type"),
HeaderName::from_static("authorization"),
HeaderName::from_static("x-requested-with"),
HeaderName::from_static("accept"),
HeaderName::from_static("origin"),
HeaderName::from_static("access-control-request-method"),
HeaderName::from_static("access-control-request-headers"),
HeaderName::from_static("x-strava-client-id"),
HeaderName::from_static("x-strava-client-secret"),
HeaderName::from_static("x-fitbit-client-id"),
HeaderName::from_static("x-fitbit-client-secret"),
HeaderName::from_static("x-pierre-api-key"),
HeaderName::from_static("x-tenant-name"),
HeaderName::from_static("x-tenant-id"),
])
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::OPTIONS,
Method::PATCH,
])
}
}
Configuration examples:
# Development: allow all origins
export CORS_ALLOWED_ORIGINS="*"
# Production: specific origins only
export CORS_ALLOWED_ORIGINS="https://app.pierre.fitness,https://admin.pierre.fitness"
Security: The platform allows custom headers for:
- Provider OAuth:
x-strava-client-id,x-fitbit-client-idfor dynamic OAuth configuration - Multi-tenancy:
x-tenant-name,x-tenant-idfor tenant routing - API keys:
x-pierre-api-keyfor alternative authentication
Rust Idiom: filter_map for parsing
The CORS configuration uses filter_map to parse origin strings while skipping invalid entries:
#![allow(unused)]
fn main() {
config.cors.allowed_origins
.split(',')
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None // Skip empty strings
} else {
HeaderValue::from_str(trimmed).ok() // Parse or skip invalid
}
})
.collect();
}
This handles malformed configuration gracefully without panicking.
Rate Limiting Headers
The platform adds standard HTTP rate limiting headers to all responses:
Source: src/middleware/rate_limiting.rs:17-32
#![allow(unused)]
fn main() {
/// HTTP header names for rate limiting
pub mod headers {
/// HTTP header name for maximum requests allowed in the current window
pub const X_RATE_LIMIT_LIMIT: &str = "X-RateLimit-Limit";
/// HTTP header name for remaining requests in the current window
pub const X_RATE_LIMIT_REMAINING: &str = "X-RateLimit-Remaining";
/// HTTP header name for Unix timestamp when rate limit resets
pub const X_RATE_LIMIT_RESET: &str = "X-RateLimit-Reset";
/// HTTP header name for rate limit window duration in seconds
pub const X_RATE_LIMIT_WINDOW: &str = "X-RateLimit-Window";
/// HTTP header name for rate limit tier information
pub const X_RATE_LIMIT_TIER: &str = "X-RateLimit-Tier";
/// HTTP header name for authentication method used
pub const X_RATE_LIMIT_AUTH_METHOD: &str = "X-RateLimit-AuthMethod";
/// HTTP header name for retry-after duration in seconds
pub const RETRY_AFTER: &str = "Retry-After";
}
}
Standard headers:
X-RateLimit-Limit: Total requests allowed (e.g., “5000”)X-RateLimit-Remaining: Requests left in window (e.g., “4832”)X-RateLimit-Reset: Unix timestamp when limit resets (e.g., “1706054400”)Retry-After: Seconds until reset for 429 responses (e.g., “3600”)
Custom headers:
X-RateLimit-Window: Duration in seconds (e.g., “2592000” for 30 days)X-RateLimit-Tier: User’s subscription tier (e.g., “free”, “premium”)X-RateLimit-AuthMethod: Authentication type (e.g., “JwtToken”, “ApiKey”)
Creating Rate Limit Headers
Source: src/middleware/rate_limiting.rs:34-82
#![allow(unused)]
fn main() {
/// Create a `HeaderMap` with rate limit headers
#[must_use]
pub fn create_rate_limit_headers(rate_limit_info: &UnifiedRateLimitInfo) -> HeaderMap {
let mut headers = HeaderMap::new();
// Add rate limit headers if we have the information
if let Some(limit) = rate_limit_info.limit {
if let Ok(header_value) = HeaderValue::from_str(&limit.to_string()) {
headers.insert(headers::X_RATE_LIMIT_LIMIT, header_value);
}
}
if let Some(remaining) = rate_limit_info.remaining {
if let Ok(header_value) = HeaderValue::from_str(&remaining.to_string()) {
headers.insert(headers::X_RATE_LIMIT_REMAINING, header_value);
}
}
if let Some(reset_at) = rate_limit_info.reset_at {
// Add reset timestamp as Unix epoch
let reset_timestamp = reset_at.timestamp();
if let Ok(header_value) = HeaderValue::from_str(&reset_timestamp.to_string()) {
headers.insert(headers::X_RATE_LIMIT_RESET, header_value);
}
// Add Retry-After header (seconds until reset)
let retry_after = (reset_at - chrono::Utc::now()).num_seconds().max(0);
if let Ok(header_value) = HeaderValue::from_str(&retry_after.to_string()) {
headers.insert(headers::RETRY_AFTER, header_value);
}
}
// Add tier and authentication method information
if let Ok(header_value) = HeaderValue::from_str(&rate_limit_info.tier) {
headers.insert(headers::X_RATE_LIMIT_TIER, header_value);
}
if let Ok(header_value) = HeaderValue::from_str(&rate_limit_info.auth_method) {
headers.insert(headers::X_RATE_LIMIT_AUTH_METHOD, header_value);
}
// Add rate limit window (always 30 days for monthly limits)
headers.insert(
headers::X_RATE_LIMIT_WINDOW,
HeaderValue::from_static("2592000"), // 30 days in seconds
);
headers
}
}
Error handling: All header insertions use if let Ok(...) to gracefully handle invalid header values. If conversion fails, the header is skipped rather than panicking.
Rust Idiom: HeaderValue::from_static
The X_RATE_LIMIT_WINDOW uses from_static for compile-time constant strings, avoiding runtime allocation. For dynamic values, use HeaderValue::from_str which validates UTF-8 and HTTP header constraints.
Rate Limit Error Responses
Source: src/middleware/rate_limiting.rs:84-111
#![allow(unused)]
fn main() {
/// Create a rate limit exceeded error response with proper headers
#[must_use]
pub fn create_rate_limit_error(rate_limit_info: &UnifiedRateLimitInfo) -> AppError {
let limit = rate_limit_info.limit.unwrap_or(0);
AppError::new(
ErrorCode::RateLimitExceeded,
format!(
"Rate limit exceeded. You have reached your limit of {} requests for the {} tier",
limit, rate_limit_info.tier
),
)
}
/// Helper function to check rate limits and return appropriate response
///
/// # Errors
///
/// Returns an error if the rate limit has been exceeded
pub fn check_rate_limit_and_respond(
rate_limit_info: &UnifiedRateLimitInfo,
) -> Result<(), AppError> {
if rate_limit_info.is_rate_limited {
Err(create_rate_limit_error(rate_limit_info))
} else {
Ok(())
}
}
}
Usage in handlers:
#![allow(unused)]
fn main() {
async fn api_handler(auth: AuthResult) -> Result<Json<Response>> {
// Check rate limit first
check_rate_limit_and_respond(&auth.rate_limit)?;
// Process request
let data = fetch_data().await?;
Ok(Json(Response { data }))
}
}
Pii Redaction and Data Protection
The platform redacts Personally Identifiable Information (PII) from logs to comply with GDPR, CCPA, and other privacy regulations:
Source: src/middleware/redaction.rs:38-95
#![allow(unused)]
fn main() {
bitflags! {
/// Redaction feature flags to control which types of data to redact
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedactionFeatures: u8 {
/// Redact HTTP headers (Authorization, Cookie, etc.)
const HEADERS = 0b0001;
/// Redact JSON body fields (client_secret, tokens, etc.)
const BODY_FIELDS = 0b0010;
/// Mask email addresses
const EMAILS = 0b0100;
/// Enable all redaction features
const ALL = Self::HEADERS.bits() | Self::BODY_FIELDS.bits() | Self::EMAILS.bits();
}
}
/// Configuration for PII redaction
#[derive(Debug, Clone)]
pub struct RedactionConfig {
/// Enable redaction globally (default: true in production, false in dev)
pub enabled: bool,
/// Which redaction features to enable
pub features: RedactionFeatures,
/// Replacement string for redacted sensitive data
pub redaction_placeholder: String,
}
impl Default for RedactionConfig {
fn default() -> Self {
Self {
enabled: true,
features: RedactionFeatures::ALL,
redaction_placeholder: "[REDACTED]".to_owned(),
}
}
}
impl RedactionConfig {
/// Create redaction config from environment
#[must_use]
pub fn from_env() -> Self {
let config = crate::constants::get_server_config();
let enabled = config.is_none_or(|c| c.logging.redact_pii);
let features = if enabled {
RedactionFeatures::ALL
} else {
RedactionFeatures::empty()
};
Self {
enabled,
features,
redaction_placeholder: config.map_or_else(
|| "[REDACTED]".to_owned(),
|c| c.logging.redaction_placeholder.clone(),
),
}
}
/// Check if redaction is disabled
#[must_use]
pub const fn is_disabled(&self) -> bool {
!self.enabled
}
}
}
Bitflags pattern: Using the bitflags! macro allows fine-grained control:
#![allow(unused)]
fn main() {
// Enable only header and email redaction, skip body fields
let features = RedactionFeatures::HEADERS | RedactionFeatures::EMAILS;
// Check if headers should be redacted
if features.contains(RedactionFeatures::HEADERS) {
redact_authorization_header();
}
}
Configuration:
# Disable PII redaction in development
export REDACT_PII=false
# Customize redaction placeholder
export REDACTION_PLACEHOLDER="***"
Sensitive Headers
The platform redacts sensitive HTTP headers before logging:
Authorization: JWT tokens and API keysCookie: Session cookiesX-API-Key: Alternative API key headerX-Strava-Client-Secret: Provider OAuth secretsX-Fitbit-Client-Secret: Provider OAuth secrets
Email Masking
Email addresses are masked to prevent PII leakage:
#![allow(unused)]
fn main() {
mask_email("john.doe@example.com")
// Returns: "j***@e***.com"
}
This preserves enough information for debugging (first letter and domain) while protecting user identity.
Middleware Ordering
Middleware order matters! The platform applies middleware in this sequence:
#![allow(unused)]
fn main() {
let app = Router::new()
.route("/api/activities", get(get_activities))
// 1. CORS (must be outermost for OPTIONS preflight)
.layer(setup_cors(&config))
// 2. Request ID (early for correlation)
.layer(middleware::from_fn(request_id_middleware))
// 3. Tracing (after request ID, before auth)
.layer(TraceLayer::new_for_http())
// 4. Authentication (extract user_id)
.layer(Extension(Arc::new(auth_middleware)))
// 5. Tenant isolation (requires user_id)
.layer(Extension(Arc::new(tenant_isolation)))
// 6. Rate limiting (requires auth context)
.layer(Extension(Arc::new(rate_limiter)));
}
Ordering rules:
- CORS first: Must handle OPTIONS preflight before other middleware
- Request ID early: Needed for all subsequent logs
- Tracing after ID: Span can include request ID immediately
- Auth before tenant: Need user_id to look up tenant
- Tenant before rate limit: Rate limits may be per-tenant
- Handlers last: Process after all middleware
Rust Idiom: Tower layers are applied bottom-to-top
Axum uses Tower’s Layer trait, which applies middleware in reverse order. The outermost .layer() call wraps the innermost. Visualize as:
CORS(RequestID(Tracing(Auth(Handler))))
Security Headers
The platform adds security headers to all responses:
X-Request-ID: Request correlation IDX-Content-Type-Options: nosniff: Prevent MIME sniffingX-Frame-Options: DENY: Prevent clickjackingStrict-Transport-Security: Force HTTPS (production only)Content-Security-Policy: Restrict resource loading
Example: Adding security headers in middleware:
#![allow(unused)]
fn main() {
pub async fn security_headers_middleware(req: Request, next: Next) -> Response {
let mut response = next.run(req).await;
let headers = response.headers_mut();
headers.insert(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
);
headers.insert(
HeaderName::from_static("x-frame-options"),
HeaderValue::from_static("DENY"),
);
if is_production() {
headers.insert(
HeaderName::from_static("strict-transport-security"),
HeaderValue::from_static("max-age=31536000; includeSubDomains"),
);
}
response
}
}
Key Takeaways
-
Middleware stack: Layered architecture processes requests through CORS, request ID, tracing, auth, tenant isolation, and rate limiting before reaching handlers.
-
Request ID: Every request gets a UUID v4 for distributed tracing. Included in response headers and all log entries.
-
Request context:
RequestContextflows through the request lifecycle, accumulating user_id, tenant_id, and auth_method for structured logging. -
CORS configuration: Environment-driven origin allowlist supports development (
*) and production (specific domains). Custom headers for provider OAuth and multi-tenancy. -
Rate limit headers: Standard
X-RateLimit-*headers inform clients about usage limits.Retry-Aftertells clients when to retry 429 responses. -
PII redaction: Configurable redaction of authorization headers, email addresses, and sensitive JSON fields protects user privacy in logs.
-
Middleware ordering: CORS → Request ID → Tracing → Auth → Tenant → Rate Limit → Handler. Order matters for dependencies.
-
Span creation: Helper functions (
create_request_span,create_mcp_span,create_database_span) provide consistent tracing across the platform. -
Type-safe extensions: Axum’s extension system allows storing typed data (RequestId, RequestContext) in requests for handler access.
-
Security headers: Platform adds
X-Content-Type-Options,X-Frame-Options, andStrict-Transport-Securityto prevent common web vulnerabilities.
End of Part II: Authentication & Security
You’ve completed the authentication and security section of the Pierre platform tutorial. You now understand:
- Error handling with structured errors (Chapter 2)
- Configuration management (Chapter 3)
- Dependency injection with Arc (Chapter 4)
- Cryptographic key management (Chapter 5)
- JWT authentication with RS256 (Chapter 6)
- Multi-tenant database isolation (Chapter 7)
- Middleware and request context (Chapter 8)
Next Chapter: Chapter 09: JSON-RPC 2.0 Foundation - Begin Part III by learning how the Model Context Protocol (MCP) builds on JSON-RPC 2.0 for structured client-server communication.