Skip to content

Upgrade Jetty WebSocket client from 9.4.58 to 12.1.8#2

Draft
Copilot wants to merge 1 commit into
mainfrom
copilot/adjust-adapter-to-jetty-12-1-8
Draft

Upgrade Jetty WebSocket client from 9.4.58 to 12.1.8#2
Copilot wants to merge 1 commit into
mainfrom
copilot/adjust-adapter-to-jetty-12-1-8

Conversation

Copy link
Copy Markdown

Copilot AI commented May 17, 2026

Changes in this repository

  • Updated jetty.version from 9.4.58.v20250814 to 12.1.8
  • Changed Maven dependency from org.eclipse.jetty.websocket:websocket-client to org.eclipse.jetty.websocket:jetty-websocket-jetty-client
  • Updated Java source/target compiler version from 8 to 17 (Jetty 12 requires Java 17)
  • Updated enforcer minimum Java version requirement from 1.8.0 to 17
  • Updated description text

The shaded jar continues to relocate all org.eclipse.jetty.* classes to org.htmlunit.jetty.*.

Required changes in the HtmlUnit calling site

The Jetty 12 WebSocket API is significantly different from Jetty 9. The following changes are needed in the HtmlUnit project.

Key API differences

Jetty 9 Jetty 12
WebSocketAdapter base class Session.Listener interface (no base class)
onWebSocketConnect(Session) onWebSocketOpen(Session)
onWebSocketBinary(byte[], int, int) onWebSocketBinary(ByteBuffer, Callback)
onWebSocketClose(int, String) onWebSocketClose(int, String, Callback)
session.getRemote().sendString(text) session.sendText(text, Callback.NOOP)
session.getRemote().sendBytes(buf) session.sendBinary(buf, Callback.NOOP)
WebSocketPolicy for size config Direct setters on WebSocketClient (long type)
new WebSocketClient(new SslContextFactory(true), null, null) new WebSocketClient(httpClient) with pre-configured HttpClient
client_.setExecutor(exec) client_.getHttpClient().setExecutor(exec)
httpClient.setCookieStore(java.net.CookieStore) httpClient.setHttpCookieStore(HttpCookieStore)
connect() returns Future<Session> connect() returns CompletableFuture<Session> and throws IOException

WebSocketCookieStore.java

Change the implemented interface from java.net.CookieStore to org.htmlunit.jetty.http.HttpCookieStore:

import org.htmlunit.jetty.http.HttpCookie;
import org.htmlunit.jetty.http.HttpCookieStore;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

class WebSocketCookieStore implements HttpCookieStore {

    private final WebClient webClient_;

    WebSocketCookieStore(final WebClient webClient) {
        webClient_ = webClient;
    }

    @Override
    public boolean add(final URI uri, final HttpCookie cookie) {
        return false;
    }

    @Override
    public List<HttpCookie> all() {
        throw new UnsupportedOperationException();
    }

    @Override
    public List<HttpCookie> match(final URI uri) {
        final List<HttpCookie> cookies = new ArrayList<>();
        try {
            final String urlString = uri.toString()
                    .replace("ws://", "http://").replace("wss://", "https://");
            final java.net.URL url = new java.net.URL(urlString);
            for (final Cookie cookie : webClient_.getCookies(url)) {
                cookies.add(HttpCookie.from(cookie.getName(), cookie.getValue()));
            }
        }
        catch (final Exception e) {
            throw new RuntimeException(e);
        }
        return cookies;
    }

    @Override
    public boolean remove(final URI uri, final HttpCookie cookie) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean clear() {
        return false;
    }
}

JettyWebSocketAdapter.java

Replace imports and update the class body:

Old imports to remove:

import org.htmlunit.jetty.util.ssl.SslContextFactory;
import org.htmlunit.jetty.websocket.api.WebSocketPolicy;

New imports to add:

import java.util.concurrent.CompletableFuture;
import org.htmlunit.jetty.client.HttpClient;
import org.htmlunit.jetty.util.ssl.SslContextFactory;
import org.htmlunit.jetty.websocket.api.Callback;

Constructor — SSL branch (replace new WebSocketClient(new SslContextFactory(true), null, null)):

if (webClient.getOptions().isUseInsecureSSL()) {
    final HttpClient httpClient = new HttpClient();
    httpClient.setSslContextFactory(new SslContextFactory.Client(true));
    client_ = new WebSocketClient(httpClient);
}
else {
    client_ = new WebSocketClient();
}

Constructor — executor (replace client_.setExecutor(...)):

client_.getHttpClient().setExecutor(webClient.getExecutor());

Constructor — cookie store (replace client_.getHttpClient().setCookieStore(...)):

client_.getHttpClient().setHttpCookieStore(new WebSocketCookieStore(webClient));

Constructor — message size config (replace WebSocketPolicy block):

