Skip to content

fix(qbit): treat qBittorrent's "all" sentinel as unfiltered instead of as a literal value - #367

Open
Davixk wants to merge 1 commit into
sirrobot01:betafrom
Davixk:fix/qbit-filter-all-returns-empty
Open

Davixk wants to merge 1 commit into
sirrobot01:betafrom
Davixk:fix/qbit-filter-all-returns-empty

Conversation

@Davixk

@Davixk Davixk commented Jul 29, 2026

Copy link
Copy Markdown

What breaks: GET /api/v2/torrents/info?filter=all returns [], even when torrents exist. Same for hashes=all.

Why: qBittorrent uses all to mean no filtering for both parameters, and both are matched literally.

  • handleTorrentsInfo forwards filter straight through as a storage.TorrentState, so ListFilterFunc evaluates t.State != "all" — true for every entry, so nothing matches. (The neighbouring strings.Trim(value, "") is also a no-op: an empty cutset trims nothing, so a padded value isn't trimmed either.)
  • ListFilterFunc adds every supplied hash to the match set, so hashes=all filters for a torrent whose infohash is the literal string all.

all is the qBittorrent default, so any client that sends it explicitly sees an empty queue rather than an error — which reads as "nothing is downloading" rather than as a bug.

The fix: treat the sentinel as unfiltered in both places.

Normalisation moved into normalizeStateFilter, which the handler calls, so the behaviour is tested through the production path rather than through a copy of the logic in a test file. For hashes, only a lone all is the sentinel — a list containing it stays a literal selection, since a multi-hash request is a genuine choice of torrents. Unrecognised filter values are still passed through verbatim rather than guessed at, and every other filter still applies alongside the sentinel.

Evidence: running in production on a ~47,000-entry library, where this was found while diagnosing an unrelated issue: a same-instant census showed /api/torrents returning 117 rows while ?filter=all returned 0.

Both tests fail on the current behaviour at the described defect, and the "a real state" / "a real infohash" cases pass with and without the change, so neither can pass by over-correcting.

… literal value

qBittorrent uses "all" to mean "no filtering" for both the `filter` and `hashes`
query parameters. Both were matched literally, so each returned an empty list:
no entry has the state "all", and no torrent has the infohash "all".

- handleTorrentsInfo forwarded `filter` straight through as a
  storage.TorrentState, so `?filter=all` matched nothing. The surrounding
  strings.Trim(value, "") was also a no-op -- an empty cutset trims nothing, so
  a padded value was not trimmed either. Normalisation now lives in
  normalizeStateFilter, which the handler calls, so it is testable directly
  rather than through a copy of its logic.

- ListFilterFunc added every supplied hash to the match set, so `hashes=all`
  filtered for a torrent whose infohash is the string "all". A lone "all" is now
  treated as the sentinel; a list containing it stays a literal selection, since
  a multi-hash request is a genuine choice of torrents.

Unrecognised filter values are still passed through verbatim rather than being
guessed at, and every other filter continues to apply alongside the sentinel.

Both tests fail on the previous behaviour at the described defect and pass with
a real state or a real infohash either way, so neither can pass by
over-correcting.
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.

1 participant