Retry weather fetch on transient failures

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean Chevronnet 2026-07-29 18:08:43 +00:00
parent ea26e25546
commit f3cb9a6f30
No known key found for this signature in database
GPG key ID: 439666D63A9477E4

View file

@ -111,46 +111,93 @@ impl Store {
fn fetch_weather(location: &str) -> Result<String, String> { fn fetch_weather(location: &str) -> Result<String, String> {
let path = format!("/{}?format=%l:+%C+%t+feels+%f+%w+%h&m", url_encode(location)); let path = format!("/{}?format=%l:+%C+%t+feels+%f+%w+%h&m", url_encode(location));
let body = https_get("wttr.in", &path)?; let mut last = "no response".to_string();
let line: String = body for attempt in 0..3 {
.lines() if attempt > 0 {
.map(str::trim) std::thread::sleep(Duration::from_millis(250));
.find(|l| !l.is_empty()) }
.unwrap_or("") match https_get("wttr.in", &path) {
.chars() Ok(body) => {
.take(250) let line: String = body
.collect(); .lines()
if line.is_empty() { .map(str::trim)
return Err("no data".to_string()); .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<String, String> { struct HttpErr {
msg: String,
retry: bool,
}
fn https_get(host: &str, path: &str) -> Result<String, HttpErr> {
let addr = (host, 443) let addr = (host, 443)
.to_socket_addrs() .to_socket_addrs()
.map_err(|e| e.to_string())? .map_err(|e| HttpErr { msg: e.to_string(), retry: false })?
.next() .next()
.ok_or_else(|| "dns lookup failed".to_string())?; .ok_or(HttpErr { msg: "dns lookup failed".to_string(), retry: false })?;
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| e.to_string())?; 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_read_timeout(Some(Duration::from_secs(5))).ok();
stream.set_write_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 connector = TlsConnector::new().map_err(|e| HttpErr { msg: e.to_string(), retry: false })?;
let mut tls = connector.connect(host, stream).map_err(|e| e.to_string())?; let mut tls = connector
.connect(host, stream)
.map_err(|e| HttpErr { msg: e.to_string(), retry: true })?;
let req = format!( 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" "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(); 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); let text = String::from_utf8_lossy(&buf);
match text.split_once("\r\n\r\n") { let Some((head, body)) = text.split_once("\r\n\r\n") else {
Some((_, body)) => Ok(body.to_string()), return Err(HttpErr { msg: "malformed response".to_string(), retry: true });
None => Err("malformed response".to_string()), };
let code = head
.lines()
.next()
.unwrap_or("")
.split_whitespace()
.nth(1)
.and_then(|c| c.parse::<u16>().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 { fn url_encode(s: &str) -> String {