Skip to content

feat: Add HttpMiddleware trait for transport-level HTTP concerns #82

@guyernest

Description

@guyernest

Problem

The current middleware system operates at the Protocol/JSON-RPC layer, which misses HTTP-specific concerns like:

  • Header injection (auth tokens, correlation IDs, custom headers)
  • Request/response logging with header inclusion
  • Status code-based conditionals (retry on 429, circuit break on 503)
  • HTTP-specific transformations before reaching Protocol layer

The TypeScript SDK provides fetch-wrapping middleware for these concerns. We need equivalent ergonomics.

Proposed Solution

Add a transport-specific HttpMiddleware trait that operates inside HttpTransport and StreamableHttpTransport before sending to hyper/reqwest:

#[async_trait]
pub trait HttpMiddleware: Send + Sync {
    /// Called before HTTP request is sent
    async fn on_request(
        &self,
        request: &mut http::Request<Vec<u8>>,
        context: &HttpMiddlewareContext,
    ) -> Result<()>;

    /// Called after HTTP response is received
    async fn on_response(
        &self,
        response: &mut http::Response<Vec<u8>>,
        context: &HttpMiddlewareContext,
    ) -> Result<()>;

    /// Priority for ordering (lower runs first)
    fn priority(&self) -> MiddlewarePriority {
        MiddlewarePriority::Normal
    }

    /// Should this middleware execute for this request?
    async fn should_execute(&self, context: &HttpMiddlewareContext) -> bool {
        true
    }
}

pub struct HttpMiddlewareContext {
    pub url: Url,
    pub method: http::Method,
    pub attempt: u32,
    pub metadata: Arc<RwLock<HashMap<String, String>>>,
}

Integration points:

  • HttpTransport::send() - Apply middleware chain before/after reqwest call
  • StreamableHttpTransport - Apply before POST requests
  • HttpTransportConfig::middleware_chain - Configure middleware on transport creation

Use Cases

1. Header Injection

struct CustomHeaderMiddleware {
    headers: HashMap<String, String>,
}

#[async_trait]
impl HttpMiddleware for CustomHeaderMiddleware {
    async fn on_request(
        &self,
        request: &mut http::Request<Vec<u8>>,
        _context: &HttpMiddlewareContext,
    ) -> Result<()> {
        for (key, value) in &self.headers {
            request.headers_mut().insert(
                HeaderName::from_str(key)?,
                HeaderValue::from_str(value)?
            );
        }
        Ok(())
    }
}

// Usage
let transport = HttpTransport::builder()
    .url(server_url)
    .middleware(CustomHeaderMiddleware::new([
        ("X-API-Key", api_key),
        ("X-Request-ID", uuid),
    ]))
    .build()?;

2. Request/Response Logging with Headers

struct HttpLoggingMiddleware {
    include_headers: bool,
    log_body: bool,
}

#[async_trait]
impl HttpMiddleware for HttpLoggingMiddleware {
    async fn on_request(&self, req: &mut http::Request<Vec<u8>>, ctx: &HttpMiddlewareContext) -> Result<()> {
        if self.include_headers {
            tracing::info!(
                method = %ctx.method,
                url = %ctx.url,
                headers = ?req.headers(),
                "HTTP request"
            );
        }
        Ok(())
    }

    async fn on_response(&self, res: &mut http::Response<Vec<u8>>, ctx: &HttpMiddlewareContext) -> Result<()> {
        tracing::info!(
            status = %res.status(),
            headers = ?res.headers(),
            "HTTP response"
        );
        Ok(())
    }
}

3. Status Code-Based Actions

struct StatusCodeMiddleware;

#[async_trait]
impl HttpMiddleware for StatusCodeMiddleware {
    async fn on_response(&self, res: &mut http::Response<Vec<u8>>, ctx: &HttpMiddlewareContext) -> Result<()> {
        match res.status() {
            StatusCode::TOO_MANY_REQUESTS => {
                if let Some(retry_after) = res.headers().get("Retry-After") {
                    ctx.set_metadata("retry_after", retry_after.to_str()?);
                }
                Err(Error::RateLimited("Rate limit exceeded".into()))
            }
            StatusCode::SERVICE_UNAVAILABLE => {
                Err(Error::Transport("Service unavailable".into()))
            }
            _ => Ok(())
        }
    }
}

Implementation Plan

  1. Phase 1: Core trait and context

    • Define HttpMiddleware trait
    • Add HttpMiddlewareContext with url, method, attempt
    • Add HttpMiddlewareChain for ordering
  2. Phase 2: Integration with transports

    • Update HttpTransport::send() to apply middleware
    • Update StreamableHttpTransport request handling
    • Add middleware() builder method to configs
  3. Phase 3: Built-in middleware

    • HeaderInjectionMiddleware - Add custom headers
    • HttpLoggingMiddleware - Request/response logging with headers
    • CorrelationIdMiddleware - Inject/propagate correlation IDs
    • StatusCodeHandlerMiddleware - Handle specific status codes
  4. Phase 4: Documentation and examples

    • Add chapter to pmcp-book on HTTP middleware
    • Example: examples/30_http_middleware.rs
    • Update transport documentation

Benefits

  • Separation of concerns: HTTP-level vs Protocol-level middleware
  • TypeScript parity: Matches TS SDK's fetch-wrapping ergonomics
  • Composability: Chain multiple HTTP concerns
  • Type safety: Rust's http crate types prevent header/status errors
  • Foundation: Enables OAuth middleware (Issue feat: Add HttpMiddleware trait for transport-level HTTP concerns #82)

References

Related Issues


Priority: High
Complexity: Medium
Dependencies: None (builds on existing middleware foundation)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions