Skip to content

Middleware Guide

Quill provides a comprehensive suite of middleware for production-ready RPC services. This guide covers authentication, rate limiting, logging, metrics, and more.

Table of Contents

Authentication

Quill supports multiple authentication schemes: Bearer tokens, API keys, and Basic auth.

API Key Authentication

use quill_server::middleware::{ApiKeyValidator, AuthLayer, AuthScheme, AuthResult};
use std::sync::Arc;

// Create an API key validator
let mut validator = ApiKeyValidator::new();
validator.add_key("key123".to_string(), "user1".to_string());
validator.add_key("key456".to_string(), "service1".to_string());

// Create auth layer
let auth = AuthLayer::new(
    AuthScheme::ApiKey {
        header_name: "X-API-Key".to_string(),
    },
    Arc::new(validator),
);

// Use in request handling
let result = auth.authenticate(&request);
match result {
    AuthResult::Authenticated(identity) => {
        println!("Authenticated as: {}", identity);
        // Process request
    }
    AuthResult::Failed(msg) => {
        // Return 401 Unauthorized
        eprintln!("Auth failed: {}", msg);
    }
    AuthResult::None => {
        // No auth provided (if optional)
    }
}

Bearer Token Authentication

use quill_server::middleware::{AuthLayer, AuthScheme, AuthValidator};

// Implement custom validator (e.g., JWT validation)
struct JwtValidator {
    secret: String,
}

impl AuthValidator for JwtValidator {
    fn validate(&self, token: &str) -> Result<String, String> {
        // Decode and validate JWT
        // Return user ID/email on success
        Ok("user@example.com".to_string())
    }
}

// Create auth layer
let auth = AuthLayer::new(
    AuthScheme::Bearer,
    Arc::new(JwtValidator {
        secret: "your-secret".to_string(),
    }),
);

Optional Authentication

Make authentication optional for public endpoints:

let auth = AuthLayer::new(scheme, validator).optional();

match auth.authenticate(&request) {
    AuthResult::Authenticated(identity) => {
        // Process as authenticated request
    }
    AuthResult::None => {
        // Process as anonymous request
    }
    AuthResult::Failed(_) => {
        // Return 401 even for optional auth if credentials were provided but invalid
    }
}

Custom Authentication Schemes

use quill_server::middleware::{AuthValidator, AuthScheme};

// Implement your own validator
struct CustomValidator;

impl AuthValidator for CustomValidator {
    fn validate(&self, token: &str) -> Result<String, String> {
        // Custom validation logic
        if token.starts_with("custom-") {
            Ok(token[7..].to_string())
        } else {
            Err("Invalid custom token".to_string())
        }
    }
}

let auth = AuthLayer::new(
    AuthScheme::Custom("MyScheme".to_string()),
    Arc::new(CustomValidator),
);

Rate Limiting

Prevent abuse with token bucket rate limiting.

Basic Rate Limiting

use quill_server::middleware::RateLimitLayer;

// 100 requests per second with burst of 200
let rate_limiter = RateLimitLayer::new(100.0, 200.0);

// Check before processing request
if !rate_limiter.check_rate_limit() {
    // Return 429 Too Many Requests
    return Err(QuillError::Rpc("Rate limit exceeded".to_string()));
}

// Process request...

Custom Rate Limiter

use quill_server::middleware::RateLimiter;
use std::sync::Arc;

// Create custom rate limiter
let limiter = Arc::new(RateLimiter::new(
    50.0,  // capacity (burst size)
    10.0,  // refill rate (tokens per second)
));

// Use in handler
if !limiter.try_acquire() {
    // Rate limited
}

// Can also acquire multiple tokens
if !limiter.try_acquire_n(5.0) {
    // Not enough tokens
}

// Check available tokens
let available = limiter.available();

Per-User Rate Limiting

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use quill_server::middleware::RateLimiter;

struct PerUserRateLimiter {
    limiters: Arc<Mutex<HashMap<String, Arc<RateLimiter>>>>,
    default_rate: f64,
    default_burst: f64,
}

impl PerUserRateLimiter {
    pub fn new(rate: f64, burst: f64) -> Self {
        Self {
            limiters: Arc::new(Mutex::new(HashMap::new())),
            default_rate: rate,
            default_burst: burst,
        }
    }

    pub fn check(&self, user_id: &str) -> bool {
        let mut limiters = self.limiters.lock().unwrap();
        let limiter = limiters.entry(user_id.to_string()).or_insert_with(|| {
            Arc::new(RateLimiter::new(self.default_burst, self.default_rate))
        });
        limiter.try_acquire()
    }
}

Request Logging

Log incoming requests and responses with automatic header sanitization.

Basic Logging

use quill_server::middleware::RequestLogger;

let logger = RequestLogger::new();

// Log incoming request
logger.log_request(&request);

// Process request...
let start = std::time::Instant::now();

// Log response
logger.log_response(http::StatusCode::OK, start.elapsed());

Output:

INFO Incoming request method=POST uri=/service/Method version=HTTP/1.1
DEBUG header=content-type value=application/proto
DEBUG header=authorization value=[REDACTED]
INFO Response sent status=200 OK duration_ms=42

Disable Logging

let logger = RequestLogger::disabled();

Structured Logging

The request logger uses tracing for structured logs:

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

// Initialize tracing
tracing_subscriber::registry()
    .with(tracing_subscriber::fmt::layer().json())
    .init();

// All request logs will now be in JSON format

Metrics Collection

Collect request/response metrics with atomic counters.

Basic Metrics

use quill_server::middleware::MetricsCollector;
use std::sync::Arc;

let metrics = Arc::new(MetricsCollector::new());

// In request handler
let metrics_clone = metrics.clone();

