fix(qbittorrent): recognize QBT_SID session cookie (newer qBittorrent)
Newer qBittorrent renamed its session cookie from "SID" to "QBT_SID" / "QBT_SID_<port>" (the diagnostic revealed cookies=['QBT_SID_5080']). The client only accepted "SID", so a valid login (cookie present in the jar) was reported as "Unexpected response". Re-entering correct credentials never helped because login was succeeding all along. Treat any cookie named "SID" OR starting with "QBT_SID" as the session cookie, checked in both the parsed jar and the raw Set-Cookie header. qBittorrent only sets this cookie on a valid login, so it stays authoritative. Diagnostic message updated to mention both names. New regression test covers the QBT_SID_<port> case. 388/388 backend tests pass; ruff clean.
This commit is contained in:
@@ -75,14 +75,20 @@ class QbittorrentClient:
|
||||
resp.raise_for_status()
|
||||
body = resp.text.strip()
|
||||
# qBittorrent signals a successful login with the body "Ok." and/or by
|
||||
# setting a SID session cookie. Some setups (newer qBittorrent, or some
|
||||
# reverse proxies) return 204 No Content with the SID cookie and no
|
||||
# body. ``requests`` can also fail to populate the cookie jar for
|
||||
# cookies with attributes it doesn't parse, so check the raw
|
||||
# Set-Cookie header as well. qBittorrent only sets SID on a valid login.
|
||||
# setting a session cookie. The cookie is named "SID" in older versions
|
||||
# and "QBT_SID" / "QBT_SID_<port>" in newer ones. Some setups return 204
|
||||
# No Content with the cookie and no body, and ``requests`` doesn't always
|
||||
# populate the cookie jar, so check both the jar and the raw Set-Cookie
|
||||
# header. qBittorrent only sets this cookie on a valid login.
|
||||
def _is_session_cookie(name: str) -> bool:
|
||||
upper = name.strip().upper()
|
||||
return upper == "SID" or upper.startswith("QBT_SID")
|
||||
|
||||
set_cookie_hdr = resp.headers.get("Set-Cookie", "") or ""
|
||||
first_cookie_name = set_cookie_hdr.split("=", 1)[0].strip().upper()
|
||||
sid_ok = bool(resp.cookies.get("SID")) or first_cookie_name == "SID"
|
||||
first_cookie_name = set_cookie_hdr.split("=", 1)[0].strip()
|
||||
sid_ok = any(_is_session_cookie(k) for k in resp.cookies.keys()) or (
|
||||
bool(first_cookie_name) and _is_session_cookie(first_cookie_name)
|
||||
)
|
||||
if body == "Ok." or sid_ok:
|
||||
self._logged_in = True
|
||||
logger.info("qBittorrent login successful for %s", self.base_url)
|
||||
@@ -92,9 +98,10 @@ class QbittorrentClient:
|
||||
cookie_names = sorted(resp.cookies.keys()) or (["<unparsed>"] if set_cookie_hdr else [])
|
||||
raise RuntimeError(
|
||||
f"Unexpected response from qBittorrent login endpoint (HTTP {resp.status_code}, "
|
||||
f"body={body!r}, cookies={cookie_names}). Expected the text 'Ok.' or an SID cookie "
|
||||
"from /api/v2/auth/login — this usually means base_url does not reach the qBittorrent "
|
||||
"Web API (check the URL, path, and any reverse proxy in front of qBittorrent)."
|
||||
f"body={body!r}, cookies={cookie_names}). Expected the text 'Ok.' or a session "
|
||||
"cookie (SID / QBT_SID) from /api/v2/auth/login — this usually means base_url does "
|
||||
"not reach the qBittorrent Web API (check the URL, path, and any reverse proxy in "
|
||||
"front of qBittorrent)."
|
||||
)
|
||||
|
||||
def _get(self, path: str, **params: Any) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user