# Errors and retries How failed requests are reported, and which requests are retried. ## Errors Every fallible method returns `synthient::Result`: ```rust pub type Result = std::result::Result; ``` A request that fails with a non-success HTTP status returns `Error::Api`, whose `ApiError` holds the status, the problem detail, the request ID, and any `Retry-After` delay. Match on it: ```rust use std::net::Ipv4Addr; use synthient::{Error, StatusCode}; match client.lookup_ip(Ipv4Addr::new(8, 8, 8, 8)).await { Ok(info) => println!("{:?}", info.intelligence), Err(Error::Api(err)) if err.status == StatusCode::PAYMENT_REQUIRED => { eprintln!("out of lookup credits"); } Err(Error::Api(err)) if err.status == StatusCode::TOO_MANY_REQUESTS => { match err.retry_after { Some(retry_after) => eprintln!("rate limited, retry after {retry_after:?}"), None => eprintln!("monthly request budget spent"), } } Err(Error::Api(err)) => { let message = err.detail.or(err.title).unwrap_or_default(); let request_id = err.request_id.unwrap_or_default(); eprintln!("request {request_id} failed: {message}"); } Err(err) => eprintln!("lookup failed: {err}"), } ``` See [Errors](https://docs.synthient.com/errors) for what each status means. ### Error - `InvalidInput` `String` — An argument or configuration value was rejected before anything was sent, such as an empty API key, more than 1000 addresses, or a malformed export ID. The message describes the problem. - `Api` `Box` — The API, or the storage host serving a download, answered with a non-success status. - `Http` `reqwest::Error` — The HTTP client could not be built, the request could not be sent, or the response could not be read. URLs are removed from these errors because download URLs grant access to an export. - `Timeout` — The operation took longer than the client's `timeout`. - `Decode` `serde_json::Error` — A response or stream event was not the JSON the API documents. - `Protocol` `String` — A response broke the API's contract, such as an oversized body, a missing redirect, or a repeated pagination cursor. * **Matching:** `Error` is `#[non_exhaustive]`, so a `match` on it needs a wildcard arm. * **Logging:** Its `Display` output never includes server-provided details or URLs. * **Unwrapping:** `source()` returns the underlying error for `Http` and `Decode`. ### ApiError - `status` `StatusCode` — The HTTP status of the response. - `problem_type` `Option` — A URI identifying the problem type, such as `https://docs.synthient.com/errors/unauthorized`. The API names this field `type`. - `title` `Option` — A short summary of the problem type. - `detail` `Option` — 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` `BTreeMap>` — Validation messages keyed by request field. It is empty when the response has none. - `request_id` `Option` — 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` `Option` — How long the server asked the client to wait before retrying, from the `Retry-After` header in seconds, or `None` if it did not say. * **Traits:** `ApiError` implements `Display` and `std::error::Error`. * **Logging:** `Display` writes only the status, such as `api returned 402 Payment Required`, so it is safe to log. * **Untrusted fields:** `problem_type`, `title`, `detail`, `errors`, and `request_id` contain untrusted server data and may include sensitive information, so inspect them explicitly. ## Retries and timeouts Which requests are retried: - **Account and feed requests** are retried on 500, 502, 503, and 504 responses, on transport errors, including a failure while reading a JSON response body, and on 429 responses that carry `Retry-After`. - **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 returned without a retry. - **A download's request to the storage host** is not retried, and neither is a failure while reading a stream or download. - **The SDK turns off reqwest's own retries**, so no request is ever resent by the transport. How retries wait: - Retries back off exponentially from one second, doubling up to 30 seconds, with 25% jitter either way. - The wait is never shorter than `Retry-After`. - A `Retry-After` longer than a minute is returned to you instead of waited out. - When the client has a `timeout`, a retry whose wait would outlast it is skipped, so you get the last response or error rather than `Error::Timeout`. - Set the retry count with `max_retries`. By default: - Connecting gives up after 30 seconds. - A read that stalls for 60 seconds fails, which streams never hit because the server sends a keepalive line every 15 seconds. - There is no overall limit, because streams and downloads are long-lived. Set one for every operation with `timeout`, or drop a future to cancel it: ```rust use std::net::Ipv4Addr; use std::time::Duration; use synthient::{Client, reqwest}; let http = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) .read_timeout(Duration::from_secs(30)); let client = Client::builder(std::env::var("SYNTHIENT_API_KEY")?) .http_client(http) .timeout(Duration::from_secs(10)) .build()?; let info = client.lookup_ip(Ipv4Addr::new(8, 8, 8, 8)).await?; println!("{}", info.ip); ```