Errors and retries

View as markdown

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

Errors

Every error the SDK raises for a failed request is a Synthient::Error.

ClassSuperclassRaised when
Synthient::ErrorStandardErrorThe 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::ConnectionErrorSynthient::ErrorThe request could not be sent, or the response could not be read. The underlying exception is available as cause.
Synthient::TimeoutErrorSynthient::ConnectionErrorConnecting, sending, or waiting for response data took longer than the client's timeout.
Synthient::DecodeErrorSynthient::ErrorA successful response did not match the API schema.
Synthient::APIErrorSynthient::ErrorThe API, or export storage, answered with an error status.
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 for what each status means.

APIError

  • Name
    status
    Type
    Integer
    Description

    The HTTP status code of the response.

  • Name
    type
    Type
    String?
    Description

    A URI identifying the problem type.

  • Name
    title
    Type
    String?
    Description

    A short summary of the problem.

  • Name
    detail
    Type
    String?
    Description

    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.

  • Name
    errors
    Type
    Hash[String, Array[String]]
    Description

    Validation messages by request field, from a 422. It is empty when the response has none.

  • Name
    request_id
    Type
    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
    Numeric?
    Description

    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.
  • Name
    Synthient::APIError::STATUS_TEXT
    Type
    Hash[Integer, String]
    Description

    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:

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.

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