Errors and retries
Everything the SDK raises on purpose is an AniLibertyError.
Failures that are likely to go away — rate limits, server errors, network problems — are
retried automatically before an exception reaches you.
examples/14_error_handling.py
is a runnable version of this page.
The exception hierarchy
AniLibertyError
├── APIError the API (or the edge in front of it) answered with an error
│ ├── NotFoundError 404
│ ├── ValidationError 422
│ ├── RateLimitError 429
│ ├── AuthRequiredError 401 / 403 with a JSON body
│ ├── ServerError 5xx
│ └── AccessBlockedError an HTML page instead of JSON (WAF / anti-bot)
├── TransportError network error or timeout, no response at all
└── ResponseParseError a successful response that doesn't match the expected shape| Exception | When | Retried | What to do |
|---|---|---|---|
NotFoundError |
unknown ID or alias, unknown path | no | check the identifier |
ValidationError |
the API rejected a parameter, e.g. limit too big |
no | fix the call; error.errors names the parameter |
RateLimitError |
429 | yes | slow down; error.retry_after if the API sent it |
ServerError |
5xx | yes | try again later |
AccessBlockedError |
an HTML page instead of JSON | never | try the mirror or another network |
AuthRequiredError |
401 wrong credentials, 403 no valid session | no | log in again — see Authentication |
APIError |
any other error status, e.g. 405 | no | — |
TransportError |
connection error, timeout | yes | check the network; error.__cause__ is the httpx exception |
ResponseParseError |
the response didn't fit the SDK's model | no | report it; the raw data is in error.payload |
Every APIError carries status_code, message, method, url and payload (the decoded
JSON body, or its text). Values from the live API:
try:
await client.releases.get("no-such-release")
except NotFoundError as error:
error.status_code # 404
error.message # 'Not found'
error.payload # {'message': 'No query results for model [App\\...\\Release].'}
try:
await client.releases.latest(limit=51)
except ValidationError as error:
error.errors # {'limit': ['Поле limit не может быть больше 50.']}Argument errors are not API errors
Arguments the SDK can check itself are checked before anything is sent, and raise the built-in
TypeError or ValueError — for example an alias where the API only accepts a numeric ID
(client.franchises.for_release("sousou-no-frieren")), or an ID containing /. These are bugs
in the calling code, so they are deliberately not AniLibertyErrors.
Catching
Most specific first, the base class last:
try:
release = await client.releases.get(alias)
except NotFoundError:
release = None
except (TransportError, ServerError, RateLimitError):
raise # already retried: the API or the network is in trouble
except AniLibertyError:
log.exception("AniLibria API call failed")
raiseRetries
Every SDK method sends GET requests, which are retried on 429, 5xx and network
errors/timeouts:
- up to 3 retries (4 attempts in total);
- the delay is the response's
Retry-After(seconds or an HTTP date) when it has one, otherwise 1 s, 2 s, 4 s; - a
Retry-Afterlonger than 60 s isn't waited out:RateLimitErroris raised at once with that value inretry_after; - 404, 422, other 4xx and HTML block pages are never retried — the same request would get the same answer.
For requests that aren't idempotent (POST, PATCH through client.http.request()) only 429
is retried: after a 5xx or a dropped connection it's unknown whether the server already acted.
The exception you get is the one from the last attempt, so a ServerError or
TransportError means the problem lasted through about 7 seconds of backoff.
Seeing retries
The SDK logs to the standard logging logger aniliberty: every request at DEBUG, every
retry at INFO.
import logging
logging.basicConfig(format="%(levelname)s %(name)s: %(message)s")
logging.getLogger("aniliberty").setLevel(logging.INFO)INFO aniliberty: GET https://aniliberty.top/api/v1/app/status: ConnectTimeout, retrying in 1.0s (retry 1 of 3)
INFO aniliberty: GET https://aniliberty.top/api/v1/app/status: ConnectTimeout, retrying in 2.0s (retry 2 of 3)
INFO aniliberty: GET https://aniliberty.top/api/v1/app/status: ConnectTimeout, retrying in 4.0s (retry 3 of 3)Timeouts
AniLiberty(timeout=30.0) is the timeout of each attempt, in seconds, with httpx semantics
(it applies to connecting, reading, writing and waiting for a pooled connection). With retries,
the worst case for one call is about 4 × timeout + 7 s. For finer control — a short connect
timeout with a long read timeout — pass your own client with an httpx.Timeout; see
Mirrors and a custom HTTP client.
For a deadline on a whole operation, wrap it in asyncio.timeout(...) — see
Concurrency.
AccessBlockedError
The SDK always sends Accept: application/json, and with it the API answers with JSON even for
errors. An HTML page therefore comes from the Cloudflare edge in front of the API — a bot
challenge or a block of your network. Repeating the request gets the same page, so it isn't
retried. If the site opens in a browser on the same network, try the mirror
(base_url="https://anilibria.top/api/v1"); the page itself is in error.payload.
An HTML page with a 5xx status is a ServerError instead: Cloudflare renders its own
origin errors (502, 520, ...) as HTML, and those are worth retrying.
ResponseParseError
Raised when a successful response isn't JSON or doesn't fit the SDK's model — usually because
the API changed its format. Nothing is lost: error.payload holds the decoded JSON (or the
body text) and error.__cause__ the original pydantic or json exception. Please
open an issue with the URL; meanwhile
client.http.request() returns the raw JSON of the same
endpoint.