From d807548b43b073e446b8585099457c8a5f1fcd0e Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Sat, 19 Sep 2026 21:44:26 +0200 Subject: [PATCH] Fix: is_reachable() akzeptiert 401/403, aber nicht dauerhafte 5xx-Fehler Der HTTP-Zweig von is_reachable() nutzte curl --fail, was einen HTTP-Server, der auf das HEAD mit 401/403 (Auth erforderlich, aber erreichbar) antwortet, faelschlich als nicht erreichbar meldete - relevant fuer WebDAV-Cloud-Seiten, die Auth verlangen. Der tatsaechliche Statuscode wird jetzt ausgelesen: nur ein Serverfehler (5xx) gilt als nicht erreichbar, da ein dauerhaft 5xx-antwortender Server (z. B. ein kaputter Reverse-Proxy) sonst bei jedem watch-Durchlauf einen vollen, letztlich erfolglosen Umschaltversuch (inkl. MOUNT_TIMEOUT_SECS-Wartezeit) ausloesen wuerde. Co-Authored-By: Claude Sonnet 5 --- src/network/mod.rs | 69 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/network/mod.rs b/src/network/mod.rs index d5151b3..4531136 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -9,15 +9,37 @@ use std::process::{Command, Stdio}; /// (HTTP HEAD via `curl`) oder eine reine Host-/IP-Adresse (ICMP-Ping) handelt. /// /// Portiert aus dem alten `src/network/utils.rs` (v0.2.0). +/// +/// Für HTTP wird bewusst NICHT `curl --fail` verwendet: ein WebDAV-Server, der auf das HEAD +/// mit 401/403 (Auth erforderlich) antwortet, ist trotzdem erreichbar - `--fail` würde das als +/// Fehler werten. Der tatsächliche Statuscode wird stattdessen ausgelesen und geprüft: nur ein +/// Serverfehler (5xx) gilt als NICHT erreichbar, da ein dauerhaft 5xx-antwortender Server +/// (z. B. ein kaputter Reverse-Proxy) sonst bei jedem `watch`-Durchlauf einen vollen, +/// letztlich erfolglosen Umschaltversuch (inkl. `MOUNT_TIMEOUT_SECS`-Wartezeit) auslösen würde. pub fn is_reachable(addr: &str) -> bool { if addr.starts_with("http://") || addr.starts_with("https://") { Command::new("curl") - .args(["--head", "--silent", "--fail", "--max-time", "5", addr]) - .stdout(Stdio::null()) + .args([ + "--head", + "--silent", + "--max-time", + "5", + "--output", + "/dev/null", + "--write-out", + "%{http_code}", + addr, + ]) .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) + .output() + .ok() + .and_then(|o| { + String::from_utf8_lossy(&o.stdout) + .trim() + .parse::() + .ok() + }) + .is_some_and(|code| (100..500).contains(&code)) } else { Command::new("ping") .args(["-c", "1", "-W", "2", addr]) @@ -28,3 +50,40 @@ pub fn is_reachable(addr: &str) -> bool { .unwrap_or(false) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::net::TcpListener; + + #[test] + fn http_401_endpoint_is_reported_as_reachable() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let port = listener.local_addr().expect("port").port(); + + std::thread::spawn(move || { + if let Ok((mut stream, _)) = listener.accept() { + let response = "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n"; + let _ = stream.write_all(response.as_bytes()); + } + }); + + assert!(is_reachable(&format!("http://127.0.0.1:{port}"))); + } + + #[test] + fn http_500_endpoint_is_reported_as_unreachable() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let port = listener.local_addr().expect("port").port(); + + std::thread::spawn(move || { + if let Ok((mut stream, _)) = listener.accept() { + let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n"; + let _ = stream.write_all(response.as_bytes()); + } + }); + + assert!(!is_reachable(&format!("http://127.0.0.1:{port}"))); + } +}