From d8de2e40e6a174d3db3e159a55d27bfef77a7e1c Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 2 Feb 2026 17:48:05 -0800 Subject: [PATCH 1/4] Attempt to restore websocket upgrade on Tomcat 11 --- .../labkey/api/websocket/BrowserEndpoint.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/api/src/org/labkey/api/websocket/BrowserEndpoint.java b/api/src/org/labkey/api/websocket/BrowserEndpoint.java index a8d31e8a7e5..5df827091de 100644 --- a/api/src/org/labkey/api/websocket/BrowserEndpoint.java +++ b/api/src/org/labkey/api/websocket/BrowserEndpoint.java @@ -17,7 +17,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpSession; -import jakarta.websocket.ClientEndpoint; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.CloseReason; import jakarta.websocket.ContainerProvider; @@ -35,7 +34,6 @@ import org.labkey.api.security.AuthenticationManager; import org.labkey.api.security.SecurityManager; import org.labkey.api.security.User; -import org.labkey.api.util.UnexpectedException; import org.labkey.api.util.logging.LogHelper; import java.io.IOException; @@ -90,10 +88,11 @@ public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest reques @Override public void onOpen(Session session, EndpointConfig endpointConfig) { + String uri = null; try { - String uri = getWSRemoteUri(session, endpointConfig); - LOG.debug("BrowserEndpoint.onOpen( " + session.getRequestURI() + " -> " + uri); + uri = getWSRemoteUri(session, endpointConfig); + LOG.info("BrowserEndpoint.onOpen( " + session.getRequestURI() + " -> " + uri); Map> requestHeaders = (Map>) endpointConfig.getUserProperties().get("requestHeaders"); this.browserSession = session; this.serverEndpoint = new ServerEndpoint(new URI(uri), requestHeaders, endpointConfig.getUserProperties()); @@ -103,8 +102,13 @@ public void onOpen(Session session, EndpointConfig endpointConfig) } catch (URISyntaxException | IOException | DeploymentException | ServletException ex) { - LOG.debug("BrowserEndpoint.onOpen", ex); - UnexpectedException.rethrow(ex); + LOG.warn("BrowserEndpoint.onOpen failed to proxy " + session.getRequestURI() + " -> " + uri, ex); + try + { + session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, + "Failed to connect to remote WebSocket server")); + } + catch (IOException ignored) {} } } @@ -125,7 +129,6 @@ public void onError(Session session, Throwable throwable) public abstract Map> prepareProxyHeaders(URI remoteURI, Map> requestHeaders, Map properties); - @ClientEndpoint class ServerEndpoint extends Endpoint { final Session serverSession; From de7ac49a14ffaa520f85192a377918a3a4975803 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Tue, 3 Feb 2026 10:50:49 -0800 Subject: [PATCH 2/4] Another attempt --- .../labkey/api/websocket/BrowserEndpoint.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api/src/org/labkey/api/websocket/BrowserEndpoint.java b/api/src/org/labkey/api/websocket/BrowserEndpoint.java index 5df827091de..86312e1f4de 100644 --- a/api/src/org/labkey/api/websocket/BrowserEndpoint.java +++ b/api/src/org/labkey/api/websocket/BrowserEndpoint.java @@ -149,6 +149,36 @@ class ServerEndpoint extends Endpoint public void beforeRequest(Map> headers) { headers.putAll(proxyHeaders); + // Tomcat 11 requires these headers for WebSocket upgrade, but they may be filtered out by some proxy code + headers.put("Upgrade", List.of("websocket")); + headers.put("Connection", List.of("upgrade")); + + // Also pass through other WebSocket headers that might have been filtered out by proxy code + for (Map.Entry> entry : requestHeaders.entrySet()) + { + String name = entry.getKey(); + if (name.regionMatches(true, 0, "Sec-WebSocket-", 0, "Sec-WebSocket-".length()) && + !name.equalsIgnoreCase("Sec-WebSocket-Key")) + { + headers.put(name, entry.getValue()); + } + } + } + + @Override + public void afterResponse(HandshakeResponse hr) + { + // After the handshake, we might need to transfer headers back to the browser session, + // but the Jakarta WebSocket API doesn't give us an easy way to set headers on the + // already-opened browser session from here. + // However, we can log them for debugging if needed. + if (LOG.isTraceEnabled()) + { + for (Map.Entry> entry : hr.getHeaders().entrySet()) + { + LOG.trace("Response header: " + entry.getKey() + " = " + entry.getValue()); + } + } } }) .build(); From 4ed51be5c8097e8159cb4a0f2dd16516275d9e34 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 5 Feb 2026 09:33:47 -0800 Subject: [PATCH 3/4] More logging and header setting --- .../labkey/api/websocket/BrowserEndpoint.java | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/api/src/org/labkey/api/websocket/BrowserEndpoint.java b/api/src/org/labkey/api/websocket/BrowserEndpoint.java index 86312e1f4de..50a4c114c59 100644 --- a/api/src/org/labkey/api/websocket/BrowserEndpoint.java +++ b/api/src/org/labkey/api/websocket/BrowserEndpoint.java @@ -141,6 +141,13 @@ class ServerEndpoint extends Endpoint { final Map> proxyHeaders = prepareProxyHeaders(remoteURI, requestHeaders, properties); + // Log what the subclass's prepareProxyHeaders returned + LOG.info("=== WebSocket proxy: proxyHeaders from prepareProxyHeaders ==="); + for (Map.Entry> entry : proxyHeaders.entrySet()) + { + LOG.info(" proxyHeaders: " + entry.getKey() + " = " + entry.getValue()); + } + WebSocketContainer clientEndPoint = ContainerProvider.getWebSocketContainer(); ClientEndpointConfig config = ClientEndpointConfig.Builder.create() .configurator(new ClientEndpointConfig.Configurator() @@ -148,36 +155,63 @@ class ServerEndpoint extends Endpoint @Override public void beforeRequest(Map> headers) { + // Log incoming browser headers for debugging + LOG.info("=== WebSocket proxy: browser request headers ==="); + for (Map.Entry> entry : requestHeaders.entrySet()) + { + LOG.info(" Browser header: " + entry.getKey() + " = " + entry.getValue()); + } + headers.putAll(proxyHeaders); // Tomcat 11 requires these headers for WebSocket upgrade, but they may be filtered out by some proxy code headers.put("Upgrade", List.of("websocket")); headers.put("Connection", List.of("upgrade")); - // Also pass through other WebSocket headers that might have been filtered out by proxy code + // Pass through Sec-WebSocket-Protocol if present (for subprotocol negotiation) + // NOTE: Do NOT copy Sec-WebSocket-Extensions - Tomcat 11 changed how extension negotiation works + // and copying browser extensions can cause the handshake to fail. + // Also do NOT copy Sec-WebSocket-Key - each connection needs its own unique key. + // Sec-WebSocket-Version is set by Tomcat's client. for (Map.Entry> entry : requestHeaders.entrySet()) { String name = entry.getKey(); - if (name.regionMatches(true, 0, "Sec-WebSocket-", 0, "Sec-WebSocket-".length()) && - !name.equalsIgnoreCase("Sec-WebSocket-Key")) + if (name.equalsIgnoreCase("Sec-WebSocket-Protocol")) { - headers.put(name, entry.getValue()); + headers.put("Sec-WebSocket-Protocol", entry.getValue()); } } + + // Ensure User-Agent is set - Tomcat's WebSocket client doesn't send one by default, + // and some servers (like RStudio) check for browser-like User-Agent + if (!headers.containsKey("User-Agent")) + { + // Try to get from browser request headers (may be lowercase) + for (Map.Entry> entry : requestHeaders.entrySet()) + { + if (entry.getKey().equalsIgnoreCase("User-Agent")) + { + headers.put("User-Agent", entry.getValue()); + break; + } + } + } + + // Log outgoing headers to backend server + LOG.info("=== WebSocket proxy: headers being sent to backend (" + remoteURI + ") ==="); + for (Map.Entry> entry : headers.entrySet()) + { + LOG.info(" Backend header: " + entry.getKey() + " = " + entry.getValue()); + } } @Override public void afterResponse(HandshakeResponse hr) { - // After the handshake, we might need to transfer headers back to the browser session, - // but the Jakarta WebSocket API doesn't give us an easy way to set headers on the - // already-opened browser session from here. - // However, we can log them for debugging if needed. - if (LOG.isTraceEnabled()) + // Log response headers from backend server for debugging + LOG.info("=== WebSocket proxy: response headers from backend ==="); + for (Map.Entry> entry : hr.getHeaders().entrySet()) { - for (Map.Entry> entry : hr.getHeaders().entrySet()) - { - LOG.trace("Response header: " + entry.getKey() + " = " + entry.getValue()); - } + LOG.info(" Response header: " + entry.getKey() + " = " + entry.getValue()); } } }) From c95dc1de1b6dfc132d30dd24a3c5e8bd7fba929f Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 16 Feb 2026 20:14:02 -0800 Subject: [PATCH 4/4] Tone down logging --- .../labkey/api/websocket/BrowserEndpoint.java | 65 ++++--------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/api/src/org/labkey/api/websocket/BrowserEndpoint.java b/api/src/org/labkey/api/websocket/BrowserEndpoint.java index 50a4c114c59..1e4e6cf5df3 100644 --- a/api/src/org/labkey/api/websocket/BrowserEndpoint.java +++ b/api/src/org/labkey/api/websocket/BrowserEndpoint.java @@ -17,6 +17,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpSession; +import jakarta.websocket.ClientEndpoint; import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.CloseReason; import jakarta.websocket.ContainerProvider; @@ -34,6 +35,7 @@ import org.labkey.api.security.AuthenticationManager; import org.labkey.api.security.SecurityManager; import org.labkey.api.security.User; +import org.labkey.api.util.UnexpectedException; import org.labkey.api.util.logging.LogHelper; import java.io.IOException; @@ -92,7 +94,7 @@ public void onOpen(Session session, EndpointConfig endpointConfig) try { uri = getWSRemoteUri(session, endpointConfig); - LOG.info("BrowserEndpoint.onOpen( " + session.getRequestURI() + " -> " + uri); + LOG.debug("BrowserEndpoint.onOpen( {} -> {}", session.getRequestURI(), uri); Map> requestHeaders = (Map>) endpointConfig.getUserProperties().get("requestHeaders"); this.browserSession = session; this.serverEndpoint = new ServerEndpoint(new URI(uri), requestHeaders, endpointConfig.getUserProperties()); @@ -102,13 +104,8 @@ public void onOpen(Session session, EndpointConfig endpointConfig) } catch (URISyntaxException | IOException | DeploymentException | ServletException ex) { - LOG.warn("BrowserEndpoint.onOpen failed to proxy " + session.getRequestURI() + " -> " + uri, ex); - try - { - session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, - "Failed to connect to remote WebSocket server")); - } - catch (IOException ignored) {} + LOG.debug("BrowserEndpoint.onOpen failed to proxy {} -> {}", session.getRequestURI(), uri, ex); + UnexpectedException.rethrow(ex); } } @@ -129,6 +126,7 @@ public void onError(Session session, Throwable throwable) public abstract Map> prepareProxyHeaders(URI remoteURI, Map> requestHeaders, Map properties); + @ClientEndpoint class ServerEndpoint extends Endpoint { final Session serverSession; @@ -142,10 +140,10 @@ class ServerEndpoint extends Endpoint final Map> proxyHeaders = prepareProxyHeaders(remoteURI, requestHeaders, properties); // Log what the subclass's prepareProxyHeaders returned - LOG.info("=== WebSocket proxy: proxyHeaders from prepareProxyHeaders ==="); + LOG.trace("=== WebSocket proxy: proxyHeaders from prepareProxyHeaders ==="); for (Map.Entry> entry : proxyHeaders.entrySet()) { - LOG.info(" proxyHeaders: " + entry.getKey() + " = " + entry.getValue()); + LOG.trace(" proxyHeaders: {} = {}", entry.getKey(), entry.getValue()); } WebSocketContainer clientEndPoint = ContainerProvider.getWebSocketContainer(); @@ -156,62 +154,23 @@ class ServerEndpoint extends Endpoint public void beforeRequest(Map> headers) { // Log incoming browser headers for debugging - LOG.info("=== WebSocket proxy: browser request headers ==="); + LOG.trace("=== WebSocket proxy: browser request headers ==="); for (Map.Entry> entry : requestHeaders.entrySet()) { - LOG.info(" Browser header: " + entry.getKey() + " = " + entry.getValue()); + LOG.trace(" Browser header: {} = {}", entry.getKey(), entry.getValue()); } headers.putAll(proxyHeaders); - // Tomcat 11 requires these headers for WebSocket upgrade, but they may be filtered out by some proxy code - headers.put("Upgrade", List.of("websocket")); - headers.put("Connection", List.of("upgrade")); - - // Pass through Sec-WebSocket-Protocol if present (for subprotocol negotiation) - // NOTE: Do NOT copy Sec-WebSocket-Extensions - Tomcat 11 changed how extension negotiation works - // and copying browser extensions can cause the handshake to fail. - // Also do NOT copy Sec-WebSocket-Key - each connection needs its own unique key. - // Sec-WebSocket-Version is set by Tomcat's client. - for (Map.Entry> entry : requestHeaders.entrySet()) - { - String name = entry.getKey(); - if (name.equalsIgnoreCase("Sec-WebSocket-Protocol")) - { - headers.put("Sec-WebSocket-Protocol", entry.getValue()); - } - } - - // Ensure User-Agent is set - Tomcat's WebSocket client doesn't send one by default, - // and some servers (like RStudio) check for browser-like User-Agent - if (!headers.containsKey("User-Agent")) - { - // Try to get from browser request headers (may be lowercase) - for (Map.Entry> entry : requestHeaders.entrySet()) - { - if (entry.getKey().equalsIgnoreCase("User-Agent")) - { - headers.put("User-Agent", entry.getValue()); - break; - } - } - } - - // Log outgoing headers to backend server - LOG.info("=== WebSocket proxy: headers being sent to backend (" + remoteURI + ") ==="); - for (Map.Entry> entry : headers.entrySet()) - { - LOG.info(" Backend header: " + entry.getKey() + " = " + entry.getValue()); - } } @Override public void afterResponse(HandshakeResponse hr) { // Log response headers from backend server for debugging - LOG.info("=== WebSocket proxy: response headers from backend ==="); + LOG.trace("=== WebSocket proxy: response headers from backend ==="); for (Map.Entry> entry : hr.getHeaders().entrySet()) { - LOG.info(" Response header: " + entry.getKey() + " = " + entry.getValue()); + LOG.trace(" Response header: {} = {}", entry.getKey(), entry.getValue()); } } })