https://kotlinlang.org logo
#ktor
Title
# ktor
h

Helio

11/27/2023, 3:04 AM
Hello Folks, 👋🏽 I was hoping to get any advice regarding the plugin
HttpRequestRetry
. I was trying to create a mechanism to retry on ConnectionTimeoutException, more specifically like:
Copy code
install(HttpRequestRetry) {
            maxRetries = 5
            retryOnServerErrors()
            retryOnExceptionIf() { _, cause ->
                cause is ConnectTimeoutException
            }
            exponentialDelay()
        }
However, from what I can tell, it never retries and it fails immediately, even though the endpoint is returning ConnectionTimeoutException. I noticed a couple of issues reported: Issue_1, Issue_2 and I thought that it could be related since I'm also making usage of
HttpTimeout
plugin.
Copy code
install(HttpTimeout) {
            requestTimeoutMillis = 30_000
            connectTimeoutMillis = 10_000
        }
As part of the tests, I also tried to replace
retryOnExceptionIf
with
retryOnException(retryOnTimeout=true)
but no success. Would any of you be able to kindly provide any insights on the why it does not work? Any help is greatly appreciated.
a

Aleksei Tirman [JB]

11/27/2023, 10:57 AM
You can use the following configuration to retry on the connection timeout:
Copy code
install(HttpRequestRetry) {
    maxRetries = 5
    retryOnServerErrors()
    retryOnException(5, true) // this line configures retrying on ConnectTimeoutException
    exponentialDelay()
}
h

Helio

11/27/2023, 10:58 AM
You're correct. I tried using that on Ktor 2.3.6, and for some reason it never retries... it fails immediately.
Just to add a little bit more of context, this is how it is currently configured, for example:
Copy code
get("/route") {
            runCatching {
                MyService.doSomething()
            }.onFailure {
                call.respond(HttpStatusCode.InternalServerError)
            }.onSuccess {
                call.respond(HttpStatusCode.OK)
            }.getOrNull()
        }
Copy code
suspend fun doSomething() {
        createFlowToDoSomething().retry(5) {
            logger.warn("Failed to create flow. :(")
            delay(Duration.ofSeconds(3))
            it is IllegalStateException
//If I add it is ConnectionTimetoutException, the retries work... But not from the CIOEngine.

        }.collect()
    }
Copy code
private fun createFlowToDoSomething() : Flow<Unit> {
        return flow<Unit> {
            val result =
                MyKtorCIOHTTPClient.sendRequestToServer() //If the request fail with ConnectionTimeout, it is not retried.
        }.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
    }
Copy code
//This is how I'm trying to force the ConnectionTimeoutException to be thrown. (Test purposes of course)
    suspend fun sendRequestToServer () {
         throw ConnectTimeoutException("dummy_url")
    }
The HTTP Client it is currently configured as:
Copy code
private val client = HttpClient(CIO) {
        expectSuccess = true

        engine {
            endpoint {
                maxConnectionsPerRoute = 500
            }
        }
        install(HttpRequestRetry) {
            <http://logger.info|logger.info>("Retrying...")
            maxRetries = 5
            retryOnServerErrors()
            retryOnException(5, true) // this line configures retrying on ConnectTimeoutException
            exponentialDelay()
            <http://logger.info|logger.info>("Finished retry")
        }
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
            })
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 30_000
            connectTimeoutMillis = 10_000
        }

        defaultRequest {
            url {
                protocol = myHost.protocol
                port = myHost.port
                host = myHost.host
            }
            headers {
                append("Authorization", "Bearer $adminToken")
            }
        }
    }
Any suggestion by any chance? 😞
a

Aleksei Tirman [JB]

11/28/2023, 9:15 AM
So did the
retryOnException(5, true)
line make any difference? How do you reproduce the connection timeout? Does it work without any other plugins?
h

Helio

11/28/2023, 9:19 AM
So, how I was trying to reproduce the ConnectionTimeout was like that:
Copy code
//This is how I'm trying to force the ConnectionTimeoutException to be thrown. (Test purposes of course)
    suspend fun sendRequestToServer () {
         throw ConnectTimeoutException("dummy_url")
    }
