# Errors and retries 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`: | Exception | Thrown when | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ApiException` | The API answered with an error status. | | `ApiConnectionException` | No 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. | | `ApiTimeoutException` | Connecting 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`. | | `SynthientException` | The 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. | ```java 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](https://docs.synthient.com/errors) for what each status means. ### ApiException The fields come from the problem details document in the response body when there is one. - `statusCode` `int` — The HTTP status of the response. - `type` `String` — The problem type URI, or an empty string. - `title` `String` — The short problem summary, or an empty string. - `detail` `String` — 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. - `errors` `Map>` — Validation errors by field name, or an empty map. - `requestId` `String` — 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. - `retryAfter` `Duration` — 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. ```java 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> 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. ```java 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.