Table of contents

How do I implement custom middleware for HTTP requests in Rust?

Custom middleware in Rust allows you to intercept and modify HTTP requests and responses before they reach your application handlers. This is essential for implementing cross-cutting concerns like authentication, logging, rate limiting, and request validation in web applications and scraping tools.

Understanding Middleware in Rust

Middleware in Rust follows a layered architecture where each middleware component can process requests before passing them to the next layer and modify responses on their way back. Popular Rust web frameworks like Axum, Actix-web, and Warp each have their own middleware patterns.

Implementing Middleware with Axum

Axum provides a clean and type-safe approach to middleware implementation using tower's Service and Layer traits.

Basic Logging Middleware

Here's a simple logging middleware that tracks request duration:

use axum::{
    extract::Request,
    http::StatusCode,
    middleware::Next,
    response::Response,
    Router,
};
use std::time::Instant;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;

// Middleware function
async fn logging_middleware(
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let start = Instant::now();
    let method = request.method().clone();
    let uri = request.uri().clone();

    // Call the next middleware/handler
    let response = next.run(request).await;

    let duration = start.elapsed();
    println!(
        "{} {} - {} - {:?}",
        method,
        uri,
        response.status(),
        duration
    );

    Ok(response)
}

// Apply middleware to router
let app = Router::new()
    .route("/", get(handler))
    .layer(axum::middleware::from_fn(logging_middleware));

Authentication Middleware

Here's a more complex middleware for JWT authentication:

use axum::{
    extract::{Request, State},
    http::{HeaderMap, StatusCode},
    middleware::Next,
    response::Response,
    Json,
};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

#[derive(Clone)]
struct AppState {
    jwt_secret: String,
}

async fn auth_middleware(
    State(state): State<AppState>,
    headers: HeaderMap,
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // Extract Authorization header
    let auth_header = headers
        .get("Authorization")
        .and_then(|header| header.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    // Parse Bearer token
    let token = auth_header
        .strip_prefix("Bearer ")
        .ok_or(StatusCode::UNAUTHORIZED)?;

    // Validate JWT token
    let validation = Validation::default();
    let key = DecodingKey::from_secret(state.jwt_secret.as_bytes());

    let token_data = decode::<Claims>(token, &key, &validation)
        .map_err(|_| StatusCode::UNAUTHORIZED)?;

    // Add user info to request extensions
    request.extensions_mut().insert(token_data.claims);

    Ok(next.run(request).await)
}

// Usage
let app = Router::new()
    .route("/protected", get(protected_handler))
    .layer(axum::middleware::from_fn_with_state(
        AppState { jwt_secret: "secret".to_string() },
        auth_middleware
    ));

Implementing Middleware with Actix-web

Actix-web uses a different approach with the Transform and Service traits:

use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    Error, HttpMessage,
};
use futures_util::future::LocalBoxFuture;
use std::{
    future::{ready, Ready},
    rc::Rc,
    time::Instant,
};

// Middleware factory
pub struct Logging;

impl<S, B> Transform<S, ServiceRequest> for Logging
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = LoggingMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(LoggingMiddleware {
            service: Rc::new(service),
        }))
    }
}

// Middleware service
pub struct LoggingMiddleware<S> {
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for LoggingMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let start = Instant::now();
        let method = req.method().clone();
        let path = req.path().to_string();
        let service = self.service.clone();

        Box::pin(async move {
            let res = service.call(req).await?;
            let duration = start.elapsed();

            println!(
                "{} {} - {} - {:?}",
                method,
                path,
                res.status(),
                duration
            );

            Ok(res)
        })
    }
}