The line you suggested made no difference. In fact, as I mentioned (I tried using the
retryOnException
, but it didn't make any difference. 😞
a

Aleksei Tirman [JB]

11/28/2023, 9:23 AM
The problem might be in the test. Can you reproduce the problem by sending the request to a non-routable IP address (
10.255.255.1
)?
h

Helio

11/28/2023, 9:24 AM
I can give it a shot. One second... Trying to creating a minor app to reproduce the problem.
Interesting @Aleksei Tirman [JB], apparently it worked... 🤔 Would you be able to kindly explain what the test does that make it wrong? Is it because it is not running in a coroutine and it only throws the ConnectionTimeout? At least on the sample I did here. I will try tomorrow to see how it works in the real application though.
Just another quick follow-up on this... Is it expected that after the
ConnectionTimeout
, the request still waits for a response? And the RequestTimeout is also triggered... does it means we need to handle the cancelation as well? Just trying to understand the behaviour... I understand the retries, but what about those HttpTimeouts?
Copy code
2023-11-28 20:47:21.349 [DefaultDispatcher-worker-1] TRACE i.ktor.client.plugins.HttpPlainText - Adding Accept-Charset=UTF-8 to <http://10.255.255.1:8080/something>
2023-11-28 20:47:25.891 [DefaultDispatcher-worker-1] TRACE i.k.client.plugins.HttpRequestRetry - Retrying request <http://10.255.255.1:8080/something> attempt: 1
2023-11-28 20:47:32.862 [DefaultDispatcher-worker-4] TRACE i.k.client.plugins.HttpRequestRetry - Retrying request <http://10.255.255.1:8080/something> attempt: 2
2023-11-28 20:47:43.590 [DefaultDispatcher-worker-2] TRACE i.k.client.plugins.HttpRequestRetry - Retrying request <http://10.255.255.1:8080/something> attempt: 3
2023-11-28 20:47:51.356 [DefaultDispatcher-worker-4] TRACE io.ktor.client.plugins.HttpTimeout - Request timeout: <http://10.255.255.1:8080/something>
2023-11-28 20:47:55.891 [DefaultDispatcher-worker-4] TRACE io.ktor.client.plugins.HttpTimeout - Request timeout: <http://10.255.255.1:8080/something>
2023-11-28 20:48:02.537 [DefaultDispatcher-worker-4] TRACE i.k.client.plugins.HttpRequestRetry - Retrying request <http://10.255.255.1:8080/something> attempt: 4
2023-11-28 20:48:02.864 [DefaultDispatcher-worker-4] TRACE io.ktor.client.plugins.HttpTimeout - Request timeout: <http://10.255.255.1:8080/something>
2023-11-28 20:48:13.591 [DefaultDispatcher-worker-3] TRACE io.ktor.client.plugins.HttpTimeout - Request timeout: <http://10.255.255.1:8080/something>
2023-11-28 20:48:32.540 [DefaultDispatcher-worker-5] TRACE io.ktor.client.plugins.HttpTimeout - Request timeout: <http://10.255.255.1:8080/something>
2023-11-28 20:48:36.875 [DefaultDispatcher-worker-3] TRACE i.k.client.plugins.HttpRequestRetry - Retrying request <http://10.255.255.1:8080/something> attempt: 5
a

Aleksei Tirman [JB]

11/28/2023, 10:34 AM
What do you mean by "the request still waits for a response?"?
h

Helio

11/28/2023, 10:36 AM
Based on the logs you cam see that the request time-out of 30s seems to be respected. I thought that after the 10s Connection Time-out the request would probably be cancelled?
a

Aleksei Tirman [JB]

11/28/2023, 10:42 AM
It must be cancelled. How do you know it isn't?
h

Helio

11/28/2023, 11:00 AM
I was checking the log time of those traces.
I have 30s delay in my request timeout and an extra 3s. That's why you should see ~33s.