Skip to main content

rws_proxy/proxy/
mod.rs

1//! Reverse proxy middleware with round-robin load balancing.
2//!
3//! `ReverseProxy` implements [`Middleware`] — wrap any application with it and
4//! all matching requests are forwarded to one of the configured backends over
5//! plain HTTP/1.1.  Failed backends are skipped and the next one is tried
6//! before returning `502 Bad Gateway`.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! # extern crate rws_proxy as rust_web_server;
12//! use rust_web_server::app::App;
13//! use rust_web_server::core::New;
14//! use rust_web_server::proxy::{LoadBalancing, ReverseProxy};
15//!
16//! // Proxy every request across two backends in round-robin order.
17//! let app = App::new()
18//!     .wrap(ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
19//!         .strategy(LoadBalancing::RoundRobin));
20//!
21//! // Only proxy /api/* requests; everything else is handled locally.
22//! let app2 = App::new()
23//!     .wrap(ReverseProxy::new(["http://api-service:3000"])
24//!         .path_prefix("/api"));
25//! ```
26
27pub mod pool;
28pub mod status;
29#[cfg(feature = "http2")]
30#[doc(hidden)]
31pub mod async_upstream;
32#[cfg(not(target_arch = "wasm32"))]
33pub mod mirror;
34/// Upgraded connections (WebSocket) relayed to a backend (moved from the
35/// Gateway API controller in 18.21.0; config proxy routes use it too).
36#[cfg(not(target_arch = "wasm32"))]
37#[doc(hidden)]
38pub mod upgrade;
39#[cfg(feature = "http2")]
40#[doc(hidden)]
41pub mod h2_upstream;
42#[cfg(feature = "http3")]
43#[doc(hidden)]
44pub mod h3_upstream;
45#[cfg(feature = "http2")]
46pub mod grpc_web;
47#[cfg(feature = "http2")]
48pub use grpc_web::GrpcWebLayer;
49
50#[cfg(test)]
51mod tests;
52
53use std::io::{Read, Write};
54use crate::ext::decimal::Decimal;
55use std::net::{TcpStream, ToSocketAddrs};
56use std::sync::atomic::{AtomicUsize, Ordering};
57use std::sync::Arc;
58use std::time::Duration;
59
60pub use pool::{ConnPool, Pool};
61
62use crate::application::Application;
63use crate::middleware::Middleware;
64use crate::request::Request;
65use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
66use crate::server::ConnectionInfo;
67
68// Hop-by-hop headers that must not be forwarded (RFC 9110 §7.6.1), plus
69// `proxy-connection` (legacy clients). Headers named in `Connection` are
70// dropped too (`connection_listed`).
71const HOP_BY_HOP: &[&str] = &[
72    "connection",
73    "keep-alive",
74    "proxy-authenticate",
75    "proxy-authorization",
76    "proxy-connection",
77    "te",
78    "trailer",
79    "transfer-encoding",
80    "upgrade",
81];
82
83/// Load balancing strategy used by [`ReverseProxy`].
84pub enum LoadBalancing {
85    /// Distribute requests across backends in a cyclic order.
86    RoundRobin,
87}
88
89/// Reverse proxy middleware.
90///
91/// Forwards incoming requests to one of the configured backends over HTTP/1.1.
92/// On connection failure the next backend in the list is tried; when all
93/// backends have failed the middleware returns `502 Bad Gateway`.
94///
95/// Hop-by-hop headers are stripped before forwarding.  `X-Forwarded-For` and
96/// `Via` are added to every forwarded request.
97///
98/// Idle connections are pooled and reused across requests (up to
99/// 64 idle per backend, 60-second timeout).
100/// This eliminates per-request TCP handshake overhead and ephemeral-port
101/// exhaustion.  Use [`ReverseProxy::with_pool`] to share a pool across
102/// multiple proxy instances or to tune pool parameters.
103pub struct ReverseProxy {
104    backends: Vec<Backend>,
105    path_prefix: Option<String>,
106    connect_timeout: Duration,
107    read_timeout: Duration,
108    counter: AtomicUsize,
109    pool: Arc<ConnPool>,
110    breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
111    trust: Option<Arc<UpstreamTls>>,
112    /// What `trust` was built from, so each TLS option adds to the others.
113    tls_ca: Option<String>,
114    tls_client: Option<(String, String)>,
115}
116
117impl ReverseProxy {
118    /// Create a proxy that distributes requests across `backends` in
119    /// round-robin order.  Each entry must be `"http://host:port"` or
120    /// `"host:port"` (port defaults to 80).
121    pub fn new<I, S>(backends: I) -> Self
122    where
123        I: IntoIterator<Item = S>,
124        S: AsRef<str>,
125    {
126        Self {
127            backends: backends
128                .into_iter()
129                .filter_map(|u| Backend::parse(u.as_ref()))
130                .collect(),
131            path_prefix: None,
132            connect_timeout: Duration::from_secs(5),
133            read_timeout: Duration::from_secs(30),
134            counter: AtomicUsize::new(0),
135            pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
136            breaker: None,
137            trust: None,
138            tls_ca: None,
139            tls_client: None,
140        }
141    }
142
143    /// Wire a [`crate::circuit_breaker::CircuitBreaker`] or
144    /// [`crate::circuit_breaker::RedisCircuitBreaker`] into this proxy:
145    /// before dialing a backend, its breaker state is checked (an `Open`
146    /// backend is skipped with no TCP attempt, exactly like a caller
147    /// manually checking `is_available` around each proxied call); after a
148    /// dial, `record_success`/`record_failure` is called automatically.
149    /// No breaker is wired by default — existing `ReverseProxy` callers see
150    /// no behavior change unless they opt in here.
151    ///
152    /// ```rust,no_run
153    /// # extern crate rws_proxy as rust_web_server;
154    /// use std::sync::Arc;
155    /// use rust_web_server::circuit_breaker::global as global_breaker;
156    /// use rust_web_server::proxy::ReverseProxy;
157    ///
158    /// let proxy = ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
159    ///     .with_circuit_breaker(Arc::new(global_breaker()));
160    /// ```
161    pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
162        self.breaker = Some(breaker);
163        self
164    }
165
166    /// Only proxy requests whose URI starts with `prefix`.
167    ///
168    /// Other requests are passed through to the next layer in the middleware
169    /// chain (or the inner application).
170    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
171        self.path_prefix = Some(prefix.into());
172        self
173    }
174
175    /// Override the load balancing strategy (currently only `RoundRobin`).
176    pub fn strategy(self, _strategy: LoadBalancing) -> Self {
177        self
178    }
179
180    /// Override the TCP connect timeout (default: 5 000 ms).
181    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
182        self.connect_timeout = Duration::from_millis(ms);
183        self
184    }
185
186    /// Override the response read timeout (default: 30 000 ms).
187    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
188        self.read_timeout = Duration::from_millis(ms);
189        self
190    }
191
192    /// Attach a shared connection pool.
193    ///
194    /// Useful for sharing one pool across multiple `ReverseProxy` instances
195    /// or for tuning pool parameters (capacity, idle timeout).
196    pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
197        self.pool = pool;
198        self
199    }
200
201    /// Trust the CA certificates in the PEM file `path` for `https://`
202    /// backends, **instead of** the public WebPKI roots — for internal
203    /// services behind a private CA or a self-signed certificate.
204    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
205        self.tls_ca = Some(path.to_string());
206        self.rebuild_trust()?;
207        Ok(self)
208    }
209
210    /// Present the client certificate in the PEM file `cert` (with its
211    /// private key in `key`) to `https://` backends that require one —
212    /// mutual TLS to the upstream. Since 18.3.0.
213    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
214        self.tls_client = Some((cert.to_string(), key.to_string()));
215        self.rebuild_trust()?;
216        Ok(self)
217    }
218
219    fn rebuild_trust(&mut self) -> Result<(), String> {
220        let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
221        self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
222        Ok(())
223    }
224
225    /// Set the maximum number of idle connections per backend (default: 64).
226    pub fn max_idle_conns(mut self, n: usize) -> Self {
227        self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
228        self
229    }
230
231    fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
232        if self.backends.is_empty() {
233            return Err("no backends configured".to_string());
234        }
235        let n = self.backends.len();
236        let start = self.counter.fetch_add(1, Ordering::Relaxed);
237        let mut last_err = "all backends failed".to_string();
238        for attempt in 0..n {
239            let idx = (start + attempt) % n;
240            let backend = &self.backends[idx];
241            let key = format!("{}:{}", backend.host, backend.port);
242
243            if let Some(breaker) = &self.breaker {
244                if !breaker.is_available(&key) {
245                    last_err = format!("circuit open for {}", key);
246                    continue;
247                }
248            }
249
250            match self.try_backend(request, connection, backend) {
251                Ok(resp) => {
252                    if let Some(breaker) = &self.breaker {
253                        breaker.record_success(&key);
254                    }
255                    return Ok(resp);
256                }
257                Err(e) => {
258                    if let Some(breaker) = &self.breaker {
259                        breaker.record_failure(&key);
260                    }
261                    last_err = e;
262                }
263            }
264        }
265        Err(last_err)
266    }
267
268    fn try_backend(
269        &self,
270        request: &Request,
271        connection: &ConnectionInfo,
272        backend: &Backend,
273    ) -> Result<Response, String> {
274        forward_upstream_trusting(
275            &self.pool,
276            request,
277            crate::proxy::Peer::of(connection),
278            &backend.host,
279            backend.port,
280            backend.tls,
281            self.trust.as_deref(),
282            self.connect_timeout,
283            self.read_timeout,
284        )
285    }
286}
287
288impl Middleware for ReverseProxy {
289    fn handle(
290        &self,
291        request: &Request,
292        connection: &ConnectionInfo,
293        next: &dyn Application,
294    ) -> Result<Response, String> {
295        if let Some(prefix) = &self.path_prefix {
296            if !request.request_uri.starts_with(prefix.as_str()) {
297                return next.execute(request, connection);
298            }
299        }
300        match self.proxy(request, connection) {
301            Ok(mut resp) => {
302                status::received(&mut resp, None);
303                Ok(resp)
304            }
305            Err(e) => {
306                let mut resp = bad_gateway();
307                status::failed_with(&mut resp, &e, None);
308                Ok(resp)
309            }
310        }
311    }
312}
313
314// ── helpers ───────────────────────────────────────────────────────────────────
315
316/// The client as the forwarders need it: its address for `X-Forwarded-For`
317/// and whether it connected over TLS, for `X-Forwarded-Proto`.
318#[derive(Clone, Copy, Debug)]
319#[doc(hidden)]
320pub struct Peer<'a> {
321    pub ip: &'a str,
322    pub tls: bool,
323}
324
325impl<'a> Peer<'a> {
326    #[doc(hidden)]
327    pub fn of(connection: &'a ConnectionInfo) -> Self {
328        Peer { ip: &connection.client.ip, tls: connection.tls }
329    }
330
331    /// A plain-HTTP client at `ip` (tests and callers without a connection).
332    #[doc(hidden)]
333    pub fn plain(ip: &'a str) -> Self {
334        Peer { ip, tls: false }
335    }
336
337    #[doc(hidden)]
338    pub fn proto(&self) -> &'static str {
339        if self.tls { "https" } else { "http" }
340    }
341}
342
343/// The headers a client sets to describe earlier hops.
344const FORWARDING_HEADERS: [&str; 4] = ["x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "forwarded"];
345
346/// `request` as the proxy may pass it on: from a peer outside
347/// `trusted_proxies` its `X-Forwarded-For/Proto/Host` and `Forwarded` are
348/// dropped (rws sets its own), so a client can't claim `https` or another
349/// host (18.24.0). Borrowed when nothing needs dropping.
350#[doc(hidden)]
351pub fn from_peer<'a>(request: &'a Request, client: Peer<'_>) -> std::borrow::Cow<'a, Request> {
352    let has = request.headers.iter().any(|h| FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
353    if !has || crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
354        return std::borrow::Cow::Borrowed(request);
355    }
356    let mut owned = request.clone();
357    owned.headers.retain(|h| !FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
358    std::borrow::Cow::Owned(owned)
359}
360
361/// The `Forwarded` value for this hop (RFC 7239, 18.20.0): the request's
362/// own `Forwarded` (from proxies before this one) extended with
363/// `for=<client>;proto=<http|https>;host=<Host>`; `None` when
364/// `forwarded_header = "off"`.
365#[doc(hidden)]
366pub fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
367    if !crate::server_config::ServerConfig::current().forwarded_header {
368        return None;
369    }
370    let mut element = format!("for={};proto={}", forwarded_node(client.ip), client.proto());
371    if let Some(host) = request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()) {
372        push_forwarded_host(&mut element, host);
373    }
374    let prior = request.headers.iter().filter(|h| h.name.eq_ignore_ascii_case("forwarded")).map(|h| h.value.trim()).filter(|v| !v.is_empty() && !v.bytes().any(|b| b < 0x20)).collect::<Vec<_>>();
375    if prior.is_empty() {
376        // A trusted proxy that sent only X-Forwarded-* (most do): its hops
377        // become Forwarded elements first, the client's with the proto and
378        // host it reported (RFC 7239 §7.4, since 18.27.0), so the chain says
379        // proto=https like X-Forwarded-Proto instead of just this hop's http.
380        if let Some(earlier) = forwarded_from_x_forwarded(request, client) {
381            return Some(format!("{earlier}, {element}"));
382        }
383    }
384    Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
385}
386
387/// An RFC 7239 node for an address: IPv4 bare, IPv6 quoted in brackets,
388/// `unknown` for anything else.
389fn forwarded_node(ip: &str) -> String {
390    let ip = ip.trim();
391    let parsed = ip.parse::<std::net::IpAddr>().ok().or_else(|| ip.parse::<std::net::SocketAddr>().ok().map(|a| a.ip()));
392    match parsed {
393        Some(std::net::IpAddr::V4(v4)) => v4.to_string(),
394        Some(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
395            Some(v4) => v4.to_string(),
396            None => format!("\"[{v6}]\""),
397        },
398        None => "unknown".to_string(),
399    }
400}
401
402fn push_forwarded_host(element: &mut String, host: &str) {
403    if host.is_empty() || !host.bytes().all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\') {
404        return;
405    }
406    if host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)) {
407        element.push_str(&format!(";host={host}"));
408    } else {
409        element.push_str(&format!(";host=\"{host}\""));
410    }
411}
412
413/// `X-Forwarded-For` hops from a trusted peer as `Forwarded` elements, the
414/// first with `X-Forwarded-Proto` and `-Host`; `None` without one.
415fn forwarded_from_x_forwarded(request: &Request, client: Peer<'_>) -> Option<String> {
416    if !crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
417        return None;
418    }
419    let hops: Vec<&str> = request.headers.iter()
420        .filter(|h| h.name.eq_ignore_ascii_case("x-forwarded-for"))
421        .flat_map(|h| h.value.split(','))
422        .map(str::trim)
423        .filter(|h| !h.is_empty())
424        .collect();
425    if hops.is_empty() {
426        return None;
427    }
428    let mut elements = Vec::with_capacity(hops.len());
429    for (i, hop) in hops.iter().enumerate() {
430        let mut element = format!("for={}", forwarded_node(hop));
431        if i == 0 {
432            if let Some(proto) = request.header("X-Forwarded-Proto").map(|h| h.value.trim().to_ascii_lowercase()).filter(|p| p == "http" || p == "https") {
433                element.push_str(&format!(";proto={proto}"));
434            }
435            if let Some(host) = request.header("X-Forwarded-Host").map(|h| h.value.trim().to_string()) {
436                push_forwarded_host(&mut element, &host);
437            }
438        }
439        elements.push(element);
440    }
441    Some(elements.join(", "))
442}
443
444#[doc(hidden)]
445pub fn build_request(
446    request: &Request,
447    backend_host: &str,
448    client: Peer<'_>,
449    keep_alive: bool,
450) -> Vec<u8> {
451    let request = &*from_peer(request, client);
452    // One buffer, sized up front and filled with extend_from_slice — no
453    // write!/format machinery, no lower-cased copy of each header name.
454    // `Content-Length` is regenerated from the (possibly de-chunked) body and
455    // `X-Forwarded-For` is extended below, so the client's copies are dropped:
456    // two `Content-Length` lines are a protocol error and a desync risk.
457    let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
458    // Inside a traced request the current span's `traceparent` (and the
459    // request's `baggage`) replace the client's.
460    #[cfg(not(target_arch = "wasm32"))]
461    let trace = crate::otel::propagation_headers();
462    #[cfg(target_arch = "wasm32")]
463    let trace: Option<(String, Option<String>)> = None;
464    let forwarded_header = forwarded_value(request, client);
465    let forwarded = |h: &&crate::header::Header| {
466        // An empty name would be sent as a malformed ": value" line.
467        !h.name.is_empty()
468            && !is_hop_by_hop_or_host(&h.name)
469            && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
470            && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
471            && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
472            && !connection.is_some_and(|c| connection_listed(c, &h.name))
473            && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
474    };
475    let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
476    // The Host the client asked for, unless an outer proxy already said
477    // (its X-Forwarded-Host passes through like any header).
478    // http or https, as the client connected, unless an outer proxy said.
479    let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
480    let forwarded_host = match request.header("X-Forwarded-Host") {
481        Some(_) => None,
482        None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
483    };
484    let body_len = Decimal::new(request.body.len() as u128);
485    let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
486        + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
487        + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
488        + forwarded_host.map_or(0, |v| v.len() + 20)
489        + forwarded_proto.map_or(0, |v| v.len() + 21)
490        + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
491        + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
492        + 13
493        + 24
494        + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
495        + 2
496        + request.body.len();
497    let mut out: Vec<u8> = Vec::with_capacity(capacity);
498    for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
499        out.extend_from_slice(part.as_bytes());
500    }
501    for h in request.headers.iter().filter(forwarded) {
502        for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
503            out.extend_from_slice(part.as_bytes());
504        }
505    }
506    out.extend_from_slice(b"X-Forwarded-For: ");
507    if let Some(prior) = prior_xff {
508        out.extend_from_slice(prior.as_bytes());
509        out.extend_from_slice(b", ");
510    }
511    for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
512        out.extend_from_slice(part.as_bytes());
513    }
514    if let Some(host) = forwarded_host {
515        for part in ["X-Forwarded-Host: ", host, "\r\n"] {
516            out.extend_from_slice(part.as_bytes());
517        }
518    }
519    if let Some(proto) = forwarded_proto {
520        for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
521            out.extend_from_slice(part.as_bytes());
522        }
523    }
524    if let Some(value) = &forwarded_header {
525        for part in ["Forwarded: ", value.as_str(), "\r\n"] {
526            out.extend_from_slice(part.as_bytes());
527        }
528    }
529    if let Some((traceparent, baggage)) = &trace {
530        for part in ["traceparent: ", traceparent, "\r\n"] {
531            out.extend_from_slice(part.as_bytes());
532        }
533        if let Some(baggage) = baggage {
534            for part in ["baggage: ", baggage, "\r\n"] {
535                out.extend_from_slice(part.as_bytes());
536            }
537        }
538    }
539    if keep_alive {
540        out.extend_from_slice(b"Connection: keep-alive\r\n");
541    } else {
542        out.extend_from_slice(b"Connection: close\r\n");
543    }
544    if !request.body.is_empty() {
545        out.extend_from_slice(b"Content-Length: ");
546        out.extend_from_slice(body_len.as_str().as_bytes());
547        out.extend_from_slice(b"\r\n");
548    }
549    out.extend_from_slice(b"\r\n");
550    out.extend_from_slice(&request.body);
551    out
552}
553
554/// Whether a request header is dropped on the way upstream: hop-by-hop
555/// headers and `Host` (rws sends its own). Matches `to_lowercase()` against
556/// the lower-case list — Unicode lower-casing, so a non-ASCII name (where
557/// e.g. the Kelvin sign lower-cases to `k`) still takes that path.
558fn is_hop_by_hop_or_host(name: &str) -> bool {
559    if name.is_ascii() {
560        name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
561    } else {
562        let lower = name.to_lowercase();
563        lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
564    }
565}
566
567/// Whether `name` is one of the comma-separated tokens of a `Connection`
568/// header value — those headers are hop-by-hop for this connection only.
569fn connection_listed(connection: &str, name: &str) -> bool {
570    connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
571}
572
573/// Decode HTTP/1.1 chunked transfer-encoding from `stream`.
574///
575/// `buf[header_end..]` may already contain some body bytes that arrived in
576/// the same read as the headers.  Returns the fully decoded body.
577fn decode_chunked<S: Read>(
578    stream: &mut S,
579    buf: &[u8],
580    header_end: usize,
581    tmp: &mut [u8],
582) -> Result<Vec<u8>, String> {
583    // Seed `raw` with any body bytes already buffered alongside the headers.
584    let mut raw: Vec<u8> = buf[header_end..].to_vec();
585    let mut decoded: Vec<u8> = Vec::new();
586
587    loop {
588        // Wait until we have at least one complete chunk-size line (ends with \r\n).
589        let crlf = loop {
590            if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
591                break p;
592            }
593            let n = stream.read(tmp).map_err(|e| e.to_string())?;
594            if n == 0 {
595                return Err("chunked: premature EOF reading chunk size".to_string());
596            }
597            raw.extend_from_slice(&tmp[..n]);
598        };
599
600        // Chunk size is hex, optionally followed by chunk-extensions (";…").
601        let size_line = std::str::from_utf8(&raw[..crlf])
602            .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
603        let size_str = size_line.split(';').next().unwrap_or("").trim();
604        let chunk_size = usize::from_str_radix(size_str, 16)
605            .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
606        raw.drain(..crlf + 2); // consume "<size>\r\n"
607
608        if chunk_size == 0 {
609            // Last chunk: skip trailer fields up to the empty line that ends
610            // the message. Leaving any of it unread would corrupt the next
611            // response on a pooled connection.
612            loop {
613                let line_end = loop {
614                    if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
615                        break p;
616                    }
617                    let n = stream.read(tmp).map_err(|e| e.to_string())?;
618                    if n == 0 {
619                        return Err("chunked: premature EOF in trailer".to_string());
620                    }
621                    raw.extend_from_slice(&tmp[..n]);
622                };
623                raw.drain(..line_end + 2);
624                if line_end == 0 {
625                    break;
626                }
627            }
628            break;
629        }
630
631        // Read chunk data + trailing CRLF.
632        while raw.len() < chunk_size + 2 {
633            let n = stream.read(tmp).map_err(|e| e.to_string())?;
634            if n == 0 {
635                return Err("chunked: premature EOF reading chunk body".to_string());
636            }
637            raw.extend_from_slice(&tmp[..n]);
638        }
639        decoded.extend_from_slice(&raw[..chunk_size]);
640        raw.drain(..chunk_size + 2); // consume "<data>\r\n"
641    }
642
643    Ok(decoded)
644}
645
646/// Rewrite `buf` in-place: strip `Transfer-Encoding`, add `Content-Length`,
647/// replace the old (undecoded) body with `decoded`.
648fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
649    let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
650    buf.clear();
651    for line in header_str.lines() {
652        if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
653            continue;
654        }
655        buf.extend_from_slice(line.as_bytes());
656        buf.extend_from_slice(b"\r\n");
657    }
658    let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
659    buf.extend_from_slice(decoded);
660}
661
662// Lives in rws-http (`response`): the disk cache parses stored responses with it too.
663pub(crate) use crate::response::REGENERATED_OR_HOP_BY_HOP;
664
665/// Make a buffered upstream response safe to re-serialize. The body is
666/// already held in `content_range_list`, from which `Response::generate`
667/// (and the HTTP/2 / HTTP/3 writers) emit `Content-Type` and
668/// `Content-Length` themselves, so the upstream's own copies would be
669/// duplicated — and a duplicate `content-length` is a protocol error that
670/// HTTP/2 clients reject outright. The upstream's `Content-Type` is moved
671/// onto the body instead; the status line is re-emitted as HTTP/1.1
672/// regardless of the backend's version.
673#[cfg_attr(not(feature = "http2"), allow(dead_code))]
674#[doc(hidden)]
675pub fn normalize_upstream_response(mut response: Response) -> Response {
676    response.http_version.clear();
677    response.http_version.push_str(crate::http::VERSION.http_1_1);
678    let has_body = !response.content_range_list.is_empty();
679    if let [range] = response.content_range_list.as_mut_slice() {
680        if let Some(h) = response
681            .headers
682            .iter()
683            .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
684        {
685            range.content_type.clear();
686            range.content_type.push_str(&h.value);
687        }
688    }
689    // Compared in place (the list is lower-case ASCII), not lower-cased
690    // into a new String per header.
691    response.headers.retain(|h| {
692        if h.name.eq_ignore_ascii_case("content-type") {
693            // Keep it only when there's no body to carry it (e.g. HEAD, 304).
694            return !has_body;
695        }
696        !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
697    });
698    response
699}
700
701#[doc(hidden)]
702pub fn bad_gateway() -> Response {
703    crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
704}
705
706// ── Streaming proxy helpers ───────────────────────────────────────────────────
707
708/// Responses larger than this threshold are streamed instead of buffered.
709const STREAM_THRESHOLD: usize = 1024 * 1024; // 1 MB
710
711/// A `Read` implementation that drains a prefix buffer before reading from the
712/// inner stream. Used to replay body bytes that arrived with the HTTP headers.
713#[doc(hidden)]
714pub struct ConcatReader<R: Read + Send> {
715    prefix: Vec<u8>,
716    prefix_pos: usize,
717    inner: R,
718}
719
720impl<R: Read + Send> ConcatReader<R> {
721    fn new(prefix: Vec<u8>, inner: R) -> Self {
722        ConcatReader { prefix, prefix_pos: 0, inner }
723    }
724}
725
726impl<R: Read + Send> Read for ConcatReader<R> {
727    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
728        if self.prefix_pos < self.prefix.len() {
729            let avail = &self.prefix[self.prefix_pos..];
730            let n = buf.len().min(avail.len());
731            buf[..n].copy_from_slice(&avail[..n]);
732            self.prefix_pos += n;
733            return Ok(n);
734        }
735        self.inner.read(buf)
736    }
737}
738
739/// A streamed upstream body with a `Content-Length`: ends after exactly that
740/// many bytes, and fails if the upstream ends sooner.
741///
742/// Without it the reader went on reading the backend's keep-alive connection
743/// after the last byte and only stopped at the read timeout: HTTP/2 clients
744/// got the whole body and then `RST_STREAM(INTERNAL_ERROR)` 30 s later (Git
745/// LFS downloads behind rws), HTTP/1.1 clients waited 30 s for the end.
746#[doc(hidden)]
747pub struct LengthLimitedReader {
748    inner: Box<dyn Read + Send>,
749    remaining: u64,
750    declared: u64,
751}
752
753impl LengthLimitedReader {
754    #[doc(hidden)]
755    pub fn new(inner: Box<dyn Read + Send>, length: u64) -> Self {
756        LengthLimitedReader { inner, remaining: length, declared: length }
757    }
758}
759
760impl Read for LengthLimitedReader {
761    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
762        if self.remaining == 0 || buf.is_empty() {
763            return Ok(0);
764        }
765        let want = buf.len().min(self.remaining.min(usize::MAX as u64) as usize);
766        let n = self.inner.read(&mut buf[..want])?;
767        if n == 0 {
768            return Err(std::io::Error::new(
769                std::io::ErrorKind::UnexpectedEof,
770                format!("upstream body ended after {} of {} bytes", self.declared - self.remaining, self.declared),
771            ));
772        }
773        self.remaining -= n as u64;
774        Ok(n)
775    }
776}
777
778/// The streamed body reader for an upstream response: bounded by its
779/// `Content-Length` unless it is chunked (the chunked framing ends it).
780#[doc(hidden)]
781pub fn bounded_body(reader: Box<dyn Read + Send>, chunked: bool, content_length: Option<usize>) -> Box<dyn Read + Send> {
782    match content_length {
783        Some(len) if !chunked => Box::new(LengthLimitedReader::new(reader, len as u64)),
784        _ => reader,
785    }
786}
787
788
789/// Idle backend connections kept per `host:port`. 64 matches common nginx
790/// `keepalive` settings and the benchmark's concurrency; fewer meant most
791/// requests at 64-way concurrency still paid a TCP connect.
792#[doc(hidden)]
793pub const POOL_MAX_IDLE: usize = 64;
794
795/// The process-wide backend connection pool used by config-driven proxy
796/// routes (`proxy_config::DynamicProxy`), keyed by `host:port`.
797#[doc(hidden)]
798pub fn shared_pool() -> &'static ConnPool {
799    static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
800    POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
801}
802
803/// A backend connection the pooled forwarder drives: plain TCP, or TLS
804/// over TCP for `https://` backends.
805#[doc(hidden)]
806pub trait UpstreamIo: Read + Write + Send + 'static {
807    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
808}
809
810impl UpstreamIo for TcpStream {
811    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
812        self.set_read_timeout(Some(read))?;
813        self.set_write_timeout(Some(write))
814    }
815}
816
817#[cfg(any(feature = "http-client", feature = "http2"))]
818#[doc(hidden)]
819pub type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
820
821#[cfg(any(feature = "http-client", feature = "http2"))]
822impl UpstreamIo for TlsStream {
823    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
824        self.sock.set_io_timeouts(read, write)
825    }
826}
827
828/// Forward `request` to an HTTP or HTTPS backend at `host:port`, over a
829/// pooled keep-alive connection either way: plain backends use `plain_pool`,
830/// TLS backends the process-wide TLS pool (one handshake per connection, not
831/// per request). The single entry point for `ReverseProxy`, config-driven
832/// routes, `canary` and `ingress`.
833#[doc(hidden)]
834pub fn forward_upstream(
835    plain_pool: &ConnPool,
836    request: &Request,
837    client: Peer<'_>,
838    host: &str,
839    port: u16,
840    tls: bool,
841    connect_timeout: Duration,
842    read_timeout: Duration,
843) -> Result<Response, String> {
844    forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
845}
846
847/// [`forward_upstream`] with the trust settings of one upstream: `trust`
848/// replaces the WebPKI roots for TLS backends (a private CA).
849#[allow(clippy::too_many_arguments)]
850#[doc(hidden)]
851pub fn forward_upstream_trusting(
852    plain_pool: &ConnPool,
853    request: &Request,
854    client: Peer<'_>,
855    host: &str,
856    port: u16,
857    tls: bool,
858    trust: Option<&UpstreamTls>,
859    connect_timeout: Duration,
860    read_timeout: Duration,
861) -> Result<Response, String> {
862    forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
863}
864
865/// [`forward_upstream_trusting`], sending `host_header` as the `Host` instead
866/// of the backend's address (a config route's `preserve_host` /
867/// `upstream_host`).
868#[allow(clippy::too_many_arguments)]
869#[doc(hidden)]
870pub fn forward_upstream_as(
871    plain_pool: &ConnPool,
872    request: &Request,
873    client: Peer<'_>,
874    host: &str,
875    port: u16,
876    host_header: &str,
877    tls: bool,
878    trust: Option<&UpstreamTls>,
879    connect_timeout: Duration,
880    read_timeout: Duration,
881) -> Result<Response, String> {
882    // Inside a traced request: a client span per forward, current while the
883    // request is built, so the upstream's `traceparent` names it.
884    #[cfg(not(target_arch = "wasm32"))]
885    let span = crate::otel::outbound_span(&request.method);
886    #[cfg(not(target_arch = "wasm32"))]
887    if let Some(span) = &span {
888        let scheme = if tls { "https" } else { "http" };
889        let path = request.request_uri.as_str();
890        for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
891            span.set_attribute(key, value);
892        }
893    }
894    let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
895    #[cfg(not(target_arch = "wasm32"))]
896    if let Some(span) = &span {
897        span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
898    }
899    result
900}
901
902#[allow(clippy::too_many_arguments)]
903fn forward_upstream_untraced(
904    plain_pool: &ConnPool,
905    request: &Request,
906    client: Peer<'_>,
907    host: &str,
908    port: u16,
909    host_header: &str,
910    tls: bool,
911    trust: Option<&UpstreamTls>,
912    connect_timeout: Duration,
913    read_timeout: Duration,
914) -> Result<Response, String> {
915    if tls {
916        #[cfg(any(feature = "http-client", feature = "http2"))]
917        {
918            let (pool, config) = match trust {
919                Some(t) => (t.pool, Arc::clone(&t.config)),
920                None => (shared_tls_pool(), tls_client_config()),
921            };
922            return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
923                connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
924            });
925        }
926        #[cfg(not(any(feature = "http-client", feature = "http2")))]
927        {
928            let _ = trust;
929            return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
930        }
931    }
932    forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
933}
934
935/// Trust settings for one upstream's TLS backends: the certificates in a CA
936/// file **instead of** the WebPKI roots — for internal services behind a
937/// private CA or a self-signed certificate (nginx's
938/// `proxy_ssl_trusted_certificate`) — and/or a client certificate rws
939/// presents to backends that require one (`proxy_ssl_certificate`, since
940/// 18.3.0). Connections are pooled per setting, so two upstreams at the same
941/// address with different trust never share one.
942pub struct UpstreamTls {
943    #[cfg(any(feature = "http-client", feature = "http2"))]
944    config: Arc<rustls::ClientConfig>,
945    #[cfg(any(feature = "http-client", feature = "http2"))]
946    pool: &'static Pool<TlsStream>,
947    /// Connections for the non-blocking forwarder, made with `config`.
948    #[cfg(feature = "http2")]
949    async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
950    /// `config` offering HTTP/2 by ALPN, for HTTP/2 and gRPC upstreams.
951    #[cfg(feature = "http2")]
952    h2_config: Arc<rustls::ClientConfig>,
953}
954
955impl UpstreamTls {
956    /// Load the PEM certificates in `path` as the only trusted roots.
957    pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
958        UpstreamTls::new(Some(path), None)
959    }
960
961    /// Trust the PEM certificates in `ca_file` (the WebPKI roots when `None`)
962    /// and, with `client_cert` = `(certificate chain PEM, private key PEM)`,
963    /// present that certificate to backends that ask for one.
964    pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
965        #[cfg(any(feature = "http-client", feature = "http2"))]
966        {
967            use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
968            let roots = match ca_file {
969                Some(path) => {
970                    let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
971                    let certs = CertificateDer::pem_slice_iter(&bytes)
972                        .collect::<Result<Vec<_>, _>>()
973                        .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
974                    if certs.is_empty() {
975                        return Err(format!("no certificates in CA file '{path}'"));
976                    }
977                    let mut roots = rustls::RootCertStore::empty();
978                    for cert in certs {
979                        roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
980                    }
981                    roots
982                }
983                None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
984            };
985            let builder = crate::tls_client_builder().with_root_certificates(roots);
986            let config = match client_cert {
987                None => builder.with_no_client_auth(),
988                Some((cert_path, key_path)) => {
989                    let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
990                    let chain = CertificateDer::pem_slice_iter(&bytes)
991                        .collect::<Result<Vec<_>, _>>()
992                        .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
993                    if chain.is_empty() {
994                        return Err(format!("no certificate in '{cert_path}'"));
995                    }
996                    let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
997                    let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
998                    builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
999                }
1000            };
1001            let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
1002            Ok(UpstreamTls::with_config(config, &label))
1003        }
1004        #[cfg(not(any(feature = "http-client", feature = "http2")))]
1005        {
1006            let _ = client_cert;
1007            Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
1008        }
1009    }
1010
1011    #[cfg(any(feature = "http-client", feature = "http2"))]
1012    fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
1013        #[cfg(feature = "http2")]
1014        let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
1015        UpstreamTls {
1016            config: Arc::new(config),
1017            pool: tls_pool_for_ca(label),
1018            #[cfg(feature = "http2")]
1019            async_pool: async_tls_pool_for_ca(label),
1020            #[cfg(feature = "http2")]
1021            h2_config,
1022        }
1023    }
1024
1025    /// The client configuration for HTTP/2 upstreams (ALPN `h2`).
1026    #[cfg(feature = "http2")]
1027    #[doc(hidden)]
1028    pub fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
1029        &self.h2_config
1030    }
1031
1032    /// Trust nothing: every TLS handshake fails, so the upstream answers 502.
1033    /// Used when a configured CA file can't be loaded — failing closed rather
1034    /// than falling back to the public roots.
1035    #[doc(hidden)]
1036    pub fn reject_all(label: &str) -> UpstreamTls {
1037        #[cfg(any(feature = "http-client", feature = "http2"))]
1038        {
1039            let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
1040            UpstreamTls::with_config(config, &format!("reject-all:{label}"))
1041        }
1042        #[cfg(not(any(feature = "http-client", feature = "http2")))]
1043        {
1044            let _ = label;
1045            UpstreamTls {}
1046        }
1047    }
1048
1049    /// How the non-blocking forwarder reaches these backends.
1050    #[cfg(feature = "http2")]
1051    #[doc(hidden)]
1052    pub fn async_tls(&self) -> async_upstream::AsyncTls {
1053        async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
1054    }
1055
1056    /// The client configuration (for health checks).
1057    #[cfg(any(feature = "http-client", feature = "http2"))]
1058    #[doc(hidden)]
1059    pub fn client_config(&self) -> Arc<rustls::ClientConfig> {
1060        Arc::clone(&self.config)
1061    }
1062}
1063
1064/// One non-blocking connection pool per CA file, for the life of the process
1065/// (a reload that keeps the same file keeps its warm connections).
1066#[cfg(feature = "http2")]
1067fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
1068    static POOLS: std::sync::OnceLock<
1069        std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
1070    > = std::sync::OnceLock::new();
1071    let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
1072    pools
1073        .entry(path.to_string())
1074        .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
1075}
1076
1077/// One pool per CA file, for the life of the process.
1078#[cfg(any(feature = "http-client", feature = "http2"))]
1079fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
1080    static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
1081        std::sync::OnceLock::new();
1082    let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
1083    pools
1084        .entry(path.to_string())
1085        .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
1086}
1087
1088/// Forward `request` to `host:port` over plain HTTP/1.1, reusing an idle
1089/// connection from `pool` when there is one and returning the connection to
1090/// `pool` after a complete, reusable response.
1091#[doc(hidden)]
1092pub fn forward_pooled(
1093    pool: &ConnPool,
1094    request: &Request,
1095    client: Peer<'_>,
1096    host: &str,
1097    port: u16,
1098    connect_timeout: Duration,
1099    read_timeout: Duration,
1100) -> Result<Response, String> {
1101    forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
1102}
1103
1104/// [`forward_pooled`], sending `host_header` as the `Host` (the client's, for
1105/// a gateway that must preserve it) instead of the backend's address.
1106#[cfg(feature = "gateway-api")]
1107#[allow(clippy::too_many_arguments)]
1108#[doc(hidden)]
1109pub fn forward_pooled_with_host(
1110    pool: &ConnPool,
1111    request: &Request,
1112    client: Peer<'_>,
1113    host: &str,
1114    port: u16,
1115    host_header: &str,
1116    connect_timeout: Duration,
1117    read_timeout: Duration,
1118) -> Result<Response, String> {
1119    forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
1120}
1121
1122/// The pooled request/response cycle, for any connection type.
1123///
1124/// A backend may close an idle keep-alive connection at any moment, so a
1125/// pooled connection that fails before *any* response byte arrives is
1126/// retried once on a fresh connection — but only for idempotent methods
1127/// (RFC 9110 §9.2.2): the request may already have reached the backend.
1128#[allow(clippy::too_many_arguments)]
1129fn forward_via<S: UpstreamIo>(
1130    pool: &Pool<S>,
1131    request: &Request,
1132    client: Peer<'_>,
1133    host: &str,
1134    port: u16,
1135    host_header: &str,
1136    read_timeout: Duration,
1137    connect: impl Fn() -> Result<S, String>,
1138) -> Result<Response, String> {
1139    let key = format!("{}:{}", host, port);
1140    if let Some(stream) = pool.acquire(&key) {
1141        match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1142            Exchange::Done(response) => return Ok(response),
1143            Exchange::NoResponse(_) if is_idempotent(&request.method) => {} // stale: retry fresh
1144            Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
1145        }
1146    }
1147    let stream = connect()?;
1148    match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1149        Exchange::Done(response) => Ok(response),
1150        Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1151    }
1152}
1153
1154/// Connect to the first address of `host:port` that accepts. `localhost`
1155/// resolves to `::1` before `127.0.0.1` on many systems; a backend listening
1156/// on IPv4 only must still be reachable by name.
1157fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1158    let key = format!("{}:{}", host, port);
1159    let addrs: Vec<_> = key
1160        .to_socket_addrs()
1161        .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1162        .collect();
1163    let mut last_error = format!("no address resolved for {}", key);
1164    for addr in addrs {
1165        match TcpStream::connect_timeout(&addr, connect_timeout) {
1166            Ok(stream) => {
1167                let _ = stream.set_nodelay(true);
1168                return Ok(stream);
1169            }
1170            Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1171        }
1172    }
1173    Err(last_error)
1174}
1175
1176/// The process-wide pool of TLS connections to `https://` backends.
1177#[cfg(any(feature = "http-client", feature = "http2"))]
1178fn shared_tls_pool() -> &'static Pool<TlsStream> {
1179    static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1180    POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
1181}
1182
1183/// TLS client settings for backends: the Mozilla root store, no client
1184/// certificate. Built once — parsing the root store per request was a large
1185/// part of the per-request cost of HTTPS upstreams.
1186#[cfg(any(feature = "http-client", feature = "http2"))]
1187#[doc(hidden)]
1188pub fn tls_client_config() -> Arc<rustls::ClientConfig> {
1189    static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1190    Arc::clone(CONFIG.get_or_init(|| {
1191        let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1192        Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1193    }))
1194}
1195
1196#[cfg(any(feature = "http-client", feature = "http2"))]
1197fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1198    let tcp = connect_tcp(host, port, connect_timeout)?;
1199    let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1200        .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1201    let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1202    Ok(rustls::StreamOwned::new(conn, tcp))
1203}
1204
1205fn is_idempotent(method: &str) -> bool {
1206    matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1207}
1208
1209enum Exchange {
1210    Done(Response),
1211    /// Failed before any response byte (write error, or closed/errored on the first read).
1212    NoResponse(String),
1213    Failed(String),
1214}
1215
1216/// One request/response on `stream`. A fully read, keep-alive response puts
1217/// the connection back in `pool`; a streamed response keeps it until done.
1218fn exchange<S: UpstreamIo>(
1219    pool: &Pool<S>,
1220    key: &str,
1221    mut stream: S,
1222    request: &Request,
1223    host: &str,
1224    client: Peer<'_>,
1225    read_timeout: Duration,
1226) -> Exchange {
1227    if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1228        return Exchange::Failed(e.to_string());
1229    }
1230    // keep_alive = true: ask the backend to hold the connection open.
1231    let req_bytes = build_request(request, host, client, true);
1232    if let Err(e) = stream.write_all(&req_bytes) {
1233        return Exchange::NoResponse(format!("write to backend failed: {}", e));
1234    }
1235
1236    let mut tmp = [0u8; 4096];
1237    let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1238        Ok(head) => head,
1239        Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1240        Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1241    };
1242    let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1243
1244    if response_has_no_body(&request.method, &header_lower) {
1245        if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1246            pool.release(key, stream);
1247        }
1248        return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1249            Ok(response) => Exchange::Done(response),
1250            Err(e) => Exchange::Failed(e),
1251        };
1252    }
1253
1254    if should_stream_response(&header_lower) {
1255        // Streaming path — pipe bytes straight to the client; the connection
1256        // can't be reused while the body is in flight.
1257        return match parse_status_and_headers(&header_bytes) {
1258            Ok(mut response) => {
1259                let facts = HeadFacts::scan(&header_bytes);
1260                let body: Box<dyn Read + Send> = Box::new(ConcatReader::new(body_prefix, stream));
1261                response.stream_pipe = Some(bounded_body(body, facts.chunked, facts.content_length));
1262                Exchange::Done(response)
1263            }
1264            Err(e) => Exchange::Failed(e),
1265        };
1266    }
1267    // Buffered path — read the full body, then return the connection to the pool.
1268    match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1269        Ok((resp_bytes, reusable)) => {
1270            if reusable {
1271                pool.release(key, stream);
1272            }
1273            match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1274                Ok(response) => Exchange::Done(response),
1275                Err(e) => Exchange::Failed(e),
1276            }
1277        }
1278        Err(e) => Exchange::Failed(e),
1279    }
1280}
1281
1282/// Read exactly the HTTP response headers (up to and including `\r\n\r\n`).
1283///
1284/// Returns `(header_bytes, body_prefix)` where `body_prefix` contains any
1285/// body bytes that arrived in the same TCP segment as the headers.
1286#[cfg(test)]
1287fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1288    read_response_head(stream, tmp).map_err(|e| match e {
1289        HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1290    })
1291}
1292
1293/// Why reading a response head failed: before any byte arrived (the request
1294/// may never have been processed — e.g. a pooled connection the backend had
1295/// already closed) or part-way through.
1296enum HeadError {
1297    NothingReceived(String),
1298    Partial(String),
1299}
1300
1301fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1302    read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1303}
1304
1305/// The final response head: interim `1xx` heads (`100 Continue`,
1306/// `103 Early Hints`, …) are read past (RFC 9110 §15.2); a `103`'s `Link`
1307/// values are passed on to the client (`early_hints::forward_upstream`). Returning one as
1308/// the response left the real response in the connection, and once pooled
1309/// the next request — possibly another client's — received it.
1310fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1311    let (mut head, mut rest) = read_response_head(stream, tmp)?;
1312    let mut interim = 0;
1313    while is_interim_response(&head) {
1314        crate::early_hints::forward_upstream(&head);
1315        interim += 1;
1316        if interim > MAX_INTERIM_RESPONSES {
1317            return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1318        }
1319        (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1320    }
1321    Ok((head, rest))
1322}
1323
1324/// Read until a complete head is in `buf` (which may already hold bytes
1325/// that followed a previous head). `received` — bytes already came from this
1326/// response, so a failure is never "nothing received".
1327fn read_response_head_from<S: Read>(
1328    stream: &mut S,
1329    tmp: &mut [u8],
1330    mut buf: Vec<u8>,
1331    received: bool,
1332) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1333    loop {
1334        if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1335            let body_prefix = buf[pos + 4..].to_vec();
1336            buf.truncate(pos + 4);
1337            return Ok((buf, body_prefix));
1338        }
1339        let nothing = buf.is_empty() && !received;
1340        let n = match stream.read(tmp) {
1341            Ok(n) => n,
1342            Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1343            Err(e) => return Err(HeadError::Partial(e.to_string())),
1344        };
1345        if n == 0 {
1346            let msg = "backend closed connection before headers were complete".to_string();
1347            return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1348        }
1349        buf.extend_from_slice(&tmp[..n]);
1350    }
1351}
1352
1353/// Interim responses tolerated before the final one.
1354#[doc(hidden)]
1355pub const MAX_INTERIM_RESPONSES: usize = 16;
1356
1357/// An interim (1xx) response head other than `101 Switching Protocols`
1358/// (which ends the HTTP exchange): the final response follows it.
1359#[doc(hidden)]
1360pub fn is_interim_response(head: &[u8]) -> bool {
1361    let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1362    let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1363    status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1364}
1365
1366/// RFC 9112 §6.3: a response to HEAD, and any 1xx, 204 or 304 response, ends
1367/// with its head — whatever `Content-Length` says (for HEAD it describes the
1368/// GET body). Waiting for that body hung until the read timeout (a 502).
1369#[doc(hidden)]
1370pub fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1371    let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1372    method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1373}
1374
1375/// Returns `true` when the response should be streamed rather than buffered.
1376///
1377/// Streams when any of the following hold:
1378/// - `Content-Type: text/event-stream` — SSE
1379/// - `Transfer-Encoding: chunked` — AI token streams, etc.
1380/// - `Content-Length` exceeds 1 MB — large file downloads
1381#[doc(hidden)]
1382pub fn should_stream_response(header_lower: &str) -> bool {
1383    let is_sse = header_lower.lines().any(|l| {
1384        l.starts_with("content-type:") && l.contains("text/event-stream")
1385    });
1386    let is_chunked = header_lower.lines().any(|l| {
1387        l.starts_with("transfer-encoding:") && l.contains("chunked")
1388    });
1389    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1390        l.strip_prefix("content-length:")?.trim().parse().ok()
1391    });
1392    let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1393    is_sse || is_chunked || is_large
1394}
1395
1396/// What the forwarders need from an upstream response head, found in one
1397/// case-insensitive pass over the raw bytes — the same answers as
1398/// [`response_has_no_body`], [`should_stream_response`] and the
1399/// `connection:` / `content-length:` scans over a lower-cased copy, without
1400/// making that copy.
1401#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1402#[doc(hidden)]
1403pub struct HeadFacts {
1404    /// The second whitespace-separated token of the head (the status code).
1405    #[doc(hidden)]
1406    pub status_no_body: bool,
1407    #[doc(hidden)]
1408    pub sse: bool,
1409    #[doc(hidden)]
1410    pub chunked: bool,
1411    /// The first `content-length:` value that parses.
1412    #[doc(hidden)]
1413    pub content_length: Option<usize>,
1414    #[doc(hidden)]
1415    pub connection_close: bool,
1416}
1417
1418#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1419impl HeadFacts {
1420    #[doc(hidden)]
1421    pub fn scan(head: &[u8]) -> HeadFacts {
1422        let text = std::str::from_utf8(head).unwrap_or("");
1423        let status = text.split_whitespace().nth(1).unwrap_or("");
1424        let mut facts = HeadFacts {
1425            status_no_body: status.starts_with('1') || status == "204" || status == "304",
1426            sse: false,
1427            chunked: false,
1428            content_length: None,
1429            connection_close: false,
1430        };
1431        for line in text.lines() {
1432            if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1433                facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1434            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1435                facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1436            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1437                if facts.content_length.is_none() {
1438                    facts.content_length = value.trim().parse().ok();
1439                }
1440            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1441                facts.connection_close |= contains_ignore_ascii_case(value, "close");
1442            }
1443        }
1444        facts
1445    }
1446
1447    /// [`response_has_no_body`].
1448    #[doc(hidden)]
1449    pub fn no_body(&self, method: &str) -> bool {
1450        method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1451    }
1452
1453    /// [`should_stream_response`].
1454    #[doc(hidden)]
1455    pub fn should_stream(&self) -> bool {
1456        self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1457    }
1458}
1459
1460fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1461    let bytes = line.as_bytes();
1462    // `prefix` is ASCII, so a byte match ends on a char boundary.
1463    (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1464        .then(|| &line[prefix.len()..])
1465}
1466
1467fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1468    haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1469}
1470
1471/// Parse the status line and headers from raw header bytes (ending at `\r\n\r\n`).
1472fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1473    let s = std::str::from_utf8(header_bytes)
1474        .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1475    let mut lines = s.lines();
1476    let status_line = lines.next().ok_or("empty backend response")?;
1477    let mut parts = status_line.splitn(3, ' ');
1478    let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1479    let status_code: i16 = parts
1480        .next()
1481        .unwrap_or("502")
1482        .parse()
1483        .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1484    let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1485    let mut headers = Vec::new();
1486    for line in lines {
1487        let line = line.trim_end_matches('\r');
1488        if line.is_empty() { break; }
1489        if let Some(colon) = line.find(':') {
1490            headers.push(crate::header::Header {
1491                name: line[..colon].trim().to_string().into(),
1492                value: (line[colon + 1..].trim().to_string()).into(),
1493            });
1494        }
1495    }
1496    Ok(Response {
1497        http_version,
1498        status_code,
1499        reason_phrase,
1500        headers,
1501        content_range_list: vec![],
1502        stream_file: None,
1503        stream_pipe: None,
1504        trailers: None,
1505    })
1506}
1507
1508/// Read the remaining body after headers have already been read.
1509///
1510/// `header_bytes` ends with `\r\n\r\n`; `body_prefix` holds any body bytes
1511/// that arrived in the same TCP read.  Handles all three body mechanisms
1512/// (chunked, content-length, read-to-EOF).  Returns `(full_response_bytes, can_reuse)`.
1513fn read_response_from_partial<S: Read>(
1514    stream: &mut S,
1515    header_bytes: Vec<u8>,
1516    body_prefix: Vec<u8>,
1517    tmp: &mut [u8],
1518) -> Result<(Vec<u8>, bool), String> {
1519    let header_end = header_bytes.len();
1520    let mut buf = header_bytes;
1521    buf.extend_from_slice(&body_prefix);
1522
1523    let header_lower =
1524        std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1525    let connection_close =
1526        header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1527    let is_chunked = header_lower
1528        .lines()
1529        .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1530    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1531        l.strip_prefix("content-length:")?.trim().parse().ok()
1532    });
1533
1534    if is_chunked {
1535        let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1536        rewrite_as_content_length(&mut buf, header_end, &decoded);
1537        Ok((buf, !connection_close))
1538    } else if let Some(len) = content_length {
1539        while buf.len() < header_end + len {
1540            let n = stream.read(tmp).map_err(|e| e.to_string())?;
1541            if n == 0 { break; }
1542            buf.extend_from_slice(&tmp[..n]);
1543        }
1544        Ok((buf, !connection_close))
1545    } else {
1546        loop {
1547            let n = stream.read(tmp).map_err(|e| e.to_string())?;
1548            if n == 0 { break; }
1549            buf.extend_from_slice(&tmp[..n]);
1550        }
1551        Ok((buf, false))
1552    }
1553}
1554
1555// ── Backend URL parsing ───────────────────────────────────────────────────────
1556
1557struct Backend {
1558    host: String,
1559    port: u16,
1560    /// Whether the upstream connection should use TLS.
1561    /// Set when the URL scheme is `https://`, `h2s://`, or `grpcs://`.
1562    tls: bool,
1563}
1564
1565impl Backend {
1566    fn parse(url: &str) -> Option<Self> {
1567        let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1568            (r, true, 443u16)
1569        } else if let Some(r) = url.strip_prefix("h2s://") {
1570            (r, true, 443u16)
1571        } else if let Some(r) = url.strip_prefix("grpcs://") {
1572            (r, true, 443u16)
1573        } else if let Some(r) = url.strip_prefix("http://") {
1574            (r, false, 80u16)
1575        } else if let Some(r) = url.strip_prefix("h2://") {
1576            (r, false, 80u16)
1577        } else if let Some(r) = url.strip_prefix("grpc://") {
1578            (r, false, 80u16)
1579        } else {
1580            (url, false, 80u16)
1581        };
1582        // Drop any path component.
1583        let host_port = rest.split('/').next().unwrap_or(rest);
1584        let (host, port) = if let Some(colon) = host_port.rfind(':') {
1585            let port_str = &host_port[colon + 1..];
1586            if let Ok(p) = port_str.parse::<u16>() {
1587                (host_port[..colon].to_string(), p)
1588            } else {
1589                (host_port.to_string(), default_port)
1590            }
1591        } else {
1592            (host_port.to_string(), default_port)
1593        };
1594        if host.is_empty() {
1595            return None;
1596        }
1597        Some(Backend { host, port, tls })
1598    }
1599}
1600
1601// ── HTTP/2 reverse proxy ──────────────────────────────────────────────────────
1602
1603/// Reverse proxy that forwards requests to HTTP/2 backends.
1604///
1605/// Wraps [`ReverseProxy`] and forces HTTP/2 (`h2`) for all upstream connections.
1606/// Requires the `http2` Cargo feature.
1607///
1608/// This proxy also transparently handles gRPC traffic
1609/// (`Content-Type: application/grpc*`) — gRPC DATA frames are forwarded
1610/// as-is because gRPC is layered directly on HTTP/2.
1611#[cfg(feature = "http2")]
1612pub struct H2ReverseProxy {
1613    inner: ReverseProxy,
1614}
1615
1616#[cfg(feature = "http2")]
1617impl H2ReverseProxy {
1618    /// Create a proxy distributing requests across `backends` in round-robin order.
1619    ///
1620    /// Each backend entry can be:
1621    /// - `"host:port"` — plain TCP (HTTP/2 cleartext)
1622    /// - `"h2://host:port"` — plain TCP (explicit scheme)
1623    /// - `"h2s://host:port"` — TLS (HTTP/2 over HTTPS; port defaults to 443)
1624    /// - `"https://host:port"` — TLS (same as `h2s://`)
1625    ///
1626    /// TLS backends require the `http2` Cargo feature (includes `rustls` +
1627    /// `webpki-roots`).  Certificate verification uses the WebPKI trust store.
1628    pub fn new<I, S>(backends: I) -> Self
1629    where
1630        I: IntoIterator<Item = S>,
1631        S: AsRef<str>,
1632    {
1633        H2ReverseProxy {
1634            inner: ReverseProxy::new(backends),
1635        }
1636    }
1637
1638    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1639    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1640        self.inner = self.inner.path_prefix(prefix);
1641        self
1642    }
1643
1644    /// Override the TCP connect timeout (default: 5 s).
1645    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1646        self.inner = self.inner.connect_timeout_ms(ms);
1647        self
1648    }
1649
1650    /// Override the read timeout (default: 30 s). It bounds each wait for
1651    /// the upstream — the handshake, the response head, every body frame — so
1652    /// a streaming response (a gRPC server stream) ends only when it goes
1653    /// quiet for that long.
1654    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1655        self.inner = self.inner.read_timeout_ms(ms);
1656        self
1657    }
1658
1659    /// Trust the CA certificates in the PEM file `path` for `h2s://` /
1660    /// `https://` backends instead of the WebPKI roots. Since 18.3.0.
1661    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1662        self.inner = self.inner.tls_ca_file(path)?;
1663        Ok(self)
1664    }
1665
1666    /// Present a client certificate to TLS backends that require one (mutual
1667    /// TLS). Since 18.3.0.
1668    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1669        self.inner = self.inner.tls_client_cert(cert, key)?;
1670        Ok(self)
1671    }
1672
1673    fn matches(&self, request: &Request) -> bool {
1674        self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1675    }
1676}
1677
1678#[cfg(feature = "http2")]
1679impl crate::middleware::Middleware for H2ReverseProxy {
1680    fn handle(
1681        &self,
1682        request: &crate::request::Request,
1683        connection: &crate::server::ConnectionInfo,
1684        next: &dyn crate::application::Application,
1685    ) -> Result<crate::response::Response, String> {
1686        if !self.matches(request) {
1687            return next.execute(request, connection);
1688        }
1689        if self.inner.backends.is_empty() {
1690            let mut response = bad_gateway();
1691            status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1692            return Ok(response);
1693        }
1694        // A streamed request body can be sent once: no second backend then.
1695        let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1696        let n = self.inner.backends.len();
1697        let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1698        let mut last_error = String::new();
1699        for attempt in 0..attempts {
1700            let backend = &self.inner.backends[(start + attempt) % n];
1701            let tls = backend.tls.then(|| match &self.inner.trust {
1702                Some(trust) => trust.h2_config(),
1703                None => h2_upstream::default_tls(),
1704            });
1705            let target = h2_upstream::Target {
1706                backend,
1707                tls,
1708                connect_timeout: self.inner.connect_timeout,
1709                read_timeout: self.inner.read_timeout,
1710                authority: None,
1711            };
1712            match h2_upstream::forward(request, Peer::of(connection), &target) {
1713                Ok(mut response) => {
1714                    status::received(&mut response, None);
1715                    return Ok(response);
1716                }
1717                Err(e) => last_error = e,
1718            }
1719        }
1720        let mut response = bad_gateway();
1721        status::failed_with(&mut response, &last_error, None);
1722        Ok(response)
1723    }
1724
1725    /// Headers only: a request this proxy doesn't forward passes through
1726    /// untouched.
1727    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1728        true
1729    }
1730
1731    /// Requests this proxy forwards are sent as their body arrives (gRPC
1732    /// client streaming and bidirectional calls).
1733    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1734        self.matches(request)
1735    }
1736}
1737
1738// ── HTTP/3 reverse proxy ──────────────────────────────────────────────────────
1739
1740/// Reverse proxy that forwards requests to `https://` backends over HTTP/3
1741/// (QUIC). Since 18.13.0; requires the `http3` feature (default).
1742///
1743/// A backend QUIC can't reach (UDP blocked, no handshake within the connect
1744/// timeout, a TLS failure) is sent the request over HTTPS on TCP instead,
1745/// and stays on TCP for five minutes; `rws_upstream_h3_fallbacks_total`
1746/// counts it under the proxy's [`name`](Self::name).
1747///
1748/// ```rust,no_run
1749/// # extern crate rws_proxy as rust_web_server;
1750/// use rust_web_server::app::App;
1751/// use rust_web_server::core::New;
1752/// use rust_web_server::proxy::H3ReverseProxy;
1753///
1754/// let proxy = H3ReverseProxy::new(["https://edge1.internal:443", "https://edge2.internal:443"])
1755///     .path_prefix("/api")
1756///     .tls_ca_file("/etc/rws/internal-ca.pem")
1757///     .unwrap();
1758/// let app = App::new().wrap(proxy);
1759/// ```
1760#[cfg(feature = "http3")]
1761pub struct H3ReverseProxy {
1762    inner: ReverseProxy,
1763    name: String,
1764    /// Backends on TCP until the time kept here.
1765    tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1766}
1767
1768#[cfg(feature = "http3")]
1769impl H3ReverseProxy {
1770    /// A proxy distributing requests across `backends` (`https://host:port`,
1771    /// port 443 by default) in round-robin order. Certificates are checked
1772    /// against the WebPKI roots unless [`tls_ca_file`](Self::tls_ca_file)
1773    /// says otherwise. A backend that isn't `https://` is reached over
1774    /// HTTP/1.1 on TCP.
1775    pub fn new<I, S>(backends: I) -> Self
1776    where
1777        I: IntoIterator<Item = S>,
1778        S: AsRef<str>,
1779    {
1780        H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1781    }
1782
1783    /// The `upstream` label of this proxy's fallback metric.
1784    pub fn name(mut self, name: impl Into<String>) -> Self {
1785        self.name = name.into();
1786        self
1787    }
1788
1789    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1790    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1791        self.inner = self.inner.path_prefix(prefix);
1792        self
1793    }
1794
1795    /// QUIC handshake (and TCP connect) timeout (default: 5 s).
1796    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1797        self.inner = self.inner.connect_timeout_ms(ms);
1798        self
1799    }
1800
1801    /// Longest wait for the backend — the response head and each body
1802    /// chunk (default: 30 s).
1803    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1804        self.inner = self.inner.read_timeout_ms(ms);
1805        self
1806    }
1807
1808    /// Trust the CA certificates in the PEM file `path` instead of the
1809    /// WebPKI roots.
1810    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1811        self.inner = self.inner.tls_ca_file(path)?;
1812        Ok(self)
1813    }
1814
1815    /// Present a client certificate to backends that require one.
1816    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1817        self.inner = self.inner.tls_client_cert(cert, key)?;
1818        Ok(self)
1819    }
1820
1821    fn matches(&self, request: &Request) -> bool {
1822        self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1823    }
1824
1825    fn on_tcp(&self, key: &str) -> bool {
1826        let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1827        match tcp_until.get(key) {
1828            Some(until) if std::time::Instant::now() < *until => true,
1829            Some(_) => {
1830                tcp_until.remove(key);
1831                false
1832            }
1833            None => false,
1834        }
1835    }
1836
1837    /// One attempt at `backend`: HTTP/3 unless it's on TCP, TCP when QUIC
1838    /// can't connect.
1839    fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1840        let key = format!("{}:{}", backend.host, backend.port);
1841        if backend.tls && !self.on_tcp(&key) {
1842            let config = match &self.inner.trust {
1843                Some(trust) => trust.client_config(),
1844                None => tls_client_config(),
1845            };
1846            let target = h3_upstream::Target {
1847                host: &backend.host,
1848                port: backend.port,
1849                authority: None,
1850                tls: &config,
1851                connect_timeout: self.inner.connect_timeout,
1852                read_timeout: self.inner.read_timeout,
1853            };
1854            match h3_upstream::forward(request, Peer::of(connection), &target) {
1855                Err(h3_upstream::H3Error::Connect(e)) => {
1856                    self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1857                    crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1858                    eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1859                }
1860                result => return result.map_err(|e| e.to_string()),
1861            }
1862        }
1863        forward_upstream_trusting(
1864            &self.inner.pool,
1865            request,
1866            Peer::of(connection),
1867            &backend.host,
1868            backend.port,
1869            backend.tls,
1870            self.inner.trust.as_deref(),
1871            self.inner.connect_timeout,
1872            self.inner.read_timeout,
1873        )
1874    }
1875}
1876
1877#[cfg(feature = "http3")]
1878impl crate::middleware::Middleware for H3ReverseProxy {
1879    fn handle(
1880        &self,
1881        request: &crate::request::Request,
1882        connection: &crate::server::ConnectionInfo,
1883        next: &dyn crate::application::Application,
1884    ) -> Result<crate::response::Response, String> {
1885        if !self.matches(request) {
1886            return next.execute(request, connection);
1887        }
1888        if self.inner.backends.is_empty() {
1889            let mut response = bad_gateway();
1890            status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1891            return Ok(response);
1892        }
1893        // A streamed request body can be sent once: no second backend then.
1894        let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1895        let n = self.inner.backends.len();
1896        let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1897        let mut last_error = String::new();
1898        for attempt in 0..attempts {
1899            match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1900                Ok(mut response) => {
1901                    status::received(&mut response, None);
1902                    return Ok(response);
1903                }
1904                Err(e) => last_error = e,
1905            }
1906        }
1907        let mut response = bad_gateway();
1908        status::failed_with(&mut response, &last_error, None);
1909        Ok(response)
1910    }
1911
1912    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1913        true
1914    }
1915
1916    /// Requests this proxy forwards are sent as their body arrives.
1917    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1918        self.matches(request)
1919    }
1920}
1921
1922/// Forward `request` over HTTP/2 to `url` (`h2://`, `grpc://`, `h2s://`,
1923/// `grpcs://`, `https://` or `host:port`) — the config file's `grpc` action.
1924/// `tls` forces TLS for a bare `host:port` (`[[upstream]] tls = true`).
1925#[cfg(feature = "http2")]
1926#[doc(hidden)]
1927pub fn forward_h2(
1928    request: &Request,
1929    client: Peer<'_>,
1930    url: &str,
1931    tls: bool,
1932    trust: Option<&UpstreamTls>,
1933    connect_timeout: Duration,
1934    read_timeout: Duration,
1935) -> Result<Response, String> {
1936    let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1937    backend.tls |= tls;
1938    let tls = backend.tls.then(|| match trust {
1939        Some(t) => t.h2_config(),
1940        None => h2_upstream::default_tls(),
1941    });
1942    h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1943}
1944
1945/// Forward over TLS to `ip:port` with `config`, `server_name` as SNI (and
1946/// the name `config`'s verifier checks), `host_header` as the `Host` — the
1947/// Gateway API router's BackendTLSPolicy backends. Connections are pooled
1948/// per `label` (one per distinct policy content).
1949#[cfg(feature = "gateway-api")]
1950#[allow(clippy::too_many_arguments)]
1951#[doc(hidden)]
1952pub fn forward_tls_with_host(
1953    config: &Arc<rustls::ClientConfig>,
1954    label: &str,
1955    request: &Request,
1956    client: Peer<'_>,
1957    ip: &str,
1958    port: u16,
1959    server_name: &str,
1960    host_header: &str,
1961    connect_timeout: Duration,
1962    read_timeout: Duration,
1963) -> Result<Response, String> {
1964    let pool = tls_pool_for_ca(label);
1965    forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1966        let tcp = connect_tcp(ip, port, connect_timeout)?;
1967        let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1968        let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1969        Ok(rustls::StreamOwned::new(conn, tcp))
1970    })
1971}
1972
1973/// Forward over cleartext HTTP/2 with prior knowledge (h2c) to `host:port`,
1974/// sending `authority` as `:authority` — the Gateway API router's
1975/// `kubernetes.io/h2c` backends.
1976#[cfg(feature = "gateway-api")]
1977#[doc(hidden)]
1978pub fn forward_h2c_with_host(
1979    request: &Request,
1980    client: Peer<'_>,
1981    host: &str,
1982    port: u16,
1983    authority: &str,
1984    connect_timeout: Duration,
1985    read_timeout: Duration,
1986) -> Result<Response, String> {
1987    let backend = Backend { host: host.to_string(), port, tls: false };
1988    h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1989}
1990
1991// ── gRPC proxy ────────────────────────────────────────────────────────────────
1992
1993/// gRPC reverse proxy middleware.
1994///
1995/// Recognises requests with `Content-Type: application/grpc*` and forwards them
1996/// to a backend over HTTP/2, leaving all other requests to the next layer.
1997///
1998/// Requires the `http2` Cargo feature.
1999///
2000/// # Example
2001///
2002/// ```rust,no_run
2003/// # extern crate rws_proxy as rust_web_server;
2004/// use rust_web_server::app::App;
2005/// use rust_web_server::core::New;
2006/// use rust_web_server::proxy::GrpcProxy;
2007///
2008/// let app = App::new()
2009///     .wrap(GrpcProxy::new(["grpc-service:50051"]));
2010/// ```
2011#[cfg(feature = "http2")]
2012pub struct GrpcProxy {
2013    inner: H2ReverseProxy,
2014}
2015
2016#[cfg(feature = "http2")]
2017impl GrpcProxy {
2018    /// Create a proxy distributing gRPC connections across `backends` in round-robin order.
2019    ///
2020    /// Each backend entry can be:
2021    /// - `"host:port"` — plain TCP (gRPC cleartext)
2022    /// - `"grpc://host:port"` — plain TCP (explicit scheme)
2023    /// - `"grpcs://host:port"` — TLS (gRPC over TLS; port defaults to 443)
2024    /// - `"https://host:port"` — TLS (same as `grpcs://`)
2025    ///
2026    /// TLS backends require the `http2` Cargo feature.
2027    pub fn new<I, S>(backends: I) -> Self
2028    where
2029        I: IntoIterator<Item = S>,
2030        S: AsRef<str>,
2031    {
2032        GrpcProxy { inner: H2ReverseProxy::new(backends) }
2033    }
2034
2035    /// Only proxy requests whose URI starts with `prefix`; pass others through.
2036    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
2037        self.inner = self.inner.path_prefix(prefix);
2038        self
2039    }
2040
2041    /// See [`H2ReverseProxy::connect_timeout_ms`].
2042    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
2043        self.inner = self.inner.connect_timeout_ms(ms);
2044        self
2045    }
2046
2047    /// See [`H2ReverseProxy::read_timeout_ms`]: a stream quiet for longer ends.
2048    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
2049        self.inner = self.inner.read_timeout_ms(ms);
2050        self
2051    }
2052
2053    /// Trust this CA for `grpcs://` backends. Since 18.3.0.
2054    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
2055        self.inner = self.inner.tls_ca_file(path)?;
2056        Ok(self)
2057    }
2058
2059    /// Present a client certificate to `grpcs://` backends (mutual TLS).
2060    /// Since 18.3.0.
2061    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
2062        self.inner = self.inner.tls_client_cert(cert, key)?;
2063        Ok(self)
2064    }
2065}
2066
2067#[cfg(feature = "http2")]
2068impl crate::middleware::Middleware for GrpcProxy {
2069    fn handle(
2070        &self,
2071        request: &crate::request::Request,
2072        connection: &crate::server::ConnectionInfo,
2073        next: &dyn crate::application::Application,
2074    ) -> Result<crate::response::Response, String> {
2075        if is_grpc(request) {
2076            self.inner.handle(request, connection, next)
2077        } else {
2078            next.execute(request, connection)
2079        }
2080    }
2081
2082    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
2083        true
2084    }
2085
2086    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
2087        is_grpc(request) && self.inner.matches(request)
2088    }
2089}
2090
2091#[cfg(feature = "http2")]
2092fn is_grpc(request: &Request) -> bool {
2093    request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
2094}
2095
2096// ── Backend::parse unit tests ─────────────────────────────────────────────────
2097
2098#[cfg(test)]
2099mod backend_parse_tests {
2100    use super::Backend;
2101
2102    fn parse(url: &str) -> Option<(String, u16, bool)> {
2103        Backend::parse(url).map(|b| (b.host, b.port, b.tls))
2104    }
2105
2106    #[test]
2107    fn bare_host_port() {
2108        assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
2109    }
2110
2111    #[test]
2112    fn http_scheme() {
2113        assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
2114    }
2115
2116    #[test]
2117    fn h2_scheme_plain() {
2118        assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
2119    }
2120
2121    #[test]
2122    fn grpc_scheme_plain() {
2123        assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
2124    }
2125
2126    #[test]
2127    fn https_scheme_sets_tls_and_default_port() {
2128        assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
2129    }
2130
2131    #[test]
2132    fn https_scheme_explicit_port() {
2133        assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
2134    }
2135
2136    #[test]
2137    fn h2s_scheme_sets_tls() {
2138        assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
2139    }
2140
2141    #[test]
2142    fn h2s_scheme_explicit_port() {
2143        assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
2144    }
2145
2146    #[test]
2147    fn grpcs_scheme_sets_tls() {
2148        assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
2149    }
2150
2151    #[test]
2152    fn grpcs_scheme_explicit_port() {
2153        assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
2154    }
2155
2156    #[test]
2157    fn empty_host_returns_none() {
2158        assert_eq!(None, parse("https://"));
2159    }
2160
2161    #[test]
2162    fn bare_host_no_port_defaults_to_80() {
2163        assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
2164    }
2165}