Errors and retries

View as markdown

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

Errors

Every exception the SDK throws for a failed call is unchecked and extends SynthientException:

ExceptionThrown when
ApiExceptionThe API answered with an error status.
ApiConnectionExceptionNo response arrived: the connection failed or was lost, or the calling thread was interrupted. After an interruption, the thread's interrupt flag is set again. getCause() holds the underlying failure.
ApiTimeoutExceptionConnecting took longer than the connect timeout, or the API sent nothing for longer than the client's timeout, either while waiting for the response headers or between parts of the body. It extends ApiConnectionException.
SynthientExceptionThe API sent a response the SDK could not use, such as invalid JSON, a missing required field, or a body over the size limit. It is also the parent of the others.
try {
    IpInfo info = client.lookupIp("8.8.8.8");
    System.out.println(info.ip());
} catch (ApiException e) {
    switch (e.statusCode()) {
        case 402 -> System.out.println("out of lookup credits");
        case 429 -> System.out.println(
                e.retryAfter() == null ? "monthly request budget spent" : "rate limited, retry after " + e.retryAfter());
        default -> System.out.println(
                "request " + e.requestId() + " failed: " + (e.detail().isEmpty() ? e.title() : e.detail()));
    }
} catch (ApiConnectionException e) {
    System.out.println("no response: " + e.getMessage());
}
  • Before a request: Invalid arguments, such as an address that is not an IP literal, an invalid export ID, or a missing API key, throw IllegalArgumentException, and no request is sent.
  • Invalid state: Calls on a closed client, and a second iteration of an EventStream, throw IllegalStateException.
  • Downloads: Reading the InputStream that download returns throws IOException, such as java.net.SocketTimeoutException, rather than an SDK exception.

See Errors for what each status means.

ApiException

The fields come from the problem details document in the response body when there is one.

  • Name
    statusCode
    Type
    int
    Description

    The HTTP status of the response.

  • Name
    type
    Type
    String
    Description

    The problem type URI, or an empty string.

  • Name
    title
    Type
    String
    Description

    The short problem summary, or an empty string.

  • Name
    detail
    Type
    String
    Description

    The server's explanation of the problem, or an empty string. For a plain-text error, such as the edge's 401 for a malformed key, it is the whole body.

  • Name
    errors
    Type
    Map<String, List<String>>
    Description

    Validation errors by field name, or an empty map.

  • Name
    requestId
    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, and is otherwise an empty string.

  • Name
    retryAfter
    Type
    Duration
    Description

    How long the server asked the client to wait before retrying, or null if it did not say. A 429 without it means the monthly budget is spent or too many streams are open, so retrying will not help.

  • Logging: getMessage() holds only the status, such as 403 Forbidden, so the exception is safe to log.
  • Untrusted fields: type(), title(), detail(), errors(), and requestId() contain untrusted server data, so inspect them explicitly.

Constructors

The exception constructors are public, for example to stub the SDK in tests.

public SynthientException(String message)
public SynthientException(String message, @Nullable Throwable cause)
public ApiConnectionException(String message, @Nullable Throwable cause)
public ApiTimeoutException(String message, @Nullable Throwable cause)
public ApiException(
        int statusCode,
        String type,
        String title,
        String detail,
        Map<String, List<String>> errors,
        String requestId,
        @Nullable Duration retryAfter,
        @Nullable Throwable cause)
  • Input: ApiException takes an empty string or map for a missing field, and cause is the failure that interrupted reading the error body, if any.

Retries and timeouts

Which requests are retried:

  • Account and feed requests (account, listExports, each page of allExports, exportMeta, downloadUrl, the API request of download, and stream connections) are retried on 500, 502, 503, and 504 responses, on 429 responses that carry Retry-After, and on connection failures and timeouts that happen before the response arrives, other than TLS failures and interruptions.
  • Billed lookups (lookupIp, lookupIps, and lookupDomain) 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 thrown without a retry.
  • Reads: A failure while reading a response body, stream, or download is not retried, and neither is a download's transfer from the export storage host.
  • OkHttp itself never resends a request.

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 throws instead of retrying.
  • Interrupting the waiting thread throws ApiConnectionException.
  • Set the retry count with maxRetries on the builder.

The client sets no overall request timeout, because streams and downloads are long-lived. Instead, the builder's timeout, 60 seconds by default, bounds how long the client waits for the response headers and then for each part of the body, and exceeding it throws ApiTimeoutException, after any retries for account and feed requests.

try {
    IpInfo info = client.lookupIp("8.8.8.8");
    System.out.println(info.ip());
} catch (ApiTimeoutException e) {
    System.err.println("no response in time: " + e.getMessage());
}
  • Connection: Connecting has its own timeout of 10 seconds, or your OkHttpClient's if you pass one, and running out of it also throws ApiTimeoutException.
  • Cancellation: Interrupting a thread that is waiting for a response or for a retry cancels the call, which throws ApiConnectionException. A stream or download that is already reading may not notice an interrupt until more data arrives, so close it from another thread instead.