mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-10-23 08:12:22 +02:00
255 lines
12 KiB
Markdown
255 lines
12 KiB
Markdown
# Error Responses
|
|
|
|
Every endpoint of the unshackle REST API reports failures in a single, predictable
|
|
shape. The cause can be a missing field, a service that refuses to authenticate, or a
|
|
download worker that crashed. In each case the server answers with a JSON body
|
|
that carries a machine-readable **error code**, a human-readable **message**, and a
|
|
matching **HTTP status**. This page documents that format and catalogues every error
|
|
code the server can send.
|
|
|
|
If you write a client against the API, read this page alongside the endpoint
|
|
reference. Each endpoint lists the specific codes it raises, and this page gives the
|
|
meanings of those codes.
|
|
|
|
!!! note "Where this comes from"
|
|
`unshackle/core/api/errors.py` defines the error format, and the wrapper
|
|
that turns raised errors into responses lives in `unshackle/core/api/routes.py`.
|
|
Nothing on this page is invented. It reflects the actual server behaviour.
|
|
|
|
## The standard error shape
|
|
|
|
A structured error is always a JSON object with `status` set to the literal string
|
|
`"error"`. The full set of fields:
|
|
|
|
```json title="Error response body"
|
|
{
|
|
"status": "error",
|
|
"error_code": "INVALID_PARAMETERS",
|
|
"message": "vcodec must be one of: H264, H265, ...",
|
|
"timestamp": "2026-07-03T12:34:56.789012+00:00",
|
|
"details": { "field": "vcodec" },
|
|
"retryable": true,
|
|
"debug_info": {
|
|
"exception_type": "APIError",
|
|
"traceback": "..."
|
|
}
|
|
}
|
|
```
|
|
|
|
| Field | Type | Always present? | Meaning |
|
|
|---|---|---|---|
|
|
| `status` | string | Yes | Always the literal `"error"`. Lets a client distinguish an error body from a success body at a glance. |
|
|
| `error_code` | string | Yes | A stable, machine-readable code from the [catalogue below](#error-code-catalogue). Match on this, not on `message`. |
|
|
| `message` | string | Yes | A human-readable description of what went wrong. Wording may change between versions, so do not parse it. |
|
|
| `timestamp` | string | Yes | ISO 8601 timestamp (UTC) of when the error response was built. |
|
|
| `details` | object | Only when non-empty | Structured, error-specific context (for example the invalid field, a list of available titles, or the set of currently active jobs). |
|
|
| `retryable` | boolean | Only when `true` | Present and set to `true` when retrying the same request may succeed later (network blips, rate limiting, transient outages). Absent otherwise. |
|
|
| `debug_info` | object | Only with `--debug-api` | Technical diagnostics; see [Debug information](#debug-information). |
|
|
|
|
!!! tip "Match on `error_code`, branch on HTTP status"
|
|
Treat `error_code` as the primary signal your code reacts to, and the HTTP status
|
|
as a coarse fallback for anything you do not recognise. The two are consistent:
|
|
every code maps to a default status (see the catalogue), and the pairing is
|
|
stable across releases.
|
|
|
|
### The `details` object
|
|
|
|
When present, `details` carries extra context specific to the error. It is only
|
|
included when it is non-empty, so a client must treat a missing `details` as "no
|
|
extra information". Some concrete examples you may encounter:
|
|
|
|
- A validation failure may include the offending field or the list of valid values.
|
|
- A "title not found" error on a remote session includes `available_titles`.
|
|
- A maintenance conflict includes `active_jobs`, the jobs blocking the operation.
|
|
- Categorised exceptions add a `reason` tag, such as `"network_connectivity"` or
|
|
`"geofence_restriction"`.
|
|
|
|
### The `retryable` hint
|
|
|
|
The `retryable` field appears **only when it is `true`**. It is set for conditions
|
|
the server believes are transient: network errors, rate limiting, and temporary
|
|
service unavailability. When you see it, you can back off and try the request again.
|
|
When it is absent, assume the request will keep failing until you change something.
|
|
|
|
!!! example "A retryable network error"
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"error_code": "NETWORK_ERROR",
|
|
"message": "Network error occurred: Connection timed out",
|
|
"timestamp": "2026-07-03T12:00:00+00:00",
|
|
"details": { "reason": "network_connectivity" },
|
|
"retryable": true
|
|
}
|
|
```
|
|
|
|
## Error code catalogue
|
|
|
|
Every code below is a member of the `APIErrorCode` enumeration. The **Status** column
|
|
is the default HTTP status the server returns for that code. A handler can override
|
|
the status in special cases (for example, a network error is always forced to `503`),
|
|
but the defaults hold for the overwhelming majority of responses.
|
|
|
|
### Client errors (4xx)
|
|
|
|
| Code | Status | Meaning |
|
|
|---|---|---|
|
|
| `INVALID_INPUT` | 400 | A required field is missing, or the request body could not be parsed. Also used as the generic "your input was malformed" code. |
|
|
| `INVALID_SERVICE` | 400 | The named service is unknown, or is not permitted for your API key. |
|
|
| `INVALID_PROXY` | 400 | The supplied proxy specification could not be resolved or is malformed. |
|
|
| `INVALID_PARAMETERS` | 400 | One or more download or query parameters failed validation (bad codec, bitrate, sort field, and so on). |
|
|
| `AUTH_FAILED` | 401 | Authentication with the streaming service failed (bad credentials or cookies). |
|
|
| `FORBIDDEN` | 403 | The action is not allowed. Raised by server-side gates (per-key restrictions on CDM or credential overrides) and by remote session IP binding. |
|
|
| `GEOFENCE` | 403 | The title is not available in the applicable region. |
|
|
| `NOT_FOUND` | 404 | A requested resource (title, history entry, and so on) does not exist. |
|
|
| `NO_CONTENT` | 404 | The request was valid but produced nothing: no matching titles, tracks, episodes, or keys. |
|
|
| `JOB_NOT_FOUND` | 404 | The referenced download job does not exist. |
|
|
| `SESSION_NOT_FOUND` | 404 | The remote-dl session does not exist or has expired. |
|
|
| `TRACK_NOT_FOUND` | 404 | A referenced track ID is not present in the remote session. |
|
|
| `CONFLICT` | 409 | The target resource is in a state that disallows the action (retrying a running job, prioritising a non-queued job, clearing cache during an active download). |
|
|
| `RATE_LIMITED` | 429 | The streaming service is rate-limiting requests. Retryable. |
|
|
|
|
### Server errors (5xx)
|
|
|
|
| Code | Status | Meaning |
|
|
|---|---|---|
|
|
| `INTERNAL_ERROR` | 500 | An unexpected server-side error. This is the fallback for any exception the server could not categorise. |
|
|
| `DOWNLOAD_ERROR` | 500 | The download process itself failed. |
|
|
| `WORKER_ERROR` | 500 | The download worker subprocess errored out. |
|
|
| `SERVICE_ERROR` | 502 | The streaming service's API returned an error, or the service does not support the requested operation. |
|
|
| `DRM_ERROR` | 502 | DRM or license acquisition failed. Retryable. |
|
|
| `NETWORK_ERROR` | 503 | A network connectivity problem (connection, timeout, DNS, TLS). Retryable. |
|
|
| `SERVICE_UNAVAILABLE` | 503 | The service is temporarily unavailable or under maintenance. Retryable. |
|
|
|
|
!!! note "Fallback status"
|
|
If a code is ever emitted without a mapped status, the server falls back to
|
|
`500`. In practice every code above has an explicit mapping.
|
|
|
|
## How the server produces errors
|
|
|
|
Understanding where an error comes from helps interpret it.
|
|
|
|
**Explicitly raised errors.** Most endpoints raise a structured `APIError` directly.
|
|
For example, a missing `service` field yields `INVALID_INPUT`, and a disallowed
|
|
service yields `INVALID_SERVICE`. These carry a precise code, message, and often a
|
|
populated `details` object.
|
|
|
|
**Categorised exceptions.** When an endpoint calls into a streaming service and an
|
|
uncategorised exception bubbles up, the server runs it through a classifier that
|
|
inspects the exception type and message and maps it to the most appropriate code.
|
|
The classification is keyword-based, checked in order:
|
|
|
|
| If the error mentions... | It becomes | Retryable |
|
|
|---|---|---|
|
|
| `auth`, `login`, `credential`, `unauthorized`, `forbidden`, `token` | `AUTH_FAILED` (401) | no |
|
|
| `connection`, `timeout`, `network`, `unreachable`, `socket`, `dns`, `resolve` (or `ConnectionError`, `TimeoutError`, `URLError`, `SSLError`) | `NETWORK_ERROR` (503) | yes |
|
|
| `geofence`, `region`, `not available in`, `territory` | `GEOFENCE` (403) | no |
|
|
| `not found`, `404`, `does not exist`, `invalid id` | `NOT_FOUND` (404) | no |
|
|
| `rate limit`, `too many requests`, `429`, `throttle` | `RATE_LIMITED` (429) | yes |
|
|
| `drm`, `license`, `widevine`, `playready`, `decrypt` | `DRM_ERROR` (502) | no |
|
|
| `service unavailable`, `503`, `maintenance`, `temporarily unavailable` | `SERVICE_UNAVAILABLE` (503) | yes |
|
|
| `invalid`, `malformed`, `validation` (or `ValueError`, `ValidationError`) | `INVALID_INPUT` (400) | no |
|
|
| anything else | `INTERNAL_ERROR` (500) | no |
|
|
|
|
Categorised errors include a `reason` tag inside `details` (such as
|
|
`"network_connectivity"` or `"drm_failure"`) so a client can react without parsing
|
|
the message text.
|
|
|
|
**Invalid JSON bodies.** If a `POST` endpoint receives a body that is not valid JSON,
|
|
the server returns `INVALID_INPUT` (400) with the parse error in
|
|
`details.error`:
|
|
|
|
```json title="Malformed request body"
|
|
{
|
|
"status": "error",
|
|
"error_code": "INVALID_INPUT",
|
|
"message": "Invalid JSON request body",
|
|
"timestamp": "2026-07-03T12:00:00+00:00",
|
|
"details": { "error": "Expecting value: line 1 column 1 (char 0)" }
|
|
}
|
|
```
|
|
|
|
## The authentication error exception
|
|
|
|
There is one place where the response does **not** follow the standard shape: the
|
|
API key authentication middleware. When you send a request to a protected endpoint
|
|
without a valid `X-Secret-Key` header, the middleware short-circuits with a legacy
|
|
body that uses an integer `status` and no `error_code` or `timestamp`.
|
|
|
|
=== "Missing key"
|
|
|
|
```json
|
|
{ "status": 401, "message": "Secret Key is Empty." }
|
|
```
|
|
|
|
=== "Invalid key"
|
|
|
|
```json
|
|
{ "status": 401, "message": "Secret Key is Invalid." }
|
|
```
|
|
|
|
!!! warning "Handle both shapes"
|
|
A client must expect two error formats: the standard structured error
|
|
(string `status` of `"error"`, with `error_code`), and this authentication error
|
|
(integer `status`, no `error_code`). The `GET /api/health` endpoint is exempt
|
|
from authentication, so it never returns the key-error shape.
|
|
|
|
## Responses with no body
|
|
|
|
Not every failure or completion has a JSON body. Some endpoints answer with
|
|
**`204 No Content`** and an empty body, notably deleting a terminal download job and
|
|
deleting a history entry.
|
|
|
|
!!! warning "Do not parse `204` bodies"
|
|
A `204` response has no body. Clients must examine the status code before they
|
|
decode JSON, or the decode will fail on an empty string.
|
|
|
|
## Debug information
|
|
|
|
By default, error responses never include tracebacks or internal diagnostics. If you
|
|
start the server with the `--debug-api` flag, every structured error gains a
|
|
`debug_info` object:
|
|
|
|
```json title="Error with --debug-api enabled"
|
|
{
|
|
"status": "error",
|
|
"error_code": "INTERNAL_ERROR",
|
|
"message": "An unexpected error occurred: ...",
|
|
"timestamp": "2026-07-03T12:00:00+00:00",
|
|
"debug_info": {
|
|
"exception_type": "KeyError",
|
|
"traceback": "Traceback (most recent call last): ..."
|
|
}
|
|
}
|
|
```
|
|
|
|
The `debug_info` object always carries `exception_type` and, for exception-derived
|
|
errors, a formatted `traceback`. Some endpoints add extra diagnostics such as
|
|
captured job worker stderr.
|
|
|
|
!!! warning "Developer use only"
|
|
`--debug-api` exposes internal tracebacks in responses. Enable it only for local
|
|
debugging, never on a server reachable by untrusted clients.
|
|
|
|
## Client handling recommendations
|
|
|
|
For anyone writing a consumer of the API, a durable strategy is:
|
|
|
|
1. **Read the HTTP status first.** It tells you the broad class of outcome, and it
|
|
lets you find the `204`/no-body and authentication-error cases before you read the
|
|
body.
|
|
2. **Branch on `error_code`.** For structured errors, this is the stable identifier
|
|
to switch on. Do not use `message` as the identifier.
|
|
3. **Respect `retryable`.** When it is `true`, back off and try the request again. The
|
|
reference client retries `429`, `500`, `502`, `503`, and `504` with exponential
|
|
backoff. When absent, fix the request rather than repeating it.
|
|
4. **Fall back gracefully.** For an unrecognised code, show the `message` and the
|
|
HTTP status.
|
|
|
|
!!! note "Reference consumer"
|
|
unshackle's own remote-download client treats any status of `400` or above as
|
|
fatal: it logs `Server error [<error_code>]: <message>` and exits. It also wraps
|
|
requests in a retry adapter (five attempts, backing off on `429`, `500`, `502`,
|
|
`503`, and `504`). This is a reasonable template for your own client's error
|
|
handling.
|