Retry weather fetch on transient failures
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea26e25546
commit
f3cb9a6f30
1 changed files with 70 additions and 23 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue