(tomcat) 01/03: Handle HTTP sessionID changes for WsSession close on HTTP session expiry

[email protected]
Newsgroups gmane.comp.jakarta.tomcat.devel
Message-ID <[email protected]>
This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 4b41a73a2f1a16647d7444ad6ee87d41a3ec414b
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Aug 7 12:38:32 2026 +0100

    Handle HTTP sessionID changes for WsSession close on HTTP session expiry
---
 .../apache/catalina/ha/session/DeltaSession.java   |  74 ++++++-----
 java/org/apache/tomcat/websocket/WsSession.java    |  14 +-
 .../server/WsHttpSessionBindingListener.java       |  61 +++++++++
 .../websocket/server/WsHttpUpgradeHandler.java     |  16 +--
 java/org/apache/tomcat/websocket/server/WsSci.java |   1 -
 .../tomcat/websocket/server/WsServerContainer.java | 121 +++++++++++++-----
 .../tomcat/websocket/server/WsSessionListener.java |  36 ------
 .../tomcat/websocket/TestWebSocketFrameClient.java | 142 ++++++++++++++++++++-
 webapps/docs/changelog.xml                         |   5 +
 9 files changed, 355 insertions(+), 115 deletions(-)

diff --git a/java/org/apache/catalina/ha/session/DeltaSession.java b/java/org/apache/catalina/ha/session/DeltaSession.java
index f3f751fecd..0549d599c7 100644
--- a/java/org/apache/catalina/ha/session/DeltaSession.java
+++ b/java/org/apache/catalina/ha/session/DeltaSession.java
@@ -116,11 +116,11 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     }
 
     /**
-     * Create a DeltaRequest instance. This protected method enables subclasses to override and use
-     * custom DeltaRequest implementations.
+     * Create a DeltaRequest instance. This protected method enables subclasses to override and use custom DeltaRequest
+     * implementations.
      *
-     * @param sessionId         Session identifier
-     * @param recordAllActions  Record all actions, including duplicates
+     * @param sessionId        Session identifier
+     * @param recordAllActions Record all actions, including duplicates
      *
      * @return New DeltaRequest instance
      */
@@ -313,8 +313,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set the maximum inactive interval.
      *
