quarkusio/quarkus · error · IllegalStateException

Not a coroutine invoker

Error message

Not a coroutine invoker

What it means

CoroutineInvocationHandler.handle runs after the endpoint method has been invoked. The generated invoker must implement CoroutineEndpointInvoker for Kotlin suspend endpoints; if the cached invoker is a different type, the handler flags IllegalStateException via handleException instead of crashing the request loop. This is an internal invariant violation, typically a codegen/dispatch mismatch.

Source

Thrown at extensions/resteasy-reactive/rest-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/CoroutineInvocationHandler.kt:28

import org.jboss.resteasy.reactive.server.spi.ServerRestHandler
import org.slf4j.LoggerFactory

private val logger = LoggerFactory.getLogger(CoroutineInvocationHandler::class.java)

class CoroutineInvocationHandler(
    private val invoker: EndpointInvoker,
    private val coroutineScope: CoroutineScope,
) : ServerRestHandler {

    private val originalTCCL: ClassLoader = Thread.currentThread().contextClassLoader

    override fun handle(requestContext: ResteasyReactiveRequestContext) {
        if (requestContext.result != null) {
            return
        }
        if (invoker !is CoroutineEndpointInvoker) {
            requestContext.handleException(IllegalStateException("Not a coroutine invoker"), true)
            return
        }

        val requestScope = requestContext.captureCDIRequestScope()
        val dispatcher: CoroutineDispatcher =
            Vertx.currentContext()?.let { VertxDispatcher(it, requestScope, requestContext) }
                ?: throw IllegalStateException("No Vertx context found")

        logger.trace("Handling request with dispatcher {}", dispatcher)

        requestContext.suspend()
        val done = AtomicBoolean()
        val canceled = AtomicBoolean()

        val job =
            coroutineScope.launch(context = dispatcher) {
                // ensure the proper CL is not lost in dev-mode
                Thread.currentThread().contextClassLoader = originalTCCL
                try {

View on GitHub (pinned to e1c734241f)

Solutions

  1. Clean and rebuild the project (./mvnw clean install) to regenerate endpoint invokers
  2. Verify the endpoint method is actually a Kotlin suspend fun and the rest-kotlin extension is on the classpath
  3. Check for mixed deployment artifacts — remove stale target/ classes from previous builds
Defensive patterns

Strategy: try-catch

Type guard

fun isCoroutineInvoker(invoker: Any): Boolean = invoker is CoroutineEndpointInvoker

Try / catch

try {
    chain.next(requestContext)
} catch (e: IllegalStateException) {
    if (e.message == "Not a coroutine invoker") {
        // rebuild artifacts: ./mvnw clean install and redeploy
    }
    throw e
}

Prevention

When it happens

Trigger: At runtime, requestContext.result is null and the `invoker` field is not an instance of CoroutineEndpointInvoker — i.e. a non-coroutine invoker was wired into a coroutine-handled endpoint.

Common situations: Mixed Kotlin/Java endpoint compilation where the Kotlin coroutine handler is applied to a Java-generated invoker; stale generated classes after adding/removing `suspend`; build cache corruption.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/ebdc823ec73dd3cc. Report an issue: GitHub.