Skip to main content

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