Proxy Overview

Two ways to use rws as a reverse proxy — zero-code config-driven mode and programmatic library mode.

rust-web-server ships a full-featured proxy stack. You can use it in two distinct ways depending on how much control you need.

Mode 1 — Config-driven proxy (no code)

Drop a rws.config.toml file next to the binary and run rws. As soon as the file contains at least one [[route]] or [[upstream]] section, the server automatically switches into proxy mode. No Rust code is required.

# rws.config.toml
[[upstream]]
name = "api"
backends = ["localhost:3000", "localhost:3001"]

[[route]]
name = "api-proxy"

[route.match]
path = "/api/*"

[route.action]
type = "proxy"

[route.action.proxy]
upstream = "api"
rws          # proxy mode activated automatically

Best for: ops teams, sidecar containers, API gateways, and situations where you just need to route traffic without touching Rust.

Mode 2 — Library / programmatic

Add rust-web-server as a dependency and compose the proxy middleware stack in code.

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::proxy::ReverseProxy;
use rust_web_server::server::Server;

let app = App::new()
    .wrap(ReverseProxy::new(["http://backend:3000"])
        .path_prefix("/api"));

let (listener, pool) = Server::setup().unwrap();
Server::run(listener, pool, app); // async builds: Server::run_tls(listener, pool, app).await

Best for: services that need custom routing logic, per-request auth, conditional proxying, or tight integration with application code.

Side-by-side comparison

Config-driven Library
Requires Rust code No Yes
Hot-reload via SIGHUP Yes (routes + upstreams) No
Health checks Yes (built-in) Manual (BackendPool + CircuitBreaker)
L4 TCP / UDP / WS proxies Yes (config sections) Yes (standalone structs)
Path rewriting Yes (strip_path_prefix, add_path_prefix) Yes (RewriteLayer)
Per-route rate limiting Yes Yes (RateLimitLayer)
Auth Yes (bearer token from env var, JWT, htpasswd basic) Yes (JwtLayer, BasicAuthLayer)
Response caching Yes ([route.middleware.cache]) Yes (CacheLayer)
gRPC Yes (type = "grpc") Yes (GrpcProxy, requires http2 feature)
Canary / traffic splitting No (planned) Yes (CanaryLayer)
Service discovery No (planned) Yes (BackendPool)
Circuit breaker / retry No (planned) Yes (CircuitBreaker, RetryLayer)

Behind an L4 load balancer (PROXY protocol)

A TCP load balancer (HAProxy in mode tcp, AWS NLB, GCP's TCP proxy) hides the client's address: rws sees the balancer's. With the PROXY protocol the balancer prepends the client's address to each connection; turn it on in rws (since 18.3.0):

proxy_protocol = true   # RWS_CONFIG_PROXY_PROTOCOL

Every connection to the main listener must then start with a version 1 or version 2 header — before the TLS handshake on HTTPS — and the address in it is the client address: in access logs, rate limits, IP filters and X-Forwarded-For to upstreams. A connection without a valid header within 10 seconds is closed, so only enable it when every client comes through the balancer. The balancer's own health checks (v2 LOCAL, v1 UNKNOWN) keep its address. The setting is read at startup.

# HAProxy
backend rws
    mode tcp
    server rws1 10.0.0.5:443 send-proxy-v2   # or send-proxy for version 1

HTTP/3 (QUIC) has no PROXY protocol; it is unaffected.

Detection logic

At startup main() calls ProxyConfig::is_proxy_mode(), which reads rws.config.toml (or the path in RWS_CONFIG_FILE) and returns true if the text contains [[route]] or [[upstream]]. When true, build_from_file() compiles the config into a ConfigDrivenApp that replaces the default App.

Edit this page on GitHub