From 7fefcbac46509ca950646b7528940ca0070b6123 Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 29 Jul 2026 18:08:43 +0000 Subject: [PATCH] Retry weather fetch on transient failures --- src/bot/modules/weather.rs | 93 ++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/src/bot/modules/weather.rs b/src/bot/modules/weather.rs index 2ff4263..0f92267 100644 --- a/src/bot/modules/weather.rs +++ b/src/bot/modules/weather.rs @@ -111,46 +111,93 @@ impl Store { fn fetch_weather(location: &str) -> Result { let path = format!("/{}?format=%l:+%C+%t+feels+%f+%w+%h&m", url_encode(location)); - let body = https_get("wttr.in", &path)?; - let line: String = body - .lines() - .map(str::trim) - .find(|l| !l.is_empty()) - .unwrap_or("") - .chars() - .take(250) - .collect(); - if line.is_empty() { - return Err("no data".to_string()); + let mut last = "no response".to_string(); + for attempt in 0..3 { + if attempt > 0 { + std::thread::sleep(Duration::from_millis(250)); + } + match https_get("wttr.in", &path) { + Ok(body) => { + let line: String = body + .lines() + .map(str::trim) + .find(|l| !l.is_empty()) + .unwrap_or("") + .chars() + .take(250) + .collect(); + if !line.is_empty() { + return Ok(line); + } + last = "empty response".to_string(); + } + Err(e) => { + let retry = e.retry; + last = e.msg; + if !retry { + break; + } + } + } } - Ok(line) + Err(last) } -fn https_get(host: &str, path: &str) -> Result { +struct HttpErr { + msg: String, + retry: bool, +} + +fn https_get(host: &str, path: &str) -> Result { let addr = (host, 443) .to_socket_addrs() - .map_err(|e| e.to_string())? + .map_err(|e| HttpErr { msg: e.to_string(), retry: false })? .next() - .ok_or_else(|| "dns lookup failed".to_string())?; - let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| e.to_string())?; + .ok_or(HttpErr { msg: "dns lookup failed".to_string(), retry: false })?; + let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(4)) + .map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?; stream.set_read_timeout(Some(Duration::from_secs(5))).ok(); stream.set_write_timeout(Some(Duration::from_secs(5))).ok(); - let connector = TlsConnector::new().map_err(|e| e.to_string())?; - let mut tls = connector.connect(host, stream).map_err(|e| e.to_string())?; + let connector = TlsConnector::new().map_err(|e| HttpErr { msg: e.to_string(), retry: false })?; + let mut tls = connector + .connect(host, stream) + .map_err(|e| HttpErr { msg: e.to_string(), retry: true })?; let req = format!( "GET {path} HTTP/1.0\r\nHost: {host}\r\nUser-Agent: curl/rubot\r\nAccept: text/plain\r\nConnection: close\r\n\r\n" ); - tls.write_all(req.as_bytes()).map_err(|e| e.to_string())?; + tls.write_all(req.as_bytes()) + .map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?; let mut buf = Vec::new(); - tls.read_to_end(&mut buf).map_err(|e| e.to_string())?; + tls.read_to_end(&mut buf) + .map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?; + let text = String::from_utf8_lossy(&buf); - match text.split_once("\r\n\r\n") { - Some((_, body)) => Ok(body.to_string()), - None => Err("malformed response".to_string()), + let Some((head, body)) = text.split_once("\r\n\r\n") else { + return Err(HttpErr { msg: "malformed response".to_string(), retry: true }); + }; + let code = head + .lines() + .next() + .unwrap_or("") + .split_whitespace() + .nth(1) + .and_then(|c| c.parse::().ok()) + .unwrap_or(0); + if !(200..300).contains(&code) { + return Err(HttpErr { retry: code == 429 || code >= 500, msg: format!("http {code}") }); } + Ok(body.to_string()) +} + +fn retryable_io(e: &io::Error) -> bool { + use io::ErrorKind::{BrokenPipe, ConnectionAborted, ConnectionRefused, ConnectionReset, UnexpectedEof}; + matches!( + e.kind(), + ConnectionReset | ConnectionAborted | ConnectionRefused | BrokenPipe | UnexpectedEof + ) } fn url_encode(s: &str) -> String {