From dbbc9c00cc6402d076b52771eee953f6a9177d74 Mon Sep 17 00:00:00 2001 From: M1 Date: Wed, 18 Mar 2026 12:33:22 +0400 Subject: [PATCH] fix: run cert check concurrently with request so a hanging TCP connect can't block the timeout --- apps/monitor/src/runner.rs | 64 +++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/apps/monitor/src/runner.rs b/apps/monitor/src/runner.rs index bac76ad..8d35ec0 100644 --- a/apps/monitor/src/runner.rs +++ b/apps/monitor/src/runner.rs @@ -70,16 +70,6 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op let method = monitor.method.as_deref().unwrap_or("GET").to_uppercase(); let timeout = std::time::Duration::from_millis(monitor.timeout_ms.unwrap_or(30000)); - // Check cert expiry for HTTPS URLs — bounded by the same timeout - let cert_expiry_days = if monitor.url.starts_with("https://") { - match tokio::time::timeout(timeout, check_cert_expiry(&monitor.url)).await { - Ok(Ok(days)) => days, - _ => None, - } - } else { - None - }; - let req_method = reqwest::Method::from_bytes(method.as_bytes()) .unwrap_or(reqwest::Method::GET); @@ -100,27 +90,43 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op req = req.body(body.clone()); } - // Wrap the entire request + body read in a single timeout so slow response - // bodies don't slip through after headers arrive within the window. - let timed = tokio::time::timeout(timeout, async { - let resp = req.send().await?; - let status = resp.status(); - let headers: HashMap = resp.headers().iter() - .filter_map(|(k, v)| Some((k.to_string(), v.to_str().ok()?.to_string()))) - .collect(); + let is_https = monitor.url.starts_with("https://"); + let url_clone = monitor.url.clone(); - const MAX_BODY_BYTES: usize = 10 * 1024 * 1024; - let body = { - let content_len = resp.content_length().unwrap_or(0) as usize; - if content_len > MAX_BODY_BYTES { - format!("[body truncated: Content-Length {} exceeds 10MB limit]", content_len) + // Run the HTTP request and cert check concurrently, both under the same timeout. + // This prevents a hanging TCP connect in the cert check from blocking the whole check. + let timed = tokio::time::timeout(timeout, async { + let cert_future = async { + if is_https { + check_cert_expiry(&url_clone).await.ok().flatten() } else { - let bytes = resp.bytes().await?; - let truncated = &bytes[..bytes.len().min(MAX_BODY_BYTES)]; - String::from_utf8_lossy(truncated).into_owned() + None } }; - Ok::<_, reqwest::Error>((status, headers, body)) + + let req_future = async { + let resp = req.send().await?; + let status = resp.status(); + let headers: HashMap = resp.headers().iter() + .filter_map(|(k, v)| Some((k.to_string(), v.to_str().ok()?.to_string()))) + .collect(); + + const MAX_BODY_BYTES: usize = 10 * 1024 * 1024; + let body = { + let content_len = resp.content_length().unwrap_or(0) as usize; + if content_len > MAX_BODY_BYTES { + format!("[body truncated: Content-Length {} exceeds 10MB limit]", content_len) + } else { + let bytes = resp.bytes().await?; + let truncated = &bytes[..bytes.len().min(MAX_BODY_BYTES)]; + String::from_utf8_lossy(truncated).into_owned() + } + }; + Ok::<_, reqwest::Error>((status, headers, body)) + }; + + let (cert_result, req_result) = tokio::join!(cert_future, req_future); + req_result.map(|(status, headers, body)| (status, headers, body, cert_result)) }).await; let latency_ms = start.elapsed().as_millis() as u64; @@ -141,10 +147,10 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op latency_ms: Some(latency_ms), up: false, error: Some(e), - cert_expiry_days, + cert_expiry_days: None, meta: None, }, - Ok((status_raw, headers, body)) => { + Ok((status_raw, headers, body, cert_expiry_days)) => { let status = status_raw.as_u16(); // Evaluate query if present