HTTP Retry Strategies
uni's HTTP client includes automatic retry for transient failures.
Default Retry Behavior
Retry is enabled by default: up to 3 retries with exponential backoff.
scala
import wvlet.uni.http.{Http, HttpRequest}
val client = Http.client.newSyncClient
val request = HttpRequest.get("https://api.example.com/users")
// Automatically retries on transient failures
val response = client.send(request)Configuring Retries
scala
// Set max retry count for every request from this client
val client = Http.client.withMaxRetry(5).newSyncClient
// Or derive a client with a different retry count
val response = client.withMaxRetry(3).send(request)Disabling Retry
scala
// Disable for specific request
val response = client.noRetry.send(request)
// Or for the whole client
val noRetryClient = Http.client.noRetry.newSyncClientRetryable Conditions
The HTTP client retries on:
| Condition | Description |
|---|---|
| Connection errors | EOFException; connection refused, closed channel, protocol errors (JVM) |
| Timeouts | TimeoutException; SocketTimeoutException (JVM) |
| 5xx Server errors | All 5xx statuses; 503 adds extra wait |
| 408, 410, 429, 499 | Request timeout, gone, rate limiting, client closed request |
Non-Retryable Conditions
The following are not retried:
| Condition | Description |
|---|---|
| Other 4xx Client errors | 400, 401, 403, 404, etc. |
| SSL errors | Handshake, key, and peer verification failures (JVM) |
CircuitBreakerOpenException | Circuit breaker is open |
A non-retryable status is thrown immediately as an HttpException; a non-retryable exception (for example an SSL handshake failure) is rethrown as is.
Exception Classification
scala
import wvlet.uni.http.HttpExceptionClassifier
// Check if an exception is retryable
HttpExceptionClassifier.classifyExecutionFailure(exception).isRetryable match
case true => // Will be retried
case false => // Will not be retriedBackoff Strategy
The default client uses exponential backoff without jitter:
- First retry: 1s
- Second retry: 1.5s
- Third retry: ~2.3s
- ...growing 1.5x per retry, capped at 30s
Handling Max Retry Exceeded
HttpMaxRetryException is not an HttpException; catch it separately:
scala
import wvlet.uni.http.HttpMaxRetryException
try
val response = client.send(request)
catch
case e: HttpMaxRetryException =>
println(s"Failed after ${e.retryContext.retryCount} retries")
println(s"Last error: ${e.getCause.getMessage}")
// Use fallback or failCustom Retry Logic
For more control, use the control module:
A noRetry client reports each retryable failure (connection errors, 429, 5xx) as an HttpMaxRetryException whose cause is the original error:
scala
import java.io.IOException
import wvlet.uni.control.{ResultClass, Retry}
import wvlet.uni.http.HttpMaxRetryException
val response = Retry
.withBackOff(maxRetry = 10, initialIntervalMillis = 500)
.retryOn {
case e: HttpMaxRetryException =>
e.getCause match
case _: IOException =>
ResultClass.retryableFailure(e)
case _ if e.statusCode == 429 =>
ResultClass.retryableFailure(e)
case _ =>
ResultClass.nonRetryableFailure(e)
case e =>
ResultClass.nonRetryableFailure(e)
}
.run {
client.noRetry.send(request)
}Rate Limiting
Handle rate limits gracefully:
scala
import wvlet.uni.control.Retry
import wvlet.uni.http.HttpMaxRetryException
val response = Retry
.withBackOff(maxRetry = 5)
.beforeRetry { ctx =>
ctx.lastError match
case e: HttpMaxRetryException if e.statusCode == 429 =>
// Wait longer for rate limits
Thread.sleep(ctx.retryCount * 1000L)
case _ =>
()
}
.run {
client.noRetry.send(request)
}Combining with Circuit Breaker
scala
import wvlet.uni.control.CircuitBreaker
val breaker = CircuitBreaker.withConsecutiveFailures(5)
val response = breaker.run {
client.send(request) // Has built-in retry
}Best Practices
- Use default retry for most cases
- Disable for idempotent checks like auth validation
- Monitor retry rates to detect service issues
- Set reasonable limits to avoid long waits
- Log retry attempts for debugging