Skip to content

feat(qbit): support qBittorrent ≥5.2 Bearer API key auth - #44

Merged
JeremiahM37 merged 1 commit into
JeremiahM37:mainfrom
klopstack:feat/qbittorrent-api-key
Sep 7, 2026
Merged

feat(qbit): support qBittorrent ≥5.2 Bearer API key auth#44
JeremiahM37 merged 1 commit into
JeremiahM37:mainfrom
klopstack:feat/qbittorrent-api-key

Conversation

@benklop

@benklop benklop commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Prefer QB_API_KEY (Bearer) over username/password when set
  • Probe /api/v2/app/version instead of /auth/login (API keys cannot use auth endpoints per qB wiki)
  • Unit tests cover accept/reject and AddTorrent Authorization header

Test plan

  • go test ./internal/qbit/ ./internal/config/
  • Against live qBittorrent ≥5.2: set QB_API_KEY, confirm Settings → Connection Tests → qBittorrent succeeds and a grab queues a torrent

Made with Cursor

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>
Copilot AI lite review requested due to automatic review settings September 6, 2026 21:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_KEY when 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 sawAuth across goroutines (httptest handler vs. test goroutine) without synchronization; this is a data race under go 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 authenticated from 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

  • postWithReauth only 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 via login()) 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.

Comment thread internal/qbit/client.go
return false
}
defer resp.Body.Close()
if resp.StatusCode == 403 {
Comment on lines +444 to +468
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)
}
}
JeremiahM37 added a commit that referenced this pull request Sep 7, 2026
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
@JeremiahM37
JeremiahM37 merged commit ae46ebb into JeremiahM37:main Sep 7, 2026
2 of 3 checks passed
@JeremiahM37

Copy link
Copy Markdown
Owner

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 test job here failed at its very first step on gofmt (one trailing blank line in internal/qbit/client_test.go). That gate runs before everything else, so build, vet, race tests, staticcheck and govulncheck never actually executed on this branch.

klopstack is an organization, and GitHub does not accept maintainer pushes to fork branches owned by an organization — the "allow edits by maintainers" flag reads as enabled but the push is refused. So I could not put the fix on this branch. Instead the branch was merged to main with a follow-up commit on top, in 37c1236, where the full suite passes: gofmt, build, race tests, vet, staticcheck, govulncheck, smoke, docker and e2e. Your commit is unchanged and keeps your authorship. The red mark above is the old run against the un-fixed head commit and will not re-run.

What the follow-up commit changed, all worth knowing about:

  • QB_API_KEY is now read through strings.TrimSpace. It was compared against "" to choose Bearer auth, so a key read from a Docker or Kubernetes secret file (which keeps its trailing newline) or a .env line with a trailing space would select the Bearer path and discard working user/pass credentials. With a newline every request then fails inside net/http with invalid header field value for "Authorization", which points nowhere near the config; with a space the client reports itself authenticated while never contacting the login endpoint at all.
  • The 403 retry is skipped on the key path. Re-authenticating and retrying is right for a session cookie, which expires, but a Bearer key is fixed for the life of the process, so the retry re-sends the same rejected key and cannot succeed. At the watcher's 30s poll that turns one mistyped key into thousands of ERROR lines a day. The cookie path keeps its retry and a test pins that it does.
  • Added a test that runs every exported client method against a server requiring the header, and asserts it actually reached each endpoint. Header coverage was already complete — every request routes through the auth helpers — but only AddTorrent was covered, so a method added later that built its own request would fail only against a real qBittorrent, as a 403 that reads like a bad key rather than a missing header.
  • README compose example gained a commented QB_API_KEY line, noting it should be omitted rather than left blank.

If you have a qBittorrent ≥5.2 to hand, the live check in your test plan is still the one thing not covered here.

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