# Errors and retries How failed requests are reported, and which requests are retried. ## Errors Every error the SDK raises for a failed request is a `Synthient::Error`. | Class | Superclass | Raised when | | ---------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `Synthient::Error` | `StandardError` | The base class. It is also raised directly when an export listing repeats a cursor or an export download hits a redirect it cannot follow. | | `Synthient::ConnectionError` | `Synthient::Error` | The request could not be sent, or the response could not be read. The underlying exception is available as `cause`. | | `Synthient::TimeoutError` | `Synthient::ConnectionError` | Connecting, sending, or waiting for response data took longer than the client's timeout. | | `Synthient::DecodeError` | `Synthient::Error` | A successful response did not match the API schema. | | `Synthient::APIError` | `Synthient::Error` | The API, or export storage, answered with an error status. | ```ruby begin client.lookup_ip("8.8.8.8") rescue Synthient::APIError => e case e.status when 402 puts "out of lookup credits" when 429 puts e.retry_after ? "rate limited, retry after #{e.retry_after}s" : "monthly request budget spent" else puts "request #{e.request_id} failed: #{e.detail || e.title}" end end ``` - **Before a request:** Invalid arguments raise `ArgumentError` or `TypeError` instead, before any request is sent. - **Other errors:** Requests on a closed client raise `IOError`, and exceptions raised by a `response_hook` propagate unchanged. Neither is a `Synthient::Error`. See [Errors](https://docs.synthient.com/errors) for what each status means. ### APIError - `status` `Integer` — The HTTP status code of the response. - `type` `String?` — A URI identifying the problem type. - `title` `String?` — A short summary of the problem. - `detail` `String?` — Explains this occurrence of the problem. For a `text/plain` error, such as the edge's 401 for a malformed key, it is the response body. - `errors` `Hash[String, Array[String]]` — Validation messages by request field, from a 422. It is empty when the response has none. - `request_id` `String?` — 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` `Numeric?` — Seconds the server asked the client to wait before retrying, or `nil` if it did not say. * **Logging:** The message holds only the status code and its standard description, such as `"429 Too Many Requests"`, so it is safe to log. * **Problem details:** The problem details the API sent are available only through the attributes above. - `Synthient::APIError::STATUS_TEXT` `Hash[Integer, String]` — The standard descriptions used in messages, by status code. A status without one, such as `418`, is shown as the bare number. `APIError.new` builds an error, for example to stub the client in tests: ```ruby def initialize: (status: Integer, ?type: String?, ?title: String?, ?detail: String?, ?errors: Hash[String, Array[String]], ?request_id: String?, ?retry_after: Numeric?) -> void ``` ## Retries and timeouts Which requests are retried: - **Billed lookups** (`lookup_ip`, `lookup_ips`, and `lookup_domain`) and `health` are retried only on 429 responses that carry `Retry-After`, which the rate limiter returns before doing any work, so a retried lookup is never charged twice. They are never resent after a network failure, because the API may already have done the work. - **Account and feed requests** are retried on 500, 502, 503, and 504 responses, on network failures and timeouts before a response arrives (except TLS errors), and on 429 responses that carry `Retry-After`. - **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, and neither is a failure while reading a stream or a download. - **`Net::HTTP`'s own retry** is disabled, so a stale keep-alive connection never replays a billed lookup. 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 60 seconds raises the error instead of retrying. - Set the retry count with `max_retries:`. `open_timeout:` bounds each wait for a connection and `timeout:` bounds each read or write. Exceeding either raises `Synthient::TimeoutError`. The client sets no overall deadline, because streams and downloads are long-lived. A quiet stream receives only a blank keepalive line about every 15 seconds, so keep `timeout:` above that. ```ruby client = Synthient::Client.new(timeout: 30, open_timeout: 5) begin puts client.lookup_ip("8.8.8.8").ip rescue Synthient::TimeoutError warn "Synthient did not answer in time" ensure client.close end ```