# Errors and retries How failed requests are reported, and which requests are retried. ## Errors A call that fails with an HTTP error status throws an `ApiException`. ```csharp 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](https://docs.synthient.com/sdks/csharp/client#dispose). See [Errors](https://docs.synthient.com/errors) for what each status means. ### SynthientException The base type of the exceptions the SDK throws for failed API calls. ```csharp 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. ```csharp public sealed class ApiException : SynthientException public ApiException(int statusCode, Exception? innerException = null) ``` - `StatusCode` `int` — The HTTP status code. - `Type` `string?` — The problem type URI, such as `"https://docs.synthient.com/errors/validation-failed"`. - `Title` `string?` — A short summary of the problem. - `Detail` `string?` — 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. - `Errors` `IReadOnlyDictionary>` — Validation errors by request field, from a 422 response. It is empty when there are none. - `RequestId` `string?` — 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. - `RetryAfter` `TimeSpan?` — How long the server asked the client to wait before retrying, or `null` if the response had no valid `Retry-After` header. - `Headers` `IReadOnlyDictionary>` — 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. ```csharp 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. ```csharp 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: ```csharp 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); ```