fix(dl): only log in when qBittorrent asks
Logging in before the first call turned a stale or wrong password into a hard failure on calls that would have succeeded: an instance that bypasses authentication for arr's address answers everything, and never needed the credentials at all. Authentication is now established on the first 403. The health probe was reading a 403 from `app/version` as healthy, on the reasoning that a live daemon is all the lamp claims. That is exactly the state arr cannot use, and it left the lamp green while every grab and every reaper tick failed on rejected credentials. It now lists torrents through the configured client, so the lamp fails when arr's own calls do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab4f95b166
commit
9ecbfde656
@@ -181,21 +181,21 @@ async fn probe_prowlarr(state: &AppState) -> Check {
|
||||
}
|
||||
}
|
||||
|
||||
/// A live `WebUI` answers `/api/v2/app/version`, and answers `403` when the
|
||||
/// probe carries no session. Both are a running daemon, which is all this
|
||||
/// lamp claims; whether arr's credentials work shows up on the first grab.
|
||||
/// The probe is a real torrent list through the configured client, not a bare
|
||||
/// reachability check.
|
||||
///
|
||||
/// A `WebUI` that wants credentials answers `403` to anything unauthenticated,
|
||||
/// so "the port is open" is true of an instance arr cannot use at all. Reading
|
||||
/// that as healthy once left the lamp green while every grab and every reaper
|
||||
/// tick failed on a rejected password, which is the one thing this lamp exists
|
||||
/// to catch.
|
||||
async fn probe_qbit(state: &AppState) -> Check {
|
||||
let base = state.upstreams().qbittorrent_url.trim_end_matches('/');
|
||||
let request = state.http().get(format!("{base}/api/v2/app/version"));
|
||||
match request.send().await {
|
||||
Ok(response)
|
||||
if response.status().is_success()
|
||||
|| response.status() == reqwest::StatusCode::FORBIDDEN =>
|
||||
{
|
||||
Check::ok()
|
||||
}
|
||||
Ok(response) => Check::unreachable(format!("http {}", response.status().as_u16())),
|
||||
Err(err) => Check::unreachable(describe(err)),
|
||||
let Some(qbit) = state.qbit() else {
|
||||
return Check::unconfigured("no qBittorrent client is configured");
|
||||
};
|
||||
match qbit.list_torrents().await {
|
||||
Ok(_) => Check::ok(),
|
||||
Err(error) => Check::unreachable(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,8 +191,8 @@ mod tests {
|
||||
|
||||
let qbit = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v2/app/version"))
|
||||
.respond_with(ResponseTemplate::new(403).set_body_string("Forbidden"))
|
||||
.and(path("/api/v2/torrents/info"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
|
||||
.mount(&qbit)
|
||||
.await;
|
||||
|
||||
@@ -237,6 +237,7 @@ mod tests {
|
||||
.with_tmdb_api_key(Some("key".into())),
|
||||
)
|
||||
.expect("state")
|
||||
.with_qbit(arr_dl::QbitClient::new(&qbit.uri()).expect("client"))
|
||||
// The subtitle binaries default to `PATH`; pin them to something
|
||||
// every machine has so this test stays about the classic upstreams.
|
||||
.with_syncer(arr_subs::Syncer::new().with_binary("sh"))
|
||||
@@ -253,7 +254,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn a_missing_tmdb_key_is_unconfigured_not_an_outage() {
|
||||
let (prowlarr, qbit) = upstreams_up().await;
|
||||
let state = AppState::new(Upstreams::new(prowlarr.uri(), qbit.uri())).expect("state");
|
||||
let state = AppState::new(Upstreams::new(prowlarr.uri(), qbit.uri()))
|
||||
.expect("state")
|
||||
.with_qbit(arr_dl::QbitClient::new(&qbit.uri()).expect("client"));
|
||||
|
||||
let body = report(state).await;
|
||||
assert_eq!(body["tmdb"]["status"], "unconfigured");
|
||||
@@ -266,8 +269,9 @@ mod tests {
|
||||
|
||||
// Port 1 is privileged and nothing binds it, so the probe gets a
|
||||
// refused connection immediately instead of waiting out the timeout.
|
||||
let state =
|
||||
AppState::new(Upstreams::new("http://127.0.0.1:1".into(), qbit.uri())).expect("state");
|
||||
let state = AppState::new(Upstreams::new("http://127.0.0.1:1".into(), qbit.uri()))
|
||||
.expect("state")
|
||||
.with_qbit(arr_dl::QbitClient::new(&qbit.uri()).expect("client"));
|
||||
|
||||
let body = report(state).await;
|
||||
assert_eq!(body["status"], "degraded");
|
||||
@@ -307,7 +311,8 @@ mod tests {
|
||||
.with_tmdb_url(tmdb.uri())
|
||||
.with_tmdb_api_key(Some("super-secret".into())),
|
||||
)
|
||||
.expect("state");
|
||||
.expect("state")
|
||||
.with_qbit(arr_dl::QbitClient::new(&qbit.uri()).expect("client"));
|
||||
|
||||
let body = report(state).await;
|
||||
assert_eq!(body["tmdb"]["status"], "unreachable");
|
||||
|
||||
Reference in New Issue
Block a user