Skip to content

v4 api compatibility #52

@taylorcox75

Description

@taylorcox75

[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:

  1. Broken on v4 — endpoints that exist in both versions but fail under v4 conditions we don’t handle
  2. v5-only fields — fields we read/write that don’t exist in v4 responses
  3. 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.

  • Create the file at utils/apiVersion.ts

  • Export an ApiVersion interface/type with fields: raw (string), major (number), minor (number), patch (number)

  • Export a parseApiVersion(raw: string): ApiVersion function that:

    • Splits the version string on . and parses each segment as a number
    • Returns { 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)
    • Logs a console warning when the fallback is used
  • Export an apiAtLeast(current: ApiVersion, major: number, minor: number, patch?: number): boolean function that returns true if current is >= the given version

  • Export the following named feature flag functions, each taking an ApiVersion and returning a boolean. Each one must call apiAtLeast internally — no magic numbers at call sites:

    Flag name Minimum API version What it gates
    API_HAS_INDEX_FILE_PRIO 2.8.2 /torrents/files response includes index field; filePrio should use it
    API_HAS_TAG_FILTER 2.8.3 /torrents/info accepts a tag query parameter
    API_HAS_ADD_RATIO_LIMITS 2.8.1 /torrents/add accepts ratioLimit and seedingTimeLimit
    API_HAS_SEEDING_TIME 2.8.1 /torrents/info response includes seeding_time field
    API_HAS_CONTENT_PATH 2.6.1 /torrents/info response includes content_path field
    API_HAS_IS_PRIVATE 2.9.0 /torrents/info and /torrents/properties include isPrivate field
    API_HAS_INACTIVE_SEEDING_LIMIT 2.9.0 setShareLimits accepts inactiveSeedingTimeLimit param
  • Verify: every flag returns true when called with { major: 2, minor: 9, patch: 0 } (v5 baseline)


Step 2 — Create context/ApiVersionContext.tsx

This is a new file. It stores the parsed API version after a successful login.

  • Create the file at context/ApiVersionContext.tsx
  • Create a React context that holds an ApiVersion | null value and a setter
  • Export an ApiVersionProvider component that wraps children and owns the state
  • Export a useApiVersion() hook that:
    • Returns the stored ApiVersion if set
    • Returns the fallback { raw: '2.9.0', major: 2, minor: 9, patch: 0 } if not yet set — never returns null to callers
  • Export a way for the connect/login flow to call the setter (either expose it from the hook or export a separate setter hook)

Step 3 — Wire ApiVersionProvider into the app root

  • Find the root component or layout file where all other context providers live (likely App.tsx or app/_layout.tsx)
  • Wrap the existing provider tree with ApiVersionProvider — it should be an outer wrapper, not nested inside a provider that depends on auth state, since the version fetch happens during login

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).

  • Immediately after a successful login, make a GET request to /api/v2/app/webapiVersion
  • Pass the response string into parseApiVersion and store the result via the ApiVersionContext setter
  • Wrap the version fetch in a try/catch — it is non-fatal. On failure, store the fallback '2.9.0' so v5 users are not affected
  • The version fetch must complete before any subsequent API calls that use feature flags

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:

  • Add a catch block that specifically checks for HTTP status 409
  • On 409, throw or return a human-readable error message — do not swallow it silently. The message must tell the user that queueing needs to be enabled in qBittorrent settings
  • On any other error, re-throw as-is so existing error handling is unchanged
  • Do not add any version check here — this 409 behavior applies to all API versions

Step 6 — Disable priority UI when queueing is off

Find the component(s) that render the priority controls (increase, decrease, top, bottom buttons).

  • Locate where server_state from /api/v2/sync/maindata is consumed — it contains a queueing boolean field reflecting whether torrent queueing is currently enabled on the server
  • Surface the queueing value from server_state into state or context so priority UI components can read it
  • Disable all four priority buttons when queueing is false
  • Show a tooltip or helper text on the disabled buttons explaining that queueing must be enabled in qBittorrent settings
  • Ensure that the error thrown in Step 5 is surfaced to the user (toast, alert, or inline message) — do not silently drop it at the UI call site

Step 7 — Guard addTorrent payload params

Find the function or hook that builds the payload for POST /api/v2/torrents/add.

  • Import useApiVersion and API_HAS_ADD_RATIO_LIMITS from the new files
  • Conditionally include ratioLimit in the payload only when API_HAS_ADD_RATIO_LIMITS(apiVersion) is true
  • Conditionally include seedingTimeLimit in the payload only when API_HAS_ADD_RATIO_LIMITS(apiVersion) is true
  • All other existing payload fields must remain unchanged
  • Verify: both fields are still included when connected to a v5 server

Step 8 — Guard setShareLimits payload params

Find the function that calls POST /api/v2/torrents/setShareLimits.

  • Import useApiVersion and API_HAS_INACTIVE_SEEDING_LIMIT
  • Conditionally include inactiveSeedingTimeLimit in the payload only when API_HAS_INACTIVE_SEEDING_LIMIT(apiVersion) is true
  • All other params (hashes, ratioLimit, seedingTimeLimit) must remain unchanged
  • Verify: inactiveSeedingTimeLimit is still included when connected to a v5 server

Step 9 — Guard tag filter param on torrent list fetch

Find the function that calls GET /api/v2/torrents/info.

  • Import useApiVersion and API_HAS_TAG_FILTER
  • Conditionally include the tag query parameter only when API_HAS_TAG_FILTER(apiVersion) is true
  • All other query params (filter, category, sort, reverse, limit, offset, hashes) must remain unchanged
  • Verify: the tag param is still sent when connected to a v5 server

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.

  • Import API_HAS_INDEX_FILE_PRIO
  • When building the id list from the file array returned by /torrents/files:
    • If API_HAS_INDEX_FILE_PRIO(apiVersion) is true, use each file’s index field
    • If false (old v4), use the file’s position in the response array (0-based)
  • The filePrio call itself and all other params must not change
  • Verify: the index field is used when connected to a v5 server

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)

  • Every render site that displays a “private tracker” indicator uses strict equality (=== true) rather than relying on truthiness — undefined must not render the badge
  • No component assumes isPrivate is always a boolean

seeding_time (absent on API < 2.8.1)

  • Every render site that displays seeding time checks for != null before rendering
  • No component will display NaN or crash if the field is missing

content_path (absent on API < 2.6.1)

  • Every render site that uses content_path null-guards before rendering or using the value
  • No component crashes if the field is missing

Step 12 — Verify nothing broke for v5

Before closing this issue, confirm via code review or manual testing against a v5 server:

  • All features function identically to before this change on a v5 server
  • useApiVersion() returns major: 2, minor: 9 or higher when connected to v5
  • All 7 feature flags in apiVersion.ts return true for a v5 connection
  • ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit, tag, and index are all still sent/used on v5
  • Priority controls are enabled and functional on v5 with queueing on
  • The API version is fetched exactly once at connect time — not on every API call

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions