# Resilient HTTP client with Apache HttpClient using HttpRequestRetryHandler and ServiceNotAvailableStrategy

When using HTTP calls you eventually run into issues such as:

  • Connect Timeouts
  • Socket Timeouts
  • SSL handshake errors
  • Connection interruptions
  • Service unavailability
  • Gateway Timeouts
  • Bad Gateways
  • ...

In most of these cases, a simple retry will help. To retry HTTP calls on errors, we can make use of the built-in HttpRequestRetryHandler and the ServiceUnavailableRetryStrategy.

Let's set up a simple closeable HTTP client first:

val requestConfig = RequestConfig.custom()
    .setCookieSpec(CookieSpecs.STANDARD)
    .setConnectTimeout(5 * 1000)
    .setSocketTimeout(60 * 1000)
    .build()

val httpClient = HttpClientBuilder
    .create()
    .setDefaultRequestConfig(requestConfig)
    .build()

# HTTP Request Retry Handler

Most of the issues listed above will cause an IOException.

Any timeout or SSL negotiation issues will cause an exception that is inherited by the IOException class.

To retry your calls when an IOException occurs, we can set a HttpRequestRetryHandler.

 val retryHandler = HttpRequestRetryHandler { exception, executionCount, context ->
    // 3 retries at max
    if (executionCount > 3) {
        return@HttpRequestRetryHandler false
    } else {
        // wait a second before retrying again
        Thread.sleep(1000)
        return@HttpRequestRetryHandler true
    }
}

You can customize that behavior.

To use your custom retry handler, simply specify it when building your HTTP client.

HttpClientBuilder
    .create()
    .setRetryHandler(retryHandler)
    .build()

# Service Unavailable Retry Strategy

The HttpRequestRetryHandler will only be used when an IOException occurs. When the SSL handshake succeeds and no socket/connect timeout occurs we might still get a HTTP response with status codes such as 502 (Bad Gateway) or 503 (Gateway Timeout).

To retry in those cases aswell, we can make use of the ServiceUnavailableRetryStrategy

val serviceUnavailableRetryStrategy: ServiceUnavailableRetryStrategy = object : ServiceUnavailableRetryStrategy {
    override fun getRetryInterval(): Long {
        // milliseconds
        return 1000
    }

    override fun retryRequest(response: HttpResponse?, executionCount: Int, context: HttpContext?): Boolean {
        val statusCode = response!!.statusLine.statusCode

        // retry if status code is 502 or 503 and no more than three retries were done yet
        return statusCode in listOf(502, 503) && executionCount < 3
    }

}

You can customize the retry behavior to your demand.

As with the HTTP request retry handler, simply specify the strategy when building your HTTP client.

HttpClientBuilder
    .create()
    .setServiceUnavailableRetryStrategy(serviceUnavailableRetryStrategy)
    .build()

When using both, the HTTP request retry handler and the service unavailable retry strategy, your HTTP client will be a lot more robust. Small network/service hickups will not lead to failure.