From 31a5136b95616bb596826fa25966a80621fde2ee Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:22:56 -0400 Subject: [PATCH 1/2] refactor: replace the WebSocketClient builder with setters Follow the Restlet API conventions used by the RPC extensions: constructors, getter and void setter pairs, and a start/stop lifecycle instead of a fluent builder. - Remove WebSocketClient.Builder, builder() and create(). Settings are now configured with setters on a new WebSocketClient(). - Add start(), stop() and isStarted(). close() stops the client, which can be started again. connect() starts a stopped client, like Restlet.handle(). - Reject setting changes while the client is started. The extension is unreleased, so the builder is removed rather than deprecated. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com> --- .../ext/websocket/WebSocketClient.java | 355 +++++++++++------- .../WebSocketCompressionTestCase.java | 16 +- .../websocket/WebSocketSessionTestCase.java | 59 ++- .../ext/websocket/WebSocketTestCase.java | 3 +- 4 files changed, 265 insertions(+), 168 deletions(-) diff --git a/org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/WebSocketClient.java b/org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/WebSocketClient.java index ab3ccc1e63..a8f21476d8 100644 --- a/org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/WebSocketClient.java +++ b/org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/WebSocketClient.java @@ -21,198 +21,263 @@ /** * Opens WebSocket client sessions ("ws" and "wss" URIs) over HTTP/1.1 upgrades. Each session uses - * its own connection. Close the client to release its I/O threads. + * its own connection. + * + *

Configure the client before starting it. {@link #connect(String)} starts a stopped client, + * like {@link org.restlet.Restlet#handle(org.restlet.Request)}. Stop or close the client to release + * its I/O threads; it can be started again afterwards. */ public final class WebSocketClient implements AutoCloseable { - private final NettyWebSocketConnector connector; + private SSLContext sslContext; - private WebSocketClient(Builder builder) throws IOException { - this.connector = - new NettyWebSocketConnector( - builder.sslContext, - builder.connectTimeout.toMillis(), - builder.idleTimeout.toMillis(), - builder.maxMessageSize, - (builder.maxFrameSize > 0) ? builder.maxFrameSize : builder.maxMessageSize, - builder.compression, - builder.hostnameVerification, - Logger.getLogger(WebSocketClient.class.getName())); - } + private Duration connectTimeout = Duration.ofSeconds(15); + + private Duration idleTimeout = Duration.ofSeconds(30); + + private int maxMessageSize = 65536; + + private int maxFrameSize; + + private boolean compression = true; + + private boolean hostnameVerification = true; + + private NettyWebSocketConnector connector; + + /** Constructor using the default settings. */ + public WebSocketClient() {} /** - * Creates a client with the default settings. + * Returns the SSL context used for "wss" URIs. * - * @return The client. - * @throws IOException If the client cannot be created. + * @return The SSL context, or null for the JVM default context. */ - public static WebSocketClient create() throws IOException { - return builder().build(); + public synchronized SSLContext getSslContext() { + return sslContext; } /** - * Returns a builder. + * Sets the SSL context used for "wss" URIs. Defaults to the JVM default context. * - * @return The builder. + * @param sslContext The SSL context. */ - public static Builder builder() { - return new Builder(); + public synchronized void setSslContext(SSLContext sslContext) { + requireStopped(); + this.sslContext = sslContext; } /** - * Opens a session, blocking until the handshake completes. + * Returns the connect and handshake timeout. * - * @param uri The "ws" or "wss" URI. - * @return The open session. - * @throws IOException If the connection or the handshake fails. + * @return The timeout; zero for none. */ - public WebSocketSession connect(String uri) throws IOException { - return connect(URI.create(uri), List.of(), Map.of()); + public synchronized Duration getConnectTimeout() { + return connectTimeout; } /** - * Opens a session, blocking until the handshake completes. + * Sets the connect and handshake timeout. Defaults to 15 seconds; zero for none. * - * @param uri The "ws" or "wss" URI. - * @param subprotocols The subprotocols to offer, in preference order. - * @param headers Additional handshake request headers, for example "Authorization". - * @return The open session. - * @throws IOException If the connection or the handshake fails. + * @param connectTimeout The timeout. */ - public WebSocketSession connect(URI uri, List subprotocols, Map headers) - throws IOException { - return connector.connect( - Objects.requireNonNull(uri, "uri"), - List.copyOf(subprotocols), - new LinkedHashMap<>(headers)); + public synchronized void setConnectTimeout(Duration connectTimeout) { + requireStopped(); + this.connectTimeout = requireNonNegative(connectTimeout, "connectTimeout"); } - @Override - public void close() { - connector.close(); + /** + * Returns the idle time after which the server is pinged. + * + * @return The idle timeout; zero when disabled. + */ + public synchronized Duration getIdleTimeout() { + return idleTimeout; } - /** Builds a {@link WebSocketClient}. */ - public static final class Builder { - - private SSLContext sslContext; + /** + * Sets the idle time after which the server is pinged, the connection being closed if it stays + * idle as long again. Defaults to 30 seconds; zero to disable. + * + * @param idleTimeout The idle timeout. + */ + public synchronized void setIdleTimeout(Duration idleTimeout) { + requireStopped(); + this.idleTimeout = requireNonNegative(idleTimeout, "idleTimeout"); + } - private Duration connectTimeout = Duration.ofSeconds(15); + /** + * Returns the maximum size in bytes of a received message. + * + * @return The maximum message size. + */ + public synchronized int getMaxMessageSize() { + return maxMessageSize; + } - private Duration idleTimeout = Duration.ofSeconds(30); + /** + * Sets the maximum size in bytes of a received message. Defaults to 65536. + * + * @param maxMessageSize The maximum message size. + */ + public synchronized void setMaxMessageSize(int maxMessageSize) { + requireStopped(); + if (maxMessageSize <= 0) { + throw new IllegalArgumentException("maxMessageSize must be positive"); + } + this.maxMessageSize = maxMessageSize; + } - private int maxMessageSize = 65536; + /** + * Returns the maximum size in bytes of a received frame. + * + * @return The maximum frame size, the maximum message size when not set. + */ + public synchronized int getMaxFrameSize() { + return (maxFrameSize > 0) ? maxFrameSize : maxMessageSize; + } - private int maxFrameSize; + /** + * Sets the maximum size in bytes of a received frame. A message may be fragmented in several + * frames. Defaults to the maximum message size, and is capped by it. + * + * @param maxFrameSize The maximum frame size. + */ + public synchronized void setMaxFrameSize(int maxFrameSize) { + requireStopped(); + if (maxFrameSize <= 0) { + throw new IllegalArgumentException("maxFrameSize must be positive"); + } + this.maxFrameSize = maxFrameSize; + } - private boolean compression = true; + /** + * Indicates whether "permessage-deflate" compression (RFC 7692) is offered to the server. + * + * @return True if compression is offered. + */ + public synchronized boolean isCompression() { + return compression; + } - private boolean hostnameVerification = true; + /** + * Sets whether "permessage-deflate" compression (RFC 7692) is offered to the server. Defaults + * to true; it is used only if the server accepts it. + * + * @param compression True to offer compression. + */ + public synchronized void setCompression(boolean compression) { + requireStopped(); + this.compression = compression; + } - private Builder() {} + /** + * Indicates whether the server host name is verified for "wss" URIs. + * + * @return True if the host name is verified. + */ + public synchronized boolean isHostnameVerification() { + return hostnameVerification; + } - /** - * Sets the SSL context used for "wss" URIs. Defaults to the JVM default context. - * - * @param sslContext The SSL context. - * @return This builder. - */ - public Builder sslContext(SSLContext sslContext) { - this.sslContext = sslContext; - return this; - } + /** + * Sets whether the server host name is verified for "wss" URIs. Defaults to true. + * + * @param hostnameVerification True to verify the host name. + */ + public synchronized void setHostnameVerification(boolean hostnameVerification) { + requireStopped(); + this.hostnameVerification = hostnameVerification; + } - /** - * Sets the connect and handshake timeout. Defaults to 15 seconds; zero for none. - * - * @param connectTimeout The timeout. - * @return This builder. - */ - public Builder connectTimeout(Duration connectTimeout) { - this.connectTimeout = requireNonNegative(connectTimeout, "connectTimeout"); - return this; + /** + * Starts the client, creating its I/O threads. Does nothing if already started. + * + * @throws IOException If the client cannot be started. + */ + public synchronized void start() throws IOException { + if (connector == null) { + connector = + new NettyWebSocketConnector( + sslContext, + connectTimeout.toMillis(), + idleTimeout.toMillis(), + maxMessageSize, + getMaxFrameSize(), + compression, + hostnameVerification, + Logger.getLogger(WebSocketClient.class.getName())); } + } - /** - * Sets the idle time after which the server is pinged, the connection being closed if it - * stays idle as long again. Defaults to 30 seconds; zero to disable. - * - * @param idleTimeout The idle timeout. - * @return This builder. - */ - public Builder idleTimeout(Duration idleTimeout) { - this.idleTimeout = requireNonNegative(idleTimeout, "idleTimeout"); - return this; - } + /** + * Indicates whether the client is started. + * + * @return True if the client is started. + */ + public synchronized boolean isStarted() { + return connector != null; + } - /** - * Sets the maximum size in bytes of a received message. Defaults to 65536. - * - * @param maxMessageSize The maximum message size. - * @return This builder. - */ - public Builder maxMessageSize(int maxMessageSize) { - if (maxMessageSize <= 0) { - throw new IllegalArgumentException("maxMessageSize must be positive"); - } - this.maxMessageSize = maxMessageSize; - return this; + /** Stops the client, releasing its I/O threads. Does nothing if already stopped. */ + public synchronized void stop() { + if (connector != null) { + connector.close(); + connector = null; } + } - /** - * Sets the maximum size in bytes of a received frame. A message may be fragmented in - * several frames. Defaults to the maximum message size, and is capped by it. - * - * @param maxFrameSize The maximum frame size. - * @return This builder. - */ - public Builder maxFrameSize(int maxFrameSize) { - if (maxFrameSize <= 0) { - throw new IllegalArgumentException("maxFrameSize must be positive"); - } - this.maxFrameSize = maxFrameSize; - return this; - } + /** + * Opens a session, blocking until the handshake completes. Starts the client if needed. + * + * @param uri The "ws" or "wss" URI. + * @return The open session. + * @throws IOException If the connection or the handshake fails. + */ + public WebSocketSession connect(String uri) throws IOException { + return connect(URI.create(uri), List.of(), Map.of()); + } - /** - * Sets whether "permessage-deflate" compression (RFC 7692) is offered to the server. - * Defaults to true; it is used only if the server accepts it. - * - * @param compression True to offer compression. - * @return This builder. - */ - public Builder compression(boolean compression) { - this.compression = compression; - return this; + /** + * Opens a session, blocking until the handshake completes. Starts the client if needed. + * + * @param uri The "ws" or "wss" URI. + * @param subprotocols The subprotocols to offer, in preference order. + * @param headers Additional handshake request headers, for example "Authorization". + * @return The open session. + * @throws IOException If the connection or the handshake fails. + */ + public WebSocketSession connect(URI uri, List subprotocols, Map headers) + throws IOException { + NettyWebSocketConnector current; + synchronized (this) { + start(); + current = connector; } + return current.connect( + Objects.requireNonNull(uri, "uri"), + List.copyOf(subprotocols), + new LinkedHashMap<>(headers)); + } - /** - * Sets whether the server host name is verified for "wss" URIs. Defaults to true. - * - * @param hostnameVerification True to verify the host name. - * @return This builder. - */ - public Builder hostnameVerification(boolean hostnameVerification) { - this.hostnameVerification = hostnameVerification; - return this; - } + /** Stops the client. */ + @Override + public void close() { + stop(); + } - /** - * Builds the client. - * - * @return The client. - * @throws IOException If the client cannot be created. - */ - public WebSocketClient build() throws IOException { - return new WebSocketClient(this); + private void requireStopped() { + if (connector != null) { + throw new IllegalStateException( + "Stop the WebSocket client before changing its settings"); } + } - private static Duration requireNonNegative(Duration value, String name) { - Objects.requireNonNull(value, name); - if (value.isNegative()) { - throw new IllegalArgumentException(name + " cannot be negative"); - } - return value; + private static Duration requireNonNegative(Duration value, String name) { + Objects.requireNonNull(value, name); + if (value.isNegative()) { + throw new IllegalArgumentException(name + " cannot be negative"); } + return value; } } diff --git a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketCompressionTestCase.java b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketCompressionTestCase.java index 0cb1b5304f..3e0e47882d 100644 --- a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketCompressionTestCase.java +++ b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketCompressionTestCase.java @@ -145,7 +145,9 @@ void compressedMessagesRoundTrip() throws Exception { int port = start(new Context()); String large = "compressible ".repeat(10_000); - try (WebSocketClient client = WebSocketClient.builder().maxMessageSize(1 << 20).build(); + WebSocketClient client = new WebSocketClient(); + client.setMaxMessageSize(1 << 20); + try (client; WebSocketSession session = client.connect("ws://localhost:" + port + "/")) { // 130 KB of text: larger than the server default limit (64 KB) once inflated, so the // server would close the session if the limit were applied to compressed frames @@ -166,7 +168,7 @@ void inflatedMessagesAreCappedByTheMessageSize() throws Exception { context.getParameters().add("websocket.maxMessageSize", "1024"); int port = start(context); - try (WebSocketClient client = WebSocketClient.create()) { + try (WebSocketClient client = new WebSocketClient()) { WebSocketSession session = client.connect("ws://localhost:" + port + "/"); // Compresses to far less than 1 KB on the wire, but inflates to 100 KB. session.sendText("a".repeat(100_000)); @@ -182,7 +184,9 @@ void framesLargerThanTheFrameSizeCloseTheSession() throws Exception { context.getParameters().add("websocket.maxFrameSize", "16"); int port = start(context); - try (WebSocketClient client = WebSocketClient.builder().compression(false).build()) { + WebSocketClient client = new WebSocketClient(); + client.setCompression(false); + try (client) { WebSocketSession session = client.connect("ws://localhost:" + port + "/"); session.sendText("small"); assertEquals("small", session.receive().getText()); @@ -197,8 +201,10 @@ void framesLargerThanTheFrameSizeCloseTheSession() throws Exception { void clientFrameSizeLimitAppliesToServerMessages() throws Exception { int port = start(new Context()); - try (WebSocketClient client = - WebSocketClient.builder().compression(false).maxFrameSize(16).build()) { + WebSocketClient client = new WebSocketClient(); + client.setCompression(false); + client.setMaxFrameSize(16); + try (client) { WebSocketSession session = client.connect("ws://localhost:" + port + "/"); session.sendText("y".repeat(64)); assertNull(session.receive()); diff --git a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java index 482afa241e..247854bd1e 100644 --- a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java +++ b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java @@ -9,6 +9,7 @@ package org.restlet.ext.websocket; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -113,12 +114,11 @@ void echoesOverTls() throws Exception { context.getParameters().add("keyStoreType", "PKCS12"); int port = start(Protocol.HTTPS, context, ECHO); - try (WebSocketClient client = - WebSocketClient.builder() - .sslContext(trustingContext()) - .hostnameVerification(true) - .maxMessageSize(1024) - .build(); + WebSocketClient client = new WebSocketClient(); + client.setSslContext(trustingContext()); + client.setHostnameVerification(true); + client.setMaxMessageSize(1024); + try (client; WebSocketSession session = client.connect("wss://localhost:" + port + "/")) { session.sendText("secure"); assertEquals("secure", session.receive().getText()); @@ -131,8 +131,9 @@ void idleSessionsArePingedAndKeptAliveByPongs() throws Exception { context.getParameters().add("connector.idleTimeout", "100"); int port = start(Protocol.HTTP, context, ECHO); - try (WebSocketClient client = - WebSocketClient.builder().idleTimeout(Duration.ofMillis(100)).build(); + WebSocketClient client = new WebSocketClient(); + client.setIdleTimeout(Duration.ofMillis(100)); + try (client; WebSocketSession session = client.connect("ws://localhost:" + port + "/")) { // Several idle periods: both sides ping, and the pongs keep the session open. Thread.sleep(600); @@ -152,7 +153,7 @@ void handlerFailureClosesWithInternalError() throws Exception { throw new IllegalStateException("boom"); }); - try (WebSocketClient client = WebSocketClient.create()) { + try (WebSocketClient client = new WebSocketClient()) { WebSocketSession session = client.connect("ws://localhost:" + port + "/"); assertNull(session.receive()); assertEquals(WebSocketSession.INTERNAL_ERROR, session.getCloseCode()); @@ -166,8 +167,9 @@ void connectionFailuresAreReported() throws Exception { try (ServerSocket socket = new ServerSocket(0)) { freePort = socket.getLocalPort(); } - try (WebSocketClient client = - WebSocketClient.builder().connectTimeout(Duration.ofSeconds(2)).build()) { + WebSocketClient client = new WebSocketClient(); + client.setConnectTimeout(Duration.ofSeconds(2)); + try (client) { String refusedUri = "ws://localhost:" + freePort + "/"; String httpUri = "http://localhost:" + freePort + "/"; assertThrows(ConnectException.class, () -> client.connect(refusedUri)); @@ -177,14 +179,37 @@ void connectionFailuresAreReported() throws Exception { } @Test - void builderValidatesSettings() { - WebSocketClient.Builder builder = WebSocketClient.builder(); - assertThrows(IllegalArgumentException.class, () -> builder.maxMessageSize(0)); + void clientValidatesSettings() { + WebSocketClient client = new WebSocketClient(); + assertThrows(IllegalArgumentException.class, () -> client.setMaxMessageSize(0)); + assertThrows(IllegalArgumentException.class, () -> client.setMaxFrameSize(0)); assertThrows( - IllegalArgumentException.class, () -> builder.idleTimeout(Duration.ofSeconds(-1))); + IllegalArgumentException.class, + () -> client.setIdleTimeout(Duration.ofSeconds(-1))); assertThrows( IllegalArgumentException.class, - () -> builder.connectTimeout(Duration.ofSeconds(-1))); + () -> client.setConnectTimeout(Duration.ofSeconds(-1))); + } + + @Test + void settingsAreFixedWhileStarted() throws Exception { + WebSocketClient client = new WebSocketClient(); + assertEquals(65536, client.getMaxFrameSize()); + client.setMaxMessageSize(1024); + assertEquals(1024, client.getMaxFrameSize()); + + client.start(); + try { + assertTrue(client.isStarted()); + assertThrows(IllegalStateException.class, () -> client.setMaxMessageSize(2048)); + assertThrows(IllegalStateException.class, () -> client.setCompression(false)); + } finally { + client.stop(); + } + + assertFalse(client.isStarted()); + client.setMaxMessageSize(2048); + assertEquals(2048, client.getMaxMessageSize()); } @Test @@ -202,7 +227,7 @@ void invalidServerParametersAreRejected() throws Exception { context.getParameters().add("websocket.maxMessageSize", "0"); int port = start(Protocol.HTTP, context, ECHO); - try (WebSocketClient client = WebSocketClient.create()) { + try (WebSocketClient client = new WebSocketClient()) { assertThrows(IOException.class, () -> client.connect("ws://localhost:" + port + "/")); } } diff --git a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketTestCase.java b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketTestCase.java index 7a33d55ce2..8e5160502f 100644 --- a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketTestCase.java +++ b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketTestCase.java @@ -60,7 +60,8 @@ void setUp() throws IOException { Engine.register(false); Engine.getInstance().getRegisteredServers().add(new HttpServerHelper(null)); Engine.getInstance().getRegisteredClients().add(new HttpClientHelper(null)); - client = WebSocketClient.builder().connectTimeout(Duration.ofSeconds(5)).build(); + client = new WebSocketClient(); + client.setConnectTimeout(Duration.ofSeconds(5)); } @AfterEach From b7f6a1ad1d8c20bebddab7ceba4f3d8e03094bc7 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:45:02 -0400 Subject: [PATCH 2/2] test: cover reconnecting a stopped WebSocketClient with new settings Also describe the setter-based configuration and restartable lifecycle of WebSocketClient in the changes log. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com> --- changes.md | 4 ++- .../websocket/WebSocketSessionTestCase.java | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/changes.md b/changes.md index fea3b6525b..695e7c88a3 100644 --- a/changes.md +++ b/changes.md @@ -70,7 +70,9 @@ Changes log `WebSocketUpgrader` (or `WebSocketUpgrade.accept`) accepts upgrades that went through the normal routing, filters and authenticators, and runs a `WebSocketHandler` on its own virtual thread with a blocking `WebSocketSession` (`receive`, `sendText`, `sendBinary`, - `close`). `WebSocketClient` opens client sessions on "ws" and "wss" URIs. The server + `close`). `WebSocketClient` opens client sessions on "ws" and "wss" URIs; it is configured + with setters, which are rejected while it is started, and can be stopped, reconfigured + and started again (`connect` starts a stopped client). The server answers pings, pings idle sessions, and supports the "websocket.maxSessions", "websocket.maxMessageSize", "websocket.maxFrameSize" and "websocket.compression" parameters. Server and client support "permessage-deflate" compression (RFC 7692), diff --git a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java index 247854bd1e..0c6dac5a8d 100644 --- a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java +++ b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java @@ -212,6 +212,36 @@ void settingsAreFixedWhileStarted() throws Exception { assertEquals(2048, client.getMaxMessageSize()); } + @Test + void stoppedClientReconnectsWithUpdatedSettings() throws Exception { + int port = start(Protocol.HTTP, new Context(), ECHO); + String uri = "ws://localhost:" + port + "/"; + String large = "x".repeat(64); + + try (WebSocketClient client = new WebSocketClient()) { + client.setMaxMessageSize(1024); + WebSocketSession first = client.connect(uri); + first.sendText(large); + assertEquals(large, first.receive().getText()); + + client.stop(); + assertNull(first.receive()); + assertFalse(first.isOpen()); + + client.setMaxMessageSize(16); + try (WebSocketSession second = client.connect(uri)) { + assertTrue(client.isStarted()); + second.sendText("small"); + assertEquals("small", second.receive().getText()); + + // The echo exceeds the new client limit. + second.sendText(large); + assertNull(second.receive()); + assertEquals(WebSocketSession.MESSAGE_TOO_BIG, second.getCloseCode()); + } + } + } + @Test void messagesHaveValueSemantics() { assertEquals(WebSocketMessage.text("a"), WebSocketMessage.text("a"));