mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
feat(api)!: add a server drain and never answer with a 5xx
POST/DELETE /api/dashboard/drain refuses new sessions and jobs (SERVER_DRAINING, 409) and warns open sessions. A CDN can replace a 5xx body, so server errors are now 422, retryable ones and the tunnel check 424, and a new middleware turns any 5xx into 422. BREAKING CHANGE: INTERNAL_ERROR, DOWNLOAD_ERROR, WORKER_ERROR and the fallback return 422 instead of 500; NETWORK_ERROR and SERVICE_UNAVAILABLE return 424 instead of 503; /api/tunnel/check failures return 424 instead of 502.
This commit is contained in:
@@ -39,11 +39,13 @@ unshackle serve --remote-only --quiet
|
||||
|
||||
## Endpoints
|
||||
|
||||
All are `GET`.
|
||||
All are `GET`, except the two `drain` routes.
|
||||
|
||||
| Path | Returns |
|
||||
|------|---------|
|
||||
| `/api/dashboard/status` | Version, `code_hash`, bind host and port, `mode` (`full`, `api_only`, `remote_only`), uptime, `requests_total`, `requests_rejected` (401s), `requests_by_key` (label → count), `errors` (error counts by service tag, then by `error_code`), configured service count (the same services as `/api/dashboard/services`), session count, `max_sessions` (`null` when the operator configured no limit), `session_ttl`, job counts by status. The service count includes services that failed to import - use `/api/dashboard/services` for the number that loaded. |
|
||||
| `/api/dashboard/status` | Version, `code_hash`, bind host and port, `mode` (`full`, `api_only`, `remote_only`), uptime, `requests_total`, `requests_rejected` (401s), `requests_by_key` (label → count), `errors` (error counts by service tag, then by `error_code`), configured service count (the same services as `/api/dashboard/services`), session count, `max_sessions` (`null` when the operator configured no limit), `session_ttl`, job counts by status. `draining` is `true` while a drain is on. The service count includes services that failed to import - use `/api/dashboard/services` for the number that loaded. |
|
||||
| `POST /api/dashboard/drain` | Starts a drain before a restart and returns the `/status` body. The server then refuses each new remote session, download job and job retry with `SERVER_DRAINING` (409). Open remote sessions and jobs continue, and each open remote session gets a warning in its service log, which the client shows to the user. Poll `/status` until `sessions` and the queued and downloading jobs are 0, then restart. The drain ends when the process stops. A second call changes nothing. |
|
||||
| `DELETE /api/dashboard/drain` | Cancels the drain and returns the `/status` body. The server accepts new work again. |
|
||||
| `/api/dashboard/sessions` | Every live remote session: `id`, `owner` (username or masked key), `creator_ip`, `service`, `title_id`, `titles`, `tracks`, `title` (the display name of the title the client is on: the last one it asked tracks for, else the first resolved title), `auth_status`, `auth_error`, `server_account` (the server profile lent to the remote session, else `null`), `server_cdm` (`false` when the client's own device licenses the remote session), `server_cdm_max_height` (the tallest video the server CDM licenses live, else `null`), `client`, `actions`, `created_at` and `last_accessed` (ISO 8601) with `created_ts` and `last_accessed_ts` (Unix epoch, the same clock as log `ts`), `age_seconds`, `idle_seconds`, `log_seq`. `client` is whatever the remote client sent as `client` in its session create request. The CLI sends `version`, `code_hash` (the commit the client runs, `null` when its source cannot be read), `platform` and `argv`. `argv` is the command line the user ran: proxy and URL userinfo, secret query parameters and credential values become `***`, home and install paths shorten as in the logs, and the line is cut at 3000 characters. An older client sends less or nothing and the field is `{}`. `log_seq` is the last sequence number in the remote session's service log - poll `/api/dashboard/sessions/{id}/logs` when it changes. `actions` is the session's request log, newest last, capped at 500: `ts`, `method`, `action` (`titles`, `tracks`, `segments`, `license`, `prompt`, …), `query`, `status`, `ms`, `bytes_in`, `bytes_out`. |
|
||||
| `/api/dashboard/jobs` | Every download job with full detail, regardless of owner. Empty in `--remote-only` mode. |
|
||||
| `/api/dashboard/logs` | `{"seq": N, "records": [...]}`: the last 1000 log records. `?since=<seq>` returns only newer records; `?level=WARNING` sets the minimum level; `?logger=serve` keeps one logger and its children (`aiohttp.access` is the noisiest). `ts` is a Unix epoch in seconds. |
|
||||
|
||||
@@ -131,7 +131,7 @@ Field notes:
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `200` | Service list returned. |
|
||||
| `500` | Unexpected server error (`INTERNAL_ERROR`). |
|
||||
| `422` | Unexpected server error (`INTERNAL_ERROR`). |
|
||||
|
||||
### `POST /api/search`
|
||||
|
||||
@@ -296,7 +296,7 @@ These keys are sent only when the title carries them, so a title without them se
|
||||
| `400` | `INVALID_SERVICE` | Unknown or disallowed service. |
|
||||
| `401` | `AUTH_FAILED` | Authentication with the service failed. |
|
||||
| `404` | `NOT_FOUND` | Title not found. |
|
||||
| `500` | `INTERNAL_ERROR` | Unexpected error. |
|
||||
| `422` | `INTERNAL_ERROR` | Unexpected error. |
|
||||
|
||||
### `POST /api/list-tracks`
|
||||
|
||||
@@ -1290,7 +1290,7 @@ The server serialises every structured error raised inside a handler to the same
|
||||
|
||||
The [error code catalogue](errors.md#error-code-catalogue) gives the default HTTP status of each error code.
|
||||
|
||||
When a generic (uncategorized) exception reaches a handler, unshackle inspects its message and type to pick a code. For example, messages mentioning "connection" or "timeout" become `NETWORK_ERROR` (503, retryable), "rate limit" or "429" become `RATE_LIMITED` (429, retryable), and anything unrecognized collapses to `INTERNAL_ERROR` (500).
|
||||
When a generic (uncategorized) exception reaches a handler, unshackle inspects its message and type to pick a code. For example, messages mentioning "connection" or "timeout" become `NETWORK_ERROR` (424, retryable), "rate limit" or "429" become `RATE_LIMITED` (429, retryable), and anything unrecognized collapses to `INTERNAL_ERROR` (422).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+19
-16
@@ -100,15 +100,16 @@ When it is absent, assume the request will keep failing until you change somethi
|
||||
|
||||
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.
|
||||
the status in special cases, but the defaults hold for the overwhelming majority of responses.
|
||||
|
||||
The server never answers with a `5xx` status. A proxy such as Cloudflare can replace the
|
||||
body of a `5xx` with its own page, and the JSON error is then lost. Server-side failures
|
||||
use `422`, and the ones that a retry can fix use `424`. Read `error_code` to tell them apart.
|
||||
A `5xx` that a client receives comes from a proxy in front of the server, for example
|
||||
while the server restarts.
|
||||
|
||||
### Client errors (4xx)
|
||||
|
||||
`SERVICE_ERROR` and `DRM_ERROR` are upstream failures, but they use `422`, not `502`.
|
||||
A proxy such as Cloudflare replaces the body of a `502` or `504` with its own page, and
|
||||
the JSON error is then lost.
|
||||
|
||||
| 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. |
|
||||
@@ -125,6 +126,7 @@ the JSON error is then lost.
|
||||
| `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). |
|
||||
| `SERVER_DRAINING` | 409 | The server is about to restart and starts no new remote session or download job. Open work continues. Retryable after the restart. |
|
||||
| `SERVICE_ERROR` | 422 | The streaming service's API returned an error, or the service does not support the requested operation. |
|
||||
| `DRM_ERROR` | 422 | DRM or license acquisition failed. |
|
||||
| `RATE_LIMITED` | 429 | The streaming service is rate-limiting requests. Retryable. |
|
||||
@@ -138,19 +140,20 @@ the JSON error is then lost.
|
||||
the window is fixed, so an early retry only spends the next window. See
|
||||
[Authentication](authentication.md#rate-limit-rejections).
|
||||
|
||||
### Server errors (5xx)
|
||||
### Server errors (422 and 424)
|
||||
|
||||
| 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. |
|
||||
| `NETWORK_ERROR` | 503 | A network connectivity problem (connection, timeout, DNS, TLS). Retryable. |
|
||||
| `SERVICE_UNAVAILABLE` | 503 | The service is temporarily unavailable or under maintenance. Retryable. |
|
||||
| `INTERNAL_ERROR` | 422 | An unexpected server-side error. This is the fallback for any exception the server could not categorise, and for an exception that no handler caught. |
|
||||
| `DOWNLOAD_ERROR` | 422 | The download process itself failed. |
|
||||
| `WORKER_ERROR` | 422 | The download worker subprocess errored out. |
|
||||
| `NETWORK_ERROR` | 424 | A network connectivity problem (connection, timeout, DNS, TLS). Retryable. |
|
||||
| `SERVICE_UNAVAILABLE` | 424 | 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.
|
||||
`422`. In practice every code above has an explicit mapping. A middleware
|
||||
in every server mode also changes any `5xx` answer, such as one from a CDM subapplication, to `422`.
|
||||
|
||||
## How the server produces errors
|
||||
|
||||
@@ -169,14 +172,14 @@ 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 |
|
||||
| `connection`, `timeout`, `network`, `unreachable`, `socket`, `dns`, `resolve` (or `ConnectionError`, `TimeoutError`, `URLError`, `SSLError`) | `NETWORK_ERROR` (424) | yes |
|
||||
| `geofence`, `geoblock`, `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` (422) | no |
|
||||
| `service unavailable`, `503`, `maintenance`, `temporarily unavailable` | `SERVICE_UNAVAILABLE` (503) | yes |
|
||||
| `service unavailable`, `503`, `maintenance`, `temporarily unavailable` | `SERVICE_UNAVAILABLE` (424) | yes |
|
||||
| `invalid`, `malformed`, `validation` (or `ValueError`, `ValidationError`) | `INVALID_INPUT` (400) | no |
|
||||
| anything else | `INTERNAL_ERROR` (500) | no |
|
||||
| anything else | `INTERNAL_ERROR` (422) | no |
|
||||
|
||||
Categorised errors include a `reason` tag inside `details` (such as
|
||||
`"network_connectivity"` or `"drm_failure"`) so a client can react without parsing
|
||||
|
||||
@@ -443,8 +443,8 @@ The `error_code` maps to an HTTP status. The ones you will meet most often:
|
||||
| `CONFLICT` | 409 | Illegal state transition (e.g. retry a running job) |
|
||||
| `SERVICE_ERROR` | 422 | The service raised an error |
|
||||
| `RATE_LIMITED` | 429 | Upstream throttling (retryable) |
|
||||
| `INTERNAL_ERROR` | 500 | Unexpected server-side failure |
|
||||
| `NETWORK_ERROR` | 503 | Connection/timeout reaching the service (retryable) |
|
||||
| `INTERNAL_ERROR` | 422 | Unexpected server-side failure |
|
||||
| `NETWORK_ERROR` | 424 | Connection/timeout reaching the service (retryable) |
|
||||
|
||||
The [error code catalogue](errors.md#error-code-catalogue) lists every code.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user