fix(qbittorrent): reuse authenticated client across widget fetches
QbittorrentWidgetSource built a brand-new QbittorrentClient on every fetch, logging in each time. With three qBittorrent widgets polling every 5-30s and qBittorrent verifying passwords with slow PBKDF2 hashing, the concurrent login load saturates its web thread pool and the reverse proxy returns 504 gateway timeouts on /api/v2/auth/login. The client was already designed for reuse (login once, SID cookie reuse, 403 re-login) — the source just wasn't using it. Cache one QbittorrentClient per service (lru_cache keyed by service id, URL, credentials, timeout) so the SID cookie persists across fetches and login happens once. Mirrors dependencies._jellyfin_client_for. A credentials/URL change produces a new cache key, so stale clients aren't reused after reconfiguration. Also surface 502/503/504 from the login as a clear "reverse proxy returned HTTP <code> ... qBittorrent may be down/starting/overloaded" RuntimeError instead of a bare HTTPError, so future gateway issues read as infrastructure, not auth. Tests: autouse fixture clears the client cache between tests; new gateway-error login test. 385/385 backend tests pass; ruff clean.
This commit is contained in:
@@ -61,6 +61,17 @@ class QbittorrentClient:
|
||||
timeout=self.timeout,
|
||||
headers={"Referer": self.base_url},
|
||||
)
|
||||
# 502/503/504 come from the reverse proxy when qBittorrent is down,
|
||||
# starting up, or can't answer within the proxy's forwarding timeout
|
||||
# (qBittorrent's PBKDF2 password check is intentionally slow, so a
|
||||
# flood of concurrent logins can trip this). Surface it clearly rather
|
||||
# than as a bare HTTPError.
|
||||
if resp.status_code in (502, 503, 504):
|
||||
raise RuntimeError(
|
||||
f"qBittorrent login failed: reverse proxy returned HTTP {resp.status_code} "
|
||||
f"for {resp.url}. qBittorrent may be down, starting up, or unable to "
|
||||
"answer within the proxy's forwarding timeout."
|
||||
)
|
||||
resp.raise_for_status()
|
||||
body = resp.text.strip()
|
||||
# Some reverse proxies forward the SID cookie but mangle the text body;
|
||||
|
||||
Reference in New Issue
Block a user