Action Chaining, Sessions & Scopes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Struts 7.4.0 (the current 7.x release, verified on 2026-10-08), running on Apache Tomcat 11.0.26 and Java 21; Struts 7 needs Java 17 and Jakarta Servlet 6 at least. The facts were checked against the Struts core-developer guide, the Getting Started tutorial on the HTTP session, the 7.4.0 Javadoc and the 7.4.0 sources (ActionChainResult, ChainingInterceptor, ScopeInterceptor, MessageStoreInterceptor and its pre-result listener, CreateSessionInterceptor, ClearSessionInterceptor, SessionMap), and every example was built into a Maven WAR with the struts2-bom 7.4.0, deployed to a scratch Tomcat 11.0.26 and exercised with curl; the outputs shown are the outputs that run produced. A Struts action lives for one request, so everything that must survive from one request to the next, or from one action to another action in the same request, needs a deliberate carrier: a chain, a redirect with parameters, the HTTP session, the application scope, or a copy of the messages in the session. This page covers those carriers. The running scenario is the Bookshelf catalogue of Part II: a reader adds a book, browses a list with a remembered filter, keeps a reading cart in the session, and is told on the next page whether the form was saved. The pages of this part on actions, interceptors and results are linked, and so are the pages on the value stack, parameter injection, validation, model-driven actions, file upload and tokens.

Where State Can Live

Before a single line of configuration, a map of the carriers helps, because most mistakes with chaining and sessions come from using the wrong one. The first column is the Bookshelf need; the other columns say how long the data lives and who can see it.

Table 1. Carriers for state in a Struts application
Carrier Lives for Visible to Typical Bookshelf use

Action instance fields

One request (one action instance per request)

That action and the view that renders it (through the value stack)

The submitted title and author of the "add book" form

Value stack with chained actions

One request, across several actions

Every action in the chain and the final view

A confirmation page that shows what an earlier action computed

Request parameters of a redirect

The next request only, in the URL

Anyone who sees or edits the URL

confirm?title=Dune after a redirect

Request attributes (#request)

One request (and its forwards and includes)

Code running in that request

The name of the servlet or action that served the page

HTTP session (#session)

Until the session expires or is invalidated (default 30 minutes idle on Tomcat, set in web.xml)

One browser (one cookie)

The reading cart, the logged-in reader, the remembered filter

Application scope (#application)

Until the web application stops

Every user

A shared setting such as the page length

Session-stored messages (store interceptor)

Until the next request that retrieves them

One browser

"Book saved" after a redirect

Servlet concepts of scope (request, session, application) are the ones of the Servlet fundamentals page; Struts exposes the same three scopes as maps. The JSP implicit objects and scopes page shows the same scopes from the JSP side with sessionScope and applicationScope.

flowchart LR subgraph req["One request"] A1[Action A] -->|chain result and chaining interceptor| A2[Action B] A2 --> V[View] end req -->|redirect with parameters| N1[Next request] req -->|session map| S[(HTTP session)] S --> N1 req -->|application map| P[(Application scope)] P --> N1 S -.->|store interceptor| M[Stored messages] M -.-> N1

The Bookshelf Actions Used on This Page

All examples belong to one scratch WAR built from the bookshelf-struts base: struts2-core 7.4.0 from the struts2-bom, log4j-slf4j2-impl, the Servlet and Pages APIs as provided, the filter StrutsPrepareAndExecuteFilter mapped to /* in web.xml (the descriptor of the Struts getting started page), and a package default that extends struts-default in the root namespace plus a package admin in the namespace /admin. The views are plain-text JSPs with session="false" so that a view never creates a session by itself (the reason for that is in the section on sessions). The default extension of 7.4.0 is struts.action.extension=action,,, so /add-chain and /add-chain.action reach the same mapping, and the constants struts.parameters.requireAnnotations (request parameters need @StrutsParameter, see Parameter Injection and @StrutsParameter) and struts.allowlist.enable have their 7.4.0 defaults. The target action of most chain examples is ConfirmAction; its show.jsp prints the properties it finds on the value stack.

public class AddBookAction extends ActionSupport {
    private String title;
    private String author;
    private String newId;

    @Override
    public String execute() {
        newId = "B-" + (title == null ? 0 : title.length());
        addActionMessage("Added " + title);
        return SUCCESS;
    }

    public String getTitle() { return title; }
    @StrutsParameter public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    @StrutsParameter public void setAuthor(String author) { this.author = author; }
    public String getNewId() { return newId; }
    public List<String> getShelves() { return List.of("Fiction", "Science"); }
}

public class ConfirmAction extends ActionSupport {
    private String title;
    private String author;
    private String newId;

    @Override
    public String execute() {
        addActionMessage("Confirm ran");
        return SUCCESS;
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public String getNewId() { return newId; }
    public void setNewId(String newId) { this.newId = newId; }
}

AddBookAction has annotated setters because a request parameter must reach it; ConfirmAction has plain setters because nothing is supposed to reach it from a request. That difference matters in the section on chaining and security. The classes ActionSupport and StrutsParameter are described on the Actions page.

Action Chaining

Action chaining runs a second action inside the same request, after the first one returned a result code. It has two parts that the framework combines: a result type, chain, which starts the second action, and an interceptor, chain, which copies properties from the first action into the second. The Core Developer Guide opens its page with a warning that the rest of this section repeats with evidence: as a rule, action chaining is not recommended, and the Redirect After Post technique should be explored first.

The chain Result

The chain result type, registered in struts-default.xml as org.apache.struts2.result.ActionChainResult, "invokes an entire other action, complete with its own interceptor stack and result". The target is a full action invocation: it runs its interceptors, its method and its result, and the response is whatever the target’s result writes. The result of the first action is never rendered. The parameters of the result are:

Table 2. Parameters of the chain result
Parameter Meaning

actionName (default)

The name of the action to chain to. This is the parameter that the short form <result type="chain">confirm</result> sets

namespace

The namespace of the target; when it is not set, the namespace of the current action

method

A method of the target action to invoke instead of execute; the method must be listed in the target’s allowed-methods

skipActions

Comma-separated action names that the infinite-recursion check ignores (explained below)

The simplest chain uses the default parameter. add-chain runs AddBookAction and, when it returns success, chains to confirm; the second action uses the default stack, which contains the chain interceptor (see the next section).

<package name="default" extends="struts-default" namespace="/">
    <action name="add-chain" class="org.example.bookshelf.AddBookAction">
        <result type="chain">confirm</result>
    </action>
    <action name="confirm" class="org.example.bookshelf.ConfirmAction">
        <result>/WEB-INF/views/show.jsp</result>
    </action>
</package>
$ curl "http://localhost:18080/g9chain/add-chain?title=Dune&author=Herbert"
title=Dune
author=Herbert
newId=B-4
shelves=[Fiction, Science]
approved=
messages=[Confirm ran]
errors=[] fieldErrors={}

The output shows what chaining does. title and author were set by the params interceptor on AddBookAction and copied by the chain interceptor onto ConfirmAction; newId was computed by AddBookAction.execute() and copied too. The line shelves comes from a property that only AddBookAction has: the view found it because the first action stays on the value stack below the second one. And messages shows only Confirm ran: the message Added Dune that the first action added is not copied, because the interceptor does not copy messages, errors or field errors by default.

sequenceDiagram participant B as Browser participant F as StrutsPrepareAndExecuteFilter participant A as add-chain (AddBookAction) participant R as chain result participant C as confirm (ConfirmAction) B->>F: GET /add-chain?title=Dune F->>A: interceptors, params sets title and author, execute returns success A->>R: result code success maps to the chain result R->>C: new ActionProxy, same value stack, same chain history Note over C: chain interceptor copies properties from the stack onto ConfirmAction C->>C: params, validation, execute returns success C->>B: show.jsp rendered, one response, URL still add-chain

The browser’s URL is still /add-chain; there was one request and one response. That is the first of the problems of chaining: a reload repeats the first action (for a form that creates a record, a duplicate), and the address bar names an action whose result the user never sees.

Chaining to Another Namespace or Method

The namespace parameter reaches an action in another package. With the admin package in the namespace /admin, which has its own confirm action, the first action can chain across namespaces:

<action name="add-chain-ns" class="org.example.bookshelf.AddBookAction">
    <result type="chain">
        <param name="actionName">confirm</param>
        <param name="namespace">/admin</param>
    </result>
</action>

<package name="admin" extends="struts-default" namespace="/admin">
    <action name="confirm" class="org.example.bookshelf.ConfirmAction">
        <result>/WEB-INF/views/show.jsp</result>
    </action>
</package>

The run of add-chain-ns?title=Dune&author=Herbert printed the same title, author and newId as the first chain, so the namespace parameter, like the target’s stack, behaves the same way as in the same namespace. See the Packages and namespaces page for how a namespace is chosen.

The method parameter invokes another method of the target. In Struts 7 the method must be allowed on the target, as for any action invocation (the allowed-methods element; the default is execute and the methods are checked at invocation, not only for the request URL). Without it the chain ends in an error page:

<action name="add-chain-method" class="org.example.bookshelf.AddBookAction">
    <result type="chain">
        <param name="actionName">method</param>
        <param name="method">audit</param>
    </result>
</action>
<action name="method" class="org.example.bookshelf.MethodAction">
    <result>/WEB-INF/views/show.jsp</result>
    <allowed-methods>execute,audit</allowed-methods>
</action>

With the allowed-methods element the run answered messages=[audit() ran] (the method audit of MethodAction ran, execute did not). Without the element the same configuration answered HTTP 404 with the message Method audit for action method is not allowed!. See the Wildcard mappings and method invocation page for allowed-methods.

Chain History and Infinite Recursion

The chain result keeps a history of the actions invoked in the current chain (the key CHAIN_HISTORY of the action context, a list of namespace/action!method strings). Before it invokes the target it checks whether the same namespace, action and method is already in the history; if it is, it throws a StrutsException "Infinite recursion detected" with the history. Two actions that chain to each other end like that:

<action name="loop-a" class="org.example.bookshelf.LoopAction">
    <result type="chain">loop-b</result>
</action>
<action name="loop-b" class="org.example.bookshelf.LoopAction">
    <result type="chain">loop-a</result>
</action>

A request for loop-a answered HTTP 500 with the message Infinite recursion detected and the history starting //loop-a!execute. The parameter skipActions is the escape hatch, and its documentation is easy to misread: the Core Developer Guide calls it the list of "actions that could be chained to", but in the 7.4.0 source it is only a list of action names that the recursion check does not apply to (the check compares the action name only, not the namespace). A bounded loop, an action that chains to itself a fixed number of times, needs it:

public class CountAction extends ActionSupport {
    private int round;

    @Override
    public String execute() {
        var request = ServletActionContext.getRequest();
        Integer n = (Integer) request.getAttribute("round");
        round = n == null ? 1 : n + 1;
        request.setAttribute("round", round);
        return round < 3 ? "again" : SUCCESS;
    }

    public int getRound() { return round; }
    public String getTitle() { return "round " + round; }
}
<action name="count" class="org.example.bookshelf.CountAction">
    <result name="again" type="chain">
        <param name="actionName">count</param>
        <param name="skipActions">count</param>
    </result>
    <result>/WEB-INF/views/show.jsp</result>
</action>

With skipActions the request to count printed title=round 3 (three rounds in one request); the same mapping without the parameter, mapped as count-bad, answered HTTP 500 with Infinite recursion detected: [//count-bad!execute, //count-bad, not in `struts.xml.

Chain Only Runs for the chain Result

The chain interceptor does nothing unless an action is already below the current one on the value stack and the result of the current invocation is a chain result (or no result yet): in the 7.4.0 source it copies only when the stack holds more than one object. A request that goes straight to confirm, with no first action, is therefore unaffected, and the interceptor is safe in every stack, which is why defaultStack and paramsPrepareParamsStack contain it. The stack basicStack, and any stack the mapping builds without chain, does not copy:

<action name="add-chain-plain" class="org.example.bookshelf.AddBookAction">
    <result type="chain">confirm-plain</result>
</action>
<action name="confirm-plain" class="org.example.bookshelf.ConfirmAction">
    <interceptor-ref name="basicStack"/>
    <result>/WEB-INF/views/show.jsp</result>
</action>

The run printed title=, author= and newId= empty, and shelves=[Fiction, Science] present: the chain happened (the first action is still on the stack and the view can read shelves from it), but no property was copied, so The own title of ConfirmAction stayed null and OGNL found that null on the top of the stack before it looked below. The distinction between "the first action is on the stack" and "its properties were copied" is the one that explains most surprises with chaining.

The Chaining Interceptor

The chain interceptor (org.apache.struts2.interceptor.ChainingInterceptor) "copies all the properties of every object in the value stack to the currently executing object, except for any object that implements Unchainable`". The 7.4.0 source shows the algorithm: take the value stack’s root, drop the top (the target action itself), reverse the rest so that the oldest object comes first, and copy each object that is not `Unchainable onto the action with the reflection provider. Later objects overwrite earlier ones. The interceptor works for anything on the stack, not only actions: a model pushed by ModelDriven is copied as well.

What Is Copied and What Is Not

The copying goes through the setters of the target and the getters of the source. A property that has a getter on the source and a setter on the target with a compatible name is copied; everything else is skipped silently. Three collections of a ValidationAware action are excluded by default and three constants switch them on:

Table 3. Constants that control the copying of messages
Constant (7.4.0) Default Effect

struts.chaining.copyMessages

false

Copy the action messages of the source actions

struts.chaining.copyErrors

false

Copy the action errors

struts.chaining.copyFieldErrors

false

Copy the field errors

The Core Developer Guide names the constants struts.xwork.chaining.copyErrors, struts.xwork.chaining.copyFieldErrors and struts.xwork.chaining.copyMessages. In 7.4.0 the names are the ones above, struts.chaining.* (the StrutsConstants class has struts.chaining.copyErrors, struts.chaining.copyFieldErrors and struts.chaining.copyMessages). The old names are not an error: a WAR with <constant name="struts.xwork.chaining.copyMessages" value="true"/> started and ran, and add-chain still printed messages=[Confirm ran], so the setting was silently ignored. A WAR with <constant name="struts.chaining.copyMessages" value="true"/> printed messages=[Added Dune, Confirm ran].

Narrowing the Copy: includes and excludes

The interceptor’s parameters includes and excludes limit the properties that are copied; the page warns that giving both has undefined results. The stack that carries them is chainStack, whose reference name is chain (so the parameter prefix is chain.). To copy only the title:

<action name="add-chain-includes" class="org.example.bookshelf.AddBookAction">
    <result type="chain">confirm-includes</result>
</action>
<action name="confirm-includes" class="org.example.bookshelf.ConfirmAction">
    <interceptor-ref name="chainStack">
        <param name="chain.includes">title</param>
    </interceptor-ref>
    <result>/WEB-INF/views/show.jsp</result>
</action>

The run printed title=Dune and empty author and newId. The opposite, chain.excludes set to author,newId, printed the same three lines, and one more line that deserves attention: messages=[Added Dune, Confirm ran]. The message copied although struts.chaining.copyMessages was not set. The reason is in the source: the default exclusion of actionErrors, actionMessages and fieldErrors is built only when the excludes parameter is null. An explicit excludes list replaces the built-in one, so a list that does not name the three properties lets messages and errors through. When excludes is used, add actionErrors, actionMessages and fieldErrors to it unless copying them is the intent.

Unchainable

An object that implements the marker interface org.apache.struts2.Unchainable is never used as a source. A source action that must not leak its properties declares it:

public class UnchainedAction extends AddBookAction implements Unchainable {
}

Chained from add-unchainable?title=Dune&author=Herbert, the target printed empty title, author and newId; its messages were again only Confirm ran.

Chaining and Security

Chaining makes the data path into the target implicit: the target is populated from whatever happens to be on the value stack. Before Struts 7.2, a property of the first action that no request could set, because its setter had no @StrutsParameter, still reached the target through the chain interceptor, which ignored the annotation. Since 7.2.0 the annotation extends to chaining, but only if the constant struts.chaining.requireAnnotations is true; its default is false, so a default application keeps copying every property. The Bookshelf shows the risk with two actions:

public class ApproveAction extends ActionSupport {
    private String title;

    @Override
    public String execute() { return SUCCESS; }

    // a public property that a request parameter could never reach, but the chain can
    public boolean isApproved() { return true; }
    public String getTitle() { return title; }
    @StrutsParameter public void setTitle(String title) { this.title = title; }
}

public class SecretAction extends ActionSupport {
    private boolean approved;
    private String title;

    @Override
    public String execute() { return SUCCESS; }

    public boolean isApproved() { return approved; }
    public void setApproved(boolean approved) { this.approved = approved; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
}
<action name="approve-chain" class="org.example.bookshelf.ApproveAction">
    <result type="chain">secret</result>
</action>
<action name="secret" class="org.example.bookshelf.SecretAction">
    <result>/WEB-INF/views/show.jsp</result>
</action>

A request straight to secret?approved=true&title=Dune printed approved=false and an empty title: the setters have no @StrutsParameter, so the params interceptor refused them. A request to approve-chain?title=Dune printed title=Dune and approved=true: the chain copied approved from the first action’s getter to the target’s setter, which a request could not reach. In an application where the target action trusts approved, that is a privilege path. The guide’s advice, which the example confirms, is to carry no authorization, trust, identity or approval state across a chain: re-derive it from the session or the security context inside the target.

Two defences exist, and they combine. The constant struts.chaining.requireAnnotations set to true (a global constant, there is no per-interceptor override) makes the interceptor copy only the target setters that carry @StrutsParameter:

<constant name="struts.chaining.requireAnnotations" value="true"/>

With the constant, approve-chain?title=Dune printed an empty title and approved=false (nothing was copied, because SecretAction has unannotated setters), add-chain printed no title, author or newId either (ConfirmAction is not annotated), and a chain to an annotated target worked: add-chain-annotated?title=Dune&author=Herbert to the action show-book (which has @StrutsParameter on its setters) printed title=Dune and author=Herbert. The interceptor is fail-closed: if the target class cannot be introspected, nothing is copied. The rejected properties are logged at WARN. The second defence is Unchainable on classes that must never be a source, and includes/excludes to copy only what the target needs.

The remark of the printed newId=B-4 in the annotated run is not a leak: the target has no newId, so the view read it from the first action that is still on the value stack. Chaining always leaves the whole chain visible to the view, whatever the interceptor copies.

Redirect After Post Instead of Chaining

The guide’s recommendation, "explore other options, such as the Redirect After Post technique", has a concrete reason in the Bookshelf. After a POST that adds a book, a chain answers the POST with the confirmation page. The browser’s current page is the result of a POST, so reload and back-and-forth re-submit the form, and the address bar still shows the action that changed the data. A redirect after post ends the POST with a 302 to a GET URL; the browser loads the confirmation page as a new request, and a reload only repeats the GET.

<action name="add-redirect" class="org.example.bookshelf.AddBookAction">
    <result type="redirectAction">
        <param name="actionName">show-book</param>
        <param name="title">${title}</param>
        <param name="author">${author}</param>
    </result>
</action>
<action name="show-book" class="org.example.bookshelf.ShowBookAction">
    <result>/WEB-INF/views/show.jsp</result>
</action>

The first request, add-redirect?title=Dune&author=Herbert, answered 302 with Location: /g9chain/show-book.action?title=Dune&author=Herbert; following the redirect printed title=Dune and author=Herbert. The values that cross the redirect are in the URL, so they are visible and editable, and they go through the params interceptor of the target, which means the target’s setters need @StrutsParameter (ShowBookAction has it). A first version of this example redirected to ConfirmAction, whose setters are not annotated, and the confirmation page printed an empty title: the redirect worked and the parameters were silently ignored. See the Results and result types page for the redirectAction parameters and the redirect result.

Table 4. Chain, redirectAction and dispatcher compared for "show the result of the action that was just run"
Aspect chain redirectAction dispatcher (a JSP result)

Requests

One

Two (the second is a GET)

One

URL after the response

The first action’s URL

The second action’s URL

The first action’s URL

Reload after a POST

Repeats the first action

Repeats only the GET

Repeats the first action

Data that reaches the second step

Properties copied from the value stack, implicit

Request parameters in the URL (visible, bounded) and, optionally, messages stored in the session

Nothing is copied; the view reads the first action

Second step’s interceptors and result

Run in full

Run in full, as a separate request

Not an action, so none

Where trust problems arise

The copy ignores @StrutsParameter unless struts.chaining.requireAnnotations is true

Parameters of the URL go through params with the annotations

None added

The guide gives one legitimate use of chaining: a lookup list, such as the shelves of a dropdown that a form needs. Even there the guide mentions two alternatives, executing an action from the view with the action tag, and the redirect action result. A shared service called from both actions, or a method of the first action, is usually the cleaner design: the guide asks "could the logic in Action2 be pushed back to a support class or business facade so that Action1 can call it too?" and recommends actions that are as short as possible and only call methods.

Sessions and the SessionMap

A web application recognizes a browser across requests with the HTTP session, a server-side object identified by the JSESSIONID cookie (or a rewritten URL when cookies are off). The Servlet API’s HttpSession stores attributes by name; Struts hides it behind a java.util.Map<String, Object> so that an action does not need the servlet API and a unit test can pass a plain HashMap. The map is an instance of org.apache.struts2.dispatcher.SessionMap, which delegates each get, put and remove to the HttpSession.

SessionAware and the Other Aware Interfaces

The way to receive the map is the interface org.apache.struts2.action.SessionAware, which has one method, withSession(Map<String, Object>). The servletConfig interceptor of the default stack calls it before the action runs. Its siblings do the same for the other scopes: ApplicationAware (withApplication), ServletRequestAware (withServletRequest) and ServletResponseAware. The Core Developer Guide recommends these interfaces over ActionContext.getContext().getSession(), which it calls a bad practice.

The Bookshelf’s reading cart is a list in the session. The action, a normal SessionAware class with the four methods the URL pattern cart-* selects:

public class CartAction extends ActionSupport implements SessionAware {
    private Map<String, Object> session;
    private String title;

    @Override
    public void withSession(Map<String, Object> session) {
        this.session = session;
    }

    @SuppressWarnings("unchecked")
    private List<String> cart() {
        return (List<String>) session.computeIfAbsent("cart", k -> new ArrayList<String>());
    }

    public String add() {
        cart().add(title);
        return SUCCESS;
    }

    @Override
    public String execute() { return SUCCESS; }

    public String clear() {
        session.remove("cart");
        return SUCCESS;
    }

    public List<String> getCart() { return cart(); }
    public String getTitle() { return title; }
    @StrutsParameter public void setTitle(String title) { this.title = title; }
    public String getSessionClass() { return session.getClass().getSimpleName(); }
}
<action name="cart-*" class="org.example.bookshelf.CartAction" method="{1}">
    <result>/WEB-INF/views/cart.jsp</result>
    <allowed-methods>add,clear,execute</allowed-methods>
</action>

The view reads the session through the OGNL root name #session: <s:property value="#session.cart"/> in cart.jsp. A run with one cookie jar:

$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=Dune"
cart=[Dune] mapClass=SessionMap
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=Emma"
cart=[Dune, Emma] mapClass=SessionMap
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-clear"
cart= mapClass=SessionMap

The tutorial page uses an older signature, public void setSession(Map<String, Object> session), in its HelloWorldAction and describes an interceptor that injects the session "by calling the setSession method". In 7.4.0 the interface has withSession; a class that still has a setSession method but does not implement the new interface receives nothing. The Aware interfaces and the 6.0 rename are covered on the Actions page.

The Session Is Created Lazily

The SessionMap does not create an HttpSession when it is built. The servlet session is created on the first put (and by anything that calls request.getSession()). The Bookshelf shows the two sides. The action no-session (an action that only reads, rendering a session="false" JSP) answered without a Set-Cookie header; scopes, which calls session.merge("visits", 1, ...), answered with Set-Cookie: JSESSIONID=...; Path=/g9chain; HttpOnly. A JSP without session="false" creates a session of its own on the first request, which is the reason the Bookshelf views switch it off: a page that is only a view must not create a session per visitor (for crawlers, a very large number of them).

The same property decides what happens in the view’s #attr. The Core Developer Guide says that the #attr stack value searches the page, request, session and application scopes in that order. In the Bookshelf a JSP with <%@ page session="false" %> printed attr.visits= (empty) for a value that #session.visits printed as 2, while #attr.servedBy (a request attribute) and #attr.opened (an application attribute) were found; the same view with the default session="true" printed attr.visits=3. The guide does not mention that a JSP that opted out of the session cannot see session attributes through #attr. Use #session.name when the scope matters, which it nearly always does.

public class ScopesAction extends ActionSupport implements SessionAware, ApplicationAware, ServletRequestAware {
    // withSession, withApplication and withServletRequest store the three scopes
    @Override
    public String execute() {
        session.merge("visits", 1, (a, b) -> (Integer) a + (Integer) b);
        application.putIfAbsent("opened", "yes");
        request.setAttribute("servedBy", "ScopesAction");
        return SUCCESS;
    }
}

The Session and Concurrency

The session is shared by every request of one browser: a page that loads three Ajax calls at once runs three actions with three action instances and one session map. The action instance is per request, the session is not, so the usual servlet advice applies to anything stored in the session: store immutable values or thread-safe collections, replace values instead of mutating them in place, and make session objects Serializable so that a server that persists or replicates sessions can handle them. The cart list above is mutated in place; two simultaneous cart-add requests can interleave, and a production cart would use a thread-safe list or a service. The scope interceptor of the next section adds a lock around the session, and no other built-in interceptor does.

Session Lifetime and Fixation

The idle timeout of the session is not a Struts setting; it is session-config in web.xml (the Servlet fundamentals page covers it), with a default of 30 minutes on Tomcat. SessionMap.invalidate() ends the session, and the next put starts a new one with a new identifier. That is the answer to session fixation: when a reader logs in, the identifier the browser sent before the login must not remain valid after it. The LoginAction of the scratch app renews the session when it is asked to:

public class LoginAction extends ActionSupport implements SessionAware, ServletRequestAware {
    // fields and the two with-methods omitted
    @Override
    public String execute() {
        if (renew) {
            if (session instanceof SessionMap sm) {
                sm.invalidate();
            }
        }
        session.put("user", user);
        return SUCCESS;
    }
}

Two requests to login?user=ann printed the same session id (303C73...), and login?user=ann&renew=true printed a different one (1577A5...); the user attribute survived because it was put after the invalidation. In a real application the renewal is done with request.changeSessionId() or by the container’s authentication (the Servlet fundamentals page), and the attribute holds a user id, never a password.

The Exposed Session Trap

The Getting Started tutorial on the HTTP session ends with a security warning that is easy to skip. An action that implements SessionAware must not have a public getSession(): if the session map is reachable by name from a request, a request can write into it, because the params interceptor sets nested properties by their path. The tutorial tells the reader to do two things: omit the getter, and have the action implement ParameterNameAware (refusing names that contain session or request), or, for a whole package, set params.excludeParams on the default stack. The scratch app reproduces the dangerous shape with a getter that the annotation allows:

public class ExposedSessionAction extends ActionSupport implements SessionAware {
    private Map<String, Object> session;

    @Override
    public void withSession(Map<String, Object> session) { this.session = session; }

    @Override
    public String execute() { return SUCCESS; }

    // the classic mistake: a getter for the session map, here even annotated for parameter binding
    @StrutsParameter(depth = 1)
    public Map<String, Object> getSession() { return session; }

    // the same map under another name, annotated
    @StrutsParameter(depth = 1)
    public Map<String, Object> getStore() { return session; }
}

Against a browser that had logged in as ann, the request exposed?session.admin=true did nothing (the default parameter exclusion patterns of the params interceptor are the likely reason, since the same annotated getter under another name was open), but exposed?store.admin=true wrote admin=true into the real session: the next request to login printed admin=true. The defence by name covers only the exact words in the pattern; a getter that returns the map under another name is open. The rule is therefore two parts. Never expose the session map, or any other scope map, through a public getter, and keep @StrutsParameter away from anything whose type is a Map of session data. In 7.x the annotation is the second line of defence, because without it the request parameter does not reach the property at all; the example needed it to be reachable. The Interceptors page shows how the parameters interceptor’s exclusion patterns are replaced, not extended, when they are overridden.

createSession and clearSession

Two small interceptors, in struts-default.xml but in no built-in stack, manage the session as a whole.

Table 5. The createSession and clearSession interceptors
Interceptor What it does When it is used

createSession (CreateSessionInterceptor)

If request.getSession(false) is null, creates the HttpSession and puts a fresh SessionMap into the action context. No parameters

Before a result that writes the response without being able to start a session later, notably a FreeMarker template with the s:token tag, because FreeMarker commits the response immediately (the guide’s reason). The anti-double-submit pages explain the token

clearSession (ClearSessionInterceptor)

Calls clear() on the SessionMap. No parameters. It removes the attributes, not the session itself (the identifier stays)

A "log out" or "start over" action; for a real logout also invalidate the session

<action name="cart-session-created" class="org.example.bookshelf.CartAction">
    <interceptor-ref name="createSession"/>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/cart.jsp</result>
</action>
<action name="clear-session" class="org.example.bookshelf.CartAction">
    <interceptor-ref name="clearSession"/>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/cart.jsp</result>
</action>

The first request to cart-session-created with no cookie answered Set-Cookie: JSESSIONID=...; Path=/g9chain; HttpOnly although the action wrote nothing to the session; after two cart-add requests, clear-session printed an empty cart, and the session identifier was unchanged (the cookie jar kept the same JSESSIONID, and no new Set-Cookie was sent). The guide’s page for cookieProvider repeats the sentence "This interceptor clears the HttpSession" by mistake (an inconsistency noted on the interceptors page); the sentence is correct only on the clearSession page.

The Scope Interceptor

A session map handled by hand needs the same code in every action: read a value when the action starts, write it back when it ends. The scope interceptor (ScopeInterceptor) does that for named properties of the action. The guide introduces it for "wizard-like functionality", and its two motivating cases are application-wide parameters such as the page length of a list ("pageLen"), and values that must be remembered for the next step: a filter, a sort order, the half-filled form of a wizard.

How It Works

Before the action runs, the interceptor sets the listed properties of the action from values stored in the session (or in the application map). After the action has run, and before the result is rendered, it reads the properties back from the value stack and stores them. It uses a pre-result listener for the second step, so the saved state is the state the view is about to show, including values changed by interceptors that run later in the stack. The session is locked around the action so that two requests of the same session do not interleave their reads and writes; the lock is for the session, not the application map, and the interceptor throws a StrutsException "Deadlock in session lock" if the lock is not obtained after about 30 seconds of waiting.

flowchart TD S[Request arrives] --> L[scope interceptor locks the session] L --> R1[Set listed properties from application map] R1 --> Q{session.reset parameter present?} Q -->|yes| X[Skip restoring session values] Q -->|no| T{type is start?} T -->|yes| X T -->|no| R2[Set listed properties from session map] R2 --> A[Rest of the stack and the action run] X --> A A --> P[Pre-result listener: read the properties from the value stack] P --> W{type is end?} W -->|yes| D[Remove the session keys] W -->|no| K[Store the values under key plus property name] D --> U[Unlock the session, render the result] K --> U

Parameters

Table 6. Parameters of the scope interceptor
Parameter Meaning (checked against the 7.4.0 source)

session

Comma-separated action properties to keep in the session

application

Comma-separated action properties to keep in the application scope (shared by every user)

key

Prefix of the attribute names. CLASS (the default) uses the action class: struts.ScopeInterceptor:class org.example.bookshelf.BrowseAction; ACTION uses the namespace and action name: struts.ScopeInterceptor:/:browse-action-key; any other value is taken literally, so two actions with the same literal key share their values

type

start resets the session-scoped properties (they are not restored; the action’s defaults are saved), end removes them from the session after the action. Any other value: see the pitfall below

sessionReset

The name of the request parameter that behaves like type="start". Default: session.reset

reset

true has the same effect for every request (default false)

autoCreateSession

Whether to create the session if none exists (the guide gives no default; the source says true, and see below for how little it matters)

A Remembered Filter and a Shared Page Length

The Bookshelf’s list page has a filter, an orderBy and a pageLen. The reader’s filter and orderBy should survive from one visit to the list to the next; the page length is a setting that, for the sake of the example, is shared by all users, so it goes to the application map.

public class BrowseAction extends ActionSupport {
    private String filter = "all";
    private String orderBy = "title";
    private int pageLen = 10;

    @Override
    public String execute() { return SUCCESS; }

    public String getFilter() { return filter; }
    @StrutsParameter public void setFilter(String filter) { this.filter = filter; }
    public String getOrderBy() { return orderBy; }
    @StrutsParameter public void setOrderBy(String orderBy) { this.orderBy = orderBy; }
    public int getPageLen() { return pageLen; }
    @StrutsParameter public void setPageLen(int pageLen) { this.pageLen = pageLen; }
}
<action name="browse-before" class="org.example.bookshelf.BrowseAction">
    <interceptor-ref name="scope">
        <param name="session">filter,orderBy</param>
        <param name="application">pageLen</param>
        <param name="autoCreateSession">true</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/browse.jsp</result>
</action>

The run, with one cookie jar, and the last request from a second browser that has no cookie:

$ curl -c jar -b jar "http://localhost:18080/g9chain/browse-before?filter=sci&orderBy=author&pageLen=25"
filter=sci orderBy=author pageLen=25 session={struts.ScopeInterceptor:class org.example.bookshelf.BrowseActionfilter=sci, ...orderBy=author}
$ curl -c jar -b jar "http://localhost:18080/g9chain/browse-before"
filter=sci orderBy=author pageLen=25 session={...filter=sci, ...orderBy=author}
$ curl "http://localhost:18080/g9chain/browse-before"
filter=all orderBy=title pageLen=25 session={...filter=all, ...orderBy=title}

The second request, without parameters, got sci and author from the session. The third, from a browser with no session, got the action’s defaults all and title for the session-scoped properties, and 25 for pageLen: that was stored in the application scope by the first browser and is now the page length of everyone. Application scope has no per-user separation and no lock; a value there is global state, which is the reason for the guide’s note that application level consistency is not guaranteed. The keys in the session map are the key prefix and the property name glued together, with no separator: ...BrowseActionfilter.

Order in the Stack Matters

The placement of scope relative to defaultStack changes the behaviour. The first mapping above puts scope before the default stack: the interceptor restores the stored values first, then the params interceptor of the default stack applies the request parameters on top, so a new request parameter wins and is saved at the end. The mapping browse-after puts it after:

<action name="browse-after" class="org.example.bookshelf.BrowseAction">
    <interceptor-ref name="defaultStack"/>
    <interceptor-ref name="scope">
        <param name="session">filter,orderBy</param>
        <param name="autoCreateSession">true</param>
    </interceptor-ref>
    <result>/WEB-INF/views/browse.jsp</result>
</action>

After one request with filter=sci&orderBy=author, a request with filter=newval printed filter=sci: the request parameter was applied, then the interceptor overwrote it with the stored value. The guide’s first example lists basicStack, hibernate and scope, in that order, which has the same effect for the parameters in basicStack; the guide does not say so. Put scope before the interceptor that fills the action from the request when the request should win, which is the usual case (the reader changes the filter), and after it only when stored values must be authoritative.

Reset and Wizards

A request parameter named session.reset (the sessionReset parameter) skips the restore and saves the action’s current values, so the stored values return to the defaults. After browse-before?session.reset=1 the output was filter=all orderBy=title and the following plain request printed the same, so the reset stuck. The parameter type gives the same mechanism a start and an end, for the multi-step form of a wizard. The Bookshelf’s "add a book in three steps" stores title, author and shelf across three actions that share the key wizard:

<action name="wizard-1" class="org.example.bookshelf.WizardAction">
    <interceptor-ref name="scope">
        <param name="key">wizard</param>
        <param name="session">title,author,shelf</param>
        <param name="type">start</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/wizard.jsp</result>
</action>
<action name="wizard-2" class="org.example.bookshelf.WizardAction">
    <interceptor-ref name="scope">
        <param name="key">wizard</param>
        <param name="session">title,author,shelf</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/wizard.jsp</result>
</action>
<action name="wizard-3" class="org.example.bookshelf.WizardAction">
    <interceptor-ref name="scope">
        <param name="key">wizard</param>
        <param name="session">title,author,shelf</param>
        <param name="type">end</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/wizard.jsp</result>
</action>
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-1?title=Dune&author=Herbert&shelf=SF"
title=Dune author=Herbert shelf=SF session={wizardshelf=SF, wizardauthor=Herbert, wizardtitle=Dune, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2"
title=Dune author=Herbert shelf=SF session={wizardshelf=SF, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2?shelf=Classics"
title=Dune author=Herbert shelf=Classics session={wizardshelf=Classics, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-3"
title=Dune author=Herbert shelf=Classics session={nonce=...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2"
title= author= shelf= session={wizardtitle=NULL, wizardshelf=NULL, wizardauthor=NULL, ...}

Step 1 starts the wizard (the properties are saved but not restored), step 2 reads the earlier values and lets a request overwrite one of them, step 3 reads them and then removes the session keys (type="end"). The session map after step 3 holds only a nonce attribute that the default stack keeps there, and none of the scope interceptor’s keys; the wizard’s keys are gone. The last request shows the next surprising detail: running wizard-2 after the end re-created the keys with the value NULL. The interceptor never skips a null; it stores a placeholder object (NULLClass, whose toString is NULL) so that a session that gets serialized does not contain null references (the fix of WW-1803), and every action that lists a property saves it again after each execution. A session map printed in the view shows those placeholders; code that reads the keys of the scope interceptor directly would see them, which is another reason to read the properties from the action instead.

Sharing a Value Between Two Actions with a Key

The default key is the class of the action, so two actions of different classes do not see each other’s values. The guide’s second example, with a literal key and a getter on the first action and a setter on the second, passes an arbitrary object between two actions. The Bookshelf version has ExportAction and BrowseAction, which both list filter under the key shelf:

<action name="export-key" class="org.example.bookshelf.ExportAction">
    <interceptor-ref name="scope">
        <param name="key">shelf</param>
        <param name="session">filter</param>
        <param name="autoCreateSession">true</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/browse.jsp</result>
</action>
<action name="browse-key" class="org.example.bookshelf.BrowseAction">
    <interceptor-ref name="scope">
        <param name="key">shelf</param>
        <param name="session">filter</param>
        <param name="autoCreateSession">true</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result>/WEB-INF/views/browse.jsp</result>
</action>

After export-key?filter=fantasy (which stored shelffilter=fantasy in the session), a request to browse-key printed filter=fantasy. With the default key, export-nokey stored the value under its own class key and browse-action-key (with key ACTION) used a third key struts.ScopeInterceptor:/:browse-action-key: none of them saw the others' filter. Without a key, as the guide says, the interceptor stores the values but does not set them on the other action.

Details the Guide Leaves Out

Three details of the 7.4.0 behaviour were checked against the running scratch app.

  • autoCreateSession has no visible effect under the servlet dispatcher. The interceptor decides from ActionContext.getSession(), and under the servlet dispatcher that is always a SessionMap (it creates the servlet session on the first put), never null. browse-nocreate has autoCreateSession false and a request with no cookie still stored filter=q in the session and answered with a new session; the default true behaved the same way. The parameter matters only where the action context has no session map.

  • An invalid type is silently ignored. The setter throws IllegalArgumentException("Only start or end are allowed arguments for type") for any other value. A mapping with <param name="type">middle</param>, wizard-bad, deployed and ran, with the interceptor behaving as if the parameter was absent (the properties were restored and saved as for no type). The guide says "any other value throws IllegalArgumentException"; in 7.4.0 the failing setter is caught while the interceptor is built.

  • scopedModelDriven is a related interceptor, not the same one. It stores the model of a ScopedModelDriven action in a scope under a name; it is in defaultStack. A mapping with <param name="scope">session</param> and <param name="name">bookModel</param> stored a BookForm in the session under bookModel, and a second request with no parameters printed the stored title and author. The interceptor, the ModelDriven contract and the CRUD example belong to ModelDriven, Preparable & CRUD; the point here is only that "what the scope interceptor does" and "where a scoped model is stored" are two mechanisms.

The Message Store Interceptor: Messages Across a Redirect

A redirect after post solves the double submission, and creates a new problem: the action messages ("Book saved") and the field errors of the first request live in the first action instance, which is gone when the second request starts. The store interceptor (MessageStoreInterceptor) copies them into the session at the end of one request and puts them back into the next action, where the view prints them as usual. This is the Struts form of the "flash message" of other frameworks.

Modes

The interceptor works only with a ValidationAware action (every ActionSupport is) and, as the guide says, only if a session exists: "if no session exists, nothing will be stored", so the application is responsible for opening the session. The mode comes from the operationMode parameter:

Table 7. Operation modes of the store interceptor
operationMode Behaviour (guide and 7.4.0 source)

NONE

The default: the interceptor does nothing

STORE

After the action runs, before the result, the action’s errors, messages and field errors go to the session

RETRIEVE

Before the action runs, the stored errors, messages and field errors are merged into the action and removed from the session

AUTOMATIC

Always retrieves, and stores automatically when the result is a redirect (a result whose class implements Redirectable, such as redirect and redirectAction)

The parameters are operationMode, allowRequestParameterSwitch (default true) and requestParameterSwitch (default operationMode, the name of the request parameter that can set the mode). The guide says AUTOMATIC stores when the result is a ServletRedirectResult; the 7.4.0 pre-result listener tests for the interface Redirectable, which more result types implement (redirectAction is one).

A Save Form with Validation Errors

The Bookshelf’s form action validates the title. When it fails it must send the reader back to the form with the error, and a success must send the reader to the form with "Saved". Both responses are redirects, so the messages go through the store:

public class SubmitBookAction extends ActionSupport {
    private String title;

    @Override
    public void validate() {
        if (title == null || title.isBlank()) {
            addFieldError("title", "Title is required");
        }
    }

    @Override
    public String execute() {
        addActionMessage("Saved " + title);
        return SUCCESS;
    }

    public String getTitle() { return title; }
    @StrutsParameter public void setTitle(String title) { this.title = title; }
}
<action name="store-submit" class="org.example.bookshelf.SubmitBookAction">
    <interceptor-ref name="store">
        <param name="operationMode">STORE</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result name="input" type="redirectAction">store-form</result>
    <result type="redirectAction">store-form</result>
</action>
<action name="store-form" class="org.example.bookshelf.FormAction">
    <interceptor-ref name="store">
        <param name="operationMode">RETRIEVE</param>
    </interceptor-ref>
    <interceptor-ref name="defaultStack"/>
    <result name="input">/WEB-INF/views/flash.jsp</result>
    <result>/WEB-INF/views/flash.jsp</result>
</action>
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=x" > /dev/null    # opens the session
$ curl -c jar -b jar -L "http://localhost:18080/g9chain/store-submit?title="
messages=[] fieldErrors={title=[Title is required]} title=
$ curl -c jar -b jar "http://localhost:18080/g9chain/store-form"
messages=[] fieldErrors={} title=
$ curl -c jar -b jar -L "http://localhost:18080/g9chain/store-submit?title=Dune"
messages=[Saved Dune] fieldErrors={} title=

The first submit, with an empty title, failed validation, returned input and redirected to store-form, which printed the stored field error. The next plain request to store-form printed no errors: retrieving removes the stored values, so they are shown once. The third request redirected after a successful save and printed Saved Dune.

Four details of this configuration matter, and one of them differs from the guide’s example.

  • The target needs an input result. The retrieved field errors go back into the target action, and the workflow interceptor of defaultStack sees field errors and returns input. The guide’s example target, applicationFailed, has only the default result; with defaultStack that would give the error No result defined for action ... and result input (HTTP 404), which the scratch app produced when the target had only the default result. Either add the input result as above or leave workflow out of the target’s stack.

  • No session, no message. auto-submit (mode AUTOMATIC) with a first request that had no cookie printed no field errors on the form page: the listener refuses to store when request.getSession(false) is null. The same mapping with createSession placed before the store interceptor (auto-submit-session) stored the error and the form page printed it. In a real application the reader has a session already, or the first action opens one.

  • AUTOMATIC is the least configuration. It retrieves in every action it is in, and stores only when the result is a redirect, so one mapping serves as a source and a target. auto-submit with an existing session printed fieldErrors={title=[Title is required]} on auto-form, and a second request printed none.

  • A request parameter can change the mode. With allowRequestParameterSwitch at its default true, a request that adds operationMode=STORE or RETRIEVE switches the interceptor on a mapping that does not use it: store-dispatch?title= (AUTOMATIC with a dispatcher result stores nothing) printed its error on its own page, a second request to store-dispatch?title=&operationMode=STORE stored it, and auto-form printed the stored error. A request can therefore make an action store or consume messages that the application did not intend; set allowRequestParameterSwitch to false unless the parameter is used on purpose.

sequenceDiagram participant B as Browser participant S as store-submit (STORE) participant H as HTTP session participant F as store-form (RETRIEVE) B->>S: POST title empty S->>S: validate adds the field error, workflow returns input S->>H: pre-result listener stores errors and messages S->>B: 302 to store-form B->>F: GET store-form F->>H: retrieve and remove the stored values F->>B: flash.jsp shows the field error once

The message store stores the messages and errors, not the values of the form. A form that must be redisplayed with what the reader typed needs the value in the redirect URL, in the session through the scope interceptor, or a forward instead of a redirect. The validation framework and the input result are covered on the Validation page.

Choosing the Carrier

The sections above are alternatives for the same need, and the Bookshelf picks among them with a short list.

Table 8. Which carrier for which Bookshelf need
Need Use Why not the others

Show the confirmation after adding a book

redirectAction to a show action with the id in the URL; the message through the store interceptor

A chain repeats the POST on reload and leaves the wrong URL

List a lookup (the shelves) in a form

A method of the form action or a service called by it

A chain to a lookup action couples two actions; the action tag runs an action from the view

Remember the reader’s filter and sort order

Scope interceptor with session

Hand-written session code repeated in every action

A setting shared by all users

Application map (scope interceptor application, or a listener that fills it at startup)

The session is per browser

The reading cart

SessionAware with a thread-safe, Serializable value

Request parameters or hidden fields can be edited by the reader

A "Saved" flash after a redirect

Store interceptor, AUTOMATIC with a session open

A request parameter in the URL is visible and editable

A multi-step form

Scope interceptor with a literal key, start and end

A chain of actions would keep one request and lose the form state on back

Authorization and identity

The container or a security framework; re-derived from the session in each action

Anything copied by a chain or read from a request parameter

Pitfalls

Table 9. Pitfalls of chaining, sessions and scopes (all but the first two were reproduced on Struts 7.4.0 and Tomcat 11.0.26)
Pitfall What happens Remedy

A chain used after a POST that changes data

Reload and the back button repeat the first action; the URL names an action whose page is not shown

Redirect after post (redirectAction) with the store interceptor for messages

Chaining carries trust

A property with a getter on the source reaches a setter on the target that no request could set (approved=true through approve-chain)

Carry no authorization state; struts.chaining.requireAnnotations set to true; Unchainable

Constant names from the guide struts.xwork.chaining.*

Silently ignored in 7.4.0; messages are not copied

Use struts.chaining.copyMessages, copyErrors, copyFieldErrors

Explicit chain.excludes

Messages, errors and field errors are copied, because the built-in exclusion is replaced

Add actionErrors,actionMessages,fieldErrors to the list

Properties on the stack but not copied

The view finds the first action’s properties through the stack, hiding that the target’s own are null or empty

Print the target’s values in tests; remember the top of the stack wins

Two actions that chain to each other

HTTP 500 Infinite recursion detected

Remove the cycle; skipActions only for a bounded loop

method on the chain result

404 Method audit for action method is not allowed!

List the method in the target’s allowed-methods

A public getter for the session map

A request can write into the session (exposed?store.admin=true set admin=true)

No public getter; no @StrutsParameter on maps of scope data; ParameterNameAware or exclusion patterns

The old setSession method

Nothing is injected in 7.4.0; a NullPointerException at the first use

Implement org.apache.struts2.action.SessionAware.withSession

#attr in a JSP with session="false"

Session attributes are not found

Use #session.name

A session created by every JSP

A session per visitor, crawlers included

session="false" on views; create the session in the action that needs it

Mutable objects in the session

Concurrent requests interleave; a clustered server fails to serialize them

Immutable or thread-safe Serializable values

scope after defaultStack

The stored value overwrites the new request parameter (filter=newval printed sci)

Place scope before the stack when the request must win

Application scope for per-user data

One user’s value is visible to all (the pageLen=25 of the first browser)

Session scope, or no state at all

NULL placeholders in the scope interceptor’s session keys

A key holds an object printed as NULL after each run even if empty

Read the action’s properties, not the session keys

autoCreateSession and type expected to matter or fail

No visible effect for autoCreateSession; an invalid type is ignored without an error

Test the wizard’s start and end explicitly

store target without an input result

404 No result defined for action ... and result input after retrieving field errors

Add the input result, or leave workflow out of the target’s stack

store without a session

Messages are not stored; the form page shows none

createSession before it, or an earlier action that opens the session

allowRequestParameterSwitch left on

A request can turn storing and retrieving on or off

Set it to false

Session fixation

The identifier a visitor had before login stays valid after login

Invalidate the session or call changeSessionId at login

What Changed Recently

Table 10. Changes that touch chaining, sessions and scopes
Version Change

Struts 6.0

The Aware interfaces move to org.apache.struts2.action; SessionAware.withSession replaces setSession, ApplicationAware.withApplication replaces setApplication, and ServletRequestAware.withServletRequest replaces setServletRequest

Struts 7.0

The com.opensymphony.xwork2 package is renamed to org.apache.struts2: ActionChainResult is org.apache.struts2.result.ActionChainResult, ChainingInterceptor and ScopeInterceptor are in org.apache.struts2.interceptor, Unchainable is org.apache.struts2.Unchainable, ActionContext and ActionInvocation are in org.apache.struts2. @StrutsParameter is required for request parameters by default, so the setters of the redirect targets in this page are annotated

Struts 7.2

The @StrutsParameter authorization extends to chaining, behind the constant struts.chaining.requireAnnotations (default false in 7.4.0)

Struts 7.4.0

The chaining constants are struts.chaining.copyMessages, copyErrors, copyFieldErrors (the guide still shows struts.xwork.chaining.*); defaultStack has 24 interceptors, among them chain and scopedModelDriven, but not scope, store, createSession or clearSession; the built-in stacks reference none of the last three

Where the Books Are Outdated

The books of the section teach chaining, the session and scopes correctly as ideas; the differences are in names, defaults and security.

Table 11. Chaining, session and scope material in the books and what is current
In the books Struts 7.4.0

Struts 1 actions return an ActionForward from execute(ActionMapping, ActionForm, HttpServletRequest, HttpServletResponse), and a forward to another path is how actions are sequenced; the session is request.getSession()

Struts 1 is end-of-life since 2013 with unfixed vulnerabilities; the Struts 2 counterpart is a chain result with the chain interceptor, or, better, a redirect; the session is a Map from SessionAware

ActionChainResult and ChainingInterceptor in com.opensymphony.xwork2 packages

org.apache.struts2.result.ActionChainResult and org.apache.struts2.interceptor.ChainingInterceptor

SessionAware.setSession(Map), ServletRequestAware.setServletRequest(...) in org.apache.struts2.interceptor

withSession, withServletRequest in org.apache.struts2.action

struts.xwork.chaining.copyErrors and the other two as the way to copy messages

Ignored; use struts.chaining.copyErrors, copyFieldErrors, copyMessages

Chaining shown as the way to pass data to the next screen

Redirect after post; chaining copies properties implicitly, and in 7.x only annotated setters are meant to be settable from outside

Session objects as a convenient place for user and cart, with passwords or privileges stored in the clear

Store identifiers; authentication through the container or a security framework; the session map never exposed through a getter

Scope interceptor and store interceptor, if shown, with autoCreateSession and AUTOMATIC assumed to behave as documented

Behaviour above: autoCreateSession has no visible effect, AUTOMATIC uses Redirectable results, and the store needs a session that already exists

HttpSessionContext, session.putValue and getValue from the early Servlet API of the JSP books

Removed or deprecated for a long time; session.setAttribute and getAttribute (or the map of SessionAware)

Inconsistencies in the Official Documentation

  • The Chaining Interceptor page names the constants struts.xwork.chaining.copyErrors, copyFieldErrors and copyMessages; the 7.4.0 constants are struts.chaining.*, and the old names are ignored (verified).

  • The Chain Result page describes skipActions as the "list of comma separated action names for the actions that could be chained to"; in the 7.4.0 source the names are the actions that the infinite-recursion check does not apply to. The Javadoc of the setter says the list is "separated by a white space", while the code splits on commas.

  • The Chaining Interceptor page says an explicit includes or excludes list "controls how and which parameters are copied" but not that an excludes list replaces the default exclusion of errors and messages (verified).

  • The Scope Interceptor page says any type other than start or end throws an IllegalArgumentException; the setter does, but the exception did not reach the client in 7.4.0 (verified). The page gives no default for autoCreateSession, and the parameter has no visible effect under the servlet dispatcher (verified).

  • The Message Store Interceptor page says AUTOMATIC stores for a ServletRedirectResult; the 7.4.0 listener tests the interface Redirectable. Its first example has <interceptor-ref name="store"/> followed by a stray <param> and an unclosed </interceptor-ref>, and its target action example has no input result.

  • The HTTP Session tutorial still shows setSession(Map) and a prose "setSession method", the interface of Struts 2.x and 5.x; the interface the 7.4.0 jar has is org.apache.struts2.action.SessionAware with withSession. The page’s link to the "SessionAware interface" page (core-developers/session-aware-interface) answers 404.

  • The Accessing Application, Session, Request Objects page says #attr searches the session scope; with session="false" on the JSP it does not (verified).

Summary

Chaining runs a second action in the same request and copies properties from the first one onto it, silently, from everything on the value stack; the guide, and this page, prefer a redirect after post, with the messages carried by the store interceptor. The session is a map delivered by SessionAware.withSession, created on the first write, shared by all requests of one browser, never to be exposed through a getter, and renewed at login. The scope interceptor moves named action properties to the session or application map for wizards and remembered filters, and its order in the stack decides who wins between the request and the stored value. The Struts 7 changes, the annotation requirement and the renamed Aware interfaces, and the places where the official pages disagree with the 7.4.0 code, are in the sections above.

References