lmrc-proxy

Crates.iolmrc-proxy
lib.rslmrc-proxy
version0.3.16
created_at2025-12-02 15:35:27.134365+00
updated_at2025-12-11 13:28:42.599643+00
descriptionHTTP reverse proxy and API gateway utilities for LMRC Stack applications
homepage
repositoryhttps://gitlab.com/lemarco/lmrc-stack
max_upload_size
id1962070
size78,964
Le Marc (lemarco)

documentation

README

lmrc-proxy

HTTP reverse proxy and API gateway utilities for LMRC Stack applications.

A flexible, trait-based reverse proxy library for building API gateways and microservices routers with Axum.

Features

  • HTTP Reverse Proxy: Full-featured HTTP proxying with configurable behavior
  • Subdomain Routing: Route requests based on subdomains
  • Flexible Route Resolution: Trait-based routing for custom backend discovery
  • Axum Integration: Ready-to-use middleware for Axum web framework
  • Header Management: Automatic handling of hop-by-hop headers and X-Forwarded-* headers
  • Type-Safe: Leverages Rust's type system for safe proxy operations

Installation

[dependencies]
lmrc-proxy = "0.3.11"

Quick Start

Basic Reverse Proxy

use lmrc_proxy::{proxy_request, ProxyConfig};
use axum::{Router, routing::any, extract::Request};

async fn proxy_handler(request: Request) -> Result<axum::response::Response, lmrc_proxy::ProxyError> {
    proxy_request(request, "http://backend:8080", ProxyConfig::default()).await
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/*path", any(proxy_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Subdomain-Based Gateway

use lmrc_proxy::{
    routing::StaticRouteResolver,
    middleware::{subdomain_extractor, subdomain_proxy},
    ProxyConfig,
};
use axum::{Router, middleware};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    // Configure routes
    let resolver = Arc::new(
        StaticRouteResolver::new()
            .add_route("api", "http://api-service:8080")
            .add_route("admin", "http://admin-service:9000")
            .add_route("auth", "http://auth-service:8081")
    );

    // Build router with subdomain routing
    let app = Router::new()
        .layer(middleware::from_fn(subdomain_extractor))
        .layer(middleware::from_fn_with_state(
            (resolver, ProxyConfig::default()),
            subdomain_proxy
        ));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Requests to api.example.com route to http://api-service:8080, requests to admin.example.com route to http://admin-service:9000, etc.

Core Concepts

ProxyConfig

Configure proxy behavior:

use lmrc_proxy::ProxyConfig;
use std::time::Duration;

let config = ProxyConfig {
    preserve_host: false,              // Forward original Host header
    timeout: Duration::from_secs(30),  // Request timeout
    max_body_size: 10 * 1024 * 1024,   // 10MB max body
    add_forwarded_headers: true,       // Add X-Forwarded-* headers
};

RouteResolver Trait

Implement custom routing logic:

use lmrc_proxy::routing::RouteResolver;
use async_trait::async_trait;

struct DatabaseRouteResolver {
    // Database connection
}

#[async_trait]
impl RouteResolver for DatabaseRouteResolver {
    async fn resolve(&self, subdomain: &str) -> Option<String> {
        // Query database for backend URL
        self.db.get_backend_url(subdomain).await
    }
}

Subdomain Extraction

Extract subdomains from requests:

use lmrc_proxy::routing::extract_subdomain;

assert_eq!(extract_subdomain("api.example.com"), Some("api".to_string()));
assert_eq!(extract_subdomain("example.com"), None);
assert_eq!(extract_subdomain("localhost"), Some("infra".to_string())); // Development

Middleware

subdomain_extractor

Extracts subdomain from Host header and stores it in request extensions.

use axum::{Router, middleware};
use lmrc_proxy::middleware::subdomain_extractor;

let app = Router::new()
    .layer(middleware::from_fn(subdomain_extractor));

subdomain_proxy

Routes requests to backends based on subdomain.

use lmrc_proxy::{middleware::subdomain_proxy, routing::StaticRouteResolver, ProxyConfig};
use axum::{Router, middleware};
use std::sync::Arc;

let resolver = Arc::new(StaticRouteResolver::new()
    .add_route("api", "http://api-backend:8080"));

let app = Router::new()
    .layer(middleware::from_fn_with_state(
        (resolver, ProxyConfig::default()),
        subdomain_proxy
    ));

Advanced Usage

Custom Route Resolver with Caching

use lmrc_proxy::routing::RouteResolver;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::RwLock;

struct CachedRouteResolver {
    cache: RwLock<HashMap<String, String>>,
    backend: Box<dyn RouteResolver>,
}

#[async_trait]
impl RouteResolver for CachedRouteResolver {
    async fn resolve(&self, key: &str) -> Option<String> {
        // Check cache first
        if let Some(url) = self.cache.read().unwrap().get(key) {
            return Some(url.clone());
        }

        // Query backend
        if let Some(url) = self.backend.resolve(key).await {
            self.cache.write().unwrap().insert(key.to_string(), url.clone());
            Some(url)
        } else {
            None
        }
    }
}

Dynamic Backend Discovery

use lmrc_proxy::routing::RouteResolver;
use async_trait::async_trait;

struct ServiceDiscoveryResolver {
    consul_client: ConsulClient,
}

#[async_trait]
impl RouteResolver for ServiceDiscoveryResolver {
    async fn resolve(&self, service_name: &str) -> Option<String> {
        // Query service discovery (Consul, etcd, etc.)
        self.consul_client
            .get_service_address(service_name)
            .await
            .ok()
    }
}

Configuration Examples

Development Setup

use lmrc_proxy::{routing::StaticRouteResolver, ProxyConfig};

let resolver = StaticRouteResolver::new()
    .add_route("api", "http://localhost:8081")
    .add_route("admin", "http://localhost:8082");

Production Setup

use std::time::Duration;
use lmrc_proxy::ProxyConfig;

let config = ProxyConfig {
    preserve_host: false,
    timeout: Duration::from_secs(60),
    max_body_size: 50 * 1024 * 1024, // 50MB
    add_forwarded_headers: true,
};

Testing

cargo test -p lmrc-proxy

Integration with lmrc-http-common

Works seamlessly with lmrc-http-common for error handling, logging, and HTTP utilities.

use lmrc_http_common::middleware::{add_request_id, log_request};
use lmrc_proxy::middleware::{subdomain_extractor, subdomain_proxy};
use axum::{Router, middleware};

let app = Router::new()
    .layer(middleware::from_fn(log_request))
    .layer(middleware::from_fn(add_request_id))
    .layer(middleware::from_fn(subdomain_extractor))
    .layer(middleware::from_fn_with_state(state, subdomain_proxy));

Error Handling

All errors implement IntoResponse for seamless Axum integration:

  • ProxyError::ClientCreation - Failed to create HTTP client
  • ProxyError::RequestBody - Failed to read request body
  • ProxyError::BackendRequest - Backend request failed (returns 502 Bad Gateway)
  • ProxyError::ResponseBody - Failed to read response body
  • ProxyError::RouteNotFound - No route found (returns 404 Not Found)

Performance Considerations

  1. Connection Pooling: Uses reqwest with automatic connection pooling
  2. Body Streaming: Efficiently handles large request/response bodies
  3. Header Filtering: Automatically filters hop-by-hop headers
  4. Timeout Management: Configurable timeouts prevent hung connections

Security

  • Header Sanitization: Hop-by-hop headers are automatically filtered
  • X-Forwarded Headers: Properly sets X-Forwarded-Host for backend awareness
  • Body Size Limits: Configurable max body size prevents memory exhaustion
  • Timeout Protection: Request timeouts prevent resource exhaustion

Examples

See the gateway app for a complete example of an API gateway using lmrc-proxy.

Contributing

This library is part of the LMRC Stack monorepo.

License

Dual licensed under MIT OR Apache-2.0 (your choice).

Commit count: 0

cargo fmt