// Usage in main.rs
use actix_web::{web, App, HttpServer, middleware::Logger};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(Logging)
            .route("/", web::get().to(handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Rate Limiting Middleware

Here's a rate limiting middleware using a token bucket algorithm:

use axum::{
    extract::{ConnectInfo, Request},
    http::StatusCode,
    middleware::Next,
    response::Response,
};
use std::{
    collections::HashMap,
    net::SocketAddr,
    sync::Arc,
    time::{Duration, Instant},
};
use tokio::sync::Mutex;

#[derive(Clone)]
struct TokenBucket {
    tokens: f64,
    last_refill: Instant,
    capacity: f64,
    refill_rate: f64, // tokens per second
}

impl TokenBucket {
    fn new(capacity: f64, refill_rate: f64) -> Self {
        Self {
            tokens: capacity,
            last_refill: Instant::now(),
            capacity,
            refill_rate,
        }
    }

    fn try_consume(&mut self, tokens: f64) -> bool {
        self.refill();
        if self.tokens >= tokens {
            self.tokens -= tokens;
            true
        } else {
            false
        }
    }

    fn refill(&mut self) {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
        let tokens_to_add = elapsed * self.refill_rate;

        self.tokens = (self.tokens + tokens_to_add).min(self.capacity);
        self.last_refill = now;
    }
}

type RateLimiter = Arc<Mutex<HashMap<String, TokenBucket>>>;

async fn rate_limit_middleware(
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let limiter: RateLimiter = Arc::new(Mutex::new(HashMap::new()));
    let client_ip = addr.ip().to_string();

    let mut buckets = limiter.lock().await;
    let bucket = buckets
        .entry(client_ip)
        .or_insert_with(|| TokenBucket::new(10.0, 1.0)); // 10 requests, 1 per second

    if bucket.try_consume(1.0) {
        drop(buckets);
        Ok(next.run(request).await)
    } else {
        Err(StatusCode::TOO_MANY_REQUESTS)
    }
}

Request Modification Middleware

Middleware can also modify requests before they reach handlers:

use axum::{
    body::Body,
    extract::Request,
    http::{HeaderMap, HeaderName, HeaderValue},
    middleware::Next,
    response::Response,
};

async fn cors_middleware(
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // Add custom headers to request
    request.headers_mut().insert(
        HeaderName::from_static("x-processed-by"),
        HeaderValue::from_static("rust-middleware"),
    );

    let mut response = next.run(request).await;

    // Add CORS headers to response
    let headers = response.headers_mut();
    headers.insert(
        HeaderName::from_static("access-control-allow-origin"),
        HeaderValue::from_static("*"),
    );
    headers.insert(
        HeaderName::from_static("access-control-allow-methods"),
        HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"),
    );

    Ok(response)
}

Error Handling Middleware

Robust error handling is crucial for production applications:

use axum::{
    extract::Request,
    http::StatusCode,
    middleware::Next,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

async fn error_handler_middleware(
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    match next.run(request).await {
        Ok(response) => Ok(response),
        Err(_) => {
            let error_response = Json(json!({
                "error": "Internal server error",
                "code": 500
            }));

            Ok((StatusCode::INTERNAL_SERVER_ERROR, error_response).into_response())
        }
    }
}

Chaining Multiple Middlewares

You can combine multiple middlewares using tower's ServiceBuilder:

use tower::ServiceBuilder;
use tower_http::{
    compression::CompressionLayer,
    timeout::TimeoutLayer,
};

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
            .layer(CompressionLayer::new())
            .layer(axum::middleware::from_fn(logging_middleware))
            .layer(axum::middleware::from_fn(cors_middleware))
    );

Testing Middleware

Testing middleware is important for ensuring reliability:

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        http::{Request, StatusCode},
    };
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_logging_middleware() {
        let app = Router::new()
            .route("/test", get(|| async { "OK" }))
            .layer(axum::middleware::from_fn(logging_middleware));

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/test")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }
}

Performance Considerations

When implementing middleware, consider these performance aspects:

  1. Minimize allocations: Reuse buffers and avoid unnecessary string allocations
  2. Use async/await efficiently: Don't block the async runtime
  3. Cache expensive operations: Store computed values when possible
  4. Profile your middleware: Use tools like cargo flamegraph to identify bottlenecks

Common Use Cases

Custom middleware is particularly useful for:

  • Web scraping tools: Adding retry logic and request throttling
  • API gateways: Implementing authentication and rate limiting
  • Monitoring systems: Collecting metrics and distributed tracing
  • Security applications: Validating requests and sanitizing responses

When building web scraping applications, middleware can help you handle authentication in browser automation tools by managing session tokens and cookies across requests.

For complex applications that need to monitor network requests, middleware provides a centralized location to implement logging and analytics without modifying individual handlers.

Conclusion

Custom middleware in Rust provides a powerful way to implement cross-cutting concerns in your HTTP applications. Whether you're using Axum, Actix-web, or another framework, the patterns shown here will help you build robust, maintainable middleware that enhances your application's functionality while keeping your code organized and testable.

The key to effective middleware design is keeping each middleware focused on a single responsibility and ensuring they compose well together. With Rust's type system and async capabilities, you can build high-performance middleware that scales to handle production workloads.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon