The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
Edit this Page

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.

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-websockets-next</artifactId>
</dependency>
build.gradle
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 returned io.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 and Multi 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 returned Uni 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-null Uni is sent to the client as a response.

  • Multi<X>: Indicates a non-blocking method where the items emitted by the non-null Multi are sequentially sent to the client until completion or cancellation.

  • Kotlin suspend function returning Unit: Denotes a non-blocking method where no explicit response is sent back to the client.

  • Kotlin suspend function returning X: 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 returned Uni 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-null Uni is sent to the client.

  • Multi<X>: Indicates a non-blocking method where the items emitted by the non-null Multi are sequentially sent to the client until completion or cancellation.

  • Kotlin suspend function returning Unit: Denotes a non-blocking method where no explicit message is sent back to the client.

  • Kotlin suspend function returning X: 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 returning Unit: 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) {
        //....
}
  1. 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) {
        //....
}
  1. Specify the codec to use for the deserialization of the incoming message

  2. 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 a Uni. 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.

Use standard security annotation to secure an HTTP upgrade
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.
Use HTTP Security policy to secure an HTTP upgrade
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.

Example HttpUpgradeCheck
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:

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:

Connector
@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.

Basic connector
@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 a Uni. 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.
Example server configuration
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. Referencia de configuración

Propiedad de configuración fijada en tiempo de compilación - Todas las demás propiedades de configuración son anulables en tiempo de ejecución

Configuration property

Tipo

Por defecto

Compression Extensions for WebSocket are supported by default.

See also RFC 7692

Environment variable: QUARKUS_WEBSOCKETS_NEXT_CLIENT_OFFER_PER_MESSAGE_COMPRESSION

Show more

boolean

false

The compression level must be a value between 0 and 9. The default value is io.vertx.core.http.HttpClientOptions#DEFAULT_WEBSOCKET_COMPRESSION_LEVEL.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_CLIENT_COMPRESSION_LEVEL

Show more

int

The maximum size of a message in bytes. The default values is io.vertx.core.http.HttpClientOptions#DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_CLIENT_MAX_MESSAGE_SIZE

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: QUARKUS_WEBSOCKETS_NEXT_CLIENT_AUTO_PING_INTERVAL

Show more

Duration 

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: QUARKUS_WEBSOCKETS_NEXT_CLIENT_UNHANDLED_FAILURE_STRATEGY

Show more

log-and-closeLog the error message and close the connection., closeClose the connection silently., logLog the error message., noopNo operation.

logLog the error message.

The name of the TLS configuration to use.

If a name is configured, it uses the configuration from quarkus.tls.<name>.* If a name is configured, but no TLS configuration is found with that name then an error will be thrown.

The default TLS configuration is not used by default.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_CLIENT_TLS_CONFIGURATION_NAME

Show more

string

If set to true then binary/text messages received/sent are logged if the DEBUG level is enabled for the logger io.quarkus.websockets.next.traffic.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_CLIENT_TRAFFIC_LOGGING_ENABLED

Show more

boolean

false

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: QUARKUS_WEBSOCKETS_NEXT_CLIENT_TRAFFIC_LOGGING_TEXT_PAYLOAD_LIMIT

Show more

int

100

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_SUPPORTED_SUBPROTOCOLS

Show more

list of string

Compression Extensions for WebSocket are supported by default.

See also RFC 7692

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_PER_MESSAGE_COMPRESSION_SUPPORTED

Show more

boolean

true

The compression level must be a value between 0 and 9. The default value is io.vertx.core.http.HttpServerOptions#DEFAULT_WEBSOCKET_COMPRESSION_LEVEL.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_COMPRESSION_LEVEL

Show more

int

The maximum size of a message in bytes. The default values is io.vertx.core.http.HttpServerOptions#DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_MAX_MESSAGE_SIZE

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: QUARKUS_WEBSOCKETS_NEXT_SERVER_AUTO_PING_INTERVAL

Show more

Duration 

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: QUARKUS_WEBSOCKETS_NEXT_SERVER_UNHANDLED_FAILURE_STRATEGY

Show more

log-and-closeLog the error message and close the connection., closeClose the connection silently., logLog the error message., noopNo operation.

log-and-closeLog the error message and close the connection.

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 @RolesAllowed annotation.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_SECURITY_AUTH_FAILURE_REDIRECT_URL

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: QUARKUS_WEBSOCKETS_NEXT_SERVER_DEV_MODE_CONNECTION_MESSAGES_LIMIT

Show more

long

1000

If set to true then binary/text messages received/sent are logged if the DEBUG level is enabled for the logger io.quarkus.websockets.next.traffic.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_SERVER_TRAFFIC_LOGGING_ENABLED

Show more

boolean

false

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: QUARKUS_WEBSOCKETS_NEXT_SERVER_TRAFFIC_LOGGING_TEXT_PAYLOAD_LIMIT

Show more

int

100

About the Duration format

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.

Related content