# Errors and retries How failed requests are reported, and which requests are retried. ## Errors A request that fails with a non-success HTTP status returns an `*APIError`. Inspect it with `errors.AsType`: ```go _, err := client.LookupIP(ctx, netip.MustParseAddr("8.8.8.8")) apiErr, ok := errors.AsType[*synthient.APIError](err) if ok { switch { case apiErr.StatusCode == http.StatusPaymentRequired: fmt.Println("out of lookup credits") case apiErr.StatusCode == http.StatusTooManyRequests && apiErr.RetryAfter > 0: fmt.Println("rate limited, retry after", apiErr.RetryAfter) case apiErr.StatusCode == http.StatusTooManyRequests: fmt.Println("monthly request budget spent") default: fmt.Println("request", apiErr.RequestID, "failed:", cmp.Or(apiErr.Detail, apiErr.Title)) } } ``` - **`*APIError`:** The API, or the storage host during `Download`, returned a non-success status. - **`ErrMissingAPIKey`:** `NewClient` was given an empty or all-whitespace key. - **Context errors:** A canceled or expired context returns an error that matches `context.Canceled` or `context.DeadlineExceeded` with `errors.Is`. - **Transport errors:** A network failure on an API request is returned as the `*url.Error` the HTTP client reported. `Download` removes that wrapper from storage failures to keep the presigned URL out of the message, and a failure while reading a response is wrapped with a `synthient:` prefix. - **Other errors:** An invalid argument, such as an invalid address, more than 1000 addresses, or a malformed export ID, is rejected before any request is sent, and a response that can't be decoded also fails. These are plain errors whose messages start with `synthient:`. See [Errors](https://docs.synthient.com/errors) for what each status means. ### APIError - `StatusCode` `int` — The HTTP status code of the response. - `Type` `string` — A URI identifying the problem type, for example `"https://docs.synthient.com/errors/unauthorized"`. - `Title` `string` — A short summary of the problem type. - `Detail` `string` — Explains this occurrence of the problem. For a plain-text error, such as the edge's 401 for a malformed key, it is the response body. - `Errors` `map[string][]string` — Maps request fields to validation messages. - `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. - `RetryAfter` `time.Duration` — How long the server asked the client to wait before retrying, or zero if it did not say. * **Logging:** `Error()` returns only the HTTP status code and its standard description, such as `synthient: 402 Payment Required`, so it is safe to log. * **Untrusted fields:** `Title`, `Detail`, `Errors`, and `RequestID` contain untrusted server data and may include sensitive information, so inspect them explicitly. * **Unwrapping:** `Unwrap()` returns the error from reading the response body, if any, which preserves `errors.Is` checks for cancellation and deadlines. ## Retries and timeouts Which requests are retried: - **Account and feed requests** are retried on 500, 502, 503, and 504 responses, on transport errors other than TLS certificate failures and HTTP responses to HTTPS requests, and on 429 responses that carry `Retry-After`. A failure while reading a response body is not retried, and neither is the request `Download` makes to the storage host for the file itself. - **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 as billed lookups, 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 returned without a retry. 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, or a wait that would pass the context deadline, returns the response instead of retrying. - Waits stop when the context is done. - The client retries a failed request up to twice by default. Set the count with `WithMaxRetries`, where 0 disables retries. The client sets no overall request timeout, because streams and downloads are long-lived. Bound calls with a context deadline instead: ```go ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() info, err := client.LookupIP(ctx, netip.MustParseAddr("8.8.8.8")) if err != nil { log.Fatal(err) } fmt.Println(info.IP) ``` The default HTTP client waits at most one minute for response headers.