diff --git a/pom.xml b/pom.xml
index bdab1fc068..61c98e931f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,7 +44,6 @@
5.0.0-SNAPSHOT
5.0.0-SNAPSHOT
5.0.0-SNAPSHOT
- 4.21.0
5.0.0-SNAPSHOT
4.5.14
@@ -1407,11 +1406,6 @@
htmlunit-csp
${htmlunit-csp.version}
-
- org.htmlunit
- htmlunit-websocket-client
- ${htmlunit-websocketclient.version}
-
org.apache.commons
diff --git a/src/main/java/org/htmlunit/WebClient.java b/src/main/java/org/htmlunit/WebClient.java
index 18e7fd39d0..eae5d7e5c5 100644
--- a/src/main/java/org/htmlunit/WebClient.java
+++ b/src/main/java/org/htmlunit/WebClient.java
@@ -100,7 +100,7 @@
import org.htmlunit.util.NameValuePair;
import org.htmlunit.util.StringUtils;
import org.htmlunit.util.UrlUtils;
-import org.htmlunit.websocket.JettyWebSocketAdapter.JettyWebSocketAdapterFactory;
+import org.htmlunit.websocket.JdkWebSocketAdapter.JdkWebSocketAdapterFactory;
import org.htmlunit.websocket.WebSocketAdapter;
import org.htmlunit.websocket.WebSocketAdapterFactory;
import org.htmlunit.websocket.WebSocketListener;
@@ -341,7 +341,7 @@ public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEn
}
loadQueue_ = new ArrayList<>();
- webSocketAdapterFactory_ = new JettyWebSocketAdapterFactory();
+ webSocketAdapterFactory_ = new JdkWebSocketAdapterFactory();
// The window must be constructed AFTER the script engine.
currentWindowTracker_ = new CurrentWindowTracker(this, true);
diff --git a/src/main/java/org/htmlunit/javascript/host/WebSocket.java b/src/main/java/org/htmlunit/javascript/host/WebSocket.java
index 072c878d3b..96335a2beb 100644
--- a/src/main/java/org/htmlunit/javascript/host/WebSocket.java
+++ b/src/main/java/org/htmlunit/javascript/host/WebSocket.java
@@ -421,7 +421,7 @@ public void close() throws IOException {
public void close(final Object code, final Object reason) {
if (readyState_ != CLOSED) {
try {
- webSocketImpl_.closeIncommingSession();
+ webSocketImpl_.closeIncomingSession();
}
catch (final Throwable e) {
LOG.error("WS close error - incomingSession_.close() failed", e);
diff --git a/src/main/java/org/htmlunit/websocket/JdkWebSocketAdapter.java b/src/main/java/org/htmlunit/websocket/JdkWebSocketAdapter.java
new file mode 100644
index 0000000000..bf9760176e
--- /dev/null
+++ b/src/main/java/org/htmlunit/websocket/JdkWebSocketAdapter.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2002-2026 Gargoyle Software Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.websocket;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Builder;
+import java.nio.ByteBuffer;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.htmlunit.WebClient;
+import org.htmlunit.WebClientOptions;
+
+/**
+ * JDK based implementation of the {@link WebSocketAdapter}.
+ * Uses the {@link java.net.http.HttpClient} and {@link java.net.http.WebSocket}
+ * APIs available since JDK 11.
+ *
+ * @author Ronald Brill
+ */
+public final class JdkWebSocketAdapter implements WebSocketAdapter {
+
+ /**
+ * Our {@link WebSocketAdapterFactory}.
+ */
+ public static final class JdkWebSocketAdapterFactory implements WebSocketAdapterFactory {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient,
+ final WebSocketListener webSocketListener) {
+ return new JdkWebSocketAdapter(webClient, webSocketListener);
+ }
+ }
+
+ private final Object clientLock_ = new Object();
+ private HttpClient httpClient_;
+ private final WebClient webClient_;
+ private final WebSocketListener listener_;
+
+ private volatile java.net.http.WebSocket incomingSession_;
+ private java.net.http.WebSocket outgoingSession_;
+
+ /**
+ * Ctor.
+ * @param webClient the {@link WebClient}
+ * @param listener the {@link WebSocketListener}
+ */
+ public JdkWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) {
+ super();
+ webClient_ = webClient;
+ listener_ = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void start() throws Exception {
+ synchronized (clientLock_) {
+ final WebClientOptions options = webClient_.getOptions();
+ final Executor executor = webClient_.getExecutor();
+
+ final Builder builder = HttpClient.newBuilder()
+ .executor(executor)
+ .cookieHandler(new WebSocketCookieHandler(webClient_));
+
+ if (options.isUseInsecureSSL()) {
+ builder.sslContext(createInsecureSslContext());
+ }
+
+ httpClient_ = builder.build();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void connect(final URI url) throws Exception {
+ synchronized (clientLock_) {
+ final Executor executor = webClient_.getExecutor();
+ final CompletableFuture connectFuture =
+ httpClient_.newWebSocketBuilder()
+ .buildAsync(url, new JdkWebSocketListenerImpl());
+
+ executor.execute(() -> {
+ try {
+ listener_.onWebSocketConnecting();
+ incomingSession_ = connectFuture.join();
+ }
+ catch (final Exception e) {
+ listener_.onWebSocketConnectError(e);
+ }
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void send(final Object content) throws IOException {
+ try {
+ if (content instanceof String string) {
+ outgoingSession_.sendText(string, true).join();
+ }
+ else if (content instanceof ByteBuffer buffer) {
+ outgoingSession_.sendBinary(buffer, true).join();
+ }
+ else {
+ throw new IllegalStateException(
+ "Unsupported content type for WebSocket.send(): expected String or ByteBuffer");
+ }
+ }
+ catch (final IllegalStateException e) {
+ throw e;
+ }
+ catch (final Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void closeIncomingSession() {
+ if (incomingSession_ != null) {
+ incomingSession_.sendClose(java.net.http.WebSocket.NORMAL_CLOSURE, "").join();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void closeOutgoingSession() {
+ if (outgoingSession_ != null) {
+ outgoingSession_.sendClose(java.net.http.WebSocket.NORMAL_CLOSURE, "").join();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void closeClient() throws Exception {
+ synchronized (clientLock_) {
+ // HttpClient does not have a close() in Java 17;
+ // simply drop the reference so it can be garbage-collected
+ httpClient_ = null;
+ }
+ }
+
+ private static SSLContext createInsecureSslContext()
+ throws NoSuchAlgorithmException, KeyManagementException {
+ final TrustManager[] trustAllCerts = {
+ new X509TrustManager() {
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ @Override
+ public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
+ // trust all
+ }
+
+ @Override
+ public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
+ // trust all
+ }
+ }
+ };
+ final SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustAllCerts, new SecureRandom());
+ return sslContext;
+ }
+
+ /**
+ * A {@link CookieHandler} that bridges to the {@link WebClient} cookie store.
+ */
+ private static class WebSocketCookieHandler extends CookieHandler {
+ private final WebSocketCookieStore cookieStore_;
+
+ WebSocketCookieHandler(final WebClient webClient) {
+ cookieStore_ = new WebSocketCookieStore(webClient);
+ }
+
+ @Override
+ public java.util.Map> get(
+ final URI uri,
+ final java.util.Map> requestHeaders) {
+ final java.util.List cookies = cookieStore_.get(uri);
+ final java.util.Map> result = new java.util.HashMap<>();
+ if (!cookies.isEmpty()) {
+ final java.util.List cookieValues = new java.util.ArrayList<>();
+ for (final java.net.HttpCookie cookie : cookies) {
+ cookieValues.add(cookie.toString());
+ }
+ result.put("Cookie", cookieValues);
+ }
+ return result;
+ }
+
+ @Override
+ public void put(final URI uri,
+ final java.util.Map> responseHeaders) {
+ // not needed for WebSocket connections
+ }
+ }
+
+ private class JdkWebSocketListenerImpl implements java.net.http.WebSocket.Listener {
+
+ private StringBuilder textAccumulator_;
+ private ByteArrayOutputStream binaryAccumulator_;
+
+ JdkWebSocketListenerImpl() {
+ super();
+ }
+
+ @Override
+ public void onOpen(final java.net.http.WebSocket webSocket) {
+ outgoingSession_ = webSocket;
+ listener_.onWebSocketConnect();
+ webSocket.request(1);
+ }
+
+ @Override
+ public CompletionStage> onText(final java.net.http.WebSocket webSocket,
+ final CharSequence data, final boolean last) {
+ if (textAccumulator_ == null) {
+ textAccumulator_ = new StringBuilder();
+ }
+ textAccumulator_.append(data);
+
+ if (last) {
+ final String message = textAccumulator_.toString();
+ textAccumulator_ = null;
+ listener_.onWebSocketText(message);
+ }
+
+ webSocket.request(1);
+ return null;
+ }
+
+ @Override
+ public CompletionStage> onBinary(final java.net.http.WebSocket webSocket,
+ final ByteBuffer data, final boolean last) {
+ if (binaryAccumulator_ == null) {
+ binaryAccumulator_ = new ByteArrayOutputStream();
+ }
+ if (data.hasArray()) {
+ binaryAccumulator_.write(data.array(),
+ data.arrayOffset() + data.position(), data.remaining());
+ }
+ else {
+ final byte[] temp = new byte[data.remaining()];
+ data.get(temp);
+ binaryAccumulator_.write(temp, 0, temp.length);
+ }
+
+ if (last) {
+ final byte[] bytes = binaryAccumulator_.toByteArray();
+ binaryAccumulator_ = null;
+ listener_.onWebSocketBinary(bytes, 0, bytes.length);
+ }
+
+ webSocket.request(1);
+ return null;
+ }
+
+ @Override
+ public CompletionStage> onClose(final java.net.http.WebSocket webSocket,
+ final int statusCode, final String reason) {
+ outgoingSession_ = null;
+ listener_.onWebSocketClose(statusCode, reason);
+ return null;
+ }
+
+ @Override
+ public void onError(final java.net.http.WebSocket webSocket, final Throwable error) {
+ outgoingSession_ = null;
+ listener_.onWebSocketError(error);
+ }
+ }
+}
diff --git a/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java b/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java
deleted file mode 100644
index 71eb29b340..0000000000
--- a/src/main/java/org/htmlunit/websocket/JettyWebSocketAdapter.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (c) 2002-2026 Gargoyle Software Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.htmlunit.websocket;
-
-import java.io.IOException;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.concurrent.Future;
-
-import org.htmlunit.WebClient;
-import org.htmlunit.WebClientOptions;
-import org.htmlunit.jetty.util.ssl.SslContextFactory;
-import org.htmlunit.jetty.websocket.api.Session;
-import org.htmlunit.jetty.websocket.api.WebSocketPolicy;
-import org.htmlunit.jetty.websocket.client.WebSocketClient;
-
-/**
- * Jetty9 based impl of the WebSocketAdapter.
- * To avoid conflicts with other jetty versions used by projects, we use
- * our own shaded version of jetty9 (https://github.com/HtmlUnit/htmlunit-websocket-client).
- *
- * @author Ronald Brill
- */
-public final class JettyWebSocketAdapter implements WebSocketAdapter {
-
- /**
- * Our {@link WebSocketAdapterFactory}.
- */
- public static final class JettyWebSocketAdapterFactory implements WebSocketAdapterFactory {
- /**
- * {@inheritDoc}
- */
- @Override
- public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient,
- final WebSocketListener webSocketListener) {
- return new JettyWebSocketAdapter(webClient, webSocketListener);
- }
- }
-
- private final Object clientLock_ = new Object();
- private WebSocketClient client_;
- private final WebSocketListener listener_;
-
- private volatile Session incomingSession_;
- private Session outgoingSession_;
-
- /**
- * Ctor.
- * @param webClient the {@link WebClient}
- * @param listener the {@link WebSocketListener}
- */
- public JettyWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) {
- super();
- final WebClientOptions options = webClient.getOptions();
-
- if (webClient.getOptions().isUseInsecureSSL()) {
- client_ = new WebSocketClient(new SslContextFactory(true), null, null);
- // still use the deprecated method here to be backward compatible with older jetty versions
- // see https://github.com/HtmlUnit/htmlunit/issues/36
- // client_ = new WebSocketClient(new SslContextFactory.Client(true), null, null);
- }
- else {
- client_ = new WebSocketClient();
- }
-
- listener_ = listener;
-
- // use the same executor as the rest
- client_.setExecutor(webClient.getExecutor());
-
- client_.getHttpClient().setCookieStore(new WebSocketCookieStore(webClient));
-
- final WebSocketPolicy policy = client_.getPolicy();
- int size = options.getWebSocketMaxBinaryMessageSize();
- if (size > 0) {
- policy.setMaxBinaryMessageSize(size);
- }
- size = options.getWebSocketMaxBinaryMessageBufferSize();
- if (size > 0) {
- policy.setMaxBinaryMessageBufferSize(size);
- }
- size = options.getWebSocketMaxTextMessageSize();
- if (size > 0) {
- policy.setMaxTextMessageSize(size);
- }
- size = options.getWebSocketMaxTextMessageBufferSize();
- if (size > 0) {
- policy.setMaxTextMessageBufferSize(size);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void start() throws Exception {
- synchronized (clientLock_) {
- client_.start();
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void connect(final URI url) throws Exception {
- synchronized (clientLock_) {
- final Future connectFuture = client_.connect(new JettyWebSocketAdapterImpl(), url);
- client_.getExecutor().execute(() -> {
- try {
- listener_.onWebSocketConnecting();
- incomingSession_ = connectFuture.get();
- }
- catch (final Exception e) {
- listener_.onWebSocketConnectError(e);
- }
- });
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void send(final Object content) throws IOException {
- if (content instanceof String string) {
- outgoingSession_.getRemote().sendString(string);
- }
- else if (content instanceof ByteBuffer buffer) {
- outgoingSession_.getRemote().sendBytes(buffer);
- }
- else {
- throw new IllegalStateException(
- "Not Yet Implemented: WebSocket.send() was used to send non-string value");
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void closeIncommingSession() {
- if (incomingSession_ != null) {
- incomingSession_.close();
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void closeOutgoingSession() {
- if (outgoingSession_ != null) {
- outgoingSession_.close();
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void closeClient() throws Exception {
- synchronized (clientLock_) {
- if (client_ != null) {
- client_.stop();
- client_.destroy();
-
- // TODO finally ?
- client_ = null;
- }
- }
- }
-
- private class JettyWebSocketAdapterImpl extends org.htmlunit.jetty.websocket.api.WebSocketAdapter {
-
- /**
- * Ctor.
- */
- JettyWebSocketAdapterImpl() {
- super();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onWebSocketConnect(final Session session) {
- super.onWebSocketConnect(session);
- outgoingSession_ = session;
-
- listener_.onWebSocketConnect();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onWebSocketClose(final int statusCode, final String reason) {
- super.onWebSocketClose(statusCode, reason);
- outgoingSession_ = null;
-
- listener_.onWebSocketClose(statusCode, reason);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onWebSocketText(final String message) {
- super.onWebSocketText(message);
-
- listener_.onWebSocketText(message);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onWebSocketBinary(final byte[] data, final int offset, final int length) {
- super.onWebSocketBinary(data, offset, length);
-
- listener_.onWebSocketBinary(data, offset, length);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onWebSocketError(final Throwable cause) {
- super.onWebSocketError(cause);
- outgoingSession_ = null;
-
- listener_.onWebSocketError(cause);
- }
- }
-}
diff --git a/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java b/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java
index 4442162856..ea3c1007b4 100644
--- a/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java
+++ b/src/main/java/org/htmlunit/websocket/WebSocketAdapter.java
@@ -18,7 +18,7 @@
import java.net.URI;
/**
- * Helper to have no direct dependency to the WebSockt client
+ * Helper to have no direct dependency to the WebSocket client
* implementation used by HtmlUnit.
*
* @author Ronald Brill
@@ -49,11 +49,11 @@ public interface WebSocketAdapter {
void send(Object content) throws IOException;
/**
- * Close the incomming session.
+ * Close the incoming session.
*
* @throws Exception in case of error
*/
- void closeIncommingSession() throws Exception;
+ void closeIncomingSession() throws Exception;
/**
* Close the outgoing session.
diff --git a/src/main/java/org/htmlunit/websocket/WebSocketListener.java b/src/main/java/org/htmlunit/websocket/WebSocketListener.java
index 327d96e3fe..589b0789c6 100644
--- a/src/main/java/org/htmlunit/websocket/WebSocketListener.java
+++ b/src/main/java/org/htmlunit/websocket/WebSocketListener.java
@@ -15,7 +15,7 @@
package org.htmlunit.websocket;
/**
- * Helper to have no direct dependency to the WebSockt client
+ * Helper to have no direct dependency to the WebSocket client
* implementation used by HtmlUnit.
*
* @author Ronald Brill
diff --git a/src/main/module-info/module-info.java b/src/main/module-info/module-info.java
index 157c19b0c4..5c381bb53c 100644
--- a/src/main/module-info/module-info.java
+++ b/src/main/module-info/module-info.java
@@ -22,7 +22,7 @@
requires java.xml;
requires jdk.xml.dom;
- requires htmlunit.websocket.client;
+ requires java.net.http;
requires org.htmlunit.cssparser;
requires org.htmlunit.corejs;
diff --git a/src/test/java/org/htmlunit/archunit/ArchitectureTest.java b/src/test/java/org/htmlunit/archunit/ArchitectureTest.java
index f830133fb3..cc44f0a20c 100644
--- a/src/test/java/org/htmlunit/archunit/ArchitectureTest.java
+++ b/src/test/java/org/htmlunit/archunit/ArchitectureTest.java
@@ -173,15 +173,12 @@ public class ArchitectureTest {
.should().dependOnClassesThat().haveFullyQualifiedName("org.apache.commons.lang3.math.NumberUtils");
/**
- * The jetty websocket stuff is only used by one class.
+ * The jetty websocket stuff is no longer used (replaced by JDK HttpClient WebSocket).
*/
@ArchTest
public static final ArchRule webSocketPackageRule = noClasses()
.that()
.resideOutsideOfPackage("org.htmlunit.jetty..")
- .and().doNotHaveFullyQualifiedName("org.htmlunit.websocket.JettyWebSocketAdapter")
- .and().doNotHaveFullyQualifiedName("org.htmlunit.websocket.JettyWebSocketAdapter$JettyWebSocketAdapterFactory")
- .and().doNotHaveFullyQualifiedName("org.htmlunit.websocket.JettyWebSocketAdapter$JettyWebSocketAdapterImpl")
.should()
.dependOnClassesThat().resideInAnyPackage("org.htmlunit.jetty..");