WebSockets Next extension reference guide
This technology is considered experimental. In experimental mode, early feedback is requested to mature the idea. There is no guarantee of stability nor long term presence in the platform until the solution matures. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker. For a full list of possible statuses, check our FAQ entry. |
The quarkus-websockets-next
extension provides a modern declarative API to define WebSocket server and client endpoints.
1. The WebSocket protocol
The WebSocket protocol, documented in the RFC6455, establishes a standardized method for creating a bidirectional communication channel between a client and a server through a single TCP connection. Unlike HTTP, WebSocket operates as a distinct TCP protocol but is designed to function seamlessly alongside HTTP. For example, it reuses the same ports and is compatible with the same security mechanisms.
The interaction using WebSocket initiates with an HTTP request employing the 'Upgrade' header to transition to the WebSocket protocol.
Instead of a 200 OK
response, the server replies with a 101 Switching Protocols
response to upgrade the HTTP connection to a WebSocket connection.
Following this successful handshake, the TCP socket utilized in the initial HTTP upgrade request remains open, allowing both client and server to exchange messages in both direction continually.
2. HTTP and WebSocket architecture styles
Despite WebSocket’s compatibility with HTTP and its initiation through an HTTP request, it’s crucial to recognize that the two protocols lead to distinctly different architectures and programming models.
With HTTP/REST, applications are structured around resources/endpoints that handle various HTTP methods and paths. Client interaction occurs through emitting HTTP requests with appropriate methods and paths, following a request-response pattern. The server routes incoming requests to corresponding handlers based on path, method, and headers and then replies with a well-defined response.
Conversely, WebSocket typically involves a single endpoint for the initial HTTP connection, after which all messages utilize the same TCP connection. It introduces an entirely different interaction model: asynchronous and message-driven.
WebSocket is a low-level transport protocol, in contrast to HTTP. Message formats, routing, or processing require prior agreement between the client and server regarding message semantics.
For WebSocket clients and servers, the Sec-WebSocket-Protocol
header in the HTTP handshake request allows negotiation of a higher-level messaging protocol. In its absence, the server and client must establish their own conventions.
3. Quarkus WebSockets vs. Quarkus WebSockets Next
This guide utilizes the quarkus-websockets-next
extension, an implementation of the WebSocket API boasting enhanced efficiency and usability compared to the legacy quarkus-websockets
extension.
The original quarkus-websockets
extension remains accessible, will receive ongoing support, but it’s unlikely to receive to feature development.
Unlike quarkus-websockets
, the quarkus-websockets-next
extension does not implement the Jakarta WebSocket specification.
Instead, it introduces a modern API, prioritizing simplicity of use.
Additionally, it’s tailored to integrate with Quarkus' reactive architecture and networking layer seamlessly.
The annotations utilized by the Quarkus WebSockets next extension differ from those in JSR 356 despite, sometimes, sharing the same name. The JSR annotations carry a semantic that the Quarkus WebSockets Next extension does not follow.
4. Project setup
To use the websockets-next
extension, you need to add the io.quarkus:quarkus-websockets-next
depencency to your project.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
implementation("io.quarkus:quarkus-websockets-next")
5. Endpoints
Both the server and client APIs allow you to define endpoints that are used to consume and send messages.
The endpoints are implemented as CDI beans and support injection.
Endpoints declare callback methods annotated with @OnTextMessage
, @OnBinaryMessage
, @OnPong
, @OnOpen
, @OnClose
and @OnError
.
These methods are used to handle various WebSocket events.
Typically, a method annotated with @OnTextMessage
is called when the connected client sends a message to the server and vice versa.
The client API also includes connectors that are used to configure and create new WebSocket connections. |
5.1. Server endpoints
Server endpoints are classes annotated with @io.quarkus.websockets.next.WebSocket
.
The value of WebSocket#path()
is used to define the path of the endpoint.
package org.acme.websockets;
import io.quarkus.websockets.next.WebSocket;
import jakarta.inject.Inject;
@WebSocket(path = "/chat/{username}") (1)
public class ChatWebSocket {
}
Thus, client can connect to this web socket endpoint using ws://localhost:8080/chat/your-name
.
If TLS is used, the URL is wss://localhost:8443/chat/your-name
.
The endpoint path is relative to the root context configured by the quarkus.http.root-path (which is / by default). For example, if you add quarkus.http.root-path=/api to your application.properties then a client can connect to this endpoint using http://localhost:8080/api/chat/the-name .
|
5.2. Client endpoints
Client endpoints are classes annotated with @io.quarkus.websockets.next.WebSocketClient
.
The value of WebSocketClient#path()
is used to define the path of the endpoint this client will be connected to.
package org.acme.websockets;
import io.quarkus.websockets.next.WebSocketClient;
import jakarta.inject.Inject;
@WebSocketClient(path = "/chat/{username}") (1)
public class ChatWebSocket {
}
Client endpoints are used to consume and send messages. You’ll need the connectors API to configure and open new WebSocket connections. |
5.3. Path parameters
The path of a WebSocket endpoint can contain path parameters.
The syntax is the same as for JAX-RS resources: {parameterName}
.
You can access the path parameter values using the io.quarkus.websockets.next.WebSocketConnection#pathParam(String)
method, or io.quarkus.websockets.next.WebSocketClientConnection#pathParam(String)
respectively.
Alternatively, an endpoint callback method parameter annotated with @io.quarkus.websockets.next.PathParam
is injected automatically.
WebSocketConnection#pathParam(String)
example@Inject io.quarkus.websockets.next.WebSocketConnection connection;
// ...
String value = connection.pathParam("parameterName");
Path parameter values are always strings.
If the path parameter is not present in the path, the WebSocketConnection#pathParam(String)
/WebSocketClientConnection#pathParam(String)
method returns null
.
If there is an endpoint callback method parameter annotated with @PathParam
and the parameter name is not defined in the endpoint path, then the build fails.
Query parameters are not supported. However, you can access the query using WebSocketConnection#handshakeRequest().query()
|
5.4. CDI scopes
Endpoints are managed as CDI beans.
By default, the @Singleton
scope is used.
However, developers can specify alternative scopes to fit their specific requirements.
@Singleton
and @ApplicationScoped
endpoints are shared across all WebSocket connections.
Therefore, implementations should be either stateless or thread-safe.
import jakarta.enterprise.context.SessionScoped;
@WebSocket(path = "/ws")
@SessionScoped (1)
public class MyWebSocket {
}
1 | This server endpoint is not shared and is scoped to the session. |
Each WebSocket connection is associated with its own session context.
When the @OnOpen
method is invoked, a session context corresponding to the WebSocket connection is created.
Subsequent calls to @On[Text|Binary]Message
or @OnClose
methods utilize this same session context.
The session context remains active until the @OnClose
method completes execution, at which point it is terminated.
In cases where a WebSocket endpoint does not declare an @OnOpen
method, the session context is still created.
It remains active until the connection terminates, regardless of the presence of an @OnClose
method.
Methods annotated with @OnTextMessage,
@OnBinaryMessage,
@OnOpen
, and @OnClose
also have the request scope activated for the duration of the method execution (until it produced its result).
5.5. Callback methods
A WebSocket endpoint may declare:
-
At most one
@OnTextMessage
method: Handles the text messages from the connected client/server. -
At most one
@OnBinaryMessage
method: Handles the binary messages from the connected client/server. -
At most one
@OnPongMessage
method: Handles the pong messages from the connected client/server. -
At most one
@OnOpen
method: Invoked when a connection is opened. -
At most one
@OnClose
method: Executed when the connection is closed. -
Any number of
@OnError
methods: Invoked when an error occurs; that is when an endpoint callback throws a runtime error, or when a conversion errors occurs, or when a returnedio.smallrye.mutiny.Uni
/io.smallrye.mutiny.Multi
receives a failure.
Only some endpoints need to include all methods.
However, it must contain at least @On[Text|Binary]Message
or @OnOpen
.
An error is thrown at build time if any endpoint violates these rules. The static nested classes representing sub-websockets adhere to the same guidelines.
Any methods annotated with @OnTextMessage , @OnBinaryMessage , @OnOpen , and @OnClose outside a WebSocket endpoint are considered erroneous and will result in the build failing with an appropriate error message.
|
5.6. Processing messages
Method receiving messages from the client are annotated with @OnTextMessage
or @OnBinaryMessage
.
OnTextMessage
are invoked for every text message received from the client.
OnBinaryMessage
are invoked for every binary message the client receives.
5.6.1. Invocation rules
When invoking these annotated methods, the session scope linked to the WebSocket connection remains active. In addition, the request scope is active until the completion of the method (or until it produces its result for async and reactive methods).
Quarkus WebSocket Next supports blocking and non-blocking logic, akin to Quarkus REST, determined by the method signature and additional annotations such as @Blocking
and @NonBlocking
.
Here are the rules governing execution:
-
Non-blocking methods must execute on the connection’s event loop.
-
Methods annotated with
@RunOnVirtualThread
are considered blocking and should execute on a virtual thread. -
Blocking methods must execute on a worker thread if not annotated with
@RunOnVirtualThread
. -
When
@RunOnVirtualThread
is employed, each invocation spawns a new virtual thread. -
Methods returning
CompletionStage
,Uni
andMulti
are considered non-blocking. -
Methods returning
void
or plain objects are considered blocking. -
Kotlin
suspend
functions are considered non-blocking.
5.6.2. Method parameters
The method must accept exactly one message parameter:
-
The message object (of any type).
-
A
Multi<X>
with X as the message type.
However, it may also accept the following parameters:
-
WebSocketConnection
/WebSocketClientConnection
-
HandshakeRequest
-
String
parameters annotated with@PathParam
The message object represents the data sent and can be accessed as either raw content (String
, JsonObject
, JsonArray
, Buffer
or byte[]
) or deserialized high-level objects, which is the recommended approach.
When receiving a Multi
, the method is invoked once per connection, and the provided Multi
receives the items transmitted by this connection.
The method must subscribe to the Multi
to receive these items (or return a Multi).
5.6.3. Supported return types
Methods annotated with @OnTextMessage
or @OnBinaryMessage
can return various types to handle WebSocket communication efficiently:
-
void
: Indicates a blocking method where no explicit response is sent back to the client. -
Uni<Void>
: Denotes a non-blocking method where the completion of the returnedUni
signifies the end of processing. No explicit response is sent back to the client. -
An object of type
X
represents a blocking method in which the returned object is serialized and sent back to the client as a response. -
Uni<X>
: Specifies a non-blocking method where the item emitted by the non-nullUni
is sent to the client as a response. -
Multi<X>
: Indicates a non-blocking method where the items emitted by the non-nullMulti
are sequentially sent to the client until completion or cancellation. -
Kotlin
suspend
function returningUnit
: Denotes a non-blocking method where no explicit response is sent back to the client. -
Kotlin
suspend
function returningX
: Specifies a non-blocking method where the returned item is sent to the client as a response.
Here are some examples of these methods:
@OnTextMessage
void consume(Message m) {
// Process the incoming message. The method is called on an executor thread for each incoming message.
}
@OnTextMessage
Uni<Void> consumeAsync(Message m) {
// Process the incoming message. The method is called on an event loop thread for each incoming message.
// The method completes when the returned Uni emits its item.
}
@OnTextMessage
ResponseMessage process(Message m) {
// Process the incoming message and send a response to the client.
// The method is called for each incoming message.
// Note that if the method returns `null`, no response will be sent to the client.
}
@OnTextMessage
Uni<ResponseMessage> processAsync(Message m) {
// Process the incoming message and send a response to the client.
// The method is called for each incoming message.
// Note that if the method returns `null`, no response will be sent to the client. The method completes when the returned Uni emits its item.
}
@OnTextMessage
Multi<ResponseMessage> stream(Message m) {
// Process the incoming message and send multiple responses to the client.
// The method is called for each incoming message.
// The method completes when the returned Multi emits its completion signal.
// The method cannot return `null` (but an empty multi if no response must be sent)
}
When returning a Multi, Quarkus subscribes to the returned Multi automatically and writes the emitted items until completion, failure, or cancellation. Failure or cancellation terminates the connection.
5.6.4. Streams
In addition to individual messages, WebSocket endpoints can handle streams of messages.
In this case, the method receives a Multi<X>
as a parameter.
Each instance of X
is deserialized using the same rules listed above.
The method receiving the Multi
can either return another Multi
or void
.
If the method returns a Multi
, it does not have to subscribe to the incoming multi
:
@OnTextMessage
public Multi<ChatMessage> stream(Multi<ChatMessage> incoming) {
return incoming.log();
}
This approach allows bi-directional streaming.
When the method returns void
, it must subscribe to the incoming Multi
:
@OnTextMessage
public void stream(Multi<ChatMessage> incoming) {
incoming.subscribe().with(item -> log(item));
}
5.6.5. Skipping reply
When a method is intended to produce a message written to the client, it can emit null
.
Emitting null
signifies no response to be sent to the client, allowing for skipping a response when needed.
5.6.6. JsonObject and JsonArray
Vert.x JsonObject
and JsonArray
instances bypass the serialization and deserialization mechanisms.
Messages are sent as text messages.
5.6.7. OnOpen and OnClose methods
The WebSocket endpoint can also be notified when a client connects or disconnects.
This is done by annotating a method with @OnOpen
or @OnClose
:
@OnOpen(broadcast = true)
public ChatMessage onOpen() {
return new ChatMessage(MessageType.USER_JOINED, connection.pathParam("username"), null);
}
@Inject WebSocketConnection connection;
@OnClose
public void onClose() {
ChatMessage departure = new ChatMessage(MessageType.USER_LEFT, connection.pathParam("username"), null);
connection.broadcast().sendTextAndAwait(departure);
}
@OnOpen
is triggered upon client connection, while @OnClose
is invoked upon disconnection.
These methods have access to the session-scoped WebSocketConnection
bean.
5.6.8. Parameters
Methods annotated with @OnOpen
and @OnClose
may accept the following parameters:
-
WebSocketConnection
/WebSocketClientConnection
-
HandshakeRequest
-
String
parameters annotated with@PathParam
An endpoint method annotated with @OnClose
may also accept the io.quarkus.websockets.next.CloseReason
parameter that may indicate a reason for closing a connection.
5.6.9. Supported return types
@OnOpen
and @OnClose
methods support different returned types.
For @OnOpen
methods, the same rules as @On[Text|Binary]Message
apply.
Thus, a method annotated with @OnOpen
can send messages to the client immediately after connecting.
The supported return types for @OnOpen
methods are:
-
void
: Indicates a blocking method where no explicit message is sent back to the connected client. -
Uni<Void>
: Denotes a non-blocking method where the completion of the returnedUni
signifies the end of processing. No message is sent back to the client. -
An object of type
X
: Represents a blocking method where the returned object is serialized and sent back to the client. -
Uni<X>
: Specifies a non-blocking method where the item emitted by the non-nullUni
is sent to the client. -
Multi<X>
: Indicates a non-blocking method where the items emitted by the non-nullMulti
are sequentially sent to the client until completion or cancellation. -
Kotlin
suspend
function returningUnit
: Denotes a non-blocking method where no explicit message is sent back to the client. -
Kotlin
suspend
function returningX
: Specifies a non-blocking method where the returned item is sent to the client.
Items sent to the client are serialized except for the String
, io.vertx.core.json.JsonObject
, io.vertx.core.json.JsonArray
, io.vertx.core.buffer.Buffer
, and byte[]
types.
In the case of Multi
, Quarkus subscribes to the returned Multi
and writes the items to the WebSocket
as they are emitted.
String
, JsonObject
and JsonArray
are sent as text messages.
Buffers
and byte arrays are sent as binary messages.
For @OnClose
methods, the supported return types include:
-
void
: The method is considered blocking. -
Uni<Void>
: The method is considered non-blocking. -
Kotlin
suspend
function returningUnit
: The method is considered non-blocking.
@OnClose methods declared on a server endpoint cannot send items to the connected client by returning objects.
They can only send messages to the other clients by using the WebSocketConnection object.
|
5.7. Error handling
WebSocket endpoints can also be notified when an error occurs.
A WebSocket endpoint method annotated with @io.quarkus.websockets.next.OnError
is invoked when an endpoint callback throws a runtime error, or when a conversion errors occurs,
or when a returned io.smallrye.mutiny.Uni
/io.smallrye.mutiny.Multi
receives a failure.
The method must accept exactly one error parameter, i.e. a parameter that is assignable from java.lang.Throwable
.
The method may also accept the following parameters:
-
WebSocketConnection
/WebSocketClientConnection
-
HandshakeRequest
-
String
parameters annotated with@PathParam
An endpoint may declare multiple methods annotated with @io.quarkus.websockets.next.OnError
.
However, each method must declare a different error parameter.
The method that declares a most-specific supertype of the actual exception is selected.
The @io.quarkus.websockets.next.OnError annotation can be also used to declare a global error handler, i.e. a method that is not declared on a WebSocket endpoint. Such a method may not accept @PathParam parameters. Error handlers declared on an endpoint take precedence over the global error handlers.
|
When an error occurs but no error handler can handle the failure, Quarkus uses the strategy specified by quarkus.websockets-next.server.unhandled-failure-strategy
.
For server endpoints, the error message is logged and the connection is closed by default.
For client endpoints, the error message is logged by default.
5.8. Serialization and deserialization
The WebSocket Next extension supports automatic serialization and deserialization of messages.
Objects of type String
, JsonObject
, JsonArray
, Buffer
, and byte[]
are sent as-is and bypass the serialization and deserialization.
When no codec is provided, the serialization and deserialization convert the message from/to JSON automatically.
When you need to customize the serialization and deserialization, you can provide a custom codec.
5.8.1. Custom codec
To implement a custom codec, you must provide a CDI bean implementing:
-
io.quarkus.websockets.next.BinaryMessageCodec
for binary messages -
io.quarkus.websockets.next.TextMessageCodec
for text messages
The following example shows how to implement a custom codec for a Item
class:
@Singleton
public class ItemBinaryMessageCodec implements BinaryMessageCodec<Item> {
@Override
public boolean supports(Type type) {
// Allows selecting the right codec for the right type
return type.equals(Item.class);
}
@Override
public Buffer encode(Item value) {
// Serialization
return Buffer.buffer(value.toString());
}
@Override
public Item decode(Type type, Buffer value) {
// Deserialization
return new Item(value.toString());
}
}
OnTextMessage
and OnBinaryMessage
methods can also specify which codec should be used explicitly:
@OnTextMessage(codec = MyInputCodec.class) (1)
Item find(Item item) {
//....
}
-
Specify the codec to use for both the deserialization and serialization of the message
When the serialization and deserialization must use a different codec, you can specify the codec to use for the serialization and deserialization separately:
@OnTextMessage(
codec = MyInputCodec.class, (1)
outputCodec = MyOutputCodec.class (2)
Item find(Item item) {
//....
}
-
Specify the codec to use for the deserialization of the incoming message
-
Specify the codec to use for the serialization of the outgoing message
5.9. Ping/pong messages
A ping message may serve as a keepalive or to verify the remote endpoint. A pong message is sent in response to a ping message and it must have an identical payload.
Server/client endpoints automatically respond to a ping message sent from the client/server.
In other words, there is no need for @OnPingMessage
callback declared on an endpoint.
The server can send ping messages to a connected client.
WebSocketConnection
/WebSocketClientConnection
declare methods to send ping messages; there is a non-blocking variant: sendPing(Buffer)
and a blocking variant: sendPingAndAwait(Buffer)
.
By default, the ping messages are not sent automatically.
However, the configuration properties quarkus.websockets-next.server.auto-ping-interval
and quarkus.websockets-next.client.auto-ping-interval
can be used to set the interval after which, the server/client sends a ping message to a connected client/server automatically.
quarkus.websockets-next.server.auto-ping-interval=2 (1)
1 | Sends a ping message from the server to a connected client every 2 seconds. |
The @OnPongMessage
annotation is used to define a callback that consumes pong messages sent from the client/server.
An endpoint must declare at most one method annotated with @OnPongMessage
.
The callback method must return either void
or Uni<Void>
(or be a Kotlin suspend
function returning Unit
), and it must accept a single parameter of type Buffer
.
@OnPongMessage
void pong(Buffer data) {
// ....
}
The server/client can also send unsolicited pong messages that may serve as a unidirectional heartbeat. There is a non-blocking variant: WebSocketConnection#sendPong(Buffer) and also a blocking variant: WebSocketConnection#sendPongAndAwait(Buffer) .
|
5.10. Inbound processing mode
WebSocket endpoints can define the mode used to process incoming events for a specific connection using the @WebSocket#inboundProcessingMode()
, and @WebSocketClient.inboundProcessingMode()
respectively.
An incoming event can represent a message (text, binary, pong), opening connection and closing connection.
By default, events are processed serially and ordering is guaranteed.
This means that if an endpoint receives events A
and B
(in this particular order) then callback for event B
will be invoked after the callback for event A
completed.
However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits.
For this cases, the InboundProcessingMode#CONCURRENT
should be used.
6. Server API
6.1. HTTP server configuration
This extension reuses the main HTTP server.
Thus, the configuration of the WebSocket server is done in the quarkus.http.
configuration section.
WebSocket paths configured within the application are concatenated with the root path defined by quarkus.http.root
(which defaults to /
).
This concatenation ensures that WebSocket endpoints are appropriately positioned within the application’s URL structure.
Refer to the HTTP guide for more details.
6.2. Sub-websockets endpoints
A @WebSocket
endpoint can encapsulate static nested classes, which are also annotated with @WebSocket
and represent sub-websockets.
The resulting path of these sub-websockets concatenates the path from the enclosing class and the nested class.
The resulting path is normalized, following the HTTP URL rules.
Sub-websockets inherit access to the path parameters declared in the @WebSocket
annotation of both the enclosing and nested classes.
The consumePrimary
method within the enclosing class can access the version
parameter in the following example.
Meanwhile, the consumeNested
method within the nested class can access both version
and id
parameters:
@WebSocket(path = "/ws/v{version}")
public class MyPrimaryWebSocket {
@OnTextMessage
void consumePrimary(String s) { ... }
@WebSocket(path = "/products/{id}")
public static class MyNestedWebSocket {
@OnTextMessage
void consumeNested(String s) { ... }
}
}
6.3. WebSocket connection
The io.quarkus.websockets.next.WebSocketConnection
object represents the WebSocket connection.
Quarkus provides a @SessionScoped
CDI bean that implements this interface and can be injected in a WebSocket
endpoint and used to interact with the connected client.
Methods annotated with @OnOpen
, @OnTextMessage
, @OnBinaryMessage
, and @OnClose
can access the injected WebSocketConnection
object:
@Inject WebSocketConnection connection;
Note that outside of these methods, the WebSocketConnection object is not available. However, it is possible to list all open connections.
|
The connection can be used to send messages to the client, access the path parameters, broadcast messages to all connected clients, etc.
// Send a message:
connection.sendTextAndAwait("Hello!");
// Broadcast messages:
connection.broadcast().sendTextAndAwait(departure);
// Access path parameters:
String param = connection.pathParam("foo");
The WebSocketConnection
provides both a blocking and a non-blocking method variants to send messages:
-
sendTextAndAwait(String message)
: Sends a text message to the client and waits for the message to be sent. It’s blocking and should only be called from an executor thread. -
sendText(String message)
: Sends a text message to the client. It returns aUni
. It’s non-blocking, but you must subscribe to it.
6.3.1. List open connections
It is also possible to list all open connections.
Quarkus provides a CDI bean of type io.quarkus.websockets.next.OpenConnections
that declares convenient methods to access the connections.
import io.quarkus.logging.Log;
import io.quarkus.websockets.next.OpenConnections;
class MyBean {
@Inject
OpenConnections connections;
void logAllOpenConnections() {
Log.infof("Open connections: %s", connections.listAll()); (1)
}
}
1 | OpenConnections#listAll() returns an immutable snapshot of all open connections at the given time. |
There are also other convenient methods.
For example, OpenConnections#findByEndpointId(String)
makes it easy to find connections for a specific endpoint.
6.3.2. User data
It is also possible to associate arbitrary user data with a specific connection.
The io.quarkus.websockets.next.UserData
object obtained by the WebSocketConnection#userData()
method represents mutable user data associated with a connection.
import io.quarkus.websockets.next.WebSocketConnection;
import io.quarkus.websockets.next.UserData.TypedKey;
@WebSocket(path = "/endpoint/{username}")
class MyEndpoint {
@Inject
CoolService service;
@OnOpen
void open(WebSocketConnection connection) {
connection.userData().put(TypedKey.forBoolean("isCool"), service.isCool(connection.pathParam("username"))); (1)
}
@OnTextMessage
String process(String message) {
if (connection.userData().get(TypedKey.forBoolean("isCool"))) { (2)
return "Cool message processed!";
} else {
return "Message processed!";
}
}
}
1 | CoolService#isCool() returns Boolean that is associated with the current connection. |
2 | The TypedKey.forBoolean("isCool") is the key used to obtain the data stored when the connection was created. |
6.3.3. CDI events
Quarkus fires a CDI event of type io.quarkus.websockets.next.WebSocketConnection
with qualifier @io.quarkus.websockets.next.Open
asynchronously when a new connection is opened.
Moreover, a CDI event of type WebSocketConnection
with qualifier @io.quarkus.websockets.next.Closed
is fired asynchronously when a connection is closed.
import jakarta.enterprise.event.ObservesAsync;
import io.quarkus.websockets.next.Open;
import io.quarkus.websockets.next.WebSocketConnection;
class MyBean {
void connectionOpened(@ObservesAsync @Open WebSocketConnection connection) { (1)
// This observer method is called when a connection is opened...
}
}
1 | An asynchronous observer method is executed using the default blocking executor service. |
6.4. Security
WebSocket endpoint callback methods can be secured with security annotations such as io.quarkus.security.Authenticated
,
jakarta.annotation.security.RolesAllowed
and other annotations listed in the Supported security annotations documentation.
For example:
package io.quarkus.websockets.next.test.security;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
@WebSocket(path = "/end")
public class Endpoint {
@Inject
SecurityIdentity currentIdentity;
@OnOpen
String open() {
return "ready";
}
@RolesAllowed("admin")
@OnTextMessage
String echo(String message) { (1)
return message;
}
@OnError
String error(ForbiddenException t) { (2)
return "forbidden:" + currentIdentity.getPrincipal().getName();
}
}
1 | The echo callback method can only be invoked if the current security identity has an admin role. |
2 | The error handler is invoked in case of the authorization failure. |
SecurityIdentity
is initially created during a secure HTTP upgrade and associated with the websocket connection.
When OpenID Connect extension is used and token expires, Quarkus automatically closes connection. |
6.5. Secure HTTP upgrade
An HTTP upgrade is secured when standard security annotation is placed on an endpoint class or an HTTP Security policy is defined. The advantage of securing HTTP upgrade is less processing, the authorization is performed early and only once. You should always prefer HTTP upgrade security unless, like in th example above, you need to perform action on error.
package io.quarkus.websockets.next.test.security;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
@Authenticated (1)
@WebSocket(path = "/end")
public class Endpoint {
@Inject
SecurityIdentity currentIdentity;
@OnOpen
String open() {
return "ready";
}
@OnTextMessage
String echo(String message) {
return message;
}
}
1 | Initial HTTP handshake ends with the 401 status for anonymous users.
You can also redirect the handshake request on authorization failure with the quarkus.websockets-next.server.security.auth-failure-redirect-url configuration property. |
HTTP upgrade is only secured when a security annotation is declared on an endpoint class next to the @WebSocket annotation.
Placing a security annotation on an endpoint bean will not secure bean methods, only the HTTP upgrade.
You must always verify that your endpoint is secured as intended.
|
quarkus.http.auth.permission.http-upgrade.paths=/end
quarkus.http.auth.permission.http-upgrade.policy=authenticated
6.6. Inspect and/or reject HTTP upgrade
To inspect an HTTP upgrade, you must provide a CDI bean implementing the io.quarkus.websockets.next.HttpUpgradeCheck
interface.
Quarkus calls the HttpUpgradeCheck#perform
method on every HTTP request that should be upgraded to a WebSocket connection.
Inside this method, you can perform any business logic and/or reject the HTTP upgrade.
package io.quarkus.websockets.next.test;
import io.quarkus.websockets.next.HttpUpgradeCheck;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped (1)
public class ExampleHttpUpgradeCheck implements HttpUpgradeCheck {
@Override
public Uni<CheckResult> perform(HttpUpgradeContext ctx) {
if (rejectUpgrade(ctx)) {
return CheckResult.rejectUpgrade(400); (2)
}
return CheckResult.permitUpgrade();
}
private boolean rejectUpgrade(HttpUpgradeContext ctx) {
var headers = ctx.httpRequest().headers();
// implement your business logic in here
}
}
1 | The CDI beans implementing HttpUpgradeCheck interface can be either @ApplicationScoped , @Singleton or @Dependent beans, but never the @RequestScoped beans. |
2 | Reject the HTTP upgrade. Initial HTTP handshake ends with the 400 Bad Request response status code. |
You can choose WebSocket endpoints to which the HttpUpgradeCheck is applied with the HttpUpgradeCheck#appliesTo method.
|
6.7. TLS
As a direct consequence of the fact this extension reuses the main HTTP server, all the relevant server configurations apply. See Refer to the HTTP guide for more details.
6.8. Hibernate multitenancy
The RoutingContext
is not available after the HTTP upgrade. However, it is possible to inject the WebSocketConnection
and access the headers of the initial HTTP request.
If a custom TenantResolver
is used and you would like to combine REST/HTTP and WebSockets, the code may look like this:
@RequestScoped
@PersistenceUnitExtension
public class CustomTenantResolver implements TenantResolver {
@Inject
RoutingContext context;
@Inject
WebSocketConnection connection;
@Override
public String getDefaultTenantId() {
return "public";
}
@Override
public String resolveTenantId() {
String schema;
try {
//Handle WebSocket
schema = connection.handshakeRequest().header("schema");
} catch ( ContextNotActiveException e) {
// Handle REST/HTTP
schema = context.request().getHeader( "schema" );
}
if ( schema == null || schema.equalsIgnoreCase( "public" ) ) {
return "public";
}
return schema;
}
}
For more information on Hibernate multitenancy, refer to the hibernate documentation.
7. Client API
7.1. Client connectors
The io.quarkus.websockets.next.WebSocketConnector<CLIENT>
is used to configure and create new connections for client endpoints.
A CDI bean that implements this interface is provided and can be injected in other beans.
The actual type argument is used to determine the client endpoint.
The type is validated during build - if it does not represent a client endpoint the build fails.
Let’s consider the following client endpoint:
@WebSocketClient(path = "/endpoint/{name}")
public class ClientEndpoint {
@OnTextMessage
void onMessage(@PathParam String name, String message, WebSocketClientConnection connection) {
// ...
}
}
The connector for this client endpoint is used as follows:
@Singleton
public class MyBean {
@ConfigProperty(name = "endpoint.uri")
URI myUri;
@Inject
WebSocketConnector<ClientEndpoint> connector; (1)
void openAndSendMessage() {
WebSocketClientConnection connection = connector
.baseUri(uri) (2)
.pathParam("name", "Roxanne") (3)
.connectAndAwait();
connection.sendTextAndAwait("Hi!"); (4)
}
}
1 | Inject the connector for ClientEndpoint . |
2 | If the base URI is not supplied we attempt to obtain the value from the config. The key consists of the client id and the .base-uri suffix. |
3 | Set the path param value. Throws IllegalArgumentException if the client endpoint path does not contain a parameter with the given name. |
4 | Use the connection to send messages, if needed. |
If an application attempts to inject a connector for a missing endpoint, an error is thrown. |
7.1.1. Basic connector
In the case where the application developer does not need the combination of the client endpoint and the connector, a basic connector can be used. The basic connector is a simple way to create a connection and consume/send messages without defining a client endpoint.
@Singleton
public class MyBean {
@Inject
BasicWebSocketConnector connector; (1)
void openAndConsume() {
WebSocketClientConnection connection = connector
.baseUri(uri) (2)
.path("/ws") (3)
.executionModel(ExecutionModel.NON_BLOCKING) (4)
.onTextMessage((c, m) -> { (5)
// ...
})
.connectAndAwait();
}
}
1 | Inject the connector. |
2 | The base URI must be always set. |
3 | The additional path that should be appended to the base URI. |
4 | Set the execution model for callback handlers. By default, the callback may block the current thread. However in this case, the callback is executed on the event loop and may not block the current thread. |
5 | The lambda will be called for every text message sent from the server. |
The basic connector is closer to a low-level API and is reserved for advanced users. However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans. It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus.
7.2. WebSocket client connection
The io.quarkus.websockets.next.WebSocketClientConnection
object represents the WebSocket connection.
Quarkus provides a @SessionScoped
CDI bean that implements this interface and can be injected in a WebSocketClient
endpoint and used to interact with the connected server.
Methods annotated with @OnOpen
, @OnTextMessage
, @OnBinaryMessage
, and @OnClose
can access the injected WebSocketClientConnection
object:
@Inject WebSocketClientConnection connection;
Note that outside of these methods, the WebSocketClientConnection object is not available. However, it is possible to list all open client connections.
|
The connection can be used to send messages to the client, access the path parameters, etc.
// Send a message:
connection.sendTextAndAwait("Hello!");
// Broadcast messages:
connection.broadcast().sendTextAndAwait(departure);
// Access path parameters:
String param = connection.pathParam("foo");
The WebSocketClientConnection
provides both a blocking and a non-blocking method variants to send messages:
-
sendTextAndAwait(String message)
: Sends a text message to the client and waits for the message to be sent. It’s blocking and should only be called from an executor thread. -
sendText(String message)
: Sends a text message to the client. It returns aUni
. It’s non-blocking, but you must subscribe to it.
7.2.1. List open client connections
It is also possible to list all open connections.
Quarkus provides a CDI bean of type io.quarkus.websockets.next.OpenClientConnections
that declares convenient methods to access the connections.
import io.quarkus.logging.Log;
import io.quarkus.websockets.next.OpenClientConnections;
class MyBean {
@Inject
OpenClientConnections connections;
void logAllOpenClinetConnections() {
Log.infof("Open client connections: %s", connections.listAll()); (1)
}
}
1 | OpenClientConnections#listAll() returns an immutable snapshot of all open connections at the given time. |
There are also other convenient methods.
For example, OpenClientConnections#findByClientId(String)
makes it easy to find connections for a specific endpoint.
7.2.2. User data
It is also possible to associate arbitrary user data with a specific connection.
The io.quarkus.websockets.next.UserData
object obtained by the WebSocketClientConnection#userData()
method represents mutable user data associated with a connection.
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.UserData.TypedKey;
@WebSocketClient(path = "/endpoint/{username}")
class MyEndpoint {
@Inject
CoolService service;
@OnOpen
void open(WebSocketClientConnection connection) {
connection.userData().put(TypedKey.forBoolean("isCool"), service.isCool(connection.pathParam("username"))); (1)
}
@OnTextMessage
String process(String message) {
if (connection.userData().get(TypedKey.forBoolean("isCool"))) { (2)
return "Cool message processed!";
} else {
return "Message processed!";
}
}
}
1 | CoolService#isCool() returns Boolean that is associated with the current connection. |
2 | The TypedKey.forBoolean("isCool") is the key used to obtain the data stored when the connection was created. |
7.2.3. CDI events
Quarkus fires a CDI event of type io.quarkus.websockets.next.WebSocketClientConnection
with qualifier @io.quarkus.websockets.next.Open
asynchronously when a new connection is opened.
Moreover, a CDI event of type WebSocketClientConnection
with qualifier @io.quarkus.websockets.next.Closed
is fired asynchronously when a connection is closed.
import jakarta.enterprise.event.ObservesAsync;
import io.quarkus.websockets.next.Open;
import io.quarkus.websockets.next.WebSocketClientConnection;
class MyBean {
void connectionOpened(@ObservesAsync @Open WebSocketClientConnection connection) { (1)
// This observer method is called when a connection is opened...
}
}
1 | An asynchronous observer method is executed using the default blocking executor service. |
7.3. Configuring SSL/TLS
To establish a TLS connection, you need to configure a named configuration using the TLS registry:
quarkus.tls.my-ws-client.trust-store.p12.path=server-truststore.p12
quarkus.tls.my-ws-client.trust-store.p12.password=secret
quarkus.websockets-next.client.tls-configuration-name=my-ws-client # Reference the named configuration
When using the WebSocket client, using a named configuration is required to avoid conflicts with other TLS configurations. The client will not use the default TLS configuration. |
When you configure a named TLS configuration, TLS is enabled by default.
8. Traffic logging
Quarkus can log the messages sent and received for debugging purposes.
To enable traffic logging for the server, set the quarkus.websockets-next.server.traffic-logging.enabled
configuration property to true
.
To enable traffic logging for the client, set the quarkus.websockets-next.client.traffic-logging.enabled
configuration property to true
.
The payload of text messages is logged as well.
However, the number of logged characters is limited.
The default limit is 100, but you can change this limit with the quarkus.websockets-next.server.traffic-logging.text-payload-limit
and quarkus.websockets-next.client.traffic-logging.text-payload-limit
configuration property, respectively.
The messages are only logged if the DEBUG level is enabled for the logger io.quarkus.websockets.next.traffic .
|
quarkus.websockets-next.server.traffic-logging.enabled=true (1)
quarkus.websockets-next.server.traffic-logging.text-payload-limit=50 (2)
quarkus.log.category."io.quarkus.websockets.next.traffic".level=DEBUG (3)
1 | Enables traffic logging. |
2 | Set the number of characters of a text message payload which will be logged. |
3 | Enable DEBUG level is for the logger io.quarkus.websockets.next.traffic . |
9. Configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
---|---|---|
Compression Extensions for WebSocket are supported by default. See also RFC 7692 Environment variable: Show more |
boolean |
|
The compression level must be a value between 0 and 9. The default value is Environment variable: Show more |
int |
|
The maximum size of a message in bytes. The default values is Environment variable: Show more |
int |
|
The interval after which, when set, the client sends a ping message to a connected server automatically. Ping messages are not sent automatically by default. Environment variable: Show more |
||
The strategy used when an error occurs but no error handler can handle the failure. By default, the error message is logged when an unhandled failure occurs. Note that clients should not close the WebSocket connection arbitrarily. See also RFC-6455 section 7.3. Environment variable: Show more |
|
|
The name of the TLS configuration to use. If a name is configured, it uses the configuration from The default TLS configuration is not used by default. Environment variable: Show more |
string |
|
If set to true then binary/text messages received/sent are logged if the Environment variable: Show more |
boolean |
|
The number of characters of a text message which will be logged if traffic logging is enabled. The payload of a binary message is never logged. Environment variable: Show more |
int |
|
Environment variable: Show more |
list of string |
|
Compression Extensions for WebSocket are supported by default. See also RFC 7692 Environment variable: Show more |
boolean |
|
The compression level must be a value between 0 and 9. The default value is Environment variable: Show more |
int |
|
The maximum size of a message in bytes. The default values is Environment variable: Show more |
int |
|
The interval after which, when set, the server sends a ping message to a connected client automatically. Ping messages are not sent automatically by default. Environment variable: Show more |
||
The strategy used when an error occurs but no error handler can handle the failure. By default, the error message is logged and the connection is closed when an unhandled failure occurs. Environment variable: Show more |
|
|
Quarkus redirects HTTP handshake request to this URL if an HTTP upgrade is rejected due to the authorization failure. This configuration property takes effect when you secure endpoint with a standard security annotation. For example, the HTTP upgrade is secured if an endpoint class is annotated with the Environment variable: Show more |
string |
|
The limit of messages kept for a Dev UI connection. If less than zero then no messages are stored and sent to the Dev UI view. Environment variable: Show more |
long |
|
If set to true then binary/text messages received/sent are logged if the Environment variable: Show more |
boolean |
|
The number of characters of a text message which will be logged if traffic logging is enabled. The payload of a binary message is never logged. Environment variable: Show more |
int |
|
About the Duration format
To write duration values, use the standard You can also use a simplified format, starting with a number:
In other cases, the simplified format is translated to the
|