Errors and retries

View as markdown

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

Errors

A call that fails with an HTTP error status throws an ApiException.

try
{
    await client.LookupIPAsync(IPAddress.Parse("8.8.8.8"));
}
catch (ApiException e) when (e.StatusCode == 402)
{
    Console.WriteLine("out of lookup credits");
}
catch (ApiException e) when (e.StatusCode == 429 && e.RetryAfter is { } retryAfter)
{
    Console.WriteLine($"rate limited, retry after {retryAfter}");
}
catch (ApiException e) when (e.StatusCode == 429)
{
    Console.WriteLine("monthly request budget spent");
}
catch (ApiException e)
{
    Console.WriteLine($"request {e.RequestId} failed: {e.Detail ?? e.Title}");
}
catch (ApiConnectionException e)
{
    Console.WriteLine($"no response: {e.Message}");
}
  • Transport and decoding: A call that gets no response throws an ApiConnectionException, or its subclass ApiTimeoutException when SynthientClientOptions.Timeout or the default handler's 10-second connect timeout elapses. A response the SDK cannot decode throws a SynthientException, the base type of all of them.
  • Cancellation: Cancelling a call's token throws OperationCanceledException.
  • Response hook: An exception thrown by ResponseHook fails the call unchanged.
  • Before a request: Invalid arguments, such as an IPv6 address with a scope ID or a malformed export ID, throw an ArgumentException before any request is sent.
  • Disposal: Calls on a disposed client throw ObjectDisposedException. See Dispose.

See Errors for what each status means.

SynthientException

The base type of the exceptions the SDK throws for failed API calls.

public class SynthientException : Exception
public SynthientException(string message)
public SynthientException(string message, Exception? innerException)
  • Thrown when: It is thrown directly when the API returns a response the SDK cannot use, such as a malformed body, a body or stream event larger than 16 MiB, or an export listing that repeats a cursor.

ApiException

The API returned an error status.

public sealed class ApiException : SynthientException
public ApiException(int statusCode, Exception? innerException = null)
  • Name
    StatusCode
    Type
    int
    Description

    The HTTP status code.

  • Name
    Type
    Type
    string?
    Description

    The problem type URI, such as "https://docs.synthient.com/errors/validation-failed".

  • Name
    Title
    Type
    string?
    Description

    A short summary of the problem.

  • Name
    Detail
    Type
    string?
    Description

    A description of this occurrence of the problem. For the plain-text errors that the API's edge returns, such as a 401 for a malformed key, it is the response body.

  • Name
    Errors
    Type
    IReadOnlyDictionary<string, IReadOnlyList<string>>
    Description

    Validation errors by request field, from a 422 response. It is empty when there are none.

  • Name
    RequestId
    Type
    string?
    Description

    The ID of the request, for Synthient support. It comes from the X-Request-Id header, or the problem's instance when the header is absent or empty.

  • Name
    RetryAfter
    Type
    TimeSpan?
    Description

    How long the server asked the client to wait before retrying, or null if the response had no valid Retry-After header.

  • Name
    Headers
    Type
    IReadOnlyDictionary<string, IReadOnlyList<string>>
    Description

    The response headers, with case-insensitive names. It is empty for an error from the export storage host.

  • Logging: Message holds only the status, such as 403 Forbidden, so it is safe to log.
  • Untrusted fields: The other properties contain untrusted server data and can include sensitive information, so inspect them explicitly.
  • Unwrapping: InnerException is the exception that interrupted reading the error body, if any.
  • Testing: The constructor is public so applications can stub the SDK in their own tests, and the other properties can be set with an object initializer.

ApiConnectionException

No response arrived: the connection failed or was closed, or reading the response failed.

public class ApiConnectionException : SynthientException
public ApiConnectionException(string message, Exception? innerException)
  • Processing: The server may still have processed the request.
  • Unwrapping: InnerException holds the underlying failure.

ApiTimeoutException

SynthientClientOptions.Timeout elapsed while waiting on the network, or the default handler's 10-second connect timeout elapsed.

public sealed class ApiTimeoutException : ApiConnectionException
public ApiTimeoutException(string message, Exception? innerException)
  • Cancellation: Cancelling a call's token throws OperationCanceledException instead.

Retries and timeouts

Which calls are retried:

  • Account and feed requests are retried on 500, 502, 503, and 504 responses, on 429 responses that carry Retry-After, and when no response arrives because the connection failed (other than a TLS error) or a timeout elapsed: SynthientClientOptions.Timeout while waiting for the response headers, or the default handler's 10-second connect timeout.
  • Billed lookups (LookupIPAsync, LookupIPsAsync, and LookupDomainAsync) and CheckHealthAsync are retried only on 429 responses that carry Retry-After, which the rate limiter returns before doing any work, so a lookup is never sent twice after the API may have charged for it.
  • A 429 without Retry-After means the key's monthly request budget is spent or it has too many open streams, so it is not retried.
  • A failure while reading a response body is never retried.
  • Downloads from the export storage host are never retried.

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 the response's ApiException instead of retrying.
  • Cancelling the call's token ends the wait.
  • Set the retry count with SynthientClientOptions.MaxRetries.

SynthientClientOptions.Timeout, 60 seconds by default, bounds each wait on the network rather than a whole call, so streams and downloads are not cut off. The client's own handler also gives up on a connection attempt after 10 seconds. Bound a whole call with its cancellation token:

using var cancel = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var info = await client.LookupIPAsync(IPAddress.Parse("8.8.8.8"), cancel.Token);
Console.WriteLine(info.IP);