Wire upstream_connect_timeout_secs to enforce separate connect timeout

Implement two-phase timeout in proxy_handler:
- Inner timeout uses per-site upstream_connect_timeout_secs (default 5s)
  for the connect + first-byte phase
- Outer timeout uses upstream_request_timeout_secs (default 60s) for the
  full request/response cycle
- Set connect_timeout on HttpConnector for both HTTP and HTTPS clients
  (default 5s) to enforce TCP-level connect timeouts
- Use wrap_connector for HTTPS client to apply connect_timeout on the
  underlying HttpConnector
- Add Ok(Err(_)) handler for connect timeout returning 504 Gateway Timeout
This commit is contained in:
2026-06-12 05:01:54 +00:00
parent 1da01a2336
commit 0c769e682e

View File

@@ -80,23 +80,23 @@ async fn proxy_handler(
} }
}; };
let connect_timeout = Duration::from_secs(site.upstream_connect_timeout_secs);
let request_timeout = Duration::from_secs(site.upstream_request_timeout_secs); let request_timeout = Duration::from_secs(site.upstream_request_timeout_secs);
// The timeout covers the entire HTTP round-trip including response body
// streaming. For large file downloads or slow upstreams, this means the
// timeout kills the response even if the upstream is actively sending data.
// A more precise timeout would apply only to connect + first-byte, then
// stream the body without a timeout. The `upstream_connect_timeout_secs`
// field in SiteConfig exists for a separate connect timeout (see W4).
// For Phase 1, this full-request timeout is acceptable.
let result = if upstream_scheme == "https" { let result = if upstream_scheme == "https" {
tokio::time::timeout(request_timeout, state.https_client.request(upstream_req)).await tokio::time::timeout(request_timeout, async {
tokio::time::timeout(connect_timeout, state.https_client.request(upstream_req)).await
})
.await
} else { } else {
tokio::time::timeout(request_timeout, state.http_client.request(upstream_req)).await tokio::time::timeout(request_timeout, async {
tokio::time::timeout(connect_timeout, state.http_client.request(upstream_req)).await
})
.await
}; };
match result { match result {
Ok(Ok(upstream_resp)) => { Ok(Ok(Ok(upstream_resp))) => {
let status = upstream_resp.status().as_u16(); let status = upstream_resp.status().as_u16();
let duration_ms = start.elapsed().as_millis() as u64; let duration_ms = start.elapsed().as_millis() as u64;
log_request!( log_request!(
@@ -114,7 +114,7 @@ async fn proxy_handler(
let body = Body::new(body); let body = Body::new(body);
Response::from_parts(parts, body) Response::from_parts(parts, body)
} }
Ok(Err(e)) => { Ok(Ok(Err(e))) => {
let duration_ms = start.elapsed().as_millis() as u64; let duration_ms = start.elapsed().as_millis() as u64;
if e.is_connect() { if e.is_connect() {
log_upstream_error!(&host_owned, &upstream_addr, &format!("{}", e)); log_upstream_error!(&host_owned, &upstream_addr, &format!("{}", e));
@@ -148,6 +148,21 @@ async fn proxy_handler(
resp resp
} }
} }
Ok(Err(_)) => {
let duration_ms = start.elapsed().as_millis() as u64;
log_upstream_error!(&host_owned, &upstream_addr, "upstream connect timeout");
let resp = ProxyError::UpstreamTimeout.into_response();
log_request!(
&client_ip,
&host_owned,
&method,
&path,
504u16,
&upstream,
duration_ms
);
resp
}
Err(_) => { Err(_) => {
let duration_ms = start.elapsed().as_millis() as u64; let duration_ms = start.elapsed().as_millis() as u64;
log_upstream_error!(&host_owned, &upstream_addr, "upstream timeout"); log_upstream_error!(&host_owned, &upstream_addr, "upstream timeout");
@@ -192,13 +207,21 @@ fn build_upstream_request(req: Request<Body>, upstream_uri: &Uri) -> anyhow::Res
builder.body(req.into_body()).map_err(Into::into) builder.body(req.into_body()).map_err(Into::into)
} }
const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 5;
pub fn create_http_client() -> Client<HttpConnector, Body> { pub fn create_http_client() -> Client<HttpConnector, Body> {
let mut connector = HttpConnector::new();
connector.set_connect_timeout(Some(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS)));
Client::builder(TokioExecutor::new()) Client::builder(TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(90)) .pool_idle_timeout(Duration::from_secs(90))
.build_http() .build(connector)
} }
pub fn create_https_client() -> Client<hyper_rustls::HttpsConnector<HttpConnector>, Body> { pub fn create_https_client() -> Client<hyper_rustls::HttpsConnector<HttpConnector>, Body> {
let mut http_connector = HttpConnector::new();
http_connector.set_connect_timeout(Some(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS)));
http_connector.enforce_http(false);
let tls_config = rustls::ClientConfig::builder() let tls_config = rustls::ClientConfig::builder()
.with_root_certificates(root_certs()) .with_root_certificates(root_certs())
.with_no_client_auth(); .with_no_client_auth();
@@ -207,7 +230,7 @@ pub fn create_https_client() -> Client<hyper_rustls::HttpsConnector<HttpConnecto
.with_tls_config(tls_config) .with_tls_config(tls_config)
.https_or_http() .https_or_http()
.enable_http1() .enable_http1()
.build(); .wrap_connector(http_connector);
Client::builder(TokioExecutor::new()) Client::builder(TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(90)) .pool_idle_timeout(Duration::from_secs(90))