fix(qbittorrent): recognize SID cookie from raw Set-Cookie header on login

qBittorrent (or its reverse proxy) was returning 204 No Content with an SID
cookie and no body on a successful login, but the client only treated the
response as success if body == "Ok." or the cookie was in the parsed
requests cookie jar (resp.cookies.get("SID")). In the reported case the
Set-Cookie header was present (set-cookie=yes) yet the jar was empty —
requests doesn't always populate the jar from such headers (proxy-set/oddly-
attributed cookies) — so a valid login was reported as "Unexpected response".
The user re-entering correct credentials never helped.

Detect a successful login from EITHER the parsed jar OR the raw Set-Cookie
header (cookie name == SID). qBittorrent only sets SID on a valid login, so
this remains authoritative. The diagnostic now lists the cookie names it saw
for future-proofing.

New regression test reproduces the exact 204 + Set-Cookie SID + empty jar
case. 387/387 backend tests pass; ruff clean.
This commit is contained in:
Developer
2026-07-12 10:52:31 +00:00
parent b9a79b85d1
commit 7e53edfcc6
4 changed files with 35 additions and 10 deletions
+20
View File
@@ -21,6 +21,8 @@ class QbittorrentClientTests(unittest.TestCase):
resp.text = text
resp.raise_for_status.return_value = None
resp.status_code = 200
resp.headers = {}
resp.cookies = {}
return resp
def _get_response(self, json_data: dict, status_code: int = 200) -> MagicMock:
@@ -161,6 +163,24 @@ class QbittorrentClientTests(unittest.TestCase):
self.assertTrue(self.client._logged_in)
def test_login_accepts_sid_cookie_in_header_on_204(self) -> None:
"""204 + Set-Cookie SID with no body (no jar entry) is a valid login.
Reproduces the reported case: qBittorrent (or its reverse proxy) returns
204 No Content with an SID cookie, and requests doesn't always populate
the cookie jar from such a header, so the cookie must be detected from
the raw Set-Cookie header.
"""
resp = self._login_response("")
resp.status_code = 204
resp.cookies = {} # NOT in the parsed jar
resp.headers = {"Set-Cookie": "SID=abc123; HttpOnly; path=/"}
self.session.post.return_value = resp
self.client._login()
self.assertTrue(self.client._logged_in)
def test_login_empty_body_without_cookie_is_diagnostic(self) -> None:
"""Empty 200 body with no SID surfaces a URL/proxy diagnostic hint."""
resp = self._login_response("")