# Errors and retries How failed requests are reported, and which requests are retried. ## Errors Every exception class the SDK defines derives from `SynthientError`. A request that fails with a non-success HTTP status raises an `APIError`: ```python import synthient try: client.lookup_ip("8.8.8.8") except synthient.APIError as error: if error.status_code == 402: print("out of lookup credits") elif error.status_code == 429 and error.retry_after is not None: print("rate limited, retry after", error.retry_after) elif error.status_code == 429: print("monthly request budget spent") else: print("request", error.request_id, "failed:", error.detail or error.title) ``` - **Before a request:** An invalid argument, such as a missing API key or a malformed IP address, raises `ValueError`, and no request is sent. An API key, IP address, domain, or export ID of the wrong type raises `TypeError` instead. - **Closed client:** A closed client raises `RuntimeError`. - **Decoding:** A response body that cannot be decoded raises a plain `SynthientError`. See [Errors](https://docs.synthient.com/errors) for what each status means. ### SynthientError The base class of every SDK error, including `grpcschema.SchemaError`. ### APIConnectionError A subclass of `SynthientError` raised when the connection failed, such as after a DNS, connection, or TLS failure, or broke while an API response or stream was being read. The underlying httpx exception is chained as `__cause__`. Reading a `download()` body is plain httpx, so a failure there raises the httpx exception itself, such as `httpx.ReadError`. ### APITimeoutError A subclass of `APIConnectionError` raised when a request timed out while connecting, sending, or waiting for data. A timeout while you read a `download()` body raises `httpx.ReadTimeout` instead. ### APIError A subclass of `SynthientError` raised when the API, or the storage host during `download()`, answered with a non-success HTTP status. - `status_code` `int` — The HTTP status code, such as 402 when the account is out of credits. - `type` `str` — The problem type URI, for example `"https://docs.synthient.com/errors/unauthorized"`, or `""` when the body had none. - `title` `str` — A short summary of the problem. - `detail` `str` — Explains this occurrence of the problem. For a plain-text error, such as the edge's 401 for a malformed key, it is the response body. - `errors` `dict[str, list[str]]` — Validation messages by request field, set on 422 responses. - `request_id` `str` — Identifies the request when contacting Synthient support. It comes from the `X-Request-Id` header, or the problem's `instance` when the header is absent. - `retry_after` `float | None` — Seconds the server asked the caller to wait before retrying, or `None` if it did not say. * **Logging:** `str(error)` contains only the HTTP status code and its standard phrase, such as `402 Payment Required`, so it is safe to log. * **Untrusted fields:** `type`, `title`, `detail`, `errors`, and `request_id` contain untrusted server data and may include sensitive information, so log them deliberately. * **Cause:** If reading the error body failed, the httpx exception is chained as `__cause__`. ## Retries and timeouts Which requests are retried: - **Account and feed requests** are retried on 500, 502, 503, and 504 responses, on 429 responses that carry `Retry-After`, and on network failures and timeouts before a response arrives, but not on certificate or proxy errors. A failure while reading a response body or stream is not retried. - **Billed lookups** (`lookup_ip`, `lookup_ips`, and `lookup_domain`) are retried only on 429 responses that carry `Retry-After`, which the rate limiter returns before doing any work, so a lookup is never charged twice. `health` is retried the same way, so an unhealthy API is reported at once. - **A 429 without `Retry-After`** means the key's monthly request budget is spent or it has too many open streams, so it is raised without a retry. - **Downloads** from the export storage host are never retried. How retries wait: - Retries back off exponentially from one second, doubling up to 30 seconds, with 25% jitter. - The wait is never shorter than `Retry-After`. - A `Retry-After` longer than a minute raises the error instead of retrying. - Set the retry count with `max_retries`. The `timeout` argument bounds each network operation of an attempt, such as connecting or a single read, not a whole call with its retries. The default waits up to 10 seconds to connect and 60 seconds for each read or write: ```python import httpx import synthient client = synthient.Client(timeout=httpx.Timeout(30.0, connect=5.0)) ``` A timeout raises `APITimeoutError`, except while you read a `download()` body, where httpx raises `httpx.ReadTimeout`. With `AsyncClient`, bound a whole call, including its retry waits, with `asyncio.timeout()` on asyncio, or with `anyio.fail_after()`, which works on both asyncio and Trio: ```python import anyio with anyio.fail_after(10): info = await client.lookup_ip("8.8.8.8") print(info.ip) ```