From e06e209479a1047f189e69b52dd536cfa73c70bd Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 15 May 2024 12:01:17 +0200 Subject: [PATCH 01/23] WebSockets Next: provide strategies to process unhandled failures - resolves #40648 - also add WebSocketConnection#closeReason() and WebSocketClientConnection#closeReason() --- .../asciidoc/websockets-next-reference.adoc | 6 +++ .../client/ClientMessageErrorEndpoint.java | 35 ++++++++++++++ .../test/client/ClientOpenErrorEndpoint.java | 37 ++++++++++++++ .../next/test/client/ServerEndpoint.java | 24 ++++++++++ ...dledMessageFailureDefaultStrategyTest.java | 47 ++++++++++++++++++ ...nhandledMessageFailureLogStrategyTest.java | 46 ++++++++++++++++++ ...handledOpenFailureDefaultStrategyTest.java | 46 ++++++++++++++++++ .../UnhandledOpenFailureLogStrategyTest.java | 47 ++++++++++++++++++ .../next/test/errors/EchoMessageError.java | 23 +++++++++ .../next/test/errors/EchoOpenError.java | 25 ++++++++++ ...dledMessageFailureDefaultStrategyTest.java | 46 ++++++++++++++++++ ...nhandledMessageFailureLogStrategyTest.java | 44 +++++++++++++++++ ...handledOpenFailureDefaultStrategyTest.java | 45 +++++++++++++++++ .../UnhandledOpenFailureLogStrategyTest.java | 43 +++++++++++++++++ .../websockets/next/test/utils/WSClient.java | 4 ++ .../quarkus/websockets/next/CloseReason.java | 2 + .../next/UnhandledFailureStrategy.java | 20 ++++++++ .../next/WebSocketClientConnection.java | 8 +++- .../websockets/next/WebSocketConnection.java | 8 +++- .../next/WebSocketsClientRuntimeConfig.java | 8 ++++ .../next/WebSocketsServerRuntimeConfig.java | 8 ++++ .../websockets/next/runtime/Endpoints.java | 48 ++++++++++++++----- .../next/runtime/WebSocketConnectionBase.java | 2 +- .../next/runtime/WebSocketConnectorImpl.java | 1 + .../next/runtime/WebSocketServerRecorder.java | 2 +- 25 files changed, 609 insertions(+), 16 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientMessageErrorEndpoint.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientOpenErrorEndpoint.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ServerEndpoint.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureDefaultStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledMessageFailureLogStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureDefaultStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/UnhandledOpenFailureLogStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoMessageError.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/EchoOpenError.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureDefaultStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledMessageFailureLogStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureDefaultStrategyTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/errors/UnhandledOpenFailureLogStrategyTest.java create mode 100644 extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UnhandledFailureStrategy.java diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 62039a09f8114..55203bc86b1a2 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -16,6 +16,8 @@ include::_attributes.adoc[] include::{includes}/extension-status.adoc[] +The `quarkus-websockets-next` extension provides a modern declarative API to define WebSocket server and client endpoints. + == The WebSocket protocol The _WebSocket_ protocol, documented in the https://datatracker.ietf.org/doc/html/rfc6455[RFC6455], establishes a standardized method for creating a bidirectional communication channel between a client and a server through a single TCP connection. @@ -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. 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/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/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..e262a9839bd44 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 actual 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..d8e1a3cd98551 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 actual 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/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..ce4d2c096628d 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,7 +205,8 @@ 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); } onClose.run(); if (timerId != null) { @@ -218,14 +226,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/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/WebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java index d6281e5da71f4..8b8781ccac2ed 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 @@ -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..35bdae2ca2206 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 @@ -102,7 +102,7 @@ public void handle(RoutingContext ctx) { LOG.debugf("Connection created: %s", connection); Endpoints.initialize(vertx, container, codecs, connection, ws, generatedEndpointClass, - config.autoPingInterval(), securitySupport, + config.autoPingInterval(), securitySupport, config.unhandledFailureStrategy(), () -> connectionManager.remove(generatedEndpointClass, connection)); }); } From 6c7307c6d8e79f4150bbfec35d5de9e80ab8ed16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 28 May 2024 11:56:28 +0200 Subject: [PATCH 02/23] WebSocket NEXT: automatically close connection when token expires (cherry picked from commit 514c42663b4701a1525007ca588983bc446d5364) --- .../asciidoc/websockets-next-reference.adoc | 2 + .../security/AuthenticationExpiredTest.java | 129 ++++++++++++++++++ .../websockets/next/runtime/Endpoints.java | 1 + .../next/runtime/SecuritySupport.java | 41 +++++- .../next/runtime/WebSocketServerRecorder.java | 9 +- 5 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/security/AuthenticationExpiredTest.java diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 55203bc86b1a2..6eb75e98c601e 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -641,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/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/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 ce4d2c096628d..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 @@ -208,6 +208,7 @@ public void handle(Void event) { handleFailure(unhandledFailureStrategy, r.cause(), "Unable to complete @OnClose callback", connection); } + securitySupport.onClose(); onClose.run(); if (timerId != null) { vertx.cancelTimer(timerId); 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/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index 35bdae2ca2206..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,6 +99,8 @@ 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.unhandledFailureStrategy(), () -> connectionManager.remove(generatedEndpointClass, connection)); @@ -109,14 +109,15 @@ public void handle(RoutingContext ctx) { }; } - 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; From 0c6dcbc9ffde0bf301ca5269384dc7d10ac295ec Mon Sep 17 00:00:00 2001 From: Vincent Sevel Date: Wed, 5 Jun 2024 09:30:03 +0200 Subject: [PATCH 03/23] Kafka commit strategy: clarify risk of message loss on latest (cherry picked from commit 869f1e768331959ebd52dda5953898cdfece3871) --- docs/src/main/asciidoc/kafka.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index ff532b0c198ab..04b2d9cbd0041 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -361,7 +361,9 @@ If `checkpoint.unsynced-state-max-age.ms` is set to less than or equal to 0, it For more information, see <> - `latest` commits the record offset received by the Kafka consumer as soon as the associated message is acknowledged (if the offset is higher than the previously committed offset). -This strategy provides at-least-once delivery if the channel processes the message without performing any asynchronous processing. +This strategy provides at-least-once delivery if the channel processes the message without performing any asynchronous processing. Specifically, the offset of the most recent acknowledged +message will always be committed, even if older messages have not finished being processed. In case of an incident such as a crash, processing would restart after the last commit, leading +to older messages never being successfully and fully processed, which would appear as message loss. This strategy should not be used in high load environment, as offset commit is expensive. However, it reduces the risk of duplicates. - `ignore` performs no commit. This strategy is the default strategy when the consumer is explicitly configured with `enable.auto.commit` to true. From 3f00f74ac0da982ddee4b2d180a1ef47e30780b6 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 5 Jun 2024 12:32:56 +0100 Subject: [PATCH 04/23] Replace 'bare mortal' with a more idiomatic English expression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a "what do you think?" PR, not a "you must do this" PR. The expression "for bare mortal" isn't very idiomatic in English. I did a [quick google](https://www.google.com/search?client=firefox-b-e&q=%22bare+mortal%22#ip=1) to double-check, and the top hits were all this page, and then I found one other use: "how can a bare mortal like me ...". Most other uses were a Thomas Pynchon quote, "the bare mortal world that is our home”, where the phrase has a different meaning. What I'd say is "mere mortals" instead. It's [widely used](https://www.google.com/search?q=for+mere+mortals) to mean "accessible". Now, because it's so widely used, it's less distinctive, and it lacks some of the Franglish charm of "bare mortal." In interface design, we should always go for the principle of least surprise, but I'm less sure about what's best in language use. (cherry picked from commit 7b1fe991ed87446b0c46d0f9b308a2612d4f1232) --- docs/src/main/asciidoc/mutiny-primer.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/mutiny-primer.adoc b/docs/src/main/asciidoc/mutiny-primer.adoc index 81d5ec2905b26..8f4942ce938b9 100644 --- a/docs/src/main/asciidoc/mutiny-primer.adoc +++ b/docs/src/main/asciidoc/mutiny-primer.adoc @@ -3,7 +3,7 @@ This guide is maintained in the main Quarkus repository and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// -= Mutiny - Async for bare mortal += Mutiny - Async for mere mortals include::_attributes.adoc[] :categories: reactive :topics: mutiny,reactive From 1f4d11f0096f0124a36004b4ac60efee5464c9c7 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 6 Jun 2024 10:38:45 +0300 Subject: [PATCH 05/23] Add docs note about writing extension with Java and Maven Relates to: #40999 (cherry picked from commit 5273ed005bbb2a48252d1f00e70640478cbb9592) --- docs/src/main/asciidoc/building-my-first-extension.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/main/asciidoc/building-my-first-extension.adoc b/docs/src/main/asciidoc/building-my-first-extension.adoc index 39de8983d4975..fcc64b2a35b78 100644 --- a/docs/src/main/asciidoc/building-my-first-extension.adoc +++ b/docs/src/main/asciidoc/building-my-first-extension.adoc @@ -29,6 +29,12 @@ Keep in mind it's not representative of the power of moving things to build time :prerequisites-no-graalvm: include::{includes}/prerequisites.adoc[] +[CAUTION] +==== +Writing extension with any other than Java and Maven has **not** been tested by the Quarkus team so your mileage may vary +if you stray off this path +==== + == Basic Concepts First things first, we will need to start with some basic concepts. From 517d01990de88efd8f31bce9bc25173cbdfed31e Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 6 Jun 2024 09:01:59 +0200 Subject: [PATCH 06/23] Make sure quarkusXXXCompileOnlyConfiguration extends from platform configuration (cherry picked from commit 36f5043f35758f58eddd780f36a3c83c42b9e9ce) --- .../dependency/ApplicationDeploymentClasspathBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index f3cd59c9d8f65..ffc0564ff65e0 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -234,7 +234,8 @@ private void setUpDeploymentConfiguration() { private void setUpCompileOnlyConfiguration() { if (!project.getConfigurations().getNames().contains(compileOnlyConfigurationName)) { project.getConfigurations().register(compileOnlyConfigurationName, config -> { - config.extendsFrom(project.getConfigurations().getByName(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME)); + config.extendsFrom(project.getConfigurations().getByName(platformConfigurationName), + project.getConfigurations().getByName(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME)); config.shouldResolveConsistentlyWith(getDeploymentConfiguration()); config.setCanBeConsumed(false); }); From faa319d3e6e4a4b3494ddb9399d90a41fa0f0fdf Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 5 Jun 2024 15:48:04 +0200 Subject: [PATCH 07/23] Config doc - Don't enforce the height It doesn't look very well. Let's just use the natural padding we have, it's far better. (cherry picked from commit e816a3cd3c54cbb76a609805467e7cd880b57eca) --- docs/src/main/asciidoc/stylesheet/config.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/src/main/asciidoc/stylesheet/config.css b/docs/src/main/asciidoc/stylesheet/config.css index f7b70fabae1ad..53ca077e66cf5 100644 --- a/docs/src/main/asciidoc/stylesheet/config.css +++ b/docs/src/main/asciidoc/stylesheet/config.css @@ -22,15 +22,11 @@ table.configuration-reference.tableblock > tbody > tr:nth-child(even) > th { table.configuration-reference.tableblock > tbody > tr > th { background-color: transparent; font-size: 1rem; - height: 60px; border: none; border-bottom: 1px solid #4695eb; vertical-align: bottom; } -table.configuration-reference.tableblock > tbody > tr:first-child > th { - height: 30px; -} table.configuration-reference.tableblock > tbody > tr > th:nth-child(2), table.configuration-reference.tableblock > tbody > tr > th:nth-child(3), table.configuration-reference.tableblock > tbody > tr > td:nth-child(2), From 91997dd6d19e9959cf32090bc664384bddbd7e64 Mon Sep 17 00:00:00 2001 From: David Andlinger Date: Thu, 6 Jun 2024 12:13:21 +0200 Subject: [PATCH 08/23] added missing annotation parameter name (cherry picked from commit 5286ec9f9cfc763dc03066890128ed5cdd0bb4f2) --- docs/src/main/asciidoc/websockets-next-reference.adoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 6eb75e98c601e..01c6ad2e00d8f 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -139,13 +139,13 @@ Meanwhile, the `consumeNested` method within the nested class can access both `v [source, java] ---- -@WebSocket("/ws/v{version}") +@WebSocket(path = "/ws/v{version}") public class MyPrimaryWebSocket { @OnTextMessage void consumePrimary(String s) { ... } - @WebSocket("/products/{id}") + @WebSocket(path = "/products/{id}") public static class MyNestedWebSocket { @OnTextMessage @@ -163,12 +163,12 @@ However, developers can specify alternative scopes to suit their specific requir [source,java] ---- -@WebSocket("/ws") +@WebSocket(path = "/ws") public class MyWebSocket { // Singleton scoped bean } -@WebSocket("/ws") +@WebSocket(path = "/ws") @ApplicationScoped public class MyRequestScopedWebSocket { // Application scoped. @@ -420,7 +420,7 @@ Methods annotated with `@OnOpen` can utilize server-side streaming by returning [source, java] ---- -@WebSocket("/foo") +@WebSocket(path = "/foo") @OnOpen public Multi streaming() { return Multi.createFrom().ticks().every(Duration.ofSecond(1)) From ff9218887c09e203f89588f13a6b1e9be578f417 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Thu, 6 Jun 2024 15:16:14 +0300 Subject: [PATCH 09/23] Fix invalid webjar to show 404 Signed-off-by: Phillip Kruger (cherry picked from commit ad8b37d5b492c99d01aa212480c419f55715394a) --- .../runtime/WebDependencyLocatorRecorder.java | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) 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 { From 807f45f11aa0f6ee15f8ce99caf02ce62f044201 Mon Sep 17 00:00:00 2001 From: brunobat Date: Wed, 15 May 2024 11:37:18 +0100 Subject: [PATCH 10/23] Prevent abort because of a throwable (cherry picked from commit 5ed60b905c089d1ebf809d8da2eacbd9da015702) --- .../src/main/java/io/quarkus/analytics/util/FileUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) 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(); } } } From 2a82618929c0d6f2cadcb1dd0dbebdf686230cb4 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 5 Jun 2024 16:53:10 +0200 Subject: [PATCH 11/23] WebSockets Next client: encode path param values automatically - fixes #40981 (cherry picked from commit 76af842d2865214eb419bedb7e1f8f509b9a544b) --- .../websockets/next/test/client/ClientEndpointTest.java | 9 +++++---- .../quarkus/websockets/next/BasicWebSocketConnector.java | 4 ++++ .../websockets/next/WebSocketClientConnection.java | 2 +- .../io/quarkus/websockets/next/WebSocketConnection.java | 2 +- .../io/quarkus/websockets/next/WebSocketConnector.java | 4 ++++ .../websockets/next/runtime/WebSocketConnectorBase.java | 4 +++- .../websockets/next/runtime/WebSocketConnectorImpl.java | 2 +- 7 files changed, 19 insertions(+), 8 deletions(-) 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/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/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index e262a9839bd44..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 {@code null} + * @return the value of the path parameter or {@code null} * @see WebSocketClient#path() */ String pathParam(String name); 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 d8e1a3cd98551..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 {@code null} + * @return the decoded value of the path parameter or {@code null} * @see WebSocket#path() */ String pathParam(String name); 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/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 8b8781ccac2ed..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()); From 5354da4bc000198f763169e703f955dd38d19d9f Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Fri, 31 May 2024 11:18:21 +0100 Subject: [PATCH 12/23] Improve OIDC warning when a session encryption key is generated (cherry picked from commit 8cf86637d3d2f8723a1b576f80dd09f3cbe01140) --- .../src/main/java/io/quarkus/oidc/OidcTenantConfig.java | 5 ++++- .../java/io/quarkus/oidc/runtime/TenantConfigContext.java | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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); From f15e52a2908510f19dd326b46bc79ad57b877db0 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Thu, 6 Jun 2024 11:53:11 +0300 Subject: [PATCH 13/23] Fix open-in-ide Signed-off-by: Phillip Kruger (cherry picked from commit bb7760c27c6c11c9a73a4bef9a534c1626cad10e) --- .../src/main/resources/dev-ui/qui/qui-ide-link.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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"; } } From fcd95791c4a99966a5df97ae91d7a7176d00b956 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 7 Jun 2024 10:43:10 +0200 Subject: [PATCH 14/23] Qute: fix regression for optimized generated value resolvers - fix regression introduced in https://github.com/quarkusio/quarkus/pull/33984 - if there are multiple type-safe templates with the same parameter declaration then _no_ or _incomplete_ value resolver may be generated (cherry picked from commit ba3f62a4debf2a3cf0540b4a77be8a09fca1c21f) --- .../qute/deployment/QuteProcessor.java | 2 +- .../ImplicitValueResolversTest.java | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/generatedresolvers/ImplicitValueResolversTest.java 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); + } + +} From 63cd6dc3726887b5586a60a4ef3e88a1cbd9ac63 Mon Sep 17 00:00:00 2001 From: Christian Navolskyi <11958454+ChristianNavolskyi@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:22:35 +0200 Subject: [PATCH 15/23] Fix callouts (cherry picked from commit ccc374a6238ca9ba46ac260e116dc37bd327eeeb) --- docs/src/main/asciidoc/deploying-to-kubernetes.adoc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 8dc1c5784ed1b..a8d7bd8ef94c5 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -1462,8 +1462,13 @@ spec: protocol: "TCP" serviceAccount: "kubernetes-quickstart" ---- +<1> Retained were the provided replicas, +<2> labels and +<3> environment variables. +<4> However, the image and +<5> the container port were modified. -The provided replicas <1>, labels <2> and environment variables <3> were retained. However, the image <4> and container port <5> were modified. Moreover, the default annotations have been added. +Moreover, the default annotations have been added. [NOTE] ==== From 316a9bb4de6c4b3d362618433ebe090441b07639 Mon Sep 17 00:00:00 2001 From: Christian Navolskyi <11958454+ChristianNavolskyi@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:25:16 +0200 Subject: [PATCH 16/23] Rephrase (cherry picked from commit 9a410424091400f983563b95590f681cf973d3e9) --- docs/src/main/asciidoc/deploying-to-kubernetes.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index a8d7bd8ef94c5..3d68a1fae1803 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -1462,9 +1462,9 @@ spec: protocol: "TCP" serviceAccount: "kubernetes-quickstart" ---- -<1> Retained were the provided replicas, +<1> The provided replicas, <2> labels and -<3> environment variables. +<3> environment variables were retained. <4> However, the image and <5> the container port were modified. From 8140a9a24a6983a520a089822a1597a18e64c9ab Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 7 Jun 2024 09:04:40 +0200 Subject: [PATCH 17/23] Make sure we transmit the actual debug port to next dev mode run When starting dev mode, the debug port might not be free and we might use a random one. When restarting dev mode (for instance when you adjust the pom.xml), we should try to reuse this port instead of defaulting to the initially non free one. Also the port being free is tested when we build `DevModeRunner`, so we need to make sure we stop the old runner before build()ing the new instance (and not just before we start the new one). Fixes #40848 (cherry picked from commit 2233494adc057c6b74140209677a72cb23a321cf) --- .../dev/QuarkusDevModeLauncher.java | 22 +++++++------------ .../main/java/io/quarkus/maven/DevMojo.java | 17 ++++++++------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java index 580506a736255..d5e2b05b68da4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java @@ -85,12 +85,6 @@ public B debug(String debug) { return (B) this; } - @SuppressWarnings("unchecked") - public B debugPortOk(Boolean debugPortOk) { - QuarkusDevModeLauncher.this.debugPortOk = debugPortOk; - return (B) this; - } - @SuppressWarnings("unchecked") public B suspend(String suspend) { QuarkusDevModeLauncher.this.suspend = suspend; @@ -303,10 +297,10 @@ public R build() throws Exception { private List args = new ArrayList<>(0); private String debug; - private Boolean debugPortOk; private String suspend; private String debugHost = "localhost"; private String debugPort = "5005"; + private String actualDebugPort; private File projectDir; private File buildDir; private File outputDir; @@ -390,12 +384,13 @@ protected void prepare() throws Exception { if (debug != null && debug.equalsIgnoreCase("client")) { args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=n,suspend=" + suspend); + actualDebugPort = String.valueOf(port); } else if (debug == null || !debug.equalsIgnoreCase("false")) { // if the debug port is used, we want to make an effort to pick another one // if we can't find an open port, we don't fail the process launch, we just don't enable debugging // Furthermore, we don't check this on restarts, as the previous process is still running boolean warnAboutChange = false; - if (debugPortOk == null) { + if (actualDebugPort == null) { int tries = 0; while (true) { boolean isPortUsed; @@ -408,20 +403,19 @@ protected void prepare() throws Exception { isPortUsed = false; } if (!isPortUsed) { - debugPortOk = true; + actualDebugPort = String.valueOf(port); break; } if (++tries >= 5) { - debugPortOk = false; break; } else { port = getRandomPort(); } } } - if (debugPortOk) { + if (actualDebugPort != null) { if (warnAboutChange) { - warn("Changed debug port to " + port + " because of a port conflict"); + warn("Changed debug port to " + actualDebugPort + " because of a port conflict"); } args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=y,suspend=" + suspend); @@ -547,8 +541,8 @@ public List args() { return args; } - public Boolean getDebugPortOk() { - return debugPortOk; + public String getActualDebugPort() { + return actualDebugPort; } protected abstract boolean isDebugEnabled(); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 409700ecb15ef..e8480da05aca4 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -478,15 +478,19 @@ public void close() throws IOException { } if (!changed.isEmpty()) { getLog().info("Changes detected to " + changed + ", restarting dev mode"); + + // stop the runner before we build the new one as the debug port being free + // is tested when building the runner + runner.stop(); + final DevModeRunner newRunner; try { bootstrapId = handleAutoCompile(); - newRunner = new DevModeRunner(runner.launcher.getDebugPortOk(), bootstrapId); + newRunner = new DevModeRunner(runner.launcher.getActualDebugPort(), bootstrapId); } catch (Exception e) { getLog().info("Could not load changed pom.xml file, changes not applied", e); continue; } - runner.stop(); newRunner.run(); runner = newRunner; } @@ -1171,8 +1175,8 @@ private DevModeRunner(String bootstrapId) throws Exception { launcher = newLauncher(null, bootstrapId); } - private DevModeRunner(Boolean debugPortOk, String bootstrapId) throws Exception { - launcher = newLauncher(debugPortOk, bootstrapId); + private DevModeRunner(String actualDebugPort, String bootstrapId) throws Exception { + launcher = newLauncher(actualDebugPort, bootstrapId); } Collection pomFiles() { @@ -1226,7 +1230,7 @@ void stop() throws InterruptedException { } } - private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrapId) throws Exception { + private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootstrapId) throws Exception { String java = null; // See if a toolchain is configured if (toolchainManager != null) { @@ -1244,8 +1248,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap .suspend(suspend) .debug(debug) .debugHost(debugHost) - .debugPort(debugPort) - .debugPortOk(debugPortOk) + .debugPort(actualDebugPort) .deleteDevJar(deleteDevJar); setJvmArgs(builder); From f1225b0cd921f093cc6cec12cb2fdadece20ae3a Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 6 Jun 2024 19:53:48 +0200 Subject: [PATCH 18/23] Fix log warning when application port is already used (cherry picked from commit 7bd6ce31788e4aa300f3434682186d23802572f2) --- .../java/io/quarkus/runtime/ApplicationLifecycleManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index a410463dba571..aacaab97261d0 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -183,7 +183,7 @@ public static void run(Application application, Class'."); From a6b3e24483938e33a8a13030929f3d5f5b9745ae Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Fri, 7 Jun 2024 16:20:50 +0200 Subject: [PATCH 19/23] Fix encoding of '?' in query parameter values by Encode.encodeQueryParam(..) Previously `?` in query parameter values where encoded as is which caused invalid URL values. We now replace `?` characters in query parameter values with `%3F`. Fixes #41060 Signed-off-by: Thomas Darimont (cherry picked from commit f244de0d3c9a2b7c0a536f3d48da2e6c2a3b80f5) --- .../org/jboss/resteasy/reactive/common/util/Encode.java | 2 ++ .../jboss/resteasy/reactive/common/util/EncodeTest.java | 7 +++++++ 2 files changed, 9 insertions(+) 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)); + } } From 4c0b199742cbd041f9cb6ba42225dec73d83ba82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Fri, 7 Jun 2024 12:01:02 +0200 Subject: [PATCH 20/23] Docs: clarify named queries for Panache Fixes #40987 (cherry picked from commit bdd407ca773abbecfc64cb3529ca57b26736f872) --- docs/src/main/asciidoc/hibernate-orm-panache.adoc | 4 ++-- docs/src/main/asciidoc/hibernate-reactive-panache.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 9726e5eafe168..9be07480f99a2 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -792,8 +792,8 @@ public class Person extends PanacheEntity { [WARNING] ==== -Named queries can only be defined inside your Jakarta Persistence entity classes (being the Panache entity class, or the repository parameterized type), -or on one of its super classes. +Named queries can only be defined inside your Jakarta Persistence entity classes, +or on one of their super classes. ==== === Query parameters diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc index 8b6d16a9ad2f4..3b542fc98833d 100644 --- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc @@ -553,8 +553,8 @@ public class Person extends PanacheEntity { [WARNING] ==== -Named queries can only be defined inside your Jakarta Persistence entity classes (being the Panache entity class, or the repository parameterized type), -or on one of its super classes. +Named queries can only be defined inside your Jakarta Persistence entity classes, +or on one of their super classes. ==== === Query parameters From a16cb8a01e320eaf225a135e7a26fc88fe0da119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Fri, 7 Jun 2024 17:06:34 +0200 Subject: [PATCH 21/23] Fix GZIP max input in native mode (cherry picked from commit 5d0a210b925880bc0436dda4a15675262a73f205) --- .../deployment/ResteasyCommonProcessor.java | 32 ++--------------- .../common/runtime/ResteasyCommonConfig.java | 36 +++++++++++++++++++ .../ResteasyServerCommonProcessor.java | 6 ++-- .../ResteasyConfigurationMPConfig.java | 5 +++ 4 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/ResteasyCommonConfig.java 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()) { From 28a78a787e02b214e43fa0b517f835c012d4608f Mon Sep 17 00:00:00 2001 From: Jerome Prinet Date: Mon, 10 Jun 2024 09:12:41 +0200 Subject: [PATCH 22/23] Bump up quarkus-build-caching-extension to 1.2 (cherry picked from commit fd55e782c52c2c05985d6839848c4a4d94b05575) --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index fb133d37008db..8fbd5380b8677 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -12,7 +12,7 @@ com.gradle quarkus-build-caching-extension - 1.1 + 1.2 io.quarkus.develocity From acb6fa7a821ab5012105fa1c40001b55c006be15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 10 Jun 2024 13:21:53 +0200 Subject: [PATCH 23/23] Update Agraol exception message in docs Following #40779 / agroal/agroal@6c62f82, the exception message changed and is a bit more explicit. (cherry picked from commit 2d99f1db3ac00af6b42da704a302d7704da34b0d) --- docs/src/main/asciidoc/datasource.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 5924e3b1b9b03..3b8fcd73aa948 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -531,7 +531,7 @@ would result in an exception similar to this: Caused by: java.sql.SQLException: Exception in association of connection to existing transaction at io.agroal.narayana.NarayanaTransactionIntegration.associate(NarayanaTransactionIntegration.java:130) ... -Caused by: java.sql.SQLException: Unable to enlist connection to existing transaction +Caused by: java.sql.SQLException: Failed to enlist. Check if a connection from another datasource is already enlisted to the same transaction at io.agroal.narayana.NarayanaTransactionIntegration.associate(NarayanaTransactionIntegration.java:121) ... ----