Skip to content

fix(QBittorrentProxyV2): accept empty body as auth success (qBit >= 4.5) - #162

Closed
csmarshall wants to merge 1 commit into
pennydreadful:developfrom
csmarshall:fix/qbittorrent-auth-empty-response
Closed

csmarshall wants to merge 1 commit into
pennydreadful:developfrom
csmarshall:fix/qbittorrent-auth-empty-response

Conversation

@csmarshall

@csmarshall csmarshall commented May 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Port the qBit-auth-response handling from Radarr/Sonarr to bookshelf so all three forks treat modern qBittorrent the same way.

Bugs fixed

QBittorrentProxyV2.AuthenticateClient has two related bugs against qBittorrent 4.5+ that radarr and sonarr both already fixed but bookshelf hasn't pulled in:

  1. Empty-body success is mis-reported as auth failure. qBit 4.5+ returns HTTP 204 No Content (empty body) on a successful /api/v2/auth/login instead of HTTP 200 with body "Ok.". The existing if (response.Content != "Ok.") treats "" as failure, so every successful login becomes a DownloadClientAuthenticationException.

  2. HTTP 401 routed to the wrong exception type. A bad password from outside qBit's trusted-subnet whitelist returns 401 Unauthorized. The existing HttpException catch only converts 403 Forbidden to DownloadClientAuthenticationException; 401 falls through to a generic DownloadClientException ("Failed to connect"), giving users the wrong error category.

Symptom

Persistent Failed to authenticate with qBittorrent log spam in current bookshelf releases against a known-good username/password pair — including configurations where Radarr and Sonarr on the same machine, with the same credentials, work fine.

2026-05-13 20:27:08.4|Error|QBittorrent|Unable to authenticate
[v0.4.20.129] NzbDrone.Core.Download.Clients.DownloadClientAuthenticationException: Failed to authenticate with qBittorrent.
   at NzbDrone.Core.Download.Clients.QBittorrent.QBittorrentProxyV2.AuthenticateClient(...) :line 444

Reproduces deterministically against linuxserver/qbittorrent:5.2.0 (qBittorrent 4.5+). Hand-tested with wget against /api/v2/auth/login from inside the bookshelf container:

correct password → HTTP/1.1 204 OK, content-length: 0, set-cookie: QBT_SID_<port>=...
wrong password   → HTTP/1.1 401 Unauthorized

Fix

Mirror Radarr's QBittorrentProxyV2.cs (Sonarr's copy is byte-identical in the relevant block):

                 catch (HttpException ex)
                 {
-                    _logger.Debug("qbitTorrent authentication failed.");
-                    if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
+                    _logger.Debug(ex, "qbitTorrent authentication failed.");
+
+                    if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
                     {
                         throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
                     }
                     ...
                 }
                 ...

-                // returns "Fails." on bad login
-                if (response.Content != "Ok.")
+                if (response.Content.IsNotNullOrWhiteSpace() && response.Content != "Ok.")
                 {
                     ...
                 }

The legacy "Fails." body still throws DownloadClientAuthenticationException via the non-empty/non-"Ok." path, so older qBit versions are not regressed.

Scope choice

The rest of QBittorrentProxyV2.cs has diverged ~90 lines from Radarr/Sonarr but most of that divergence is either correct-for-bookshelf (MusicCategory vs MovieCategory) or new features (ApiKey-based auth, AddTags()) that are orthogonal to this bug. Kept this PR tight to the auth-response handling so it's an obviously-safe, easy-to-review backport. Happy to do follow-up PRs for the new-feature bits if they're wanted.

Test plan

  • Manually confirmed qBit's new response shape on linuxserver/qbittorrent:5.2.0 (qBittorrent 4.5+).
  • Cross-checked the patch against Radarr's and Sonarr's QBittorrentProxyV2.AuthenticateClient — byte-for-byte equivalent in the changed region.
  • CI to verify no regression in the existing test suite.
  • Built locally: dotnet build NzbDrone.Core/Readarr.Core.csproj -c Release clean — 0 warnings, 0 errors.

