[Bug] qBittorrent v4 API compatibility — broken features, silent failures, and v5-only fields
Background
Users on qBittorrent v4.x report that features like torrent priority silently do nothing. After
auditing the qBittorrent WebUI API (v4.1 wiki)
against our implementation, there are three problem classes:
- Broken on v4 — endpoints that exist in both versions but fail under v4 conditions we don’t handle
- v5-only fields — fields we read/write that don’t exist in v4 responses
- v4 behavioral differences — places where v4 returns different shapes or values
Core constraint
v5 must not be affected. The approach is: detect API version once at connect time, store it in
context, and gate behavior additively. v5 (API >= 2.9.0) is the happy path. Every version flag
must evaluate to true for v5 — no v5 code paths change behavior.
Step 1 — Create utils/apiVersion.ts
This is a new file. It is the single source of truth for all version-gated logic in the app.
Step 2 — Create context/ApiVersionContext.tsx
This is a new file. It stores the parsed API version after a successful login.
Step 3 — Wire ApiVersionProvider into the app root
Step 4 — Fetch and store API version at connect time
Find the function or hook that handles logging into a qBittorrent server (where the auth cookie is obtained and the connection is marked active).
Step 5 — Fix priority endpoints: handle HTTP 409
Find the service functions that call each of these four endpoints:
GET /api/v2/torrents/increasePrio
GET /api/v2/torrents/decreasePrio
GET /api/v2/torrents/topPrio
GET /api/v2/torrents/bottomPrio
These endpoints return HTTP 409 when torrent queueing is disabled in qBittorrent settings. We currently do not handle this, causing silent failures on v4 (and v5 with queueing off).
For each of the four functions:
Step 6 — Disable priority UI when queueing is off
Find the component(s) that render the priority controls (increase, decrease, top, bottom buttons).
Step 7 — Guard addTorrent payload params
Find the function or hook that builds the payload for POST /api/v2/torrents/add.
Step 8 — Guard setShareLimits payload params
Find the function that calls POST /api/v2/torrents/setShareLimits.
Step 9 — Guard tag filter param on torrent list fetch
Find the function that calls GET /api/v2/torrents/info.
Step 10 — Guard file priority index vs positional id
Find the code path that builds the file id list passed to POST /api/v2/torrents/filePrio.
Step 11 — Audit render sites for v4-absent fields
The following fields are absent from v4 API responses. They will be undefined on v4 — this must
not cause crashes or incorrect UI states. For each field, find every component and hook that reads
it and confirm the following:
isPrivate (absent on API < 2.9.0)
seeding_time (absent on API < 2.8.1)
content_path (absent on API < 2.6.1)
Step 12 — Verify nothing broke for v5
Before closing this issue, confirm via code review or manual testing against a v5 server:
Out of scope — do not change
- Tracker
status field type (string pre-API 2.2.0 vs integer): only affects qBit 4.1.0 exactly, open a separate issue if reported
categories shape in sync/maindata (array pre-API 2.1.0 vs object): same — 4.1.0 only, out of scope
- Log timestamp units (milliseconds vs seconds changed in v4.5.0): display-only, out of scope
- Null handling inside
useApiVersion() — the hook must never return null; the fallback handles this entirely, so callers do not need null checks
Labels
bug v4-compat priority api
[Bug] qBittorrent v4 API compatibility — broken features, silent failures, and v5-only fields
Background
Users on qBittorrent v4.x report that features like torrent priority silently do nothing. After
auditing the qBittorrent WebUI API (v4.1 wiki)
against our implementation, there are three problem classes:
Core constraint
v5 must not be affected. The approach is: detect API version once at connect time, store it in
context, and gate behavior additively. v5 (API >= 2.9.0) is the happy path. Every version flag
must evaluate to
truefor v5 — no v5 code paths change behavior.Step 1 — Create
utils/apiVersion.tsThis is a new file. It is the single source of truth for all version-gated logic in the app.
Create the file at
utils/apiVersion.tsExport an
ApiVersioninterface/type with fields:raw(string),major(number),minor(number),patch(number)Export a
parseApiVersion(raw: string): ApiVersionfunction that:.and parses each segment as a number{ raw: '2.9.0', major: 2, minor: 9, patch: 0 }as a fallback if parsing fails (so v5 users are never degraded by an unexpected response)Export an
apiAtLeast(current: ApiVersion, major: number, minor: number, patch?: number): booleanfunction that returns true ifcurrentis >= the given versionExport the following named feature flag functions, each taking an
ApiVersionand returning a boolean. Each one must callapiAtLeastinternally — no magic numbers at call sites:API_HAS_INDEX_FILE_PRIO/torrents/filesresponse includesindexfield;filePrioshould use itAPI_HAS_TAG_FILTER/torrents/infoaccepts atagquery parameterAPI_HAS_ADD_RATIO_LIMITS/torrents/addacceptsratioLimitandseedingTimeLimitAPI_HAS_SEEDING_TIME/torrents/inforesponse includesseeding_timefieldAPI_HAS_CONTENT_PATH/torrents/inforesponse includescontent_pathfieldAPI_HAS_IS_PRIVATE/torrents/infoand/torrents/propertiesincludeisPrivatefieldAPI_HAS_INACTIVE_SEEDING_LIMITsetShareLimitsacceptsinactiveSeedingTimeLimitparamVerify: every flag returns
truewhen called with{ major: 2, minor: 9, patch: 0 }(v5 baseline)Step 2 — Create
context/ApiVersionContext.tsxThis is a new file. It stores the parsed API version after a successful login.
context/ApiVersionContext.tsxApiVersion | nullvalue and a setterApiVersionProvidercomponent that wraps children and owns the stateuseApiVersion()hook that:ApiVersionif set{ raw: '2.9.0', major: 2, minor: 9, patch: 0 }if not yet set — never returns null to callersStep 3 — Wire
ApiVersionProviderinto the app rootApp.tsxorapp/_layout.tsx)ApiVersionProvider— it should be an outer wrapper, not nested inside a provider that depends on auth state, since the version fetch happens during loginStep 4 — Fetch and store API version at connect time
Find the function or hook that handles logging into a qBittorrent server (where the auth cookie is obtained and the connection is marked active).
/api/v2/app/webapiVersionparseApiVersionand store the result via theApiVersionContextsetter'2.9.0'so v5 users are not affectedStep 5 — Fix priority endpoints: handle HTTP 409
Find the service functions that call each of these four endpoints:
GET /api/v2/torrents/increasePrioGET /api/v2/torrents/decreasePrioGET /api/v2/torrents/topPrioGET /api/v2/torrents/bottomPrioThese endpoints return HTTP
409when torrent queueing is disabled in qBittorrent settings. We currently do not handle this, causing silent failures on v4 (and v5 with queueing off).For each of the four functions:
Step 6 — Disable priority UI when queueing is off
Find the component(s) that render the priority controls (increase, decrease, top, bottom buttons).
server_statefrom/api/v2/sync/maindatais consumed — it contains aqueueingboolean field reflecting whether torrent queueing is currently enabled on the serverqueueingvalue fromserver_stateinto state or context so priority UI components can read itqueueingisfalseStep 7 — Guard
addTorrentpayload paramsFind the function or hook that builds the payload for
POST /api/v2/torrents/add.useApiVersionandAPI_HAS_ADD_RATIO_LIMITSfrom the new filesratioLimitin the payload only whenAPI_HAS_ADD_RATIO_LIMITS(apiVersion)is trueseedingTimeLimitin the payload only whenAPI_HAS_ADD_RATIO_LIMITS(apiVersion)is trueStep 8 — Guard
setShareLimitspayload paramsFind the function that calls
POST /api/v2/torrents/setShareLimits.useApiVersionandAPI_HAS_INACTIVE_SEEDING_LIMITinactiveSeedingTimeLimitin the payload only whenAPI_HAS_INACTIVE_SEEDING_LIMIT(apiVersion)is truehashes,ratioLimit,seedingTimeLimit) must remain unchangedinactiveSeedingTimeLimitis still included when connected to a v5 serverStep 9 — Guard
tagfilter param on torrent list fetchFind the function that calls
GET /api/v2/torrents/info.useApiVersionandAPI_HAS_TAG_FILTERtagquery parameter only whenAPI_HAS_TAG_FILTER(apiVersion)is truefilter,category,sort,reverse,limit,offset,hashes) must remain unchangedtagparam is still sent when connected to a v5 serverStep 10 — Guard file priority index vs positional id
Find the code path that builds the file
idlist passed toPOST /api/v2/torrents/filePrio.API_HAS_INDEX_FILE_PRIO/torrents/files:API_HAS_INDEX_FILE_PRIO(apiVersion)is true, use each file’sindexfieldfilePriocall itself and all other params must not changeindexfield is used when connected to a v5 serverStep 11 — Audit render sites for v4-absent fields
The following fields are absent from v4 API responses. They will be
undefinedon v4 — this mustnot cause crashes or incorrect UI states. For each field, find every component and hook that reads
it and confirm the following:
isPrivate(absent on API < 2.9.0)=== true) rather than relying on truthiness —undefinedmust not render the badgeisPrivateis always a booleanseeding_time(absent on API < 2.8.1)!= nullbefore renderingNaNor crash if the field is missingcontent_path(absent on API < 2.6.1)content_pathnull-guards before rendering or using the valueStep 12 — Verify nothing broke for v5
Before closing this issue, confirm via code review or manual testing against a v5 server:
useApiVersion()returns major: 2, minor: 9 or higher when connected to v5apiVersion.tsreturntruefor a v5 connectionratioLimit,seedingTimeLimit,inactiveSeedingTimeLimit,tag, andindexare all still sent/used on v5Out of scope — do not change
statusfield type (string pre-API 2.2.0 vs integer): only affects qBit 4.1.0 exactly, open a separate issue if reportedcategoriesshape insync/maindata(array pre-API 2.1.0 vs object): same — 4.1.0 only, out of scopeuseApiVersion()— the hook must never return null; the fallback handles this entirely, so callers do not need null checksLabels
bugv4-compatpriorityapi