feat(qbit): support qBittorrent ≥5.2 Bearer API key auth - #44
Conversation
Prefer QB_API_KEY over username/password when set. Probe /app/version instead of /auth/login (API keys cannot use auth endpoints). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new unit tests introduce data races (shared vars across handler/test goroutines) and the auth retry logic is overly narrow (403-only) for API-key-related 401 responses.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds qBittorrent WebAPI key authentication support (qBittorrent ≥ 5.2) to the qbit integration, preferring Bearer API keys over cookie/session auth when configured.
Changes:
- Add API-key-based auth path to the qBittorrent client, including probing a non-auth endpoint and attaching
Authorization: Bearer ...to requests. - Extend configuration and app wiring to load/use
QB_API_KEYwhen set. - Add unit tests for API key login behavior and Authorization header attachment.
File summaries
| File | Description |
|---|---|
| README.md | Documents new QB_API_KEY env var and clarifies existing qB auth vars. |
| internal/qbit/client.go | Implements API key auth support and ensures auth headers are applied to requests. |
| internal/qbit/client_test.go | Adds tests validating API key login behavior and Authorization header usage. |
| internal/config/config.go | Adds QBAPIKey to config and loads it from QB_API_KEY. |
| cmd/gamarr/main.go | Prefers API-key-backed qB client construction when configured. |
Review details
Suppressed comments (3)
internal/qbit/client_test.go:486
- Test reads/writes
sawAuthacross goroutines (httptest handler vs. test goroutine) without synchronization; this is a data race undergo test -race. Use a channel to pass the observed Authorization header from the handler to the test instead of a shared variable.
func TestAddTorrent_APIKey(t *testing.T) {
var sawAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/app/version" {
w.Write([]byte("v5.2.0"))
internal/qbit/client.go:307
- Reauth retry is only triggered on HTTP 403, but API key rejection (and some auth failures) commonly return HTTP 401. Including 401 here makes auth handling more robust and keeps
authenticatedfrom staying true after an unauthorized response.
resp, err := c.postForm("/api/v2/torrents/delete", data)
if err != nil {
return false
}
defer resp.Body.Close()
if resp.StatusCode == 403 {
c.login()
resp2, err := c.postForm("/api/v2/torrents/delete", data)
internal/qbit/client.go:345
postWithReauthonly retries on HTTP 403. If the server returns HTTP 401 for an expired/invalid session or bad API key, the retry path (and state refresh vialogin()) is skipped. Consider treating 401 like 403 for the retry decision.
resp, err := c.postForm(path, data)
if err != nil {
return 0
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
return resp.StatusCode
}
c.login()
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return false | ||
| } | ||
| defer resp.Body.Close() | ||
| if resp.StatusCode == 403 { |
| func TestLogin_APIKey(t *testing.T) { | ||
| var sawAuth string | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path == "/api/v2/auth/login" { | ||
| t.Error("API key auth must not call /auth/login") | ||
| w.WriteHeader(http.StatusForbidden) | ||
| return | ||
| } | ||
| if r.URL.Path == "/api/v2/app/version" { | ||
| sawAuth = r.Header.Get("Authorization") | ||
| w.Write([]byte("v5.2.0")) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusNotFound) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| c := NewWithAPIKey(srv.URL, "qbt_testkey0123456789abcdefghij") | ||
| if !c.Login() { | ||
| t.Fatal("expected API key login to succeed") | ||
| } | ||
| if sawAuth != "Bearer qbt_testkey0123456789abcdefghij" { | ||
| t.Errorf("Authorization=%q", sawAuth) | ||
| } | ||
| } |
feat(qbit): support qBittorrent >= 5.2 Bearer API key auth Merged by landing the branch directly: the fork is owned by an organization, and GitHub does not accept maintainer pushes to those, so the review fixes could not be pushed to the PR branch itself. Claude-Session: https://claude.ai/code/session_01UW6aMxfvwafxXnGH23M6zp
|
Merged — thanks for this, the Bearer support is a genuinely useful addition and the design was sound. A note on why this PR shows a failed check even though it is merged, so it does not mislead anyone reading it later. The
What the follow-up commit changed, all worth knowing about:
If you have a qBittorrent ≥5.2 to hand, the live check in your test plan is still the one thing not covered here. |
Summary
QB_API_KEY(Bearer) over username/password when set/api/v2/app/versioninstead of/auth/login(API keys cannot use auth endpoints per qB wiki)AddTorrentAuthorization headerTest plan
go test ./internal/qbit/ ./internal/config/QB_API_KEY, confirm Settings → Connection Tests → qBittorrent succeeds and a grab queues a torrentMade with Cursor