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.']}
Two ValidationErrors
aniliberty.ValidationError means the request was invalid (HTTP 422). It is not
pydantic.ValidationError — a response that doesn't fit the models raises
ResponseParseError instead, so you never need to catch pydantic's exception.
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")
raise
Retries¶
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.