You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2026/10/18 10:51:18 UTC
(struts) 20/22: WW-3576 fix(core): keep the bound SessionMap attached while async is started
This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch feature/WW-3576-session-map-staleness
in repository https://gitbox.apache.org/repos/asf/struts.git
commit d313368985f2962494bbe16378f365a791b14ef5
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Sun Oct 18 12:37:46 2026 +0200
WW-3576 fix(core): keep the bound SessionMap attached while async is started
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
.../struts2/dispatcher/PrepareOperations.java | 12 +++++---
.../struts2/dispatcher/PrepareOperationsTest.java | 32 ++++++++++++++++++++++
...6-10-18-WW-3576-session-map-staleness-design.md | 2 +-
3 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
index 4d07c211a..2cc8871a1 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
@@ -75,10 +75,7 @@ public class PrepareOperations {
try {
dispatcher.cleanUpRequest(request);
} finally {
- if (!request.isAsyncStarted()) {
- SessionMap.detachAll(request);
- }
- detachSessionMap();
+ detachSessionMaps(request);
ActionContext.clear();
Dispatcher.clearInstance();
ContainerHolder.clear();
@@ -87,6 +84,13 @@ public class PrepareOperations {
});
}
+ private static void detachSessionMaps(HttpServletRequest request) {
+ if (!request.isAsyncStarted()) {
+ SessionMap.detachAll(request);
+ detachSessionMap();
+ }
+ }
+
private static void detachSessionMap() {
ActionContext context = ActionContext.getContext();
if (context != null && context.getSession() instanceof SessionMap sessionMap) {
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java
index 105017f0e..2b320cbe0 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java
@@ -135,6 +135,38 @@ public class PrepareOperationsTest extends StrutsInternalTestCase {
assertNull(req.getSession(false));
}
+ public void testRequestCleanupKeepsTheBoundSessionMapAttachedWhileAsyncStarted() {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.setAsyncStarted(true);
+ SessionMap sessionMap = new SessionMap(req);
+ ActionContext.getContext().withSession(sessionMap);
+ PrepareOperations prepare = new PrepareOperations(dispatcher);
+ prepare.trackRecursion(req);
+
+ prepare.cleanupRequest(req);
+
+ sessionMap.put("key", "value");
+ assertNotNull(req.getSession(false));
+ }
+
+ public void testBoundSessionMapIsDetachedByTheAsyncDispatchCleanup() {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.setAsyncStarted(true);
+ SessionMap sessionMap = new SessionMap(req);
+ ActionContext.getContext().withSession(sessionMap);
+ PrepareOperations prepare = new PrepareOperations(dispatcher);
+ prepare.trackRecursion(req);
+ prepare.cleanupRequest(req);
+ req.setAsyncStarted(false);
+ ActionContext.of().withSession(sessionMap).bind();
+ prepare.trackRecursion(req);
+
+ prepare.cleanupRequest(req);
+
+ assertThrows(IllegalStateException.class, () -> sessionMap.put("key", "value"));
+ assertNull(req.getSession(false));
+ }
+
public void testRequestCleanupWithoutActionContext() {
HttpServletRequest req = new MockHttpServletRequest();
PrepareOperations prepare = new PrepareOperations(dispatcher);
diff --git a/docs/superpowers/specs/2026-10-18-WW-3576-session-map-staleness-design.md b/docs/superpowers/specs/2026-10-18-WW-3576-session-map-staleness-design.md
index fbaaaebd4..0e79e03cd 100644
--- a/docs/superpowers/specs/2026-10-18-WW-3576-session-map-staleness-design.md
+++ b/docs/superpowers/specs/2026-10-18-WW-3576-session-map-staleness-design.md
@@ -65,7 +65,7 @@ when that session dies (request threads), and one map that is stale *by design*
- **Detached at request end (decided after the final review):** the request cleanup in the dispatcher
detaches every `SessionMap` built for that request (each registers itself on the request at
construction), not only the bound context's: this covers the original map left behind when
- `createSession` replaces `ActionContext.SESSION`, and the maps of a nested forward. Detach is deferred while the request is in async mode, until the
+ `createSession` replaces `ActionContext.SESSION`, and the maps of a nested forward. Detach (of all request maps, including the bound context's) is deferred while the request is in async mode, until the
async dispatch's cleanup. A detached map never calls its request again: with a live
pinned session it keeps working (async `Callable`s, stored `SessionAware` fields); a `put` with no live
pinned session throws `IllegalStateException`. Reason: a background thread (sandbox escape, custom