Hey guys, are there any way to catch HttpRequestTi...
# ktor
s
Hey guys, are there any way to catch HttpRequestTimeoutException in a custom plugin? I tried to catch it in Send hook and onRequest hook, but it didn't work. I use ktor in KMP project to provide a apiclient for a swift project, so i need to let caller exec these exception.
Copy code
data class NetworkError(
    val type: NetworkErrorType,
    val originalException: Throwable? = null
)

class NetworkHandleConfig {
    lateinit var onNetworkError: ((NetworkError) -> Unit)
}

val NetworkHandle = createClientPlugin("NetworkHandle", createConfiguration = ::NetworkHandleConfig) {
    val onNetworkError = pluginConfig.onNetworkError

    on(Send) { context ->
        try {
            proceed(context)
        } catch (e: CancellationException) {
            throw e
        } catch (e: HttpRequestTimeoutException) {
            val networkError = NetworkError(
                type = NetworkErrorType.TIMEOUT_ERROR,
                originalException = e
            )
            onNetworkError(networkError)
            context.executionContext.cancel(CancellationException("Request cancelled due to timeout"))
            proceed(context)
        } catch (e: Exception) {
            val networkError = when {
                e.message?.contains("offline", ignoreCase = true) == true ||
                        e.message?.contains("connection", ignoreCase = true) == true ||
                        e.message?.contains("unreachable", ignoreCase = true) == true -> {
                    NetworkError(
                        type = NetworkErrorType.CONNECTION_ERROR,
                        originalException = e
                    )
                }

                e.message?.contains("timeout", ignoreCase = true) == true -> {
                    NetworkError(
                        type = NetworkErrorType.TIMEOUT_ERROR,
                        originalException = e
                    )
                }

                e.message?.contains("dns", ignoreCase = true) == true ||
                        e.message?.contains("host", ignoreCase = true) == true -> {
                    NetworkError(
                        type = NetworkErrorType.DNS_ERROR,
                        originalException = e
                    )
                }

                e.message?.contains("ssl", ignoreCase = true) == true ||
                        e.message?.contains("tls", ignoreCase = true) == true ||
                        e.message?.contains("certificate", ignoreCase = true) == true -> {
                    NetworkError(
                        type = NetworkErrorType.SSL_ERROR,
                        originalException = e
                    )
                }

                else -> {
                    NetworkError(
                        type = NetworkErrorType.UNKNOWN_ERROR,
                        originalException = e
                    )
                }
            }

            onNetworkError(networkError)
            context.executionContext.cancel(CancellationException("Request cancelled due to network error"))
            proceed(context)
        }
    }
    onRequest { request, _ ->
        request.executionContext.invokeOnCompletion { cause ->
            if (cause?.cause is HttpRequestTimeoutException) {
                val networkError = NetworkError(
                    type = NetworkErrorType.TIMEOUT_ERROR,
                    originalException = cause.cause
                )
                onNetworkError(networkError)
            }
        }
    }
}
Copy code
install(HttpTimeout) {
    requestTimeoutMillis = 3000
}
a
Since the
HttpTimeout
plugin cancels the execution context when the request timeout occurs, you can add a handler that will be invoked on the job completion. Here is an example:
Copy code
val client = HttpClient(CIO) {
    install(HttpTimeout) {
        requestTimeoutMillis = 1000
    }
}

client.plugin(HttpSend).intercept { request ->
    request.executionContext.invokeOnCompletion { cause ->
        if (cause is HttpRequestTimeoutException || cause?.cause is HttpRequestTimeoutException) {
            println("Request timeout")
        }
    }
    execute(request)
}

client.get("<https://httpbin.org/delay/5>")
❤️ 1
s
Copy code
HttpClient {
            if (isDebug) install(Logging)
            install(ContentNegotiation) {
                json(
                    Json {
                        prettyPrint = true
                        ignoreUnknownKeys = true
                        isLenient = true
                    },
                )
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 3000
            }
            install(NetworkHandle) {
                onNetworkError = handleNetworkErrorFunc
            }
            install(Unauthorized) {
                handleUnauthorized = handleUnauthorizedFunc
            }
            .also {
                it.plugin(HttpSend).intercept { request ->
                    println("=====================Intercepting HTTP Request=====================")
                    request.executionContext.invokeOnCompletion { cause ->
                        println(">== Request Completed with Cause: ${cause?.message} ==<")
                        if (cause is HttpRequestTimeoutException || cause?.cause is HttpRequestTimeoutException) {
                            println(">== Catch HTTP Request Timeout Exception ==<")
                            request.executionContext.cancel(CancellationException("Request cancelled due to timeout"))
                        }
                    }
                    execute(request)
                }
            }
            .also { isInitialized = true }
log:
Copy code
=====================Intercepting HTTP Request=====================
HttpClient: REQUEST: <https://aa.aaaa>
METHOD: POST
COMMON HEADERS
-> Accept: application/json
-> Accept-Charset: UTF-8
-> Authorization: Bearer aaaa
CONTENT HEADERS
-> Content-Length: 25
-> Content-Type: application/json

HttpClient: REQUEST <https://aa.aaaa> failed with exception: kotlin.coroutines.cancellation.CancellationException: Request timeout has expired [url=aa.aaaa, request_timeout=3000 ms]
Exception doesn't match @Throws-specified class list and thus isn't 

---

=====================Intercepting HTTP Request=====================
HttpClient: REQUEST: <https://aa.aaaa>
METHOD: POST
COMMON HEADERS
-> Accept: application/json
-> Accept-Charset: UTF-8
-> Authorization: Bearer aaaa
CONTENT HEADERS
-> Content-Length: 25
-> Content-Type: application/json
HttpClient: RESPONSE: 200 OK
METHOD: POST
FROM: <https://aa.aaaa>
COMMON HEADERS
-> Content-Type: application/json; charset=UTF-8
-> Date: Wed, 20 Aug 2025 13:51:57 GMT
-> Server: nginx
-> Via: 1.1 <http://aaa.cloudfront.net|aaa.cloudfront.net> (CloudFront)
-> access-control-allow-credentials: false
-> access-control-expose-headers: Content-Type, Content-Length, Authorization, Accept, X-Requested-With
-> x-amz-cf-id: aaa==
-> x-amz-cf-pop: HKG62-P1
-> x-cache: Miss from cloudfront
-> x-powered-by: PHP/7.4.33
=== Request Completed with Cause: null ===
tks for reply above is the failed log seems like that before enter the
invokeOnCompletion
block, the coroutine was be canceled, and i can't catch it in this block
a
Can you share the full stack trace of the CancellationException?
s
it's hard to print full trace, cause of that the project is compiled as a dynamic lib for swift to call, and idk how to print the stack in this case. but my problem is resolved, i found that the HttpRequestTimeoutException is wrapped in CancellationException, and catched in Send hook, in that step i should throw a new CancellationException instead of exec
Copy code
context.executionContext.cancel(CancellationException("Request cancelled due to network error"))
proceed(context)
anyway thanks for help, good day