From 9350b1fd5868dcd8038a7701eaa5c3f33c60e0e4 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:16:28 -0400 Subject: [PATCH] fix: let WebSocketClient borrow a client created without a context WebSocketClient.setClient found the borrowed client's NettyClientHelper through the "org.restlet.engine.helper" context attribute, so a client created with new Client(Protocol.HTTP), which has no context, couldn't be borrowed. Client.getHelper() is now public, as an engine extension point, and the WebSocket client uses it. The context requirement is removed from the setClient Javadoc and the error message. Fixes #1599. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com> --- changes.md | 3 ++- .../restlet/ext/websocket/WebSocketClient.java | 16 ++++++---------- .../WebSocketSharedEventLoopTestCase.java | 14 ++++++++++++++ .../src/main/java/org/restlet/Client.java | 9 ++++++--- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/changes.md b/changes.md index b11842b2dd..92d22f8ce0 100644 --- a/changes.md +++ b/changes.md @@ -140,7 +140,8 @@ Changes log "websocket.maxMessageSize", "websocket.maxFrameSize" and "websocket.compression" parameters. Server and client support "permessage-deflate" compression (RFC 7692), enabled by default. `WebSocketClient.setClient` shares the I/O threads of a borrowed, - started Netty HTTP `Client` instead of creating its own. + started Netty HTTP `Client` instead of creating its own, with or without a context + (issue #1599). `Client.getHelper()` is now public, as an engine extension point. - Added the experimental `org.restlet.ext.graphql` extension, on graphql-java. `GraphqlServerAdapter` serves a `graphql.GraphQL` instance as a Restlet, following the GraphQL over HTTP draft (`application/graphql-response+json` and `application/json`); 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 2a73a9b306..7d15465b47 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 @@ -210,10 +210,10 @@ public synchronized Client getClient() { /** * Sets a borrowed HTTP client whose I/O threads are shared, instead of creating new ones. The - * client must use the Netty HTTP connector of the core, have a context, and be started before - * this WebSocket client. Its owner stops it, after this WebSocket client: stopping it closes - * the sessions. Only the I/O threads are shared; the other settings, including TLS, are those - * of this WebSocket client. + * client must use the Netty HTTP connector of the core and be started before this WebSocket + * client. Its owner stops it, after this WebSocket client: stopping it closes the sessions. + * Only the I/O threads are shared; the other settings, including TLS, are those of this + * WebSocket client. * * @param client The borrowed client, or null to create new I/O threads. */ @@ -313,13 +313,9 @@ private EventLoopGroup borrowedEventLoopGroup() { if (client == null) { return null; } - Object helper = - (client.getContext() == null) - ? null - : client.getContext().getAttributes().get("org.restlet.engine.helper"); - if (!(helper instanceof NettyClientHelper nettyHelper)) { + if (!(client.getHelper() instanceof NettyClientHelper nettyHelper)) { throw new IllegalStateException( - "The borrowed client must have a context and use the Netty HTTP connector"); + "The borrowed client must use the Netty HTTP connector"); } EventLoopGroup group = nettyHelper.getEventLoopGroup(); if (group == null || group.isShuttingDown()) { diff --git a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSharedEventLoopTestCase.java b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSharedEventLoopTestCase.java index e19eaf1487..e7021b0d95 100644 --- a/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSharedEventLoopTestCase.java +++ b/org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSharedEventLoopTestCase.java @@ -159,6 +159,20 @@ void acceptsAClientWhoseContextIsSetLater() throws Exception { } } + @Test + void acceptsAClientWithoutContext() throws Exception { + Client plain = new Client(Protocol.HTTP); + plain.start(); + try (WebSocketClient client = new WebSocketClient()) { + client.setClient(plain); + WebSocketSession session = client.connect(uri); + session.sendText("plain"); + assertEquals("plain", session.receive().getText()); + } finally { + plain.stop(); + } + } + @Test void rejectsUnsuitableClients() throws Exception { WebSocketClient client = new WebSocketClient(); diff --git a/org.restlet/src/main/java/org/restlet/Client.java b/org.restlet/src/main/java/org/restlet/Client.java index 5b84376c5a..f24c9c7b21 100644 --- a/org.restlet/src/main/java/org/restlet/Client.java +++ b/org.restlet/src/main/java/org/restlet/Client.java @@ -107,11 +107,14 @@ public Client(String protocolName) { } /** - * Returns the helper provided by the implementation. + * Returns the helper provided by the implementation. This is an engine extension point, meant + * for Restlet extensions that share the connector's resources, such as its I/O threads. It + * doesn't depend on the client having a context. * - * @return The helper provided by the implementation. + * @return The helper provided by the implementation, or null if no connector supports the + * protocols. */ - private RestletHelper getHelper() { + public RestletHelper getHelper() { return this.helper; }