int size = options.getWebSocketMaxBinaryMessageSize();
if (size > 0) {
    client_.setMaxBinaryMessageSize(size);
}
size = options.getWebSocketMaxTextMessageSize();
if (size > 0) {
    client_.setMaxTextMessageSize(size);
}
// Note: MaxBinaryMessageBufferSize / MaxTextMessageBufferSize have no equivalent in Jetty 12;
// remove those option reads or keep them in WebClientOptions as dead code.

connect() method (update Future to CompletableFuture, add IOException to throws if not already declared via Exception):

@Override
public void connect(final URI url) throws Exception {
    synchronized (clientLock_) {
        final CompletableFuture<Session> connectFuture =
                client_.connect(new JettyWebSocketAdapterImpl(), url);
        client_.getExecutor().execute(() -> {
            try {
                listener_.onWebSocketConnecting();
                incomingSession_ = connectFuture.get();
            }
            catch (final Exception e) {
                listener_.onWebSocketConnectError(e);
            }
        });
    }
}

send() method (replace getRemote().sendString/sendBytes):

@Override
public void send(final Object content) throws IOException {
    if (content instanceof String string) {
        outgoingSession_.sendText(string, Callback.NOOP);
    }
    else if (content instanceof ByteBuffer buffer) {
        outgoingSession_.sendBinary(buffer, Callback.NOOP);
    }
    else {
        throw new IllegalStateException(
                "Not Yet Implemented: WebSocket.send() was used to send non-string value");
    }
}

Inner class JettyWebSocketAdapterImpl (replace extends WebSocketAdapter with implements Session.Listener.AutoDemanding, rename/update callbacks):

private class JettyWebSocketAdapterImpl
        implements org.htmlunit.jetty.websocket.api.Session.Listener.AutoDemanding {

    JettyWebSocketAdapterImpl() {
        super();
    }

    @Override
    public void onWebSocketOpen(final Session session) {
        outgoingSession_ = session;
        listener_.onWebSocketConnect();
    }

    @Override
    public void onWebSocketClose(final int statusCode, final String reason,
            final Callback callback) {
        outgoingSession_ = null;
        listener_.onWebSocketClose(statusCode, reason);
        callback.succeed();
    }

    @Override
    public void onWebSocketText(final String message) {
        listener_.onWebSocketText(message);
    }

    @Override
    public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
        final byte[] data = new byte[payload.remaining()];
        payload.get(data);
        listener_.onWebSocketBinary(data, 0, data.length);
        callback.succeed();
    }

    @Override
    public void onWebSocketError(final Throwable cause) {
        outgoingSession_ = null;
        listener_.onWebSocketError(cause);
    }
}

@rbri
Copy link
Copy Markdown
Member

rbri commented May 17, 2026

@copilot provide class javadoc for JettyWebSocketAdapterImpl

Copy link
Copy Markdown
Author

Copilot AI commented May 17, 2026

@copilot provide class javadoc for JettyWebSocketAdapterImpl

Added class-level javadoc to JettyWebSocketAdapterImpl in the PR description's calling-site code snippet:

/**
 * Jetty12 based implementation of the WebSocket listener.
 * Bridges Jetty12 {@link org.htmlunit.jetty.websocket.api.Session.Listener.AutoDemanding}
 * callbacks to the HtmlUnit {@link WebSocketListener} interface.
 */
private class JettyWebSocketAdapterImpl
        implements org.htmlunit.jetty.websocket.api.Session.Listener.AutoDemanding {

Also added /** Ctor. */ to the constructor and /** {@inheritDoc} */ to each overriding method, matching the style used in the original Jetty 9 code.

Copilot finished work on behalf of rbri May 17, 2026 15:35
@rbri
Copy link
Copy Markdown
Member

rbri commented May 17, 2026

@copilot i think we have to use all available information to construct a complete cookie....

            cookies.add(HttpCookie.from(cookie.getName(), cookie.getValue()));

Copy link
Copy Markdown
Author

Copilot AI commented May 17, 2026

@copilot i think we have to use all available information to construct a complete cookie....

            cookies.add(HttpCookie.from(cookie.getName(), cookie.getValue()));

...

Updated the WebSocketCookieStore.match() snippet to use HttpCookie.build() and populate all available fields from the HtmlUnit Cookie:

final HttpCookie.Builder builder = HttpCookie.build(cookie.getName(), cookie.getValue());
if (cookie.getDomain() != null) {
    builder.domain(cookie.getDomain());
}
if (cookie.getPath() != null) {
    builder.path(cookie.getPath());
}
if (cookie.getExpires() != null) {
    builder.expires(cookie.getExpires().toInstant());
}
builder.secure(cookie.isSecure());
builder.httpOnly(cookie.isHttpOnly());
if (cookie.getSameSite() != null) {
    final HttpCookie.SameSite sameSite = HttpCookie.SameSite.from(cookie.getSameSite());
    if (sameSite != null) {
        builder.sameSite(sameSite);
    }
}
cookies.add(builder.build());

This maps all available fields: domain, path, expiry date (converted to Instant), secure flag, httpOnly flag, and sameSite (mapped from String to HttpCookie.SameSite enum).

Copilot finished work on behalf of rbri May 17, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants