kotlinktor

How to Ensure StatusPages is Executed Before Monitoring Intercept in Ktor Pipeline?


I’m facing an issue in my Ktor application where the StatusPages plugin is being executed too late in the request pipeline, after the Monitoring intercept. This delay is causing problems with retrieving the response status in the catch and finally blocks of the Monitoring phase.

Here’s a simplified version of my Monitoring phase intercept:

intercept(ApplicationCallPipeline.Monitoring) {
    try {
        // Some processing here
        proceed() // Move to the next phase
    } catch (e: Throwable) {
        // Exception handling
    } finally {
        // Attempting to get the response status here
        val status = call.response.status()

        // Span cleanup
        span.end()
    }
}

If an exception is thrown during the request processing, it correctly triggers the catch and then enters the finally block. However, the problem is that at this point, call.response.status() is still null because the response status is not yet set.

The response status is later set by the StatusPages plugin, which also applies additional error handling. Ideally, I need the StatusPages plugin to execute before the Monitoring phase intercept, so that I can access the finalized response status in the finally block.

Question

Is there a way to control the order of execution for the StatusPages plugin relative to the Monitoring intercept? How can I ensure that StatusPages runs before reaching the catch and finally blocks in my Monitoring intercept?

Any guidance on managing the order of interceptors in Ktor or handling similar cases would be greatly appreciated. Thank you!


Solution

  • You can add a phase that will go before the Setup phase and intercept it instead of the Monitoring phase to ensure the response is already sent by the StatusPages plugin. Also, you need to place the interception code before installing the StatusPages plugin. Here is an example:

    embeddedServer(CIO, port = 8080) {
        val beforeSetup = PipelinePhase("beforeSetup")
        insertPhaseBefore(ApplicationCallPipeline.Setup, beforeSetup)
        intercept(beforeSetup) {
            try {
                // Some processing here
                proceed() // Move to the next phase
            } catch (e: Throwable) {
                // Exception handling
            } finally {
                // Attempting to get the response status here
                val status = call.response.status()
    
                // Span cleanup
                println(status)
            }
        }
    
        install(StatusPages) {
            status(HttpStatusCode.NotFound) { call, _ ->
                call.respond(HttpStatusCode.BadRequest)
            }
    
            exception<Throwable> { call: ApplicationCall, cause: Throwable ->
                call.respondText { cause.message!! }
            }
        }
    
        routing {
            get {
                throw RuntimeException("Error")
            }
        }
    }.start(wait = true)