Hey all, what would be the correct place to map a ...
# ktor
j
Hey all, what would be the correct place to map a
HttpRequestTimeoutException
? currently I’m doing the following:
Copy code
on(Send) {
    return try {
        proceed(request)
    } catch (e: Throwable) {
        mapException(e)
    }
}
in a custom plugin which works fine at runtime but for some reason when trying to test it using
MockEngine
like this:
Copy code
MockEngine {
    delay(10_000L)
    respondOk()
}
I get the
HttpRequestTimeoutException
directly instead of the custom one I map it to in my custom plugin
a
Can you share a complete code snippet to troubleshoot the problem?
j
Yes, I was able to reproduce the problem using the official client-timeout sample. These are the changes I made: •
Application.kt
Copy code
fun main() {
    defaultServer(Application::main).start()
    runBlocking {
        val client = HttpClient(CIO) {
            install(HttpTimeout) {
                requestTimeoutMillis = 1000
            }
            install(errorMapperPlugin)
        }

    // …
}

val errorMapperPlugin = createClientPlugin("ErrorMapperPlugin") {
    on(Send) { request ->
        return@on try {
            proceed(request)
        } catch(cause: Throwable) {
            throw when (cause) {
                is HttpRequestTimeoutException -> CustomRequestTimeoutException(
                    message = "Request timed out",
                    cause = cause
                )
                else -> cause
            }
        }
    }
}

class CustomRequestTimeoutException(
    override val message: String?,
    override val cause: Throwable?
) : RuntimeException(message, cause)
ApplicationTest
Copy code
private val mockEngine = MockEngine {
    delay(6000)
    respondOk()
}
private val testHttpClient = HttpClient(mockEngine) {
    install(HttpTimeout)
    install(errorMapperPlugin)
}

@Test
fun exceptionShouldBeMappedCorrectly(): Unit = runBlocking {
    try {
        testHttpClient.request {
            timeout {
                requestTimeoutMillis = 3000
            }
        }
    } catch (e: Throwable) {
        println("Exception is $e") // This prints out "exception is HttpRequestTimeoutException"
        assert(e is CustomRequestTimeoutException)
    }
}
build.gradle.kts
Copy code
// ...
testImplementation("io.ktor:ktor-client-mock:$ktor_version")
so far the only way I’ve been able to capture the mapped exception is by doing this instead of using the delay() function:
Copy code
MockEngine {
    throw HttpRequestTimeoutException(url = "", timeoutMillis = null)
}
however I’m still curious about the original issue
a
The problem is that your try/catch (
return@on try {
) doesn't catch any exceptions. When timeout happening the
HttpRequestBuilder.executionContext
is cancelled. You can detect the cancellation with the following code:
Copy code
val errorMapperPlugin = createClientPlugin("ErrorMapperPlugin") {
    onRequest { request, _ ->
        request.executionContext.invokeOnCompletion { cause ->
            if (cause?.cause is HttpRequestTimeoutException) {
                // ...
            }
        }
    }
    // ...
}
Unfortunately, you cannot rethrow that exception from the plugin.