Errors and Retries¶
Every failure arrives as a TypeSafeException, so a caller branches on the exception type
rather than on a status code.
The hierarchy¶
TypeSafeException
└─ TypeSafeApiException status, body, headers, endpoint, requestId,
│ errorType, errorMessage, validationErrors
├─ TypeSafeBadRequestException 400
├─ TypeSafeAuthenticationException 401
├─ TypeSafePermissionDeniedException 403
├─ TypeSafeNotFoundException 404
├─ TypeSafeUnprocessableEntityException 422
├─ TypeSafeRateLimitException 429 + retryAfterMs
├─ TypeSafeInternalServerException 5xx
│ └─ TypeSafeOverloadedException 529
└─ TypeSafeApiResponseValidationException + fieldPath
TypeSafeApiConnectionException
└─ TypeSafeApiTimeoutException
TypeSafeMissingAnswerException, TypeSafeAnswerTypeException (client side)
Twelve of these mirror the official Python SDK one for one, which the JavaScript SDK mirrors in turn. Three do not, and each earns its place:
TypeSafeOverloadedException— the API documents529 Overloadedwith retry guidance, but 529 is numerically a 5xx so both official SDKs fold it into their internal-server error. This one names it, and extendsTypeSafeInternalServerExceptionrather than sitting beside it, so catching the server error still catches an overload exactly as the Python pattern does.TypeSafeMissingAnswerException/TypeSafeAnswerTypeException— Python needs no equivalent, becauseresponse.answers["nope"]already raisesKeyErrorand reading.choiceoff a noul raisesAttributeError. Java's alternative is a bareNullPointerExceptionorClassCastException, which name the symptom instead of the cause.
Which status you actually get¶
Not always the obvious one, which is the argument for branching on the type:
| Situation | Status | Exception |
|---|---|---|
| Rejected API key | 401 | TypeSafeAuthenticationException |
| Missing API key | 403 | TypeSafePermissionDeniedException |
| Unknown model | 400 | TypeSafeBadRequestException |
| Malformed question | 400 | TypeSafeBadRequestException |
| Body fails request validation | 422 | TypeSafeUnprocessableEntityException |
| Unrouted path | 404 | TypeSafeNotFoundException |
Reading an error body¶
Error bodies arrive in a single detail envelope that takes three shapes, all read by
TypeSafeErrorDetail and reachable from any TypeSafeApiException:
| Response | detail is |
What you get |
|---|---|---|
| 400, 401, 403 | an object | errorType() (authentication_error, api_usage_error, …) and errorMessage() |
| 422 | an array | validationErrors(), each with type(), loc(), path() and msg() |
| 404 | a string | errorMessage() |
catch (TypeSafeApiException ex) {
log.warn("{} failed: {} [{}] (request {})",
ex.endpoint(), ex.errorMessage(), ex.errorType(), ex.requestId());
ex.validationErrors().forEach(e -> log.warn(" {}: {}", e.path(), e.msg()));
}
The parse is best-effort and lazy: body() always keeps the raw text, so an unrecognised
shape costs nothing and is reported as-is.
A response that arrives but cannot be read — a proxy answering 200 with HTML, or
malformed JSON — is translated too, as TypeSafeApiResponseValidationException and
TypeSafeException respectively, rather than escaping as a raw Spring exception.
Retries¶
Defaults match the official SDKs: 2 retries, exponential backoff from 500ms to 5s with
25% jitter subtracted, on 408/429/5xx and connection failures, within a 30s budget. A
429 carrying retry-after-ms overrides the computed backoff.
RetryPolicy policy = RetryPolicy.builder()
.maxRetries(3)
.initialBackoff(Duration.ofMillis(500))
.maxBackoff(Duration.ofSeconds(5))
.jitter(0.25d)
.totalTimeout(Duration.ofSeconds(30))
.build();
TypeSafeClient client = TypeSafeClient.builder().retryPolicy(policy).build();
RetryPolicy.noRetry() turns it off entirely, which is what the offline tests use.
| Method | Default | Description |
|---|---|---|
maxRetries(int) |
2 |
Repeats after the first attempt. |
initialBackoff(Duration) |
500ms |
First wait, doubling thereafter. |
maxBackoff(Duration) |
5s |
Ceiling on a single wait. |
jitter(double) |
0.25 |
Fraction subtracted at random, to avoid a thundering herd. |
retryableStatuses(Set<Integer>) |
{408, 429} |
Plus anything >= 500. |
respectRetryAfter(boolean) |
true |
Honour retry-after-ms over the computed backoff. |
retryConnectionErrors(boolean) |
true |
Retry failures with no HTTP response. |
totalTimeout(Duration) |
30s |
Budget for the whole call. |
The budget includes the next attempt
totalTimeout counts the next attempt's own HTTP timeout, not just the wait before it,
so a call never overruns the budget by a whole request. When the time left cannot fit
another attempt, the last failure is thrown instead.
Declare the transport's real timeout with timeout(...) even when you supply your own
RestClient.Builder, since that is the figure the budget uses.
Request ids¶
Every response carries x-typesafe-request-id, exposed as response.requestId() and
exception.requestId(). It travels with the data it describes — quote it when reporting a
problem.
See Also¶
- TypeSafeClient
- Batches — failures are per request, not per batch
- TypeSafe API reference