-     * @param interval          Max inactive interval in seconds
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param interval        Max inactive interval in seconds
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setMaxInactiveInterval(int interval, boolean addDeltaRequest) {
         super.maxInactiveInterval = interval;
@@ -336,8 +336,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set the new flag.
      *
-     * @param isNew             New flag value
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param isNew           New flag value
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setNew(boolean isNew, boolean addDeltaRequest) {
         super.setNew(isNew);
@@ -359,8 +359,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set the session principal.
      *
-     * @param principal         Session principal
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param principal       Session principal
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setPrincipal(Principal principal, boolean addDeltaRequest) {
         lockInternal();
@@ -382,8 +382,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set the authentication type.
      *
-     * @param authType          Authentication type
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param authType        Authentication type
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void setAuthType(String authType, boolean addDeltaRequest) {
         lockInternal();
@@ -446,8 +446,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Expire this session.
      *
-     * @param notify          Whether to notify session listeners
-     * @param notifyCluster   Whether to notify the cluster of expiration
+     * @param notify        Whether to notify session listeners
+     * @param notifyCluster Whether to notify the cluster of expiration
      */
     public void expire(boolean notify, boolean notifyCluster) {
 
@@ -519,8 +519,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Add a session listener.
      *
-     * @param listener          Session listener to add
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param listener        Session listener to add
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void addSessionListener(SessionListener listener, boolean addDeltaRequest) {
         lockInternal();
@@ -542,8 +542,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Remove a session listener.
      *
-     * @param listener          Session listener to remove
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param listener        Session listener to remove
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     public void removeSessionListener(SessionListener listener, boolean addDeltaRequest) {
         lockInternal();
@@ -698,9 +698,9 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Remove an attribute from this session.
      *
-     * @param name              Attribute name
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      *
      * @throws IllegalStateException If this session is no longer valid
      */
@@ -720,10 +720,10 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set an attribute on this session.
      *
-     * @param name              Attribute name
-     * @param value             Attribute value
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param value           Attribute value
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      *
      * @throws IllegalArgumentException If name is null
      */
@@ -743,7 +743,13 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
         lockInternal();
         try {
             super.setAttribute(name, value, notify);
-            if (addDeltaRequest && !exclude(name, value)) {
+            /*
+             * It is possible that the session expires concurrently with the attribute being added. Depending on the
+             * exact timing, one of two things will happen. Either an IllegalStateException will be thrown or the
+             * attribute will be added and then immediately removed from the session. The exception will be re-thrown.
+             * If the attribute is removed, don't update the deltaRequest.
+             */
+            if (getAttribute(name) != null && addDeltaRequest && !exclude(name, value)) {
                 deltaRequest.setAttribute(name, value);
             }
         } finally {
@@ -760,8 +766,8 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Remove a note from this session.
      *
-     * @param name              Note name
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Note name
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     @SuppressWarnings("deprecation")
     public void removeNote(String name, boolean addDeltaRequest) {
@@ -786,9 +792,9 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Set a note on this session.
      *
-     * @param name              Note name
-     * @param value             Note value
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Note name
+     * @param value           Note value
+     * @param addDeltaRequest Whether to add a delta request entry
      */
 @SuppressWarnings("deprecation")
     public void setNote(String name, Object value, boolean addDeltaRequest) {
@@ -993,9 +999,9 @@ public class DeltaSession extends StandardSession implements Externalizable, Clu
     /**
      * Remove an attribute from this session without additional validation.
      *
-     * @param name              Attribute name
-     * @param notify            Whether to notify listeners
-     * @param addDeltaRequest   Whether to add a delta request entry
+     * @param name            Attribute name
+     * @param notify          Whether to notify listeners
+     * @param addDeltaRequest Whether to add a delta request entry
      */
     protected void removeAttributeInternal(String name, boolean notify, boolean addDeltaRequest) {
         lockInternal();
diff --git a/java/org/apache/tomcat/websocket/WsSession.java b/java/org/apache/tomcat/websocket/WsSession.java
index c6ccb180a2..bcf0e123b9 100644
--- a/java/org/apache/tomcat/websocket/WsSession.java
+++ b/java/org/apache/tomcat/websocket/WsSession.java
@@ -372,6 +372,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the instance manager for this session.
+     *
      * @return the instance manager
      */
     public InstanceManager getInstanceManager() {
@@ -553,6 +554,7 @@ public class WsSession implements Session {
 
     /**
      * Checks if the session is closed.
+     *
      * @return true if the session is closed
      */
     public boolean isClosed() {
@@ -767,6 +769,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the session close timeout in milliseconds.
+     *
      * @return the session close timeout
      */
     protected long getSessionCloseTimeout() {
@@ -1038,6 +1041,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the user principal for this session.
+     *
      * @return the user principal
      */
     public Principal getUserPrincipalInternal() {
@@ -1067,6 +1071,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the local endpoint for this session.
+     *
      * @return the local endpoint
      */
     public Endpoint getLocal() {
@@ -1075,9 +1080,13 @@ public class WsSession implements Session {
 
 
     /**
-     * Returns the HTTP session ID associated with this WebSocket session.
+     * Returns the HTTP session ID associated with this WebSocket session at the time the WebSocket session was created.
+     *
      * @return the HTTP session ID, or null if not associated
+     *
+     * @deprecated Unused. Will be removed from Tomcat 12 onwards
      */
+    @Deprecated
     public String getHttpSessionId() {
         return httpSessionId;
     }
@@ -1085,6 +1094,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the text message handler for this session.
+     *
      * @return the text message handler
      */
     protected MessageHandler getTextMessageHandler() {
@@ -1094,6 +1104,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the binary message handler for this session.
+     *
      * @return the binary message handler
      */
     protected MessageHandler getBinaryMessageHandler() {
@@ -1103,6 +1114,7 @@ public class WsSession implements Session {
 
     /**
      * Returns the pong message handler for this session.
+     *
      * @return the pong message handler
      */
     protected MessageHandler.Whole<PongMessage> getPongMessageHandler() {
diff --git a/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java b/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java
new file mode 100644
index 0000000000..94cfabca63
--- /dev/null
+++ b/java/org/apache/tomcat/websocket/server/WsHttpSessionBindingListener.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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.apache.tomcat.websocket.server;
+
+import java.io.Serializable;
+
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+public class WsHttpSessionBindingListener implements HttpSessionBindingListener, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String key;
+
+    public WsHttpSessionBindingListener(String key) {
+        this.key = key;
+    }
+
+
+    public String getKey() {
+        return key;
+    }
+
+
+    @Override
+    public void valueUnbound(HttpSessionBindingEvent event) {
+        HttpSession httpSession = event.getSession();
+        /*
+         * During replication this event will be triggered when the attribute is updated. Updates should not trigger a
+         * call to the WebSocket server container. If this is an update, the session will still be valid.
+         */
+        try {
+            httpSession.getCreationTime();
+            // No exception. Session is valid. Nothing to do.
+            return;
+        } catch (IllegalStateException ise) {
+            // Ignore
+
+        }
+        Object obj = httpSession.getServletContext().getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
+        if (obj instanceof WsServerContainer) {
+            ((WsServerContainer) obj).handleHttpSessionKeyUnbound(key);
+        }
+    }
+}
diff --git a/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
index f3a9326426..bbf0b2339a 100644
--- a/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
+++ b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
@@ -90,14 +90,14 @@ public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
     /**
      * Performs initialization before the WebSocket handshake is completed.
      *
-     * @param serverEndpointConfig the endpoint configuration
-     * @param wsc the WebSocket server container
-     * @param handshakeRequest the handshake request
+     * @param serverEndpointConfig       the endpoint configuration
+     * @param wsc                        the WebSocket server container
+     * @param handshakeRequest           the handshake request
      * @param negotiatedExtensionsPhase2 negotiated extensions
-     * @param subProtocol the negotiated sub-protocol
-     * @param transformation the data transformation
-     * @param pathParameters the path parameters
-     * @param secure whether the connection is secure
+     * @param subProtocol                the negotiated sub-protocol
+     * @param transformation             the data transformation
+     * @param pathParameters             the path parameters
+     * @param secure                     whether the connection is secure
      */
     public void preInit(ServerEndpointConfig serverEndpointConfig, WsServerContainer wsc,
             WsHandshakeRequest handshakeRequest, List<Extension> negotiatedExtensionsPhase2, String subProtocol,
@@ -158,7 +158,7 @@ public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler {
                 }
                 throw new IllegalArgumentException(t);
             }
-            webSocketContainer.registerSession(serverEndpointConfig.getPath(), wsSession);
+            webSocketContainer.registerSession(serverEndpointConfig.getPath(), wsSession, session);
         } catch (DeploymentException e) {
             throw new IllegalArgumentException(e);
         } finally {
diff --git a/java/org/apache/tomcat/websocket/server/WsSci.java b/java/org/apache/tomcat/websocket/server/WsSci.java
index 7bc2776491..98deee2414 100644
--- a/java/org/apache/tomcat/websocket/server/WsSci.java
+++ b/java/org/apache/tomcat/websocket/server/WsSci.java
@@ -133,7 +133,6 @@ public class WsSci implements ServletContainerInitializer {
 
         servletContext.setAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc);
 
-        servletContext.addListener(new WsSessionListener(sc));
         // Can't register the ContextListener again if the ContextListener is
         // calling this method
         if (initBySciMechanism) {
diff --git a/java/org/apache/tomcat/websocket/server/WsServerContainer.java b/java/org/apache/tomcat/websocket/server/WsServerContainer.java
index 93e6469f1c..d1b2abaa39 100644
--- a/java/org/apache/tomcat/websocket/server/WsServerContainer.java
+++ b/java/org/apache/tomcat/websocket/server/WsServerContainer.java
@@ -20,6 +20,8 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -32,6 +34,7 @@ import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 import javax.websocket.CloseReason;
 import javax.websocket.CloseReason.CloseCodes;
 import javax.websocket.DeploymentException;
@@ -71,7 +74,9 @@ public class WsServerContainer extends WsWebSocketContainer implements ServerCon
     @SuppressWarnings("deprecation")
     private volatile boolean enforceNoAddAfterHandshake = org.apache.tomcat.websocket.Constants.STRICT_SPEC_COMPLIANCE;
     private volatile boolean addAllowed = true;
-    private final Map<String,Set<WsSession>> authenticatedSessions = new ConcurrentHashMap<>();
+    private final Object authenticatedSessionMapLock = new Object();
+    private final Map<String,Set<WsSession>> httpSessionKeyToWebSocketSession = new HashMap<>();
+    private final Map<WsSession,String> webSocketSessionToHttpSessionKey = new HashMap<>();
     private volatile boolean endpointsRegistered = false;
     private volatile boolean deploymentFailed = false;
 
@@ -338,7 +343,9 @@ public class WsServerContainer extends WsWebSocketContainer implements ServerCon
 
     /**
      * Finds the endpoint configuration that matches the given path.
+     *
      * @param path the URI path to match
+     *
      * @return the mapping result, or null if no match is found
      */
     public WsMappingResult findMapping(String path) {
@@ -409,6 +416,7 @@ public class WsServerContainer extends WsWebSocketContainer implements ServerCon
 
     /**
      * Returns the write timeout handler.
+     *
      * @return the write timeout handler
      */
     protected WsWriteTimeout getTimeout() {
@@ -425,14 +433,10 @@ public class WsServerContainer extends WsWebSocketContainer implements ServerCon
     }
 
 
-    /**
-     * {@inheritDoc} Overridden to make it visible to other classes in this package.
-     */
-    @Override
-    protected void registerSession(Object key, WsSession wsSession) {
+    protected void registerSession(Object key, WsSession wsSession, Object httpSession) {
         super.registerSession(key, wsSession);
-        if (wsSession.isOpen() && wsSession.getUserPrincipal() != null && wsSession.getHttpSessionId() != null) {
-            registerAuthenticatedSession(wsSession, wsSession.getHttpSessionId());
+        if (wsSession.isOpen() && wsSession.getUserPrincipal() != null && httpSession != null) {
+            registerAuthenticatedSession(wsSession, (HttpSession) httpSession);
         }
     }
 
@@ -442,53 +446,106 @@ public class WsServerContainer extends WsWebSocketContainer implements ServerCon
      */
     @Override
     protected void unregisterSession(Object key, WsSession wsSession) {
-        if (wsSession.getUserPrincipalInternal() != null && wsSession.getHttpSessionId() != null) {
-            unregisterAuthenticatedSession(wsSession, wsSession.getHttpSessionId());
+        if (wsSession.getUserPrincipalInternal() != null) {
+            unregisterAuthenticatedSession(wsSession);
         }
         super.unregisterSession(key, wsSession);
     }
 
 
-    private void registerAuthenticatedSession(WsSession wsSession, String httpSessionId) {
-        Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
-        if (wsSessions == null) {
-            wsSessions = ConcurrentHashMap.newKeySet();
-            authenticatedSessions.putIfAbsent(httpSessionId, wsSessions);
-            wsSessions = authenticatedSessions.get(httpSessionId);
+    private void registerAuthenticatedSession(WsSession wsSession, HttpSession httpSession) {
+        boolean mustCloseWsSession = false;
+        String httpSessionKey = null;
+
+        synchronized (authenticatedSessionMapLock) {
+            try {
+                boolean mustAddSessionAttribute = false;
+                WsHttpSessionBindingListener listener = (WsHttpSessionBindingListener) httpSession
+                        .getAttribute(WsHttpSessionBindingListener.class.getCanonicalName());
+                if (listener == null) {
+                    httpSessionKey = httpSession.getId();
+                    mustAddSessionAttribute = true;
+                } else {
+                    httpSessionKey = listener.getKey();
+                }
+                if (mustAddSessionAttribute) {
+                    /*
+                     * It is possible that the session expires concurrently with the attribute being added. Depending on
+                     * the exact timing, one of two things will happen. Either an IllegalStateException will be thrown
+                     * or the attribute will be added and then immediately removed from the session. Handle both of
+                     * these scenarios here.
+                     */
+                    httpSession.setAttribute(WsHttpSessionBindingListener.class.getCanonicalName(),
+                            new WsHttpSessionBindingListener(httpSessionKey));
+                    if (httpSession.getAttribute(WsHttpSessionBindingListener.class.getCanonicalName()) == null) {
+                        mustCloseWsSession = true;
+                    }
+                }
+            } catch (IllegalStateException ise) {
+                // Failing to set the attribute indicates that the session has already expired
+                mustCloseWsSession = true;
+            }
+
+            if (!mustCloseWsSession) {
+                Set<WsSession> wsSessions = httpSessionKeyToWebSocketSession.get(httpSessionKey);
+                if (wsSessions == null) {
+                    wsSessions = new HashSet<>();
+                    httpSessionKeyToWebSocketSession.put(httpSessionKey, wsSessions);
+                }
+                wsSessions.add(wsSession);
+                webSocketSessionToHttpSessionKey.put(wsSession, httpSessionKey);
+            }
+        }
+
+        if (mustCloseWsSession) {
+            closeAuthenticatedWebSocketSession(wsSession);
         }
-        wsSessions.add(wsSession);
     }
 
 
-    private void unregisterAuthenticatedSession(WsSession wsSession, String httpSessionId) {
-        Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
-        // wsSessions will be null if the HTTP session has ended
-        if (wsSessions != null) {
-            wsSessions.remove(wsSession);
+    private void unregisterAuthenticatedSession(WsSession wsSession) {
+        synchronized (authenticatedSessionMapLock) {
+            String httpSessionKey = webSocketSessionToHttpSessionKey.remove(wsSession);
+            if (httpSessionKey != null) {
+                Set<WsSession> wsSessions = httpSessionKeyToWebSocketSession.get(httpSessionKey);
+                if (wsSessions != null) {
+                    wsSessions.remove(wsSession);
+                }
+            }
         }
     }
 
 
     /**
      * Closes all WebSocket sessions associated with the given authenticated HTTP session.
-     * @param httpSessionId the HTTP session ID
+     *
+     * @param httpSessionKey the HTTP session key
      */
-    public void closeAuthenticatedSession(String httpSessionId) {
-        Set<WsSession> wsSessions = authenticatedSessions.remove(httpSessionId);
+    public void handleHttpSessionKeyUnbound(String httpSessionKey) {
+        Set<WsSession> wsSessions;
+
+        synchronized (authenticatedSessionMapLock) {
+            wsSessions = httpSessionKeyToWebSocketSession.remove(httpSessionKey);
+        }
 
-        if (wsSessions != null && !wsSessions.isEmpty()) {
+        if (wsSessions != null) {
             for (WsSession wsSession : wsSessions) {
-                try {
-                    wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
-                } catch (IOException ignore) {
-                    // Any IOExceptions during close will have been caught and the
-                    // onError method called.
-                }
+                closeAuthenticatedWebSocketSession(wsSession);
             }
         }
     }
 
 
+    private void closeAuthenticatedWebSocketSession(WsSession wsSession) {
+        try {
+            wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
+        } catch (IOException ignore) {
+            // Any IOExceptions during close will have been caught and the
+            // onError method called.
+        }
+    }
+
+
     private static void validateEncoders(Class<? extends Encoder>[] encoders, InstanceManager instanceManager)
             throws DeploymentException {
 
diff --git a/java/org/apache/tomcat/websocket/server/WsSessionListener.java b/java/org/apache/tomcat/websocket/server/WsSessionListener.java
deleted file mode 100644
index afa55cf183..0000000000
--- a/java/org/apache/tomcat/websocket/server/WsSessionListener.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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
- *
- *     http://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.apache.tomcat.websocket.server;
-
-import javax.servlet.http.HttpSessionEvent;
-import javax.servlet.http.HttpSessionListener;
-
-public class WsSessionListener implements HttpSessionListener {
-
-    private final WsServerContainer wsServerContainer;
-
-
-    public WsSessionListener(WsServerContainer wsServerContainer) {
-        this.wsServerContainer = wsServerContainer;
-    }
-
-
-    @Override
-    public void sessionDestroyed(HttpSessionEvent se) {
-        wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
-    }
-}
diff --git a/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
index 50282f9dbf..663944104d 100644
--- a/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
+++ b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java
@@ -16,17 +16,24 @@
  */
 package org.apache.tomcat.websocket;
 
+import java.io.IOException;
 import java.net.URI;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import javax.websocket.ClientEndpointConfig;
 import javax.websocket.ClientEndpointConfig.Configurator;
 import javax.websocket.ContainerProvider;
+import javax.websocket.HandshakeResponse;
 import javax.websocket.Session;
 import javax.websocket.WebSocketContainer;
 
@@ -37,10 +44,12 @@ import org.apache.catalina.Context;
 import org.apache.catalina.authenticator.AuthenticatorBase;
 import org.apache.catalina.servlets.DefaultServlet;
 import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.descriptor.web.LoginConfig;
 import org.apache.tomcat.util.descriptor.web.SecurityCollection;
 import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
 import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText;
+import org.apache.tomcat.websocket.TesterMessageCountClient.TesterEndpoint;
 import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint;
 
 public class TestWebSocketFrameClient extends WebSocketBaseTest {
@@ -64,10 +73,10 @@ public class TestWebSocketFrameClient extends WebSocketBaseTest {
         WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer();
 
         // BZ 62596
-        ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create()
-                .configurator(new Configurator() {
+        ClientEndpointConfig clientEndpointConfig =
+                ClientEndpointConfig.Builder.create().configurator(new Configurator() {
                     @Override
-                    public void beforeRequest(Map<String, List<String>> headers) {
+                    public void beforeRequest(Map<String,List<String>> headers) {
                         headers.put("Dummy",
                                 Collections.singletonList(String.join("", Collections.nCopies(4000, "A"))));
                         super.beforeRequest(headers);
@@ -178,6 +187,133 @@ public class TestWebSocketFrameClient extends WebSocketBaseTest {
         echoTester(URI_PROTECTED, clientEndpointConfig);
     }
 
+    @Test
+    public void testAuthenticatedWebSocketClosedWhenHttpSessionEndsWithoutRotatedSession() throws Exception {
+        doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(false);
+    }
+
+
+    @Test
+    public void testAuthenticatedWebSocketClosedWhenHttpSessionEndsWithRotatedSession() throws Exception {
+        doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(true);
+    }
+
+
+    private void doTestAuthenticatedWebSocketClosedWhenHttpSessionEnds(boolean rotateSessionID) throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+        Context ctx = tomcat.addContext(URI_PROTECTED, null);
+        ctx.addApplicationListener(TesterEchoServer.Config.class.getName());
+        Tomcat.addServlet(ctx, "default", new DefaultServlet());
+        ctx.addServletMapping("/", "default");
+        Tomcat.addServlet(ctx, "invalidate", new HttpServlet() {
+
+            @Override
+            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                req.getSession(false).invalidate();
+            }
+        });
+        ctx.addServletMapping("/invalidate", "invalidate");
+        Tomcat.addServlet(ctx, "changeSessionID", new HttpServlet() {
+
+            @Override
+            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                req.changeSessionId();
+            }
+        });
+        ctx.addServletMapping("/changeSessionID", "changeSessionID");
+
+        SecurityCollection collection = new SecurityCollection();
+        collection.addPattern("/*");
+
+        tomcat.addUser(USER, PWD);
+        tomcat.addRole(USER, ROLE);
+
+        SecurityConstraint sc = new SecurityConstraint();
+        sc.addAuthRole(ROLE);
+        sc.addCollection(collection);
+        ctx.addConstraint(sc);
+
+        LoginConfig lc = new LoginConfig();
+        lc.setAuthMethod("BASIC");
+        ctx.setLoginConfig(lc);
+
+        AuthenticatorBase basicAuthenticator = new org.apache.catalina.authenticator.BasicAuthenticator();
+        basicAuthenticator.setAlwaysUseSession(true);
+        ctx.getPipeline().addValve(basicAuthenticator);
+
+        tomcat.start();
+
+        AtomicReference<String> sessionCookie = new AtomicReference<>();
+        ClientEndpointConfig clientEndpointConfig =
+                ClientEndpointConfig.Builder.create().configurator(new Configurator() {
+
+                    @Override
+                    public void afterResponse(HandshakeResponse hr) {
+                        List<String> cookies = hr.getHeaders().get("Set-Cookie");
+                        if (cookies != null) {
+                            for (String cookie : cookies) {
+                                if (cookie.startsWith("JSESSIONID=")) {
+                                    sessionCookie.set(cookie.split(";", 2)[0]);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }).build();
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, USER);
+        clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, PWD);
+
+        WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer();
+        Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig,
+                new URI("ws://localhost:" + getPort() + URI_PROTECTED + TesterEchoServer.Config.PATH_BASIC));
+
+        CountDownLatch messageLatch = new CountDownLatch(1);
+        BasicText handler = new BasicText(messageLatch);
+        wsSession.addMessageHandler(handler);
+        wsSession.getBasicRemote().sendText("Hello");
+        Assert.assertTrue(messageLatch.await(10, TimeUnit.SECONDS));
+        Assert.assertEquals("Hello", handler.getMessages().poll());
+
+        if (rotateSessionID) {
+            Assert.assertNotNull(sessionCookie.get());
+            Map<String,List<String>> requestHeaders = new HashMap<>();
+            requestHeaders.put("Cookie", List.of(sessionCookie.get()));
+            Map<String,List<String>> responseHeaders = new HashMap<>();
+            int status = getUrl("http://localhost:" + getPort() + URI_PROTECTED + "/changeSessionID", new ByteChunk(),
+                    requestHeaders, responseHeaders);
+            List<String> cookies = responseHeaders.get("Set-Cookie");
+            Assert.assertNotNull(cookies);
+            Assert.assertEquals(1, cookies.size());
+            sessionCookie.set(cookies.get(0).split(";", 2)[0]);
+            Assert.assertEquals(HttpServletResponse.SC_OK, status);
+
+            // CyclicBarrier would be cleaner but that requires a larger refactoring
+            wsSession.removeMessageHandler(handler);
+            CountDownLatch messageLatch2 = new CountDownLatch(1);
+            BasicText handler2 = new BasicText(messageLatch2);
+            wsSession.addMessageHandler(handler2);
+            wsSession.getBasicRemote().sendText("Hello");
+            Assert.assertTrue(messageLatch2.await(10, TimeUnit.SECONDS));
+            Assert.assertEquals("Hello", handler2.getMessages().poll());
+        }
+
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        TesterEndpoint endpoint = (TesterEndpoint) wsSession.getUserProperties().get("endpoint");
+        endpoint.setLatch(closeLatch);
+
+        Assert.assertNotNull(sessionCookie.get());
+        Map<String,List<String>> requestHeaders = new HashMap<>();
+        requestHeaders.put("Cookie", List.of(sessionCookie.get()));
+        int status = getUrl("http://localhost:" + getPort() + URI_PROTECTED + "/invalidate", new ByteChunk(),
+                requestHeaders, null);
+        Assert.assertEquals(HttpServletResponse.SC_OK, status);
+
+        Assert.assertTrue(closeLatch.await(10, TimeUnit.SECONDS));
+        Assert.assertFalse(wsSession.isOpen());
+    }
+
+
     @Test
     public void testConnectToDigestEndpoint() throws Exception {
 
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index e5c8694597..234e599abf 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -288,6 +288,11 @@
         template ends in a variable without a trailing slash, that variable
         might be expanded to the empty string. (markt)
       </fix>
+      <fix>
+        Account for session ID changes when tracking WebSocket connections for
+        closure because they were created under an authenticated HTTP session
+        that has since ended. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Web applications">
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.