Errors and retries

View as markdown

How failed requests are reported, and which requests are retried.

Errors

Every fallible method returns synthient::Result:

pub type Result<T, E = Error> = std::result::Result<T, E>;

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:

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 for what each status means.

Error

  • Name
    InvalidInput
    Type
    String
    Description

    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.

  • Name
    Api
    Type
    Box<ApiError>
    Description

    The API, or the storage host serving a download, answered with a non-success status.

  • Name
    Http
    Type
    reqwest::Error
    Description

    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.

  • Name
    Timeout
    Description

    The operation took longer than the client's timeout.

  • Name
    Decode
    Type
    serde_json::Error
    Description

    A response or stream event was not the JSON the API documents.

  • Name
    Protocol
    Type
    String
    Description

    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

  • Name
    status
    Type
    StatusCode
    Description

    The HTTP status of the response.

  • Name
    problem_type
    Type
    Option<String>
    Description

    A URI identifying the problem type, such as https://docs.synthient.com/errors/unauthorized. The API names this field type.

  • Name
    title
    Type
    Option<String>
    Description

    A short summary of the problem type.

  • Name
    detail
    Type
    Option<String>
    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
    BTreeMap<String, Vec<String>>
    Description

    Validation messages keyed by request field. It is empty when the response has none.

  • Name
    request_id
    Type
    Option<String>
    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
    Option<Duration>
    Description

    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:

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);