// Record request
metrics_clone.record_request();
metrics_clone.record_bytes_received(request.len() as u64);

// Process request...
match process_request().await {
    Ok(response) => {
        metrics_clone.record_success();
        metrics_clone.record_bytes_sent(response.len() as u64);
    }
    Err(_) => {
        metrics_clone.record_failure();
    }
}

// Get metrics snapshot
let snapshot = metrics.get_metrics();
println!("Total requests: {}", snapshot.requests_total);
println!("Success rate: {:.2}%", snapshot.success_rate() * 100.0);
println!("Error rate: {:.2}%", snapshot.error_rate() * 100.0);
println!("Bytes sent: {}", snapshot.bytes_sent);
println!("Bytes received: {}", snapshot.bytes_received);

Expose Metrics Endpoint

use quill_server::{QuillServer, ServerBuilder};
use quill_server::middleware::MetricsCollector;

let metrics = Arc::new(MetricsCollector::new());

let server = QuillServer::builder()
    .register("metrics/Get", move |_| {
        let metrics = metrics.clone();
        async move {
            let snapshot = metrics.get_metrics();
            let json = serde_json::json!({
                "requests_total": snapshot.requests_total,
                "requests_success": snapshot.requests_success,
                "requests_failed": snapshot.requests_failed,
                "success_rate": snapshot.success_rate(),
                "error_rate": snapshot.error_rate(),
                "bytes_sent": snapshot.bytes_sent,
                "bytes_received": snapshot.bytes_received,
            });
            Ok(Bytes::from(json.to_string()))
        }
    })
    .build();

Prometheus Integration

For Prometheus metrics, use the prometheus crate:

use prometheus::{Counter, Encoder, Histogram, Registry, TextEncoder};

lazy_static! {
    static ref REGISTRY: Registry = Registry::new();

    static ref REQUESTS_TOTAL: Counter = Counter::new(
        "quill_requests_total",
        "Total number of RPC requests"
    ).unwrap();

    static ref REQUEST_DURATION: Histogram = Histogram::new(
        "quill_request_duration_seconds",
        "Request duration in seconds"
    ).unwrap();
}

// Register metrics
REGISTRY.register(Box::new(REQUESTS_TOTAL.clone())).unwrap();
REGISTRY.register(Box::new(REQUEST_DURATION.clone())).unwrap();

// In handler
REQUESTS_TOTAL.inc();
let timer = REQUEST_DURATION.start_timer();
// ... process request ...
timer.observe_duration();

// Expose metrics endpoint
let encoder = TextEncoder::new();
let metric_families = REGISTRY.gather();
let mut buffer = vec![];
encoder.encode(&metric_families, &mut buffer).unwrap();

Compression

See Compression Guide for detailed compression documentation.

use quill_server::middleware::{compress_zstd, decompress_zstd};

// Compress response
let compressed = compress_zstd(&data, 3)?;

// Decompress request
let decompressed = decompress_zstd(&compressed_data)?;

Tracing

See Tracing Guide for detailed OpenTelemetry documentation.

use quill_server::middleware::create_rpc_span;
use tracing::instrument;

#[instrument]
async fn handle_request() {
    let span = create_rpc_span("service.v1.Service", "Method");
    let _enter = span.enter();

    // Request processing happens within span
}

Complete Example

Combining all middleware:

use quill_server::{QuillServer, ServerBuilder};
use quill_server::middleware::*;
use std::sync::Arc;
use std::time::Instant;

#[tokio::main]
async fn main() {
    // Initialize tracing
    tracing_subscriber::fmt::init();

    // Setup middleware
    let validator = Arc::new(ApiKeyValidator::new()
        .with_key("key123".to_string(), "user1".to_string()));

    let auth = Arc::new(AuthLayer::new(
        AuthScheme::ApiKey {
            header_name: "X-API-Key".to_string(),
        },
        validator,
    ));

    let rate_limiter = Arc::new(RateLimitLayer::new(100.0, 200.0));
    let logger = Arc::new(RequestLogger::new());
    let metrics = Arc::new(MetricsCollector::new());

    // Build server with middleware
    let server = QuillServer::builder()
        .register("service/Method", move |request| {
            let auth = auth.clone();
            let rate_limiter = rate_limiter.clone();
            let logger = logger.clone();
            let metrics = metrics.clone();

            async move {
                let start = Instant::now();

                // Record request
                metrics.record_request();

                // Check rate limit
                if !rate_limiter.check_rate_limit() {
                    metrics.record_failure();
                    return Err(QuillError::Rpc("Rate limit exceeded".to_string()));
                }

                // Authenticate
                // (In real code, you'd extract request to get headers)
                // match auth.authenticate(&http_request) { ... }

                // Process request
                let response = process(request).await?;

                // Record success
                metrics.record_success();
                logger.log_response(http::StatusCode::OK, start.elapsed());

                Ok(response)
            }
        })
        .build();

    server.serve("127.0.0.1:8080".parse().unwrap()).await.unwrap();
}

Best Practices

  1. Layer Order: Apply middleware in this order:
  2. Logging (first, to log everything)
  3. Metrics (early, to track all requests)
  4. Rate limiting (before auth, to prevent auth brute force)
  5. Authentication
  6. Compression
  7. Tracing (spans around business logic)

  8. Security:

  9. Never log sensitive data (tokens, passwords)
  10. Use HTTPS/TLS in production
  11. Validate all tokens properly
  12. Set appropriate rate limits

  13. Performance:

  14. Use Arc for shared middleware
  15. Compression only for large payloads (>1KB)
  16. Sample traces in high-traffic services
  17. Aggregate metrics periodically

  18. Error Handling:

  19. Return appropriate HTTP status codes
  20. Log errors with context
  21. Track error rates in metrics
  22. Include trace IDs in errors

See Also