qBittorrent 4.5+ changed the WebUI auth response: a successful POST to
/api/v2/auth/login now returns HTTP 204 No Content with an empty body
instead of HTTP 200 with body "Ok.". A bad password from outside the
trusted-subnet whitelist returns HTTP 401 Unauthorized.

The current implementation has two related bugs against modern
qBittorrent:

  1. `if (response.Content != "Ok.")` treats the new empty-body success
     response as a failure, so every successful login is mis-reported
     as DownloadClientAuthenticationException.

  2. The HttpException catch only routes HttpStatusCode.Forbidden (403)
     to DownloadClientAuthenticationException. A 401 falls through to
     a generic DownloadClientException ("Failed to connect"), giving
     the user the wrong error category for bad credentials.

Radarr and Sonarr already converged on the fix in their identical
copies of QBittorrentProxyV2.cs; this PR ports the same two changes
to bookshelf so the three forks stay aligned:

  * Treat both Unauthorized (401) and Forbidden (403) as auth
    failures.
  * Only throw on a non-empty / non-"Ok." response body (use
    IsNotNullOrWhiteSpace so whitespace-only bodies also count as
    success). Empty body = success in qBit 4.5+.

References (current develop on both, identical methods):
  https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
  https://github.com/Sonarr/Sonarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
@csmarshall
csmarshall force-pushed the fix/qbittorrent-auth-empty-response branch from 66df69c to 0604a7b Compare May 14, 2026 01:37
@shafe123

Copy link
Copy Markdown

Possible duplicate of #158

Jaffa00 pushed a commit to Jaffa00/bookshelf that referenced this pull request Jun 9, 2026
qBittorrent 5.x added API-key authentication as an alternative to the
classic username/password cookie flow. Radarr and Sonarr support it;
bookshelf doesn't yet. Port the additions verbatim (adapted to
bookshelf's existing settings layout — no other refactors).

Changes:

  src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs
    * New `ApiKey` field at FieldDefinition(4), with PrivacyLevel.ApiKey
      so it's masked in the UI and logs.
    * Validator rejects configurations where ApiKey is set AND
      Username/Password are also set, mirroring Radarr's rule.
    * Renumber existing fields (5..14) to make room for the ApiKey
      field where users naturally look for it (right above Username).
      Property names are unchanged so existing stored configs are
      preserved.

  src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
    * BuildRequest now emits `Authorization: Bearer <key>` when ApiKey
      is set, and only attaches BasicNetworkCredential when falling back
      to username/password. Mixing the two would be redundant and could
      confuse some qBit middlewares.
    * ProcessRequest gains a fast path for the ApiKey case that skips
      AuthenticateClient (no cookie session needed) and routes 401/403
      to DownloadClientAuthenticationException directly, matching the
      Radarr/Sonarr pattern.

Independent of pennydreadful#162: this change builds and works whether or not the
auth-response-handling fix has landed yet. Users on classic
username/password auth see no behavior change.

References:
  https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs
  https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
ethanbergstrom pushed a commit to ethanbergstrom/bookshelf that referenced this pull request Jul 4, 2026
qBittorrent 5.x added API-key authentication as an alternative to the
classic username/password cookie flow. Radarr and Sonarr support it;
bookshelf doesn't yet. Port the additions verbatim (adapted to
bookshelf's existing settings layout — no other refactors).

Changes:

  src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs
    * New `ApiKey` field at FieldDefinition(4), with PrivacyLevel.ApiKey
      so it's masked in the UI and logs.
    * Validator rejects configurations where ApiKey is set AND
      Username/Password are also set, mirroring Radarr's rule.
    * Renumber existing fields (5..14) to make room for the ApiKey
      field where users naturally look for it (right above Username).
      Property names are unchanged so existing stored configs are
      preserved.

  src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
    * BuildRequest now emits `Authorization: Bearer <key>` when ApiKey
      is set, and only attaches BasicNetworkCredential when falling back
      to username/password. Mixing the two would be redundant and could
      confuse some qBit middlewares.
    * ProcessRequest gains a fast path for the ApiKey case that skips
      AuthenticateClient (no cookie session needed) and routes 401/403
      to DownloadClientAuthenticationException directly, matching the
      Radarr/Sonarr pattern.

