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 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 21:44:26 +02:00
co-authored by Claude Sonnet 5
parent 38c95cc4bf
commit d807548b43
+64 -5
View File
@@ -9,15 +9,37 @@ use std::process::{Command, Stdio};
/// (HTTP HEAD via `curl`) oder eine reine Host-/IP-Adresse (ICMP-Ping) handelt. /// (HTTP HEAD via `curl`) oder eine reine Host-/IP-Adresse (ICMP-Ping) handelt.
/// ///
/// Portiert aus dem alten `src/network/utils.rs` (v0.2.0). /// 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 { pub fn is_reachable(addr: &str) -> bool {
if addr.starts_with("http://") || addr.starts_with("https://") { if addr.starts_with("http://") || addr.starts_with("https://") {
Command::new("curl") Command::new("curl")
.args(["--head", "--silent", "--fail", "--max-time", "5", addr]) .args([
.stdout(Stdio::null()) "--head",
"--silent",
"--max-time",
"5",
"--output",
"/dev/null",
"--write-out",
"%{http_code}",
addr,
])
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .output()
.map(|s| s.success()) .ok()
.unwrap_or(false) .and_then(|o| {
String::from_utf8_lossy(&o.stdout)
.trim()
.parse::<u16>()
.ok()
})
.is_some_and(|code| (100..500).contains(&code))
} else { } else {
Command::new("ping") Command::new("ping")
.args(["-c", "1", "-W", "2", addr]) .args(["-c", "1", "-W", "2", addr])
@@ -28,3 +50,40 @@ pub fn is_reachable(addr: &str) -> bool {
.unwrap_or(false) .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}")));
}
}