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
- Remove @Blocking, @NonBlocking and @RunOnVirtualThread from the suspend function (and the class if it only has suspend callbacks)
- Use Kotlin coroutines primitives (dispatchers inside the function) instead of Quarkus execution-model annotations
- 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
- Never combine Kotlin suspend callbacks with @Blocking/@NonBlocking/@RunOnVirtualThread
- Use withContext/dispatchers inside the suspend function for threading needs
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
- @%s callback declared on a client endpoint must not broadcas
- There can be only one callback annotated with %s declared on
- Unsupported return type:
- Type ${className} must be annotated with @Embeddable, becaus
- Unable to invoke Kotlin compiler.
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/e30ff86a37428509.
Report an issue: GitHub.