Independent of pennydreadful#162: this change builds and works whether or not the
auth-response-handling fix has landed yet. Users on classic
username/password auth see no behavior change.

References:
  https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs
  https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs
3n8 pushed a commit to 3n8/bookshelf that referenced this pull request Aug 27, 2026
qBittorrent 5.2.0+ returns 204 No Content on successful /api/v2/auth/login
instead of 200 with body "Ok.". Treat empty bodies as success and map HTTP
401 to an authentication error, matching Sonarr/Radarr.

Also publish images as ghcr.io/3n8/bookshelf and grant the CI token the
permissions a fork needs to push packages.

Fixes pennydreadful#160, pennydreadful#165, pennydreadful#178 (unmerged upstream PRs pennydreadful#158, pennydreadful#162, pennydreadful#170).
@pennydreadful

Copy link
Copy Markdown
Owner

#170

pennydreadful added a commit that referenced this pull request Sep 10, 2026
## Summary

Port qBittorrent's API-key authentication support (introduced in
qBittorrent 5.x) from Radarr and Sonarr to bookshelf. Adds an `ApiKey`
field to the qBit download-client settings; when populated, requests
carry an `Authorization: Bearer <key>` header and the cookie-auth flow
is bypassed entirely.

Existing username/password configurations see no behavior change.

## Why

qBittorrent 5.x added API tokens as a more secure / scriptable
alternative to user passwords. Radarr
([source](https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs))
and Sonarr both support it; bookshelf is the odd one out among the
*arrs. Users running modern qBit who'd prefer API-key auth currently
can't.

## Changes

**`QBittorrentSettings.cs`**
- New `ApiKey` field at `FieldDefinition(4)`, with `PrivacyLevel.ApiKey`
so it's masked in the UI and exported configs.
- Validator rejects configurations where `ApiKey` is set alongside
`Username` or `Password`, mirroring Radarr's rule.
- Renumber existing `FieldDefinition` order numbers (5..14) to slot the
API Key field right above Username/Password where users naturally look
for it. Property names are unchanged — stored configs are unaffected.

**`QBittorrentProxyV2.cs`**
- `BuildRequest` emits `Authorization: Bearer <key>` when `ApiKey` is
set and only attaches `BasicNetworkCredential` when falling back to
username/password.
- `ProcessRequest` gains a fast path for the `ApiKey` case that skips
`AuthenticateClient` (no cookie session needed) and routes 401/403
directly to `DownloadClientAuthenticationException`, matching the
Radarr/Sonarr pattern.

## Independent of #162

This PR is intentionally orthogonal to
#162 (the
auth-response-handling fix). It builds and works whether or not #162 has
landed:

- `ApiKey` users follow a fast path that doesn't touch the `Content !=
"Ok."` check at all.
- Username/password users on this branch alone hit the unchanged legacy
code path, which still has the empty-body bug #162 fixes. Once both PRs
land they're additive.

## Test plan

- [x] Cross-checked the patch against Radarr's and Sonarr's
`QBittorrentSettings.cs` + `QBittorrentProxyV2.cs` for behavioral
parity.
- [x] Stored configs preserved (`FieldDefinition` numbers are
display-order only; property names are unchanged).
- [x] Built locally: `dotnet build NzbDrone.Core/Readarr.Core.csproj -c
Release` clean — 0 warnings, 0 errors.
- [ ] CI to verify build + existing tests.

## Out of scope

Other divergences between bookshelf's and Radarr/Sonarr's qBit clients
(the `AddTags()` method, base-class refactor to
`DownloadClientSettingsBase`, translation-key labels, etc.) are
deliberately not part of this PR — kept tight to the API-key feature so
it's easy to review and accept.

Co-authored-by: pennydreadful <222923085+pennydreadful@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants