--- type: API title: Web UI JSON API description: The UI-facing JSON routes served by src/ui-api.js (search, queue add/list/cancel), the 404-on-cancel contract, and the UI error surfacing. tags: [api, ui, queue, express] timestamp: 2026-10-19T00:00:00Z --- # Web UI JSON API The Web UI talks to the app over four JSON routes registered by `registerUiApi(app, { qbt, client })` in `src/ui-api.js`, which `src/index.js` mounts **before** static file serving so `/api/*` is never swallowed. `__tests__/ui-api.test.js` covers all four routes. | Route | Success | Error | |-------|---------|-------| | `GET /api/search?q=…` | `200` result array (`[]` when `q` is empty) | `500` `{error}` when the Webshare client fails | | `POST /api/queue/add` | `200` `{ok: true}` | `400` `{error: 'ident required'}`, `409` `{error: 'already in queue'}`, `500` `{error}` | | `GET /api/queue` | `200` queue array | `500` `{error}` | | `POST /api/queue/cancel` | `200` `{ok: true}` | `400` `{error: 'hash required'}`, **`404` `{error: 'hash not in queue'}`** when the hash is not in the queue, `500` `{error}` | ## Cancel returns 404 for unknown hashes `POST /api/queue/cancel` honors the return value of `qbt.cancelDownload(hash)` (see [Project Overview](./overview.md)), which is a boolean: `false` means no queued download matched the hash, and the route responds `404 {error: 'hash not in queue'}`. Before this, the route always answered `200 {ok: true}` even when nothing was cancelled. The 404 body message is deliberately human-readable — the UI surfaces `error` verbatim (below), and a raw body read is still intelligible. ## The UI surfaces the error `apiFetch` in `ui/scripts.js` parses a failed response's JSON body when `content-type` is `application/json` and throws `new Error(data.error)` (falling back to the raw text or `HTTP ` for non-JSON or malformed bodies). The cancel-button handler then shows the failure inline as a transient `[err]` button state (re-enabled after 2 s) instead of swallowing it in `catch {}`; the 2 s queue poll repaints the row. This mirrors the established inline-error pattern of the add button — the codebase has no toasts or alerts. ## The fake qBittorrent API is intentionally different The fake qBittorrent Web API in `src/qbt.js` (`POST /api/v2/torrents/delete` and friends) was **not** changed: it answers `Ok.` for real qBittorrent-API clients such as Sonarr, whose contract is always `Ok.`, and Sonarr deletes finished entries — a `404` there would break it. The two APIs serve different consumers and keep different error contracts on purpose. ## Related - [Project Overview](./overview.md) — module layout and build/test commands.