| name | rust-proxy |
| description | Rust reverse proxy patterns using hyper 1.x, tower middleware, tokio-rustls, and async connection handling. Use when working on proxy.rs, server.rs, middleware, or WebSocket passthrough. |
| scope | server |
Rust Proxy Patterns
Overview
This skill covers patterns for building high-performance reverse proxies in Rust using hyper 1.x, tower middleware, and the tokio async runtime. It focuses on connection handling, request forwarding, middleware composition, and WebSocket passthrough.
Core Patterns
1. Hyper 1.x Service Pattern
use hyper::service::service_fn;
use hyper::{Request, Response};
let service = service_fn(move |req| {
let handler = handler.clone();
async move { handler.handle(req).await }
});
http1::Builder::new()
.preserve_header_case(true)
.serve_connection(io, service)
.with_upgrades()
.await
Why this matters: hyper 1.x uses a different service model than 0.14. The service_fn wrapper creates a service from an async function.
2. HTTP/2 Auto-Detection
use hyper_util::server::conn::auto::Builder as AutoBuilder;
use hyper_util::rt::TokioExecutor;
AutoBuilder::new(TokioExecutor::new())
.serve_connection_with_upgrades(io, service)
.await
Why this matters: HTTP/2 requires TLS in practice. Using auto::Builder handles protocol negotiation automatically via ALPN.
3. TLS with ACME (Let's Encrypt)
use tokio_rustls_acme::{AcmeConfig, caches::DirCache};
let mut acme_config = AcmeConfig::new(&domains)
.cache(DirCache::new(cache_dir))
.directory_lets_encrypt(true);
if !email.is_empty() {
acme_config = acme_config.contact_push(format!("mailto:{}", email));
}
let mut tls_incoming = acme_config.incoming(tcp_incoming, Vec::new());
while let Some(tls_result) = tls_incoming.next().await {
let tls_stream = tls_result?;
}
Why this matters: tokio-rustls-acme handles certificate issuance and renewal automatically. The incoming() method returns ready-to-use TLS streams.
4. Request Forwarding with Timeout
use tokio::time::timeout;
use hyper::client::conn::http1::Builder as Http1Builder;
let stream = timeout(
timeout_duration,
TcpStream::connect(upstream_addr)
).await
.map_err(|_| ProxyError::UpstreamTimeout)?
.map_err(|e| ProxyError::UpstreamConnection(e.to_string()))?;
let io = TokioIo::new(stream);
let (mut sender, conn) = Http1Builder::new()
.preserve_header_case(true)
.handshake(io)
.await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
debug!("Upstream connection closed: {}", e);
}
});
let response = sender.send_request(req).await?;
Why this matters: Timeout prevents hanging on unresponsive upstreams. Spawning the connection handler allows the connection to be reused.
5. Forwarding Headers
fn add_forwarding_headers(req: &mut Request<Incoming>, client_ip: IpAddr, is_tls: bool) {
let headers = req.headers_mut();
let xff = if let Some(existing) = headers.get("x-forwarded-for") {
format!("{}, {}", existing.to_str().unwrap_or(""), client_ip)
} else {
client_ip.to_string()
};
headers.insert("x-forwarded-for", xff.parse().unwrap());
if !headers.contains_key("x-real-ip") {
headers.insert("x-real-ip", client_ip.to_string().parse().unwrap());
}
let proto = if is_tls { "https" } else { "http" };
headers.insert("x-forwarded-proto", proto.parse().unwrap());
}
Why this matters: Backend applications need to know the original client IP and protocol for logging, security, and redirect generation.
6. Hop-by-Hop Header Removal
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
];
fn strip_hop_by_hop_headers(req: &mut Request<Incoming>) {
let headers = req.headers_mut();
for header in HOP_BY_HOP {
headers.remove(*header);
}
}
Why this matters: Hop-by-hop headers are connection-specific and must not be forwarded to the upstream.
7. WebSocket Detection
pub fn is_websocket_upgrade(req: &Request<Incoming>) -> bool {
let get_header = |key: &str| {
req.headers()
.get(key)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_lowercase())
};
get_header("connection")
.map(|v| v.contains("upgrade"))
.unwrap_or(false)
&& get_header("upgrade")
.map(|v| v.contains("websocket"))
.unwrap_or(false)
}
Why this matters: WebSocket upgrades need special handling - the connection must be passed through bidirectionally.
8. Middleware Stack Pattern
pub struct MiddlewareStack {
rate_limiter: RateLimitMiddleware,
filter: FilterMiddleware,
cache: CacheMiddleware,
security: SecurityMiddleware,
}
impl MiddlewareStack {
pub async fn handle(
&self,
req: Request<Incoming>,
remote_addr: SocketAddr,
proxy: &ProxyHandler,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
if !self.rate_limiter.check(remote_addr.ip()) {
return Ok(error_response(StatusCode::TOO_MANY_REQUESTS));
}
if self.filter.is_blocked(req.uri().path()) {
return Ok(error_response(StatusCode::NOT_FOUND));
}
if let Some(cached) = self.cache.get(req.uri().path()).await {
return Ok(cached);
}
let response = proxy.handle(req).await?;
self.security.add_headers(response.headers_mut());
Ok(response)
}
}
Why this matters: Clear middleware ordering ensures consistent behavior. Rate limiting should be first to protect resources.
Key Files
| File | Purpose |
|---|
server.rs | HTTP server, TLS, connection handling |
proxy.rs | Request forwarding, headers |
middleware/mod.rs | Middleware stack composition |
middleware/rate_limit.rs | Token bucket rate limiting |
middleware/cache.rs | Response caching |
Common Pitfalls
-
Missing .with_upgrades(): WebSocket upgrades silently fail without this on HTTP/1.1 connections.
-
Not spawning connection handler: The hyper client connection must be spawned to handle keep-alive properly.
-
Forwarding hop-by-hop headers: Causes protocol errors with HTTP/2 upstreams.
-
Blocking in async context: Use tokio::time::timeout instead of std::time sleep.
-
TLS without ALPN: HTTP/2 requires ALPN negotiation; use tokio-rustls-acme or configure rustls manually.
Performance Tips
- Use
Arc<T> for shared state across connections
- Clone handlers before spawning tasks (move semantics)
- Use
bytes::Bytes for zero-copy body handling
- Prefer streaming responses over buffering
- Use
moka with TTL for caching (automatic cleanup)