unsupported) {
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentExplicitNameTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentExplicitNameTest.java
new file mode 100644
index 0000000000000..aa68c510d3eb4
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentExplicitNameTest.java
@@ -0,0 +1,50 @@
+package io.quarkus.websockets.next.test.args;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URI;
+
+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.OnTextMessage;
+import io.quarkus.websockets.next.PathParam;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class PathParamArgumentExplicitNameTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(MontyEcho.class, WSClient.class);
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo/monty")
+ URI testUri;
+
+ @Test
+ void testArgument() {
+ WSClient client = WSClient.create(vertx).connect(testUri);
+ assertEquals("python:monty", client.sendAndAwaitReply("python").toString());
+ }
+
+ @WebSocket(path = "/echo/{grail}")
+ public static class MontyEcho {
+
+ @OnTextMessage
+ String process(@PathParam("grail") String life, String message) throws InterruptedException {
+ return message + ":" + life;
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidNameTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidNameTest.java
new file mode 100644
index 0000000000000..f23f0343cdf23
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidNameTest.java
@@ -0,0 +1,37 @@
+package io.quarkus.websockets.next.test.args;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.PathParam;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketServerException;
+
+public class PathParamArgumentInvalidNameTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(MontyEcho.class);
+ }).setExpectedException(WebSocketServerException.class);
+
+ @Test
+ void testInvalidArgument() {
+ fail();
+ }
+
+ @WebSocket(path = "/echo/{grail}")
+ public static class MontyEcho {
+
+ @OnTextMessage
+ String process(@PathParam String life, String message) throws InterruptedException {
+ return message + ":" + life;
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidTypeTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidTypeTest.java
new file mode 100644
index 0000000000000..31097c8bf7180
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentInvalidTypeTest.java
@@ -0,0 +1,37 @@
+package io.quarkus.websockets.next.test.args;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.PathParam;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketServerException;
+
+public class PathParamArgumentInvalidTypeTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(MontyEcho.class);
+ }).setExpectedException(WebSocketServerException.class);
+
+ @Test
+ void testInvalidArgument() {
+ fail();
+ }
+
+ @WebSocket(path = "/echo/{grail}")
+ public static class MontyEcho {
+
+ @OnTextMessage
+ String process(@PathParam Double grail, String message) throws InterruptedException {
+ return message + ":" + grail;
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentTest.java
new file mode 100644
index 0000000000000..ff97071f3042d
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamArgumentTest.java
@@ -0,0 +1,50 @@
+package io.quarkus.websockets.next.test.args;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URI;
+
+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.OnTextMessage;
+import io.quarkus.websockets.next.PathParam;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+
+public class PathParamArgumentTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(MontyEcho.class, WSClient.class);
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo/monty")
+ URI testUri;
+
+ @Test
+ void testArgument() {
+ WSClient client = WSClient.create(vertx).connect(testUri);
+ assertEquals("python:monty", client.sendAndAwaitReply("python").toString());
+ }
+
+ @WebSocket(path = "/echo/{grail}")
+ public static class MontyEcho {
+
+ @OnTextMessage
+ String process(@PathParam String grail, String message) throws InterruptedException {
+ return message + ":" + grail;
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamConnectionArgumentTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamConnectionArgumentTest.java
new file mode 100644
index 0000000000000..5a5e0843a9e64
--- /dev/null
+++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/args/PathParamConnectionArgumentTest.java
@@ -0,0 +1,54 @@
+package io.quarkus.websockets.next.test.args;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URI;
+
+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.OnTextMessage;
+import io.quarkus.websockets.next.PathParam;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import io.quarkus.websockets.next.test.utils.WSClient;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.WebSocketConnectOptions;
+
+public class PathParamConnectionArgumentTest {
+
+ @RegisterExtension
+ public static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .withApplicationRoot(root -> {
+ root.addClasses(MontyEcho.class, WSClient.class);
+ });
+
+ @Inject
+ Vertx vertx;
+
+ @TestHTTPResource("echo/monty/and/foo")
+ URI testUri;
+
+ @Test
+ void testArguments() {
+ String header = "fool";
+ WSClient client = WSClient.create(vertx).connect(new WebSocketConnectOptions().addHeader("X-Test", header), testUri);
+ assertEquals("foo:python:monty:fool", client.sendAndAwaitReply("python").toString());
+ }
+
+ @WebSocket(path = "/echo/{grail}/and/{life}")
+ public static class MontyEcho {
+
+ @OnTextMessage
+ String process(@PathParam String life, @PathParam String grail, String message, WebSocketConnection connection)
+ throws InterruptedException {
+ return life + ":" + message + ":" + grail + ":" + connection.handshakeRequest().header("X-Test");
+ }
+
+ }
+
+}
diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/PathParam.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/PathParam.java
new file mode 100644
index 0000000000000..353e965e194bf
--- /dev/null
+++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/PathParam.java
@@ -0,0 +1,34 @@
+package io.quarkus.websockets.next;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Identifies an endpoint callback method parameter that should be injected with a value returned from
+ * {@link WebSocketConnection#pathParam(String)}.
+ *
+ * The parameter type must be {@link String} and the name must be defined in the relevant endpoint path, otherwise
+ * the build fails.
+ *
+ * @see WebSocketConnection#pathParam(String)
+ * @see WebSocket
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface PathParam {
+
+ /**
+ * Constant value for {@link #value()} indicating that the annotated element's name should be used as-is.
+ */
+ String ELEMENT_NAME = "<>";
+
+ /**
+ * The name of the parameter. By default, the element's name is used as-is.
+ *
+ * @return the name of the parameter
+ */
+ String value() default ELEMENT_NAME;
+
+}
diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
index 711ce1e654874..7bf534db20db4 100644
--- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
+++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java
@@ -12,11 +12,15 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import io.quarkus.vertx.core.runtime.VertxBufferImpl;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.vertx.UniHelper;
import io.vertx.core.buffer.Buffer;
+import io.vertx.core.buffer.impl.BufferImpl;
import io.vertx.core.http.ServerWebSocket;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
class WebSocketConnectionImpl implements WebSocketConnection {
@@ -71,7 +75,17 @@ public Uni sendBinary(Buffer message) {
@Override
public Uni sendText(M message) {
- return UniHelper.toUni(webSocket.writeTextMessage(codecs.textEncode(message, null).toString()));
+ String text;
+ // Use the same conversion rules as defined for the OnTextMessage
+ if (message instanceof JsonObject || message instanceof JsonArray || message instanceof BufferImpl
+ || message instanceof VertxBufferImpl) {
+ text = message.toString();
+ } else if (message.getClass().isArray() && message.getClass().arrayType().equals(byte.class)) {
+ text = Buffer.buffer((byte[]) message).toString();
+ } else {
+ text = codecs.textEncode(message, null);
+ }
+ return sendText(text);
}
@Override
diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java
index fdb032ae57ca4..8d9620f09c10a 100644
--- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java
+++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java
@@ -27,7 +27,8 @@ public abstract class WebSocketEndpointBase implements WebSocketEndpoint {
private static final Logger LOG = Logger.getLogger(WebSocketEndpointBase.class);
- protected final WebSocketConnection connection;
+ // Keep this field public - there's a problem with ConnectionArgumentProvider reading the protected field in the test mode
+ public final WebSocketConnection connection;
protected final Codecs codecs;
diff --git a/integration-tests/jpa-postgresql/src/test/resources/image-metrics/23.1/image-metrics.properties b/integration-tests/jpa-postgresql/src/test/resources/image-metrics/23.1/image-metrics.properties
index 6315f449e1bf9..507eb65104e09 100644
--- a/integration-tests/jpa-postgresql/src/test/resources/image-metrics/23.1/image-metrics.properties
+++ b/integration-tests/jpa-postgresql/src/test/resources/image-metrics/23.1/image-metrics.properties
@@ -7,7 +7,7 @@ analysis_results.methods.reachable=96465
analysis_results.methods.reachable.tolerance=3
analysis_results.fields.reachable=27025
analysis_results.fields.reachable.tolerance=3
-analysis_results.types.reflection=6048
+analysis_results.types.reflection=6100
analysis_results.types.reflection.tolerance=3
analysis_results.methods.reflection=4707
analysis_results.methods.reflection.tolerance=3