streaming() {
return Multi.createFrom().ticks().every(Duration.ofSecond(1))
@@ -457,6 +459,10 @@ The method that declares a most-specific supertype of the actual exception is se
NOTE: 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` paremeters. 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` and `quarkus.websockets-next.client.unhandled-failure-strategy`, respectively.
+By default, the connection is closed.
+Alternatively, an error message can be logged or no operation performed.
+
== Access to the WebSocketConnection
The `io.quarkus.websockets.next.WebSocketConnection` object represents the WebSocket connection.
@@ -635,6 +641,8 @@ quarkus.http.auth.permission.secured.policy=authenticated
Other options for securing HTTP upgrade requests, such as using the security annotations, will be explored in the future.
+NOTE: When OpenID Connect extension is used and token expires, Quarkus automatically closes connection.
+
[[websocket-next-configuration-reference]]
== Configuration reference
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
index ace3645ff8dd3..84290b257abb7 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
@@ -642,7 +642,10 @@ public enum Strategy {
* either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` is checked.
* Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication is
* checked.
- * The secret is auto-generated if it remains uninitialized after checking all of these properties.
+ * The secret is auto-generated every time an application starts if it remains uninitialized after checking all of these
+ * properties.
+ * Generated secret can not decrypt the session cookie encrypted before the restart, therefore a user re-authentication
+ * will be required.
*
* The length of the secret used to encrypt the tokens should be at least 32 characters long.
* A warning is logged if the secret length is less than 16 characters.
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
index a11fec4b2baef..442032e00a079 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
@@ -119,7 +119,12 @@ private static SecretKey createTokenEncSecretKey(OidcTenantConfig config) {
}
try {
if (encSecret == null) {
- LOG.warn("Secret key for encrypting tokens in a session cookie is missing, auto-generating it");
+ LOG.warn(
+ "Secret key for encrypting OIDC authorization code flow tokens in a session cookie is not configured, auto-generating it."
+ + " Note that a new secret will be generated after a restart, thus making it impossible to decrypt the session cookie and requiring a user re-authentication."
+ + " Use 'quarkus.oidc.token-state-manager.encryption-secret' to configure an encryption secret."
+ + " Alternatively, disable session cookie encryption with 'quarkus.oidc.token-state-manager.encryption-required=false'"
+ + " but only if it is considered to be safe in your application's network.");
return generateSecretKey();
}
byte[] secretBytes = encSecret.getBytes(StandardCharsets.UTF_8);
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
index 67282afd1c3db..c3cb66fbf2b9a 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
@@ -1001,7 +1001,7 @@ public String apply(String id) {
// Register all param declarations as targets of implicit value resolvers
for (ParameterDeclaration paramDeclaration : templateAnalysis.parameterDeclarations) {
Type type = TypeInfos.resolveTypeFromTypeInfo(paramDeclaration.getTypeInfo());
- if (type != null) {
+ if (type != null && !implicitClassToMembersUsed.containsKey(type.name())) {
implicitClassToMembersUsed.put(type.name(), new HashSet<>());
}
}
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/generatedresolvers/ImplicitValueResolversTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/generatedresolvers/ImplicitValueResolversTest.java
new file mode 100644
index 0000000000000..2b740c276290d
--- /dev/null
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/generatedresolvers/ImplicitValueResolversTest.java
@@ -0,0 +1,61 @@
+package io.quarkus.qute.deployment.generatedresolvers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.List;
+
+import jakarta.inject.Inject;
+
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.qute.CheckedTemplate;
+import io.quarkus.qute.Engine;
+import io.quarkus.qute.TemplateInstance;
+import io.quarkus.qute.ValueResolver;
+import io.quarkus.qute.generator.ValueResolverGenerator;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class ImplicitValueResolversTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addAsResource(new StringAsset("{name.toUpperCase}"), "templates/hello.html")
+ .addAsResource(new StringAsset("{name}"), "templates/bye.html")
+ .addAsResource(new StringAsset("{name}"), "templates/zero.html"));
+
+ @CheckedTemplate(basePath = "")
+ record hello(String name) implements TemplateInstance {
+ };
+
+ @CheckedTemplate(basePath = "")
+ record bye(String name) implements TemplateInstance {
+ };
+
+ @CheckedTemplate(basePath = "")
+ record zero(String name) implements TemplateInstance {
+ };
+
+ @Inject
+ Engine engine;
+
+ @Test
+ public void testImplicitResolvers() {
+ assertEquals("FOO", new hello("Foo").render());
+ assertEquals("Bar", new bye("Bar").render());
+ assertEquals("Baz", new zero("Baz").render());
+ List resolvers = engine.getValueResolvers();
+ ValueResolver stringResolver = null;
+ for (ValueResolver valueResolver : resolvers) {
+ if (valueResolver.getClass().getName().endsWith(ValueResolverGenerator.SUFFIX)
+ && valueResolver.getClass().getName().contains("String")) {
+ stringResolver = valueResolver;
+ }
+ }
+ assertNotNull(stringResolver);
+ }
+
+}
diff --git a/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java
index b78d5eec48b58..d2fcf7fd846d2 100644
--- a/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java
+++ b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java
@@ -58,6 +58,7 @@
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.util.ServiceUtil;
+import io.quarkus.resteasy.common.runtime.ResteasyCommonConfig;
import io.quarkus.resteasy.common.runtime.ResteasyInjectorFactoryRecorder;
import io.quarkus.resteasy.common.runtime.config.ResteasyConfigBuilder;
import io.quarkus.resteasy.common.runtime.providers.ServerFormUrlEncodedProvider;
@@ -65,10 +66,6 @@
import io.quarkus.resteasy.common.spi.ResteasyDotNames;
import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;
import io.quarkus.runtime.RuntimeValue;
-import io.quarkus.runtime.annotations.ConfigGroup;
-import io.quarkus.runtime.annotations.ConfigItem;
-import io.quarkus.runtime.annotations.ConfigRoot;
-import io.quarkus.runtime.configuration.MemorySize;
public class ResteasyCommonProcessor {
@@ -102,31 +99,6 @@ public class ResteasyCommonProcessor {
private ResteasyCommonConfig resteasyCommonConfig;
- @ConfigRoot(name = "resteasy")
- public static final class ResteasyCommonConfig {
- /**
- * Enable gzip support for REST
- */
- public ResteasyCommonConfigGzip gzip;
- }
-
- @ConfigGroup
- public static final class ResteasyCommonConfigGzip {
- /**
- * If gzip is enabled
- */
- @ConfigItem
- public boolean enabled;
- /**
- * Maximum deflated file bytes size
- *
- * If the limit is exceeded, Resteasy will return Response
- * with status 413("Request Entity Too Large")
- */
- @ConfigItem(defaultValue = "10M")
- public MemorySize maxInput;
- }
-
@BuildStep
void addStaticInitConfigSourceProvider(
Capabilities capabilities,
@@ -164,7 +136,7 @@ void disableDefaultExceptionMapper(BuildProducer system
@BuildStep
void setupGzipProviders(BuildProducer providers) {
// If GZIP support is enabled, enable it
- if (resteasyCommonConfig.gzip.enabled) {
+ if (resteasyCommonConfig.gzip().enabled()) {
providers.produce(new ResteasyJaxrsProviderBuildItem(AcceptEncodingGZIPFilter.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(GZIPDecodingInterceptor.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(GZIPEncodingInterceptor.class.getName()));
diff --git a/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/ResteasyCommonConfig.java b/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/ResteasyCommonConfig.java
new file mode 100644
index 0000000000000..aebebd4bc9ce1
--- /dev/null
+++ b/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/ResteasyCommonConfig.java
@@ -0,0 +1,36 @@
+package io.quarkus.resteasy.common.runtime;
+
+import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_AND_RUN_TIME_FIXED;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.quarkus.runtime.configuration.MemorySize;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigRoot(phase = BUILD_AND_RUN_TIME_FIXED)
+@ConfigMapping(prefix = "quarkus.resteasy")
+public interface ResteasyCommonConfig {
+
+ /**
+ * Enable gzip support for REST
+ */
+ ResteasyCommonConfigGzip gzip();
+
+ interface ResteasyCommonConfigGzip {
+ /**
+ * If gzip is enabled
+ */
+ @WithDefault("false")
+ boolean enabled();
+
+ /**
+ * Maximum deflated file bytes size
+ *
+ * If the limit is exceeded, Resteasy will return Response
+ * with status 413("Request Entity Too Large")
+ */
+ @WithDefault("10M")
+ MemorySize maxInput();
+ }
+
+}
diff --git a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java
index 3dedbf5ee5108..bee18043872b9 100644
--- a/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java
+++ b/extensions/resteasy-classic/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java
@@ -77,8 +77,8 @@
import io.quarkus.gizmo.Gizmo;
import io.quarkus.jaxrs.spi.deployment.AdditionalJaxRsResourceMethodAnnotationsBuildItem;
import io.quarkus.resteasy.common.deployment.JaxrsProvidersToRegisterBuildItem;
-import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor.ResteasyCommonConfig;
import io.quarkus.resteasy.common.runtime.QuarkusInjectorFactory;
+import io.quarkus.resteasy.common.runtime.ResteasyCommonConfig;
import io.quarkus.resteasy.common.spi.ResteasyDotNames;
import io.quarkus.resteasy.server.common.runtime.QuarkusResteasyDeployment;
import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceDefiningAnnotationBuildItem;
@@ -421,9 +421,9 @@ public void build(
deploymentCustomizer.getConsumer().accept(deployment);
}
- if (commonConfig.gzip.enabled) {
+ if (commonConfig.gzip().enabled()) {
resteasyInitParameters.put(ResteasyContextParameters.RESTEASY_GZIP_MAX_INPUT,
- Long.toString(commonConfig.gzip.maxInput.asLongValue()));
+ Long.toString(commonConfig.gzip().maxInput().asLongValue()));
}
resteasyInitParameters.put(ResteasyContextParameters.RESTEASY_UNWRAPPED_EXCEPTIONS,
ArcUndeclaredThrowableException.class.getName());
diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyConfigurationMPConfig.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyConfigurationMPConfig.java
index bdf70a82a1603..7c87d6b9426f5 100644
--- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyConfigurationMPConfig.java
+++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyConfigurationMPConfig.java
@@ -67,6 +67,11 @@ public Set getInitParameterNames() {
}
private static Optional getGzipMaxInput(Config config) {
+ if (config.getOptionalValue("resteasy.gzip.max.input", String.class).isPresent()) {
+ // resteasy-specific properties have priority
+ return Optional.empty();
+ }
+
Optional rawValue = config.getOptionalValue("quarkus.resteasy.gzip.max-input", MemorySize.class);
if (rawValue.isEmpty()) {
diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-ide-link.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-ide-link.js
index 106ddd0143fd4..7e9c9bdc43a00 100644
--- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-ide-link.js
+++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-ide-link.js
@@ -20,7 +20,7 @@ export class QuiIdeLink extends observeState(LitElement) {
static properties = {
fileName: {type: String},
lang: {type: String},
- lineNumber: {type: Number},
+ lineNumber: {type: String},
stackTraceLine: {type: String},
_fontWeight: {type: String}
};
@@ -30,7 +30,7 @@ export class QuiIdeLink extends observeState(LitElement) {
this.stackTraceLine = null;
this.fileName = null;
this.lang = "java";
- this.lineNumber = 0;
+ this.lineNumber = "0";
this._fontWeight = "normal";
}
@@ -55,7 +55,7 @@ export class QuiIdeLink extends observeState(LitElement) {
if(givenClassName && givenClassName!== "" && this._checkIfStringStartsWith(givenClassName, devuiState.ideInfo.idePackages)){
this.fileName = givenClassName;
this.lang = lang;
- this.lineNumber = parseInt(lineNumber);
+ this.lineNumber = lineNumber;
this._fontWeight = "bold";
}
}
diff --git a/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java
index 1620a9c3a67fc..8db3d5202df95 100644
--- a/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java
+++ b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java
@@ -2,6 +2,8 @@
import java.util.Map;
+import org.jboss.logging.Logger;
+
import io.quarkus.runtime.annotations.Recorder;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
@@ -11,30 +13,38 @@
@Recorder
public class WebDependencyLocatorRecorder {
+ private static final Logger LOG = Logger.getLogger(WebDependencyLocatorRecorder.class.getName());
+
public Handler getHandler(String webDependenciesRootUrl,
Map webDependencyNameToVersionMap) {
return (event) -> {
String path = event.normalizedPath();
if (path.startsWith(webDependenciesRootUrl)) {
- String rest = path.substring(webDependenciesRootUrl.length());
- String webdep = rest.substring(0, rest.indexOf('/'));
- if (webDependencyNameToVersionMap.containsKey(webdep)) {
- // Check this is not the actual path (ex: /webjars/jquery/${jquery.version}/...
- int endOfVersion = rest.indexOf('/', rest.indexOf('/') + 1);
- if (endOfVersion == -1) {
- endOfVersion = rest.length();
- }
- String nextPathEntry = rest.substring(rest.indexOf('/') + 1, endOfVersion);
- if (webDependencyNameToVersionMap.get(webdep) == null
- || nextPathEntry.equals(webDependencyNameToVersionMap.get(webdep))) {
- // go to the next handler (which should be the static resource handler, if one exists)
- event.next();
+ try {
+ String rest = path.substring(webDependenciesRootUrl.length());
+ String webdep = rest.substring(0, rest.indexOf('/'));
+ if (webDependencyNameToVersionMap.containsKey(webdep)) {
+ // Check this is not the actual path (ex: /webjars/jquery/${jquery.version}/...
+ int endOfVersion = rest.indexOf('/', rest.indexOf('/') + 1);
+ if (endOfVersion == -1) {
+ endOfVersion = rest.length();
+ }
+ String nextPathEntry = rest.substring(rest.indexOf('/') + 1, endOfVersion);
+ if (webDependencyNameToVersionMap.get(webdep) == null
+ || nextPathEntry.equals(webDependencyNameToVersionMap.get(webdep))) {
+ // go to the next handler (which should be the static resource handler, if one exists)
+ event.next();
+ } else {
+ // reroute to the real resource
+ event.reroute(webDependenciesRootUrl + webdep + "/"
+ + webDependencyNameToVersionMap.get(webdep) + rest.substring(rest.indexOf('/')));
+ }
} else {
- // reroute to the real resource
- event.reroute(webDependenciesRootUrl + webdep + "/"
- + webDependencyNameToVersionMap.get(webdep) + rest.substring(rest.indexOf('/')));
+ event.next();
}
- } else {
+ } catch (Throwable t) {
+ LOG.debug("Error while locating web jar " + path);
+ // See if someone else can handle this.
event.next();
}
} else {
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientEndpointTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientEndpointTest.java
index 5a36ee3511326..617ea30bd31d8 100644
--- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientEndpointTest.java
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientEndpointTest.java
@@ -43,14 +43,15 @@ public class ClientEndpointTest {
void testClient() throws InterruptedException {
WebSocketClientConnection connection = connector
.baseUri(uri)
- .pathParam("name", "Lu")
+ // The value will be encoded automatically
+ .pathParam("name", "Lu=")
.connectAndAwait();
- assertEquals("Lu", connection.pathParam("name"));
+ assertEquals("Lu=", connection.pathParam("name"));
connection.sendTextAndAwait("Hi!");
assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
- assertEquals("Lu:Hello Lu!", ClientEndpoint.MESSAGES.get(0));
- assertEquals("Lu:Hi!", ClientEndpoint.MESSAGES.get(1));
+ assertEquals("Lu=:Hello Lu=!", ClientEndpoint.MESSAGES.get(0));
+ assertEquals("Lu=:Hi!", ClientEndpoint.MESSAGES.get(1));
connection.closeAndAwait();
assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java
new file mode 100644
index 0000000000000..8de5fa38add05
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java
@@ -0,0 +1,35 @@
+package io.quarkus.websockets.next.test.client;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocketClient;
+
+@WebSocketClient(path = "/endpoint")
+public class ClientMessageErrorEndpoint {
+
+ static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(1);
+
+ static final List MESSAGES = new CopyOnWriteArrayList<>();
+
+ static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
+
+ @OnTextMessage
+ void message(String message) {
+ if ("foo".equals(message)) {
+ throw new IllegalStateException("I cannot do it!");
+ } else {
+ MESSAGES.add(message);
+ }
+ MESSAGE_LATCH.countDown();
+ }
+
+ @OnClose
+ void close() {
+ CLOSED_LATCH.countDown();
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java
new file mode 100644
index 0000000000000..990c85bed80c7
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java
@@ -0,0 +1,37 @@
+package io.quarkus.websockets.next.test.client;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocketClient;
+
+@WebSocketClient(path = "/endpoint")
+public class ClientOpenErrorEndpoint {
+
+ static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(1);
+
+ static final List MESSAGES = new CopyOnWriteArrayList<>();
+
+ static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
+
+ @OnOpen
+ void open() {
+ throw new IllegalStateException("I cannot do it!");
+ }
+
+ @OnTextMessage
+ void message(String message) {
+ MESSAGES.add(message);
+ MESSAGE_LATCH.countDown();
+ }
+
+ @OnClose
+ void close() {
+ CLOSED_LATCH.countDown();
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java
new file mode 100644
index 0000000000000..b2fbcbc19cd53
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java
@@ -0,0 +1,24 @@
+package io.quarkus.websockets.next.test.client;
+
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+
+@WebSocket(path = "/endpoint")
+public class ServerEndpoint {
+
+ static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);
+
+ @OnTextMessage
+ String echo(String message) {
+ return message;
+ }
+
+ @OnClose
+ void close() {
+ CLOSED_LATCH.countDown();
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java
new file mode 100644
index 0000000000000..a1d80c81a021f
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java
@@ -0,0 +1,47 @@
+package io.quarkus.websockets.next.test.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnector;
+
+public class UnhandledMessageFailureDefaultStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(ServerEndpoint.class, ClientMessageErrorEndpoint.class);
+ });
+
+ @Inject
+ WebSocketConnector connector;
+
+ @TestHTTPResource("/")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ WebSocketClientConnection connection = connector
+ .baseUri(testUri)
+ .connectAndAwait();
+ connection.sendTextAndAwait("foo");
+ assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(ClientMessageErrorEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(connection.isClosed());
+ assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), connection.closeReason().getCode());
+ assertTrue(ClientMessageErrorEndpoint.MESSAGES.isEmpty());
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java
new file mode 100644
index 0000000000000..1b047d03e5bd7
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.websockets.next.test.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnector;
+
+public class UnhandledMessageFailureLogStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(ServerEndpoint.class, ClientMessageErrorEndpoint.class);
+ }).overrideConfigKey("quarkus.websockets-next.client.unhandled-failure-strategy", "log");
+
+ @Inject
+ WebSocketConnector connector;
+
+ @TestHTTPResource("/")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ WebSocketClientConnection connection = connector
+ .baseUri(testUri)
+ .connectAndAwait();
+ connection.sendTextAndAwait("foo");
+ assertFalse(connection.isClosed());
+ connection.sendText("bar");
+ assertTrue(ClientMessageErrorEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
+ assertEquals("bar", ClientMessageErrorEndpoint.MESSAGES.get(0));
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java
new file mode 100644
index 0000000000000..decf21f2b1705
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.websockets.next.test.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnector;
+
+public class UnhandledOpenFailureDefaultStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(ServerEndpoint.class, ClientOpenErrorEndpoint.class);
+ });
+
+ @Inject
+ WebSocketConnector connector;
+
+ @TestHTTPResource("/")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ WebSocketClientConnection connection = connector
+ .baseUri(testUri)
+ .connectAndAwait();
+ assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(ClientOpenErrorEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
+ assertTrue(connection.isClosed());
+ assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), connection.closeReason().getCode());
+ assertTrue(ClientOpenErrorEndpoint.MESSAGES.isEmpty());
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java
new file mode 100644
index 0000000000000..dc5f6d41504fa
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java
@@ -0,0 +1,47 @@
+package io.quarkus.websockets.next.test.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.WebSocketClientConnection;
+import io.quarkus.websockets.next.WebSocketConnector;
+
+public class UnhandledOpenFailureLogStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(ServerEndpoint.class, ClientOpenErrorEndpoint.class);
+ }).overrideConfigKey("quarkus.websockets-next.client.unhandled-failure-strategy", "log");
+
+ @Inject
+ WebSocketConnector connector;
+
+ @TestHTTPResource("/")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ WebSocketClientConnection connection = connector
+ .baseUri(testUri)
+ .connectAndAwait();
+ connection.sendTextAndAwait("foo");
+ assertFalse(connection.isClosed());
+ assertNull(connection.closeReason());
+ assertTrue(ClientOpenErrorEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
+ assertEquals("foo", ClientOpenErrorEndpoint.MESSAGES.get(0));
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java
new file mode 100644
index 0000000000000..3d52df32d1473
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java
@@ -0,0 +1,23 @@
+package io.quarkus.websockets.next.test.errors;
+
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+
+@WebSocket(path = "/echo")
+public class EchoMessageError {
+
+ static final CountDownLatch MESSAGE_FAILURE_CALLED = new CountDownLatch(1);
+
+ @OnTextMessage
+ String echo(String message) {
+ if ("foo".equals(message)) {
+ MESSAGE_FAILURE_CALLED.countDown();
+ throw new IllegalStateException("I cannot do it!");
+ } else {
+ return message;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java
new file mode 100644
index 0000000000000..7a079a0eb45c2
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java
@@ -0,0 +1,25 @@
+package io.quarkus.websockets.next.test.errors;
+
+import java.util.concurrent.CountDownLatch;
+
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+
+@WebSocket(path = "/echo")
+public class EchoOpenError {
+
+ static final CountDownLatch OPEN_CALLED = new CountDownLatch(1);
+
+ @OnOpen
+ void open() {
+ OPEN_CALLED.countDown();
+ throw new IllegalStateException("I cannot do it!");
+ }
+
+ @OnTextMessage
+ String echo(String message) {
+ return message;
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java
new file mode 100644
index 0000000000000..1207e6689277a
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.websockets.next.test.errors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class UnhandledMessageFailureDefaultStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(EchoMessageError.class, WSClient.class);
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ try (WSClient client = WSClient.create(vertx).connect(testUri)) {
+ client.sendAndAwait("foo");
+ assertTrue(EchoMessageError.MESSAGE_FAILURE_CALLED.await(5, TimeUnit.SECONDS));
+ Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed());
+ assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), client.closeStatusCode());
+ }
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java
new file mode 100644
index 0000000000000..0061937345fcf
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java
@@ -0,0 +1,44 @@
+package io.quarkus.websockets.next.test.errors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class UnhandledMessageFailureLogStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(EchoMessageError.class, WSClient.class);
+ }).overrideConfigKey("quarkus.websockets-next.server.unhandled-failure-strategy", "log");
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo")
+ URI testUri;
+
+ @Test
+ void testErrorDoesNotCloseConnection() throws InterruptedException {
+ try (WSClient client = WSClient.create(vertx).connect(testUri)) {
+ client.sendAndAwait("foo");
+ assertTrue(EchoMessageError.MESSAGE_FAILURE_CALLED.await(5, TimeUnit.SECONDS));
+ client.sendAndAwait("bar");
+ client.waitForMessages(1);
+ assertEquals("bar", client.getLastMessage().toString());
+ }
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java
new file mode 100644
index 0000000000000..61c712d005d86
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java
@@ -0,0 +1,45 @@
+package io.quarkus.websockets.next.test.errors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class UnhandledOpenFailureDefaultStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(EchoOpenError.class, WSClient.class);
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo")
+ URI testUri;
+
+ @Test
+ void testError() throws InterruptedException {
+ try (WSClient client = WSClient.create(vertx).connect(testUri)) {
+ assertTrue(EchoOpenError.OPEN_CALLED.await(5, TimeUnit.SECONDS));
+ Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed());
+ assertEquals(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code(), client.closeStatusCode());
+ }
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java
new file mode 100644
index 0000000000000..b704e8c551cde
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java
@@ -0,0 +1,43 @@
+package io.quarkus.websockets.next.test.errors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.inject.Inject;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class UnhandledOpenFailureLogStrategyTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(EchoOpenError.class, WSClient.class);
+ }).overrideConfigKey("quarkus.websockets-next.server.unhandled-failure-strategy", "log");
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo")
+ URI testUri;
+
+ @Test
+ void testErrorDoesNotCloseConnection() throws InterruptedException {
+ try (WSClient client = WSClient.create(vertx).connect(testUri)) {
+ assertTrue(EchoOpenError.OPEN_CALLED.await(5, TimeUnit.SECONDS));
+ client.sendAndAwait("foo");
+ client.waitForMessages(1);
+ assertEquals("foo", client.getLastMessage().toString());
+ }
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AuthenticationExpiredTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AuthenticationExpiredTest.java
new file mode 100644
index 0000000000000..3351c71033053
--- /dev/null
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AuthenticationExpiredTest.java
@@ -0,0 +1,129 @@
+package io.quarkus.websockets.next.test.security;
+
+import static io.quarkus.websockets.next.test.security.SecurityTestBase.basicAuth;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicReference;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.security.Authenticated;
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.security.test.utils.TestIdentityController;
+import io.quarkus.security.test.utils.TestIdentityProvider;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.websockets.next.CloseReason;
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+
+public class AuthenticationExpiredTest {
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("end")
+ URI endUri;
+
+ @BeforeAll
+ public static void setupUsers() {
+ TestIdentityController.resetRoles()
+ .add("admin", "admin", "admin")
+ .add("user", "user", "user");
+ }
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot(root -> root.addClasses(Endpoint.class, TestIdentityProvider.class,
+ TestIdentityController.class, WSClient.class, ExpiredIdentityAugmentor.class, SecurityTestBase.class));
+
+ @Test
+ public void testConnectionClosedWhenAuthExpires() {
+ try (WSClient client = new WSClient(vertx)) {
+ client.connect(basicAuth("admin", "admin"), endUri);
+
+ long threeSecondsFromNow = Duration.ofMillis(System.currentTimeMillis()).plusSeconds(3).toMillis();
+ for (int i = 1; true; i++) {
+ if (client.isClosed()) {
+ break;
+ } else if (System.currentTimeMillis() > threeSecondsFromNow) {
+ Assertions.fail("Authentication expired, therefore connection should had been closed");
+ }
+ client.sendAndAwaitReply("Hello #" + i + " from ");
+ }
+
+ var receivedMessages = client.getMessages().stream().map(Buffer::toString).toList();
+ assertTrue(receivedMessages.size() > 2, receivedMessages.toString());
+ assertTrue(receivedMessages.contains("Hello #1 from admin"), receivedMessages.toString());
+ assertTrue(receivedMessages.contains("Hello #2 from admin"), receivedMessages.toString());
+ assertEquals(1008, client.closeStatusCode(), "Expected close status 1008, but got " + client.closeStatusCode());
+
+ Awaitility
+ .await()
+ .atMost(Duration.ofSeconds(1))
+ .untilAsserted(() -> assertTrue(Endpoint.CLOSED_MESSAGE.get()
+ .startsWith("Connection closed with reason 'Authentication expired'")));
+ }
+ }
+
+ @Singleton
+ public static class ExpiredIdentityAugmentor implements SecurityIdentityAugmentor {
+
+ @Override
+ public Uni augment(SecurityIdentity securityIdentity,
+ AuthenticationRequestContext authenticationRequestContext) {
+ return Uni
+ .createFrom()
+ .item(QuarkusSecurityIdentity
+ .builder(securityIdentity)
+ .addAttribute("quarkus.identity.expire-time", expireIn2Seconds())
+ .build());
+ }
+
+ private static long expireIn2Seconds() {
+ return Duration.ofMillis(System.currentTimeMillis())
+ .plusSeconds(2)
+ .toSeconds();
+ }
+ }
+
+ @WebSocket(path = "/end")
+ public static class Endpoint {
+
+ static final AtomicReference CLOSED_MESSAGE = new AtomicReference<>();
+
+ @Inject
+ SecurityIdentity currentIdentity;
+
+ @Authenticated
+ @OnTextMessage
+ String echo(String message) {
+ return message + currentIdentity.getPrincipal().getName();
+ }
+
+ @OnClose
+ void close(CloseReason reason, WebSocketConnection connection) {
+ CLOSED_MESSAGE.set("Connection closed with reason '%s': %s".formatted(reason.getMessage(), connection));
+ }
+ }
+
+}
diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java
index 773b9ab8d134f..955eb9c1b315c 100644
--- a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java
+++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/utils/WSClient.java
@@ -126,6 +126,10 @@ public boolean isClosed() {
return socket.get().isClosed();
}
+ public int closeStatusCode() {
+ return socket.get().closeStatusCode();
+ }
+
@Override
public void close() {
disconnect();
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java
index 7ee5be65764e7..b1e21c9b12966 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java
@@ -1,6 +1,7 @@
package io.quarkus.websockets.next;
import java.net.URI;
+import java.net.URLEncoder;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -51,6 +52,9 @@ static BasicWebSocketConnector create() {
/**
* Set the path param.
+ *
+ * The value is encoded using {@link URLEncoder#encode(String, java.nio.charset.Charset)} before it's used to build the
+ * target URI.
*
* @param name
* @param value
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java
index 55e100a9b9e7d..108c2d150b55b 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/CloseReason.java
@@ -15,6 +15,8 @@ public class CloseReason {
public static final CloseReason NORMAL = new CloseReason(WebSocketCloseStatus.NORMAL_CLOSURE.code());
+ public static final CloseReason INTERNAL_SERVER_ERROR = new CloseReason(WebSocketCloseStatus.INTERNAL_SERVER_ERROR.code());
+
private final int code;
private final String message;
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java
new file mode 100644
index 0000000000000..bdfb1f17ad2be
--- /dev/null
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java
@@ -0,0 +1,20 @@
+package io.quarkus.websockets.next;
+
+/**
+ * The strategy used when an error occurs but no error handler can handle the failure.
+ */
+public enum UnhandledFailureStrategy {
+ /**
+ * Close the connection.
+ */
+ CLOSE,
+ /**
+ * Log an error message.
+ */
+ LOG,
+ /**
+ * No operation.
+ */
+ NOOP;
+
+}
\ No newline at end of file
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java
index 5151349c559d8..393ba422b7351 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java
@@ -27,7 +27,7 @@ public interface WebSocketClientConnection extends Sender, BlockingSender {
/**
*
* @param name
- * @return the actual value of the path parameter or null
+ * @return the value of the path parameter or {@code null}
* @see WebSocketClient#path()
*/
String pathParam(String name);
@@ -42,6 +42,12 @@ public interface WebSocketClientConnection extends Sender, BlockingSender {
*/
boolean isClosed();
+ /**
+ *
+ * @return the close reason or {@code null} if the connection is not closed
+ */
+ CloseReason closeReason();
+
/**
*
* @return {@code true} if the WebSocket is open
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java
index be8acb1a93539..a63a3e2e5772e 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java
@@ -37,7 +37,7 @@ public interface WebSocketConnection extends Sender, BlockingSender {
/**
*
* @param name
- * @return the actual value of the path parameter or null
+ * @return the decoded value of the path parameter or {@code null}
* @see WebSocket#path()
*/
String pathParam(String name);
@@ -67,6 +67,12 @@ public interface WebSocketConnection extends Sender, BlockingSender {
*/
boolean isClosed();
+ /**
+ *
+ * @return the close reason or {@code null} if the connection is not closed
+ */
+ CloseReason closeReason();
+
/**
*
* @return {@code true} if the WebSocket is open
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java
index 4b771a66c7833..257094e31fe23 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java
@@ -1,6 +1,7 @@
package io.quarkus.websockets.next;
import java.net.URI;
+import java.net.URLEncoder;
import io.smallrye.common.annotation.CheckReturnValue;
import io.smallrye.common.annotation.Experimental;
@@ -28,6 +29,9 @@ public interface WebSocketConnector {
/**
* Set the path param.
+ *
+ * The value is encoded using {@link URLEncoder#encode(String, java.nio.charset.Charset)} before it's used to build the
+ * target URI.
*
* @param name
* @param value
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
index dff4780aa45c7..ecaf0bb169d0d 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsClientRuntimeConfig.java
@@ -40,4 +40,12 @@ public interface WebSocketsClientRuntimeConfig {
*/
Optional autoPingInterval();
+ /**
+ * The strategy used when an error occurs but no error handler can handle the failure.
+ *
+ * By default, the connection is closed when an unhandled failure occurs.
+ */
+ @WithDefault("close")
+ UnhandledFailureStrategy unhandledFailureStrategy();
+
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
index 28e9d284c2fce..43beffda35600 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsServerRuntimeConfig.java
@@ -46,4 +46,12 @@ public interface WebSocketsServerRuntimeConfig {
*/
Optional autoPingInterval();
+ /**
+ * The strategy used when an error occurs but no error handler can handle the failure.
+ *
+ * By default, the connection is closed when an unhandled failure occurs.
+ */
+ @WithDefault("close")
+ UnhandledFailureStrategy unhandledFailureStrategy();
+
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
index e8ed61d23620c..15980876612be 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java
@@ -13,6 +13,8 @@
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
+import io.quarkus.websockets.next.CloseReason;
+import io.quarkus.websockets.next.UnhandledFailureStrategy;
import io.quarkus.websockets.next.WebSocketException;
import io.quarkus.websockets.next.runtime.WebSocketSessionContext.SessionContextState;
import io.smallrye.mutiny.Multi;
@@ -29,7 +31,7 @@ class Endpoints {
static void initialize(Vertx vertx, ArcContainer container, Codecs codecs, WebSocketConnectionBase connection,
WebSocketBase ws, String generatedEndpointClass, Optional autoPingInterval,
- SecuritySupport securitySupport, Runnable onClose) {
+ SecuritySupport securitySupport, UnhandledFailureStrategy unhandledFailureStrategy, Runnable onClose) {
Context context = vertx.getOrCreateContext();
@@ -75,7 +77,7 @@ public void handle(Void event) {
LOG.debugf("@OnTextMessage callback consuming Multi completed: %s",
connection);
} else {
- logFailure(r.cause(),
+ handleFailure(unhandledFailureStrategy, r.cause(),
"Unable to complete @OnTextMessage callback consuming Multi",
connection);
}
@@ -93,7 +95,7 @@ public void handle(Void event) {
LOG.debugf("@OnBinaryMessage callback consuming Multi completed: %s",
connection);
} else {
- logFailure(r.cause(),
+ handleFailure(unhandledFailureStrategy, r.cause(),
"Unable to complete @OnBinaryMessage callback consuming Multi",
connection);
}
@@ -102,7 +104,7 @@ public void handle(Void event) {
});
}
} else {
- logFailure(r.cause(), "Unable to complete @OnOpen callback", connection);
+ handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnOpen callback", connection);
}
});
}
@@ -115,7 +117,8 @@ public void handle(Void event) {
if (r.succeeded()) {
LOG.debugf("@OnTextMessage callback consumed text message: %s", connection);
} else {
- logFailure(r.cause(), "Unable to consume text message in @OnTextMessage callback",
+ handleFailure(unhandledFailureStrategy, r.cause(),
+ "Unable to consume text message in @OnTextMessage callback",
connection);
}
});
@@ -130,7 +133,8 @@ public void handle(Void event) {
} catch (Throwable throwable) {
endpoint.doOnError(throwable).subscribe().with(
v -> LOG.debugf("Text message >> Multi: %s", connection),
- t -> LOG.errorf(t, "Unable to send text message to Multi: %s", connection));
+ t -> handleFailure(unhandledFailureStrategy, t, "Unable to send text message to Multi",
+ connection));
} finally {
contextSupport.end(false);
}
@@ -144,7 +148,8 @@ public void handle(Void event) {
if (r.succeeded()) {
LOG.debugf("@OnBinaryMessage callback consumed binary message: %s", connection);
} else {
- logFailure(r.cause(), "Unable to consume binary message in @OnBinaryMessage callback",
+ handleFailure(unhandledFailureStrategy, r.cause(),
+ "Unable to consume binary message in @OnBinaryMessage callback",
connection);
}
});
@@ -159,7 +164,8 @@ public void handle(Void event) {
} catch (Throwable throwable) {
endpoint.doOnError(throwable).subscribe().with(
v -> LOG.debugf("Binary message >> Multi: %s", connection),
- t -> LOG.errorf(t, "Unable to send binary message to Multi: %s", connection));
+ t -> handleFailure(unhandledFailureStrategy, t, "Unable to send binary message to Multi",
+ connection));
} finally {
contextSupport.end(false);
}
@@ -171,7 +177,8 @@ public void handle(Void event) {
if (r.succeeded()) {
LOG.debugf("@OnPongMessage callback consumed text message: %s", connection);
} else {
- logFailure(r.cause(), "Unable to consume text message in @OnPongMessage callback", connection);
+ handleFailure(unhandledFailureStrategy, r.cause(),
+ "Unable to consume text message in @OnPongMessage callback", connection);
}
});
});
@@ -198,8 +205,10 @@ public void handle(Void event) {
if (r.succeeded()) {
LOG.debugf("@OnClose callback completed: %s", connection);
} else {
- logFailure(r.cause(), "Unable to complete @OnClose callback", connection);
+ handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnClose callback",
+ connection);
}
+ securitySupport.onClose();
onClose.run();
if (timerId != null) {
vertx.cancelTimer(timerId);
@@ -218,14 +227,30 @@ public void handle(Throwable t) {
public void handle(Void event) {
endpoint.doOnError(t).subscribe().with(
v -> LOG.debugf("Error [%s] processed: %s", t.getClass(), connection),
- t -> LOG.errorf(t, "Unhandled error occurred: %s", t.toString(),
- connection));
+ t -> handleFailure(unhandledFailureStrategy, t, "Unhandled error occurred", connection));
}
});
}
});
}
+ private static void handleFailure(UnhandledFailureStrategy strategy, Throwable cause, String message,
+ WebSocketConnectionBase connection) {
+ switch (strategy) {
+ case CLOSE -> closeConnection(cause, connection);
+ case LOG -> logFailure(cause, message, connection);
+ case NOOP -> LOG.tracef("Unhandled failure ignored: %s", connection);
+ default -> throw new IllegalArgumentException("Unexpected strategy: " + strategy);
+ }
+ }
+
+ private static void closeConnection(Throwable cause, WebSocketConnectionBase connection) {
+ connection.close(CloseReason.INTERNAL_SERVER_ERROR).subscribe().with(
+ v -> LOG.debugf("Connection closed due to unhandled failure %s: %s", cause, connection),
+ t -> LOG.errorf("Unable to close connection [%s] due to unhandled failure [%s]: %s", connection.id(), cause,
+ t));
+ }
+
private static void logFailure(Throwable throwable, String message, WebSocketConnectionBase connection) {
if (isWebSocketIsClosedFailure(throwable, connection)) {
LOG.debugf(throwable,
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java
index 8ec115e085e70..eeb5f5a5ad342 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/SecuritySupport.java
@@ -1,22 +1,36 @@
package io.quarkus.websockets.next.runtime;
import java.util.Objects;
+import java.util.concurrent.TimeUnit;
import jakarta.enterprise.inject.Instance;
+import org.jboss.logging.Logger;
+
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.websockets.next.CloseReason;
+import io.vertx.core.Vertx;
public class SecuritySupport {
- static final SecuritySupport NOOP = new SecuritySupport(null, null);
+ private static final Logger LOG = Logger.getLogger(SecuritySupport.class);
+ static final SecuritySupport NOOP = new SecuritySupport(null, null, null, null);
private final Instance currentIdentity;
private final SecurityIdentity identity;
+ private final Runnable onClose;
- SecuritySupport(Instance currentIdentity, SecurityIdentity identity) {
+ SecuritySupport(Instance currentIdentity, SecurityIdentity identity, Vertx vertx,
+ WebSocketConnectionImpl connection) {
this.currentIdentity = currentIdentity;
- this.identity = currentIdentity != null ? Objects.requireNonNull(identity) : identity;
+ if (this.currentIdentity != null) {
+ this.identity = Objects.requireNonNull(identity);
+ this.onClose = closeConnectionWhenIdentityExpired(vertx, connection, this.identity);
+ } else {
+ this.identity = null;
+ this.onClose = null;
+ }
}
/**
@@ -29,4 +43,25 @@ void start() {
}
}
+ void onClose() {
+ if (onClose != null) {
+ onClose.run();
+ }
+ }
+
+ private static Runnable closeConnectionWhenIdentityExpired(Vertx vertx, WebSocketConnectionImpl connection,
+ SecurityIdentity identity) {
+ if (identity.getAttribute("quarkus.identity.expire-time") instanceof Long expireAt) {
+ long timerId = vertx.setTimer(TimeUnit.SECONDS.toMillis(expireAt) - System.currentTimeMillis(),
+ ignored -> connection
+ .close(new CloseReason(1008, "Authentication expired"))
+ .subscribe()
+ .with(
+ v -> LOG.tracef("Closed connection due to expired authentication: %s", connection),
+ e -> LOG.errorf("Unable to close connection [%s] after authentication "
+ + "expired due to unhandled failure: %s", connection, e)));
+ return () -> vertx.cancelTimer(timerId);
+ }
+ return null;
+ }
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java
index e722da795ede8..00ae0dc9e0d1f 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java
@@ -125,6 +125,6 @@ public CloseReason closeReason() {
if (ws.isClosed()) {
return new CloseReason(ws.closeStatusCode(), ws.closeReason());
}
- throw new IllegalStateException("Connection is not closed");
+ return null;
}
}
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java
index 4059996cd8369..728850f3083fd 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorBase.java
@@ -1,6 +1,8 @@
package io.quarkus.websockets.next.runtime;
import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -121,7 +123,7 @@ String replacePathParameters(String path) {
if (val == null) {
throw new WebSocketClientException("Unable to obtain the path param for: " + paramName);
}
- m.appendReplacement(sb, val);
+ m.appendReplacement(sb, URLEncoder.encode(val, StandardCharsets.UTF_8));
}
m.appendTail(sb);
return path.startsWith("/") ? sb.toString() : "/" + sb.toString();
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java
index d6281e5da71f4..ceaeab285dd80 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java
@@ -92,7 +92,7 @@ public Uni connect() {
.setPort(serverEndpointUri.getPort());
StringBuilder uri = new StringBuilder();
if (serverEndpointUri.getPath() != null) {
- uri.append(serverEndpointUri.getPath());
+ uri.append(serverEndpointUri.getRawPath());
}
if (serverEndpointUri.getQuery() != null) {
uri.append("?").append(serverEndpointUri.getQuery());
@@ -116,6 +116,7 @@ public Uni connect() {
Endpoints.initialize(vertx, Arc.container(), codecs, connection, ws,
clientEndpoint.generatedEndpointClass, config.autoPingInterval(), SecuritySupport.NOOP,
+ config.unhandledFailureStrategy(),
() -> {
connectionManager.remove(clientEndpoint.generatedEndpointClass, connection);
client.close();
diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java
index 9384f8d60fc47..2878f921d680c 100644
--- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java
+++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java
@@ -90,8 +90,6 @@ public Handler createEndpointHandler(String generatedEndpointCla
@Override
public void handle(RoutingContext ctx) {
- SecuritySupport securitySupport = initializeSecuritySupport(container, ctx);
-
Future future = ctx.request().toWebSocket();
future.onSuccess(ws -> {
Vertx vertx = VertxCoreRecorder.getVertx().get();
@@ -101,22 +99,25 @@ public void handle(RoutingContext ctx) {
connectionManager.add(generatedEndpointClass, connection);
LOG.debugf("Connection created: %s", connection);
+ SecuritySupport securitySupport = initializeSecuritySupport(container, ctx, vertx, connection);
+
Endpoints.initialize(vertx, container, codecs, connection, ws, generatedEndpointClass,
- config.autoPingInterval(), securitySupport,
+ config.autoPingInterval(), securitySupport, config.unhandledFailureStrategy(),
() -> connectionManager.remove(generatedEndpointClass, connection));
});
}
};
}
- SecuritySupport initializeSecuritySupport(ArcContainer container, RoutingContext ctx) {
+ SecuritySupport initializeSecuritySupport(ArcContainer container, RoutingContext ctx, Vertx vertx,
+ WebSocketConnectionImpl connection) {
Instance currentIdentityAssociation = container.select(CurrentIdentityAssociation.class);
if (currentIdentityAssociation.isResolvable()) {
// Security extension is present
// Obtain the current security identity from the handshake request
QuarkusHttpUser user = (QuarkusHttpUser) ctx.user();
if (user != null) {
- return new SecuritySupport(currentIdentityAssociation, user.getSecurityIdentity());
+ return new SecuritySupport(currentIdentityAssociation, user.getSecurityIdentity(), vertx, connection);
}
}
return SecuritySupport.NOOP;
diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/Encode.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/Encode.java
index 2d3c74996355d..7536e366e5b9b 100644
--- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/Encode.java
+++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/Encode.java
@@ -95,7 +95,9 @@ public class Encode {
case '.':
case '_':
case '~':
+ continue;
case '?':
+ queryNameValueEncoding[i] = "%3F";
continue;
case ' ':
queryNameValueEncoding[i] = "+";
diff --git a/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/EncodeTest.java b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/EncodeTest.java
index 9e057ce31126f..c53115d687635 100644
--- a/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/EncodeTest.java
+++ b/independent-projects/resteasy-reactive/common/runtime/src/test/java/org/jboss/resteasy/reactive/common/util/EncodeTest.java
@@ -15,4 +15,11 @@ void encodeEmoji() {
assertEquals(encodedEmoji, Encode.encodePath(emoji));
assertEquals(encodedEmoji, Encode.encodeQueryParam(emoji));
}
+
+ @Test
+ void encodeQuestionMarkQueryParameterValue() {
+ String uriQueryValue = "bar?a=b";
+ String encoded = URLEncoder.encode(uriQueryValue, StandardCharsets.UTF_8);
+ assertEquals(encoded, Encode.encodeQueryParam(uriQueryValue));
+ }
}
diff --git a/independent-projects/tools/analytics-common/src/main/java/io/quarkus/analytics/util/FileUtils.java b/independent-projects/tools/analytics-common/src/main/java/io/quarkus/analytics/util/FileUtils.java
index 609f4431d333f..fb8f7d9796e46 100644
--- a/independent-projects/tools/analytics-common/src/main/java/io/quarkus/analytics/util/FileUtils.java
+++ b/independent-projects/tools/analytics-common/src/main/java/io/quarkus/analytics/util/FileUtils.java
@@ -81,6 +81,12 @@ public static Optional read(Class clazz, Path path, MessageWriter log)
} catch (Exception e) {
log.warn("[Quarkus build analytics] Could not read {}", path.toString(), e);
return Optional.empty();
+ } catch (Throwable t) {
+ log.error("[Quarkus build analytics] Unexpected error reading class " + t.getClass().getName() +
+ " from path: " + path.toString() +
+ ". Got message: " + t.getMessage() +
+ ". Attempting to continue...");
+ return Optional.empty();
}
}
}