quarkusio/quarkus · error · WebSocketException

Kotlin `suspend` functions in WebSockets Next endpoints may

Error message

Kotlin `suspend` functions in WebSockets Next endpoints may not be annotated @Blocking, @NonBlocking or @RunOnVirtualThread: 

What it means

Kotlin suspend functions in WebSockets Next endpoints are dispatched on the Vert.x context, so they cannot also declare an execution model via @Blocking, @NonBlocking, or @RunOnVirtualThread. If a suspend function (or its class, for @RunOnVirtualThread) carries one of these annotations, the build fails with WebSocketException.

Source

Thrown at extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java:1945

                                callback.asString()));
            }
            if (validator != null) {
                validator.accept(callback);
            }
            return callback;
        }
        throw new WebSocketException(
                String.format("There can be only one callback annotated with %s declared on %s", annotationName, beanClass));
    }

    private static ExecutionModel executionModel(MethodInfo method, TransformedAnnotationsBuildItem transformedAnnotations) {
        if (KotlinUtils.isKotlinSuspendMethod(method)
                && (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)
                        || transformedAnnotations.hasAnnotation(method.declaringClass(),
                                WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)
                        || transformedAnnotations.hasAnnotation(method, WebSocketDotNames.BLOCKING)
                        || transformedAnnotations.hasAnnotation(method, WebSocketDotNames.NON_BLOCKING))) {
            throw new WebSocketException("Kotlin `suspend` functions in WebSockets Next endpoints may not be "
                    + "annotated @Blocking, @NonBlocking or @RunOnVirtualThread: " + method);
        }
        if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)
                || transformedAnnotations.hasAnnotation(method.declaringClass(), WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)) {
            return ExecutionModel.VIRTUAL_THREAD;
        } else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.BLOCKING)) {
            return ExecutionModel.WORKER_THREAD;
        } else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.NON_BLOCKING)) {
            return ExecutionModel.EVENT_LOOP;
        } else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.TRANSACTIONAL)
                || transformedAnnotations.hasAnnotation(method.declaringClass(), WebSocketDotNames.TRANSACTIONAL)) {
            // Method annotated with @Transactional or declared on a class annotated @Transactional is also treated as a blocking method
            return ExecutionModel.WORKER_THREAD;
        } else {
            return hasBlockingSignature(method) ? ExecutionModel.WORKER_THREAD : ExecutionModel.EVENT_LOOP;
        }
    }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Remove @Blocking, @NonBlocking and @RunOnVirtualThread from the suspend function (and the class if it only has suspend callbacks)
  2. Use Kotlin coroutines primitives (dispatchers inside the function) instead of Quarkus execution-model annotations
  3. Convert to a non-suspend Java-style method with @RunOnVirtualThread/@Blocking if thread-based execution is truly required

Example fix

// before
@OnTextMessage
@RunOnVirtualThread
suspend fun onMessage(msg: String) {}
// after
@OnTextMessage
suspend fun onMessage(msg: String) = withContext(Dispatchers.Default) { /* ... */ }
Defensive patterns

Strategy: validation

Validate before calling

void checkSuspendAnnotations(Method m) {
    if (!m.isAnnotationPresent(OnTextMessage.class)) return;
    for (Class<? extends Annotation> bad :
            List.of(Blocking.class, NonBlocking.class, RunOnVirtualThread.class)) {
        if (m.isAnnotationPresent(bad))
            throw new IllegalStateException("Kotlin suspend callbacks must not use " + bad.getSimpleName());
    }
}

Prevention

When it happens

Trigger: Annotating a Kotlin `suspend` callback (e.g. @OnMessage suspend fun ...) with @Blocking, @NonBlocking, or @RunOnVirtualThread, or annotating the endpoint class with @RunOnVirtualThread while it has suspend callbacks.

Common situations: Converting Java handlers to Kotlin and keeping the @RunOnVirtualThread/@Blocking annotations; cargo-culting annotations from other Quarkus reactive endpoints.

Related errors


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