Errors and retries

View as markdown

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:

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 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.

  • Name
    status_code
    Type
    int
    Description

    The HTTP status code, such as 402 when the account is out of credits.

  • Name
    type
    Type
    str
    Description

    The problem type URI, for example "https://docs.synthient.com/errors/unauthorized", or "" when the body had none.

  • Name
    title
    Type
    str
    Description

    A short summary of the problem.

  • Name
    detail
    Type
    str
    Description

    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.

  • Name
    errors
    Type
    dict[str, list[str]]
    Description

    Validation messages by request field, set on 422 responses.

  • Name
    request_id
    Type
    str
    Description

    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.

  • Name
    retry_after
    Type
    float | None
    Description

    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:

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:

import anyio

with anyio.fail_after(10):
    info = await client.lookup_ip("8.8.8.8")
print(info.ip)