Skip to content

std::http::proxy

Status: experimental

Reverse proxy on top of http::Client. Director-style request mutator + hop-by-hop strip + error handler.

API details and source

The implementation source contains the complete declarations and implementation notes. The table below lists canonical Gossamer call signatures; every item name links directly to its implementation file.

Item Canonical signature or declaration Description
Director type Director Fn(&mut Request) request mutator (Rust-side).
Proxy type Proxy Reverse-proxy handler (Rust-side).
forward fn forward(url: String, method: String, body: Vec<u8>) -> Result<http::Response, errors::Error> One-shot upstream forward: (url, method, body) -> Result<Response, Error>. Interp tier.

Writing a passthrough proxy in Gossamer

The whole proxy shape is expressible directly on the std::http surface: forward the inbound raw_body and headers upstream, relay the upstream response back. Two modes cover the spectrum - buffered (whole response in memory, simplest) and streamed (chunked passthrough, no buffering).

Hop-by-hop and engine-managed headers must not be relayed; strip them and let the client/server engines manage their own:

// Drop hop-by-hop / engine-managed names; keep everything else.
fn forwardable_headers(headers: &[(String, String)]) -> [(String, String)] {
    let mut out: [(String, String)] = []
    for (name, value) in headers {
        let n = name.to_lowercase()
        let restricted = n == "connection" || n == "content-length" ||
            n == "host" || n == "upgrade" || n == "accept-encoding"
        if !restricted {
            out.push((name, value))
        }
    }
    out
}

Buffered forward

Byte-exact in both directions: the inbound body rides request_bytes as [u8], the upstream body comes back on raw_bytes, and a struct-literal Response relays status, bytes, and content type:

fn forward_buffered(method: String, target: String, body: [u8],
                    headers: [(String, String)]) -> http::Response {
    let client = http::Client::builder().max_redirects(0).build()
    match client.request_bytes(method, target, body, headers) {
        Ok(up) => http::Response {
            status: up.status,
            body: up.raw_bytes,
            content_type: up.content_type,
            headers: [],
        },
        Err(e) => http::Response::text(502, format!("upstream error: {}", e)),
    }
}

max_redirects(0) keeps the proxy transparent: a 3xx from upstream is relayed to the caller (with its location header intact) instead of being chased by the proxy.

Streamed forward

http::stream + Response::stream connect the upstream body straight to the downstream socket as chunked frames - no buffering, first bytes flow immediately:

fn forward_stream(method: String, target: String, body: String,
                  headers: [(String, String)]) -> http::Response {
    match http::stream(&method, &target, &body, headers) {
        Ok(up) => http::Response::stream(up.status, up.content_type, up),
        Err(e) => http::Response::text(502, format!("upstream error: {}", e)),
    }
}

In stream mode, ask the upstream for an identity body (("Accept-Encoding", "identity")) - the proxy relays bytes it does not decode.

The handler

struct App { target_base: String }

impl http::Handler for App {
    fn serve(&self, r: http::Request) -> http::Response {
        let suffix = if r.query.len() == 0 { "" } else { "?" + &r.query }
        let target = format!("{}{}{}", self.target_base, r.path, suffix)
        let headers = forwardable_headers(&r.headers)
        forward_buffered(r.method, target, r.raw_body, headers)
    }
}

fn main() {
    if let Err(e) = http::serve("127.0.0.1:8080", App { target_base: "http://localhost:3000" }) {
        eprintln!("{}", e)
    }
}

A complete worked proxy - path-prefix routing, gzip/brotli decode in buffered mode, streaming flag, CLI parsing - ships as the locurlfwd port; the same pattern at production size.