Debugging, Dev Mode & Logging
|
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 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21; the facts were
checked on 2026-10-08 and 2026-10-10 against the default.properties, struts-default.xml and struts-excluded-classes.xml files
and the class files inside the 7.4.0 struts2-core jar, the Javadoc, the Core Developer Guide pages on development mode,
debugging, the debugging interceptor and logging, the Getting Started tutorial on debugging, the Security Guide, the Config Browser
plugin page and the Version Notes on the Struts wiki, and every behaviour described as observed was requested with curl on a
scratch Tomcat. A Struts application is mostly configuration and reflection, so when something does not work the cause is rarely
a stack trace in your own code: a parameter was rejected, an OGNL expression was blocked, a key is missing from a bundle, an
action is not mapped. Struts has a set of tools that make those decisions visible: one constant, struts.devMode, that turns
the framework from quiet to talkative; the debugging interceptor, which dumps the value stack and the action context for a
request; the <s:debug/> tag; the config browser plugin, which lists the loaded configuration; and a logging layer that is
plain Log4j 2. The running scenario is the Bookshelf, a catalogue of books exposed as Struts actions. This page uses it to
switch devMode on and off and compare the answers, to read the three kinds of message that explain most "it does not work"
reports, to find out why an expression renders nothing, to configure logging for development and for production, and to remove
every one of these tools before the application goes live. The page on exception handling already shows the "Struts Problem
Report" page; the page on parameter injection explains @StrutsParameter, which appears here only through the log messages it
produces. The pages on testing, security hardening and deployment cover the neighbouring topics.
One result should be read first, because it changes how you use the tools. In my tests on 7.4.0 and Tomcat 11, ?debug=xml did
not work (the document stops in the middle of the output and a problem report follows), the first configuration reload in
devMode broke the Problem Report and the /static resources until the next restart, and the config browser worked with devMode
off. The sections below say what was seen, with the Log4j 2 lines that prove it.
The Debugging Toolbox at a Glance
| Tool | What it shows | Switch | In production |
|---|---|---|---|
|
Reloading of bundles and XML, louder log levels, "Developer Notification" errors, the Problem Report page |
|
|
Debugging interceptor |
The parameters, the action context, the session and the value stack of one request |
Request parameter |
Inert: the parameter is ignored (observed) |
|
A collapsible block at the end of the page with the value stack and the context |
Only while devMode is on |
Renders nothing (observed) |
Config browser plugin |
Namespaces, actions, results, interceptors, constants, beans, jars |
A jar on the class path; independent of devMode |
Remove it or block |
Log4j 2 |
Every decision of the request cycle at |
|
|
|
A |
Constant, default |
|
A debugger |
Breakpoints in actions, interceptors and the framework |
JDWP agent on the JVM |
Never leave an agent configured |
The sections that follow take the tools in the order in which a developer meets them: the constant first, because the others depend on it, then the interceptor and the tag, then the config browser, then logging, and last the steps that return the application to production.
The Bookshelf Used on This Page
All examples below are one WAR, built from the Bookshelf of the Struts Getting Started page of this section (the struts2-bom 7.4.0,
Jakarta Servlet 6.1 and Pages 4.0 as provided dependencies) and deployed twice to Tomcat 11.0.26: once with
struts.devMode=true under the context path /g12debugging, once with false under /g12debuggingprod. The two builds differ
only in that constant, the log levels and, in the second, an operations filter that appears near the end. The action class has
one annotated and one deliberately unannotated parameter, and logs with Log4j 2:
package org.example.bookshelf;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
public class BookAction extends ActionSupport {
private static final Logger LOG = LogManager.getLogger(BookAction.class);
private String filter;
private String author;
public String getFilter() { return filter; }
@StrutsParameter
public void setFilter(String filter) { this.filter = filter; }
public String getAuthor() { return author; }
// deliberately NOT annotated with @StrutsParameter
public void setAuthor(String author) { this.author = author; }
public List<String> getTitles() { return List.of("Dune", "Neuromancer"); }
@Override
public String execute() {
LOG.debug("Listing books with filter [{}]", filter);
return SUCCESS;
}
}
Source: Struts — Logging, Usage
The struts.xml of the development build maps four actions to the same class: books (the list page), books-debug (the same
page with <s:debug/>), title (a page that prints a message from the resource bundle and a property that does not exist) and
quiet (the list page with the debugging interceptor disabled, shown in its own section):
<struts>
<constant name="struts.devMode" value="true"/>
<package name="default" extends="struts-default" namespace="/">
<action name="books" class="org.example.bookshelf.BookAction">
<result>/WEB-INF/views/books.jsp</result>
</action>
<action name="books-debug" class="org.example.bookshelf.BookAction">
<result>/WEB-INF/views/books-debug.jsp</result>
</action>
<action name="title" class="org.example.bookshelf.BookAction">
<result>/WEB-INF/views/title.jsp</result>
</action>
<action name="quiet" class="org.example.bookshelf.BookAction">
<interceptor-ref name="defaultStack">
<param name="debugging.disabled">true</param>
</interceptor-ref>
<result>/WEB-INF/views/books.jsp</result>
</action>
</package>
</struts>
The page of this section on configuration, struts.xml and constants explains how the same value can come from
the environment instead of being edited for every build. It is the recommended way to keep the literal true out of the file
that is deployed; the examples here use the literal to keep the comparison easy to read. The page on getting started has the
first Bookshelf and its project layout.
struts.devMode: What the Switch Changes
The Core Developer Guide opens its page on development mode with a warning: turn the option off before deploying, because it
can expose sensitive data of the application. The same page says what it does when it is on: the resource bundles are
reloaded on every request, the XML configuration (struts.xml and the validation files) is reloaded on every request, and
problems that are normally ignored are raised to errors. default.properties of 7.4.0 documents the same three effects next to
the constant, and its default is false.
That summary is short. The 7.4.0 jar reads the constant in 26 classes (found by searching the class files of struts2-core); the table below lists what each change looks like from
the outside. A row marked "observed" was reproduced on the scratch Tomcat, comparing the /g12debugging build with the
/g12debuggingprod one; a row marked "class file" was read from the bytecode with javap and not run.
| What | How it shows | Evidence |
|---|---|---|
Resource bundles |
A changed |
observed |
XML configuration |
On every request the log says |
observed |
"Developer Notification" |
An |
observed |
Rejected parameters |
A parameter that matches an excluded pattern or does not match the accepted pattern is a |
observed |
Problem Report |
An unhandled exception or an unmapped action name answers HTTP 200 with the "Struts Problem Report" page, instead of a container error page |
observed |
Debug tools |
The debugging interceptor and the |
observed |
OGNL guard |
The |
observed |
FreeMarker |
The template cache and the whitespace stripping of |
|
Expression failures |
|
class file |
Double-submit tokens |
On a token mismatch the token name, the form token and the session token are logged |
class file, see the page on tokens |
Other consumers |
The |
class file |
(struts.i18n.reload,
struts.configuration.xml.reload)"] DM --> NT["Developer Notification
ERROR entries and louder
WARN for rejected input"] DM --> PR["Problem Report page
(unmapped action, exception)"] DM --> DT["debugging interceptor,
s:debug, console resources"] DM --> OG["OGNL: devMode excluded lists,
missing property warnings"] DM --> FM["FreeMarker cache and
whitespace stripping off"] RL --> EX["performance cost"] FM --> EX
The picture is the reason for the warning. Four of the six boxes turn information about the application into output that anybody who can send a request can read, and the other two cost time on every request. None of them is a security control, and the Security Guide says so in its "Disable devMode" tip: in production devMode exposes too much information about the internals of the application and can lead to the evaluation of risky parameter expressions.
Reloading: What Is and Is Not Reloaded
The official page says the whole configuration is reloaded on every request. What happens in 7.4.0 is narrower, and it matters
for the speed of the development cycle. On every request ConfigurationManager asks each configuration provider whether its
file changed; only when one answers yes does the framework load everything again. To see it, the scratch application was
requested, struts.xml was touched, and the next request was logged:
DEBUG o.a.s.c.ConfigurationManager - Checking ConfigurationProviders for reload.
INFO o.a.s.c.ConfigurationManager - Detected container provider [Struts XML configuration provider (struts.xml)] needs to be reloaded.
DEBUG o.a.s.c.ConfigurationManager - Reloading all providers.
DEBUG o.a.s.c.p.XmlConfigurationProvider - Loading action configurations from: struts-default.xml
...
Source: Struts — Reloading Configuration
The resource bundles are different: on a request the framework clears the bundle cache (Resource bundles reloaded at
DEBUG), and the next message lookup reads the file again. Editing org/example/bookshelf/BookAction.properties in the exploded
WAR changed the title page from Bookshelf to the new text within a request or two in the devMode build (in one run the first request right after the edit still showed the old value), and did not change it in the
production build, where the value stayed cached for the life of the application.
An explicit setting wins over devMode. With the four constants that the official page lists for "page rendering is slow", devMode
stays on while the reloading stops: after setting struts.i18n.reload and struts.configuration.xml.reload to false in the
struts.xml of the scratch application, a changed bundle was no longer picked up and a touched struts.xml no longer produced a
reload line.
<constant name="struts.devMode" value="true" />
<constant name="struts.i18n.reload" value="false"/>
<constant name="struts.configuration.xml.reload" value="false"/>
<constant name="struts.freemarker.templatesCache" value="true"/>
<constant name="struts.freemarker.templatesCache.updateDelay" value="120"/>
<constant name="struts.freemarker.mru.max.strong.size" value="120"/>
A word of caution that the documentation does not give. In 7.4.0 on Tomcat 11 the first configuration reload in devMode left
two parts of the framework without their servlet context: after struts.xml changed on disk and the reload had run, /static/*
answered 500 (Cannot invoke "java.util.List.iterator()" because "this.pathPrefixes" is null in DefaultStaticContentLoader) and
an unmapped action name answered 404 with "Unable to show problem report: … servletContext is null" instead of the Problem
Report; the actions themselves kept working. A fresh deployment answers 200 for both. It was reproduced with and without the config
browser plugin. The practical rule is that the in-place reload is good for the XML of actions and results, and that after a
change a developer who sees a 500 under /static or a missing Problem Report should reload the context or restart; setting
struts.configuration.xml.reload to false and redeploying for every change avoids it.
Developer Notification, Warnings and What They Mean
Most of the value of devMode is in the log. Three message families explain most of the "my parameter does not arrive" and
"my page is empty" reports. The first request below sends two parameters to the Bookshelf, filter, whose setter is annotated, and
author, whose setter is not, plus a third, isbn, that has no setter at all:
$ curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:18080/g12debugging/books?filter=Dune&author=Herbert&isbn=1"
200
The response is 200 in every case, and the page lists both books. The log of the devMode build has a line that no other configuration produces:
ERROR o.a.s.i.p.StrutsParameterAuthorizer - Developer Notification (set struts.devMode to false to disable this message):
Parameter injection for method [setAuthor] on target [org.example.bookshelf.BookAction] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.
DEBUG o.a.s.i.p.StrutsParameterAuthorizer - No matching annotated method found for property [isbn] of depth [0] on target [BookAction], now also checking for public field
DEBUG o.a.s.i.p.StrutsParameterAuthorizer - Matching field for property [isbn] not found on target [BookAction]
The rule that produces the difference is in the Version Notes: 7.0.0 started to log parameter annotation issues at ERROR
level in devMode (WW-5433). A setter that exists but lacks @StrutsParameter is a developer mistake and is shown as an error; a
request parameter that matches nothing is just input and stays at DEBUG. The official page on development mode says that an
unknown field makes the framework throw an exception; in 7.4.0 nothing is thrown in either case, and the only change is the log level.
The old "Unexpected Exception caught setting" message does exist, but only when struts.parameters.requireAnnotations is
false: in the scratch application with that constant off, isbn produced
ERROR o.a.s.i.p.ParametersInterceptor - Developer Notification (set struts.devMode to false to disable this message):
Unexpected Exception caught setting 'isbn' on 'class org.example.bookshelf.BookAction: Error setting expression 'isbn' with value ['1', ]
and with struts.devMode=false none of these ERROR lines is written. That is the real gain of devMode for the Bookshelf’s
developers: the log of the production build at WARN would never mention the rejected author, which is why a developer who
tests only against the production configuration can spend an afternoon looking for the cause. The production build has the same
information at DEBUG: raising only the org.apache.struts2.interceptor.parameter logger to DEBUG there printed the rejection
of setAuthor, the unmatched isbn, and two lines for the hostile names below.
The second family is the rejection of hostile parameter names. A request such as
$ curl -s -o /dev/null "http://localhost:18080/g12debugging/books?isbn=1&author=Herbert&%23session.x=1&(x)(y)=1"
is dropped by the parameter name filters before any setter is looked at. With devMode on the log has two WARN lines, each with a
pointer to the Security Guide; with devMode off the same decisions are DEBUG:
WARN o.a.s.i.p.ParametersInterceptor - Parameter [#session.x] matches excluded pattern [(^|\%\{)((#?)(\[\d\]\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\.|\[).*]! See Accepted / Excluded patterns at ...
WARN o.a.s.i.p.ParametersInterceptor - Parameter [(x)(y)] didn't match accepted pattern [[\w+((\.\w+)|(\[\d+])|(\(\d+\))|(\['(\w-?|[一-龥]-?)+'])|(\('(\w-?|[一-龥]-?)+'\)))*]]! See Accepted / Excluded patterns at ...
The third family is a message that is not about parameters. A page that asks for a key that does not exist prints the key
itself, and the log says why; the same request prints the WARN lines in both builds, because a missing key is not a devMode
feature:
WARN o.a.s.t.AbstractLocalizedTextProvider - Missing key [book.missing.key] in bundles [[org/apache/struts2/struts-messages, org/apache/struts2/xwork-messages]]!
WARN o.a.s.u.TextProviderHelper - The first TextProvider in the ValueStack (org.example.bookshelf.BookAction) could not locate the message resource with key 'book.missing.key'
devMode and the OGNL Guard
The OGNL security settings of the framework (the allowlist, the excluded classes and packages) are described on the page on
the value stack and in the security hardening page. Two facts belong here because they only apply in devMode. First,
struts-excluded-classes.xml in the 7.4.0 jar has a second set of the exclusion constants: struts.devMode.excludedClasses,
struts.devMode.excludedPackageNames, struts.devMode.excludedPackageNamePatterns and
struts.devMode.excludedPackageExemptClasses. When devMode is on the guard uses these sets. Comparing the two sets in the jar,
the devMode class list is the normal one without org.apache.struts2.ActionContext; the package lists are the same. That is
consistent with the debugging tools, which read the context, although the files do not say so. Second, the Version Notes of 7.4.0 say that five dev-mode setters of
SecurityMemberAccess were removed while the struts.devMode.* settings themselves are unchanged (WW-5675); code that
configured the guard through those setters needs the constants instead.
At start-up the guard says which set it uses, which is the quickest way to find out whether a deployed instance still runs in devMode:
WARN o.a.s.o.SecurityMemberAccessConfig - DevMode enabled, using DevMode excluded classes and packages for OGNL security enforcement!
The Debugging Interceptor
DebuggingInterceptor is the last interceptor of defaultStack, after workflow (it is number 24 of the 24 on the page on
interceptors). It does nothing unless devMode is on. When it is on, it looks for a request parameter named debug with one of
four values, and the page that the framework builds replaces the page of the action:
debug= |
What the request returns in 7.4.0 |
|---|---|
|
The parameters, the action context, the request, the session and the value stack as an XML document. Broken in 7.4.0 (see below) |
|
A page that opens a popup window with an OGNL console from |
|
The result of the OGNL expression in the |
|
An HTML table with the properties of an object: the action by default, or the object named in the |
browser
The most useful mode in 7.4.0 is browser. For the Bookshelf, books?author=Herbert&filter=Dune&debug=browser returns a table with one row for
each property of the action; the values are those after parameter injection, so a parameter that was rejected is still null:
| Property | Value shown | Type shown |
|---|---|---|
|
There is no read method for container |
|
|
|
|
|
|
|
|
empty |
|
|
an "Expand" link |
|
The table is built from a plain request, so it also tells you whether filter arrived. With object=%23parameters (the # must be
written %23) the table shows the request parameters as the framework saw them. With object=%23session it shows the session
attributes; the Bookshelf’s session held one attribute, named nonce, a security value that a production page must never show. object=%23context returned a single row root null in 7.4.0; the documented default of
#context is not what is shown when object is absent, which is the action.
One property of the mode that is easy to miss: the action runs. The log of a debug=browser request has the line
Executing action method = execute and the application’s own Listing books with filter [Dune], then
No result returned for action ..., because the interceptor replaced the result. The Getting Started page says that the interceptor halts further execution of the
action, and the Debugging Interceptor page says that its work happens before the natural result runs. In 7.4.0 "halts" is only true of command, where the log has the
debugging interceptor and then No result returned without an execute line. A debug=xml or debug=browser request to an
action that saves a book or sends an e-mail therefore does that, once.
command and console
command evaluates an OGNL expression against the value stack and returns the text of the result. It is the engine behind the
OGNL console, and it can be called directly:
$ curl -s -G "http://localhost:18080/g12debugging/books" --data "filter=Dune&debug=command&expression=filter"
Dune
$ curl -s -G "http://localhost:18080/g12debugging/books" --data "debug=command&expression=titles.size"
2
$ curl -s -G "http://localhost:18080/g12debugging/books" --data-urlencode "debug=command" --data-urlencode "expression=titles[1]"
Neuromancer
The security guards apply to the console as they do to any expression, and the guard decides in silence: the answer for a
method call is the word null, not an error. Requesting expression=titles.size() returned null; so did titles.get(0), a static
call to java.lang.System, and the construction of a java.io.File. The reason is in the log, and finding it is the exercise
the next section works through:
WARN o.a.s.o.SecurityMemberAccess - Declaring class [class java.util.AbstractCollection] of member type [public abstract int java.util.AbstractCollection.size()] is not allowlisted! Add to 'struts.allowlist.classes' or 'struts.allowlist.packageNames' configuration.
WARN o.a.s.o.SecurityMemberAccess - Access to static method [public static java.lang.String java.lang.System.getProperty(java.lang.String)] is blocked!
WARN o.a.s.o.SecurityMemberAccess - Package [package java.io] of member class [class java.io.File] of member [null] is excluded!
debug=console is a convenience for a browser. The response is a small HTML page whose script opens /static/webconsole.html in a
popup (the popup blocker has to allow it, as the Core Developer Guide notes). Two quirks of 7.4.0 were observed. A client that has no
session yet gets the console page followed by a Problem Report with IllegalStateException: Cannot create a session after the
response has been committed, because the interceptor writes the response before it stores the value stack in the session; with a
session cookie from an earlier request it works, and the page of the action follows the popup script. And the console resources
are served only in devMode.
The console is a remote expression evaluator. Everything on this page about "never in production" is about this mode: the guard blocks the dangerous classes, but an interface that evaluates OGNL on request and is reachable by an attacker depends on that guard having no gap, which is more than a production system should rely on.
Source: Struts — Debugging
xml
The documented output of debug=xml is a <debug> element with <parameters/>, <context>, <request/>, <session/> and
<valueStack>. In 7.4.0 the response of the scratch application starts correctly and ends in the middle of a tag:
<debug>
<parameters/>
<context>
<struts.actionMapping>
<class>class org.apache.struts2.dispatcher.mapper.ActionMapping</class>
<name>books</name>
<namespace>/</namespace>
<params/>
</struts.actionMapping>
<action>
<actionErrors/>
...
</action>
<report.conversion.errors>false</report.conversion.errors>
<attr<html>
<head>
<title>Struts Problem Report</title>
The status is 200 and the content type is text/xml. The cause is in the Problem Report: UnsupportedOperationException: method makes
no sense for a simplified map, raised by AttributeMap.size when DebuggingInterceptor.serializeIt checks whether the attr map was
already serialized. It was the same on every action and with or without a session. Use browser, which does not serialize that
map; a build that needs a complete dump of one request can use <s:debug/> (next
section) or a breakpoint.
Switching It Off for One Action or for the Whole Stack
Because debugging is an ordinary interceptor, it takes the disabled parameter that every interceptor of 7.4.0 accepts (the
page on interceptors lists the parameters). The quiet action of the Bookshelf, shown at the top, passes debugging.disabled to
defaultStack; with devMode on, quiet?filter=Dune&debug=command&expression=filter returned the normal list page, while
books?filter=Dune&debug=command&expression=filter returned Dune.
$ curl -s -G ".../g12debugging/books" --data "filter=Dune&debug=command&expression=filter"
Dune
$ curl -s -G ".../g12debugging/quiet" --data "filter=Dune&debug=command&expression=filter"
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (Struts)</h1>
...
Source: Struts — Debugging Interceptor
This is useful for the one action that handles the card number of a payment and that should never be dumpable even on a
developer’s machine. It is not a substitute for turning devMode off. Also note that the stack matters: an action mapped to paramsPrepareParamsStack, as the chain-stack action of the scratch
application is, answered debug=command&expression=filter with the normal list page, because that stack, unlike defaultStack, does not
contain the debugging interceptor. Such an action has no debug modes at all.
<action name="chain-stack" class="org.example.bookshelf.BookAction">
<interceptor-ref name="paramsPrepareParamsStack"/>
<result>/WEB-INF/views/books.jsp</result>
</action>
The debug Tag
<s:debug/> renders the value stack and the stack context as a block at the point where it is placed. The Bookshelf’s books-debug
page ends with it:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head><s:head/></head>
<body>
<h1>Bookshelf (Struts)</h1>
<ul>
<s:iterator value="titles"><li><s:property/></li></s:iterator>
</ul>
<s:debug/>
</body>
</html>
With devMode on, the response ends with an inline script (toggleDebug), an inline style, a link [Debug] and a div that is
hidden until the link is clicked, with the headings "Struts ValueStack Debug", "Value Stack Contents" and "Stack Context". The
first table shows org.example.bookshelf.BookAction with its properties, which is where a developer finds out that filter
stayed null. With devMode off the same page renders nothing for the tag: a search for "Struts ValueStack Debug" found one match
in the devMode response and none in the production one. The block contains inline script and style, which a strict
Content Security Policy blocks; the page on security headers discusses the csp interceptor and nonces.
The tag is safer than the interceptor in one respect: it is in the page, so it exists only where a developer typed it. It is more
dangerous in another: a <s:debug/> that is committed to a layout file is a leak that is invisible until devMode is switched on
for a test environment that is reachable from the internet. Search the JSPs for s:debug in the release checklist.
The Config Browser Plugin
The Config Browser is a plugin, struts2-config-browser-plugin, that serves actions in the /config-browser namespace with a
read-only view of the loaded configuration. The plugin page says to use it only during development and to restrict access
"strictly"; the other-plugins page of this section lists what each of its pages shows, so this page records what matters for debugging
and for production.
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-config-browser-plugin</artifactId>
</dependency>
For debugging, the pages answer the questions that the log answers at the cost of a restart. index redirects to
actionNames, which lists the namespaces (/config-browser, the default one, /) on the left and the actions of the selected one;
showConfig.action?namespace=/&actionName=books shows the class, the default URL, the results, the exception mappings, the
interceptors and the validators of one action, showConstants.action shows every constant with its effective value (including
struts.devMode), showBeans.action lists the bean implementations and showJars.action the jars on the class path with their
versions. The Getting Started tutorial links the pages from a debugging section with <s:url action="index" namespace="config-browser"/>;
without the leading slash in the namespace the tag rendered the relative URL config-browser/index.action (followed by a jsessionid
path parameter for a client without a cookie), which only resolves from a page at the root of the context. With
namespace="/config-browser" it rendered /g12debugging/config-browser/index.action.
Four findings from the scratch Tomcat decide how to treat the plugin:
-
It does not check devMode. The production build, with
struts.devMode=false, answered 302 for/config-browser/index.action, then 200 foractionNames.action,showConstants.action,showBeans.actionandshowJars.action, without authentication. A developer who believes that "devMode off" switches the tools off is wrong about this one, andshowConstantsprints every security constant of the application. -
It brings jars. In the Bookshelf the WAR grew from 17 to 20 jars:
struts2-config-browser-plugin,struts2-velocity-pluginandvelocity-engine-core(a transitive dependency of the plugin). -
It shows its age. The footer reads
Struts (c) 2003 - 2015, the menu entry readsJars (requires Maven 2 data), andshowConfigprints the default URL of the Bookshelf’s action with a double slash (/g12debugging//books.action), because the namespace/is joined with a slash that is already there. The pages work. -
A security constraint blocks it. The block below, added to the production build’s
web.xml, made every URL under/config-browser/answer 403 (index.action,index,showConstants.action) while/booksstayed 200.
<security-constraint>
<display-name>No Config Browser in production</display-name>
<web-resource-collection>
<web-resource-name>config-browser</web-resource-name>
<url-pattern>/config-browser/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>no-users</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<description>Do not assign users to this role</description>
<role-name>no-users</role-name>
</security-role>
The better answer is to keep the jar out of the production WAR. A Maven profile that adds the dependency makes that a build
decision, and was verified: the default build had 17 jars and no config browser, and mvn package -Pdev-tools had 20 with the three
jars named above.
<profiles>
<profile>
<id>dev-tools</id>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-config-browser-plugin</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
Logging with Log4j 2
Struts logs through the Log4j 2 API and nothing else. The Core Developer Guide records the history: the XWork logging layer
(LoggerFactory, the xwork.loggerFactory property, the Commons Logging and SLF4J delegates) was deprecated in 2.5 and removed
in Struts 6.0.0 (WW-4763 in the Version Notes of 6.0.0), and Struts 7.3.0 removed the last java.util.logging and SLF4J call
sites from the framework (WW-5620); the container no longer injects a java.util.logging.Logger into a managed bean. There is
nothing to configure in struts.xml; the configuration is a normal log4j2.xml.
The Dependency Is Yours
The Logging page says that struts2-core "declares log4j-api and log4j-core". In the 7.4.0 POM only log4j-api is a normal
dependency; log4j-core is declared with test scope, so it is not transitive. A WAR that depends on struts2-core alone has the API
and no implementation. The scratch WAR without any implementation started and served pages, and its start-up wrote
ERROR Log4j API could not find a logging provider.
and from then on only ERROR entries reached Tomcat’s catalina.out, unformatted (ERROR Dispatcher Could not find action or
result: /g12debuggingnolog/nosuch); every WARN, INFO and DEBUG entry of the framework was dropped, which includes all
the messages of the previous sections. The application therefore declares the implementation. The struts2-bom does not manage its
version (the build failed with "dependencies.dependency.version is missing"), so the version is written out; the one that matches the
log4j-api that Struts 7.4.0 brings is 2.26.1:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.26.1</version>
</dependency>
The Bookshelf’s own scratch POM names log4j-slf4j2-impl, which brings log4j-core and a bridge for libraries that log to SLF4J. The
bridge is a separate concern, as the Logging page says; add log4j-slf4j2-impl (SLF4J 2) or log4j-jcl (Commons Logging) only when a library
of the application needs it. The Logging page adds that 6.11.0 still declares slf4j-api and that 7.3.0 dropped it from struts2-core; it had been optional, so
nothing changes on the class path of an application.
A Development Configuration
The configuration of the development build writes to a file that is easy to find in Tomcat’s logs directory and raises two
loggers: the framework’s root package and the application’s. The file appender uses the system property that Tomcat sets for its
base directory:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<File name="FILE" fileName="${sys:catalina.base}/logs/g12debugging.log">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5p [%t] %c{1.} - %m%n"/>
</File>
</Appenders>
<Loggers>
<Logger name="org.apache.struts2" level="debug"/>
<Logger name="org.example.bookshelf" level="debug"/>
<Root level="warn">
<AppenderRef ref="FILE"/>
</Root>
</Loggers>
</Configuration>
A word about the logger names. The Getting Started tutorial and the Security Guide both configure com.opensymphony.xwork2 next to
org.apache.struts2 (the tutorial says that "the two major packages … com.opensymphony and org.apache.struts2" are the ones to raise).
No class of 7.4.0 lives in com.opensymphony; the XWork packages were renamed in 7.0.0 (WW-3714 in its Version Notes) and
what remains is org.apache.struts2 and its sub-packages. A logger for a name that does not exist is harmless but does nothing, and
the line is a leftover that is worth deleting when you copy an old configuration.
At DEBUG the framework is chatty: one request to books?filter=Dune&author=Herbert&isbn=1 produced 96 log lines, among them the exact order in
which interceptors ran (Executing conditional interceptor: exception, alias, servletConfig, i18n, csp, prepare, chain,
scopedModelDriven, modelDriven, params, conversionError, coep, coop, fetchMetadata, validation, workflow,
debugging), every SecurityMemberAccess check, the type converter lookup for filter, Executing action method = execute, the
action’s own Listing books with filter [Dune], the result processing and the JSP forward. For a developer this is a trace of the
request cycle of the architecture page, in order, and it is worth reading once.
Source: Struts — Logging, Usage
Targeted Loggers
The 96 lines are too many for daily work. Log4j 2 loggers are hierarchical, so a configuration can leave the framework at WARN and
raise only the package one is investigating. The configuration below keeps org.apache.struts2 at warn, raises the parameter
handling (ParametersInterceptor and StrutsParameterAuthorizer) to debug, and keeps the application at debug. The same request
then produced 18 lines, which show the decision for every parameter and the application’s own message:
<Loggers>
<Logger name="org.apache.struts2" level="warn"/>
<Logger name="org.apache.struts2.interceptor.parameter" level="debug"/>
<Logger name="org.example.bookshelf" level="debug"/>
<Root level="warn">
<AppenderRef ref="FILE"/>
</Root>
</Loggers>
The loggers that answer the most common questions, with the level at which they start to speak, are:
Logger (package of org.apache.struts2) |
Level | What you learn |
|---|---|---|
|
|
Which parameters were accepted, rejected or not matched, and why |
|
|
An expression blocked by the allowlist, the excluded classes or the static method rule; the same logger at |
|
|
Each property an expression could not find |
|
|
A missing message key, with the bundles that were searched |
|
|
Which files were loaded, which provider asked for a reload |
|
|
An unmapped action or an exception in the request cycle (a |
|
|
The interceptors in order, the action method, the result returned or |
|
|
Exceptions caught by the exception interceptor, see the page on exception handling |
Logging the Missing Property
When an OGNL expression finds no property, the value is empty and nothing is logged, which is why <s:property value="owner.name"/>
prints an empty string for a typo. The constant struts.ognl.logMissingProperties (default false; the comment in default.properties warns that it is very verbose)
turns on a WARN per property. With it set to true the title page of the Bookshelf logged
WARN o.a.s.o.OgnlValueStack - Could not find property [nope]!
WARN o.a.s.o.OgnlValueStack - Could not find property [owner.name]!
WARN o.a.s.o.OgnlValueStack - Could not find property [titles.size()]!
together with a handful of lines for framework-internal names (struts, struts.actionMapping, struts.valueStack) that the
framework probes on every page. Switch it on for a debugging session and off again; the lines for the internal names make it
unsuitable as a permanent setting.
Logging from Your Own Actions
An action logs with the Log4j 2 API like any class, as the Bookshelf’s BookAction does. Two rules from the Logging page are
worth repeating. The message syntax is the {} placeholder of Log4j 2 parameterised messages (LOG.debug("... [{}]", filter)), not the
#0 syntax of the removed XWork logger. And a java.util.logging.Logger injected into a managed bean by the old container
binding no longer exists in 7.3.0 and later; declare the logger as a static final field. The LOG of the Bookshelf printed its line at
DEBUG in the development build and, in the production build, where the logger is at info, nothing.
Source: Struts — Logging, Usage
A Debugging Session on the Bookshelf
Three typical reports and the tools that settle each one, in the order in which they were used on the scratch application.
"The list is not filtered by author." The URL is books?author=Herbert. The page lists both books and the browser shows nothing
wrong. debug=browser on the same URL shows the author row as null (the action’s property never changed). The development log has the
ERROR with the Developer Notification of the previous section: setAuthor is not annotated. The fix is the annotation
(@StrutsParameter with a depth that matches the property, as the page on parameter injection explains), and the same
debug=browser then shows Herbert (a build of the Bookshelf with the annotation added showed the author row as Herbert, type
java.lang.String, instead of null, type unknown).
"The title renders as a key." The title page shows shelf.title. The log says Missing key [shelf.title] in bundles [...]
and that the first text provider on the stack was BookAction; the bundle that BookAction reads is
org/example/bookshelf/BookAction.properties (see the page on localization) and was missing there (a copy in the root of the class path is not searched for
that action). Adding shelf.title=Bookshelf to that file in the exploded WAR displayed Bookshelf on the next request in the devMode
build and, in the production build, only after a restart, which is the reloading behaviour of the table above.
"The expression returns nothing." books?debug=command&expression=titles.size() answered null. The log has the
SecurityMemberAccess warning that java.util.AbstractCollection is not allowlisted. There are two fixes. The safe one is the
property form of the same call, titles.size, which returned 2 through the console and does not need a method call; the other
is the allowlist constants named in the warning, which widen the guard for a whole class or package and are decided with the
security hardening page, not in a debugging session. The warning also shows why the debugging console cannot be used to explore
an application that has the allowlist enabled: it applies the same rules as every expression.
Turning It All Off for Production
The Security Guide has three tips for this: restrict the config browser, disable devMode, reduce the logging level. They are three of the seven steps below, and a deployment that was verified against the production build of the Bookshelf shows each of them as a difference in an answer.
| Request | struts.devMode=true |
struts.devMode=false |
|---|---|---|
|
|
The normal list page; the |
|
A "Struts ValueStack Debug" block |
No block |
|
200 with the Struts Problem Report (on a fresh deployment) |
404 from the container |
|
200 |
404 |
|
|
Nothing at |
|
302, then the pages (200) |
302, then the pages (200) unless blocked |
The steps, then, are these:
-
struts.devModeisfalsein the deployed configuration. Keep the literaltrueout of the file that is built, as the constants page of this section suggests, and search fordevModeinstruts.xml, in a packagedstruts.properties, in the filter’sinit-param`s and in `default.propertiesoverrides, because a developer’s local file can be packaged by mistake. At start-up a deployed instance that still runs in devMode logs theWARNfrom the OGNL guard quoted earlier, which a log alert can look for. -
The config browser is not in the WAR (the Maven profile) or its namespace is blocked (the constraint).
-
<s:debug/>is not in any JSP. -
The log levels are
warnfororg.apache.struts2andinfoorwarnfor the application. The Security Guide’s example does the same with a console appender; the configuration below uses a rolling file, which was loaded by the scratch Tomcat. -
The debugging interceptor is disabled for the actions that handle secrets, if devMode could ever be switched on in that environment.
-
A debugger agent is not in the start script.
-
The
struts.ognl.logMissingPropertiesconstant is off.
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Appenders>
<RollingFile name="FILE" fileName="${sys:catalina.base}/logs/bookshelf.log"
filePattern="${sys:catalina.base}/logs/bookshelf-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5p [%t] %c{1.} - %m%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="5"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.apache.struts2" level="warn"/>
<Logger name="org.example.bookshelf" level="info"/>
<Root level="warn">
<AppenderRef ref="FILE"/>
</Root>
</Loggers>
</Configuration>
monitorInterval is the attribute that makes Log4j 2 read the file again; it is the production counterpart of devMode’s reloading and
is far less risky. On the scratch Tomcat (with a value of 5 seconds, to keep the test short) editing the file in the exploded WAR
changed org.apache.struts2 from warn to info and the parameter logger from debug to warn within the interval, and the
next request wrote 3 lines instead of 18, without a restart. In production, this is the way to raise a logger for ten minutes when
a problem cannot be reproduced elsewhere, and to lower it again.
devMode for One Request
The Javadoc of PrepareOperations has a method for switching devMode on per request:
overrideDevMode(boolean) sets an override of the static devMode value for the current thread, with the warning not to set it from a
request parameter or any other unprotected source and the suggestion that a signed cookie is one safe way to turn it on per request,
and clearDevModeOverride() removes it. The override is read by the debugging interceptor, the <s:debug/> tag and the Problem Report
handler (the three classes that call getDevModeOverride() in the 7.4.0 jar); it does not change the OGNL guard or the reloading.
The filter below shows how the override is wired, and it is shown as an anti-pattern to recognise, not as part of a production
build: it switches devMode, with the debug=command OGNL evaluator, the Problem Report and <s:debug/>, on for any request that
carries a static token in a header. It belongs, if anywhere, in a staging or internal-only build, and it must never be deployed
where the Internet can reach it. The scratch application ran it on a local Tomcat only:
package org.example.bookshelf.ops;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.apache.struts2.dispatcher.PrepareOperations;
/** STAGING / INTERNAL ONLY: switches devMode on for ONE request when the caller presents the ops token. */
public class DevOverrideFilter implements Filter {
private byte[] token;
@Override
public void init(FilterConfig config) {
String configured = config.getInitParameter("opsToken"); // injected per environment
token = (configured == null || configured.isBlank())
? null : configured.getBytes(StandardCharsets.UTF_8);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
String presented = ((HttpServletRequest) req).getHeader("X-Ops-Token");
boolean allowed = token != null && presented != null
&& MessageDigest.isEqual(token, presented.getBytes(StandardCharsets.UTF_8));
try {
if (allowed) {
PrepareOperations.overrideDevMode(true);
}
chain.doFilter(req, res);
} finally {
PrepareOperations.clearDevModeOverride();
}
}
}
The filter is declared before the struts2 filter in web.xml with an opsToken init-param whose value in the scratch application is
the placeholder CHANGE_ME (a staging deployment would inject the secret from the environment and never commit it). Requested on
the scratch application with struts.devMode=false, without the header or with a wrong one, books?filter=Dune&debug=command&expression=filter returned the list
page; with the right header it returned Dune, books-debug showed the <s:debug/> block, and an unmapped action name answered
200 with the Problem Report. Without the header the same URLs behaved as in the production column of the table above.
The method exists and works, and that is the danger. The filter puts an OGNL evaluator behind a static shared secret in a header, which any proxy in the path can log, and it is the same remote evaluator that the Security Guide asks you to remove; it contradicts the rule that devMode is never on in production and the Security Hardening checklist. Do not ship it in a WAR that is deployed where the Internet can reach it, not even disabled by a missing token. For a production incident use the logging route above.
The IDE and a Debugger
Everything above works without a debugger, but for an action that does something wrong after parameter injection a breakpoint is the
fastest tool. Set the breakpoint in the action method or in a setter; the debugger’s call stack at that point is a second way to read
the architecture page, with DefaultActionInvocation.invoke calling one interceptor after another. For a servlet container the JDWP agent is started by catalina.sh jpda start and the IDE attaches to port 8000; the
JSP page of this section on testing and debugging (Attaching a Debugger)
has the exact options, the warning that a JDWP port is an unauthenticated code execution interface, and the container variant.
A breakpoint in a .jsp works through the SMAP of the compiled page, as that page explains.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
The application logs nothing from Struts and |
Only |
Add |
devMode is off but the config browser answers 200 |
The plugin does not look at devMode |
Remove the jar from the WAR (profile) or constrain |
|
|
Use |
|
The action method runs before the interceptor replaces the result |
Never debug an unsafe action in a shared environment; use |
|
No session existed when the interceptor stored the stack |
Make a request that creates the session first |
|
The first devMode reload in 7.4.0 leaves static content and error handling without the servlet context |
Reload the context; set |
The action’s setter is ignored and the page works |
The setter lacks |
Annotate it, and test with devMode on |
Nothing about the rejected parameter in the production log |
Rejected input is |
Raise |
A method call in the console, |
The OGNL guard blocked it and the answer is silent; the reason is a |
Read the log; use the property form |
The resource bundle edit is not shown |
devMode is off or |
Edit |
Log lines for |
No class of 7.x is in that package |
Configure |
A page is slow in development |
devMode disables the FreeMarker cache and reloads |
The four constants of the Development Mode page, as above |
A |
The container no longer injects a |
Declare a Log4j 2 |
Tomcat warns that a |
The Bookshelf does not stop Log4j when the web application stops |
The Log4j page for Jakarta EE describes the |
What Changed Recently
| Version | Change |
|---|---|
6.0.0 |
The deprecated XWork logging layer is dropped (WW-4763) and the profiling layer is removed (WW-4779); the list of fixed issues includes setting |
7.0.0 |
Parameter annotation problems are logged at |
7.3.0 |
SLF4J and |
7.4.0 |
Five dev-mode setters of |
Where the Books and the Official Pages Are Outdated
The older books on Struts 2 teach devMode as a one-line trick. One of them has it among the best practices of a late chapter,
as the setting that makes a developer’s life easier by reloading and by raising errors; the rest of the advice there still holds
(devMode off in production, a restart when the XML is wrong), but three of its assumptions changed. The first is that a reload
rebuilds a working application: in 7.4.0 a reload leaves the static content and the Problem Report broken until a restart, as seen
above. The second is the debugging page, which in those books is a way to dump everything in XML; on 7.4.0 the XML mode is broken
and the browser mode is the working one. The third is logging, which was a choice of three libraries in the 2.0 generation and is now
one API; and the book’s logger and package names (com.opensymphony) are org.apache.struts2.
The official pages have their own inconsistencies, found while verifying the examples of this page:
-
The Getting Started page says that the debugging interceptor halts the action;
xmlandbrowserrun it first (commanddoes not). Theobjectdefault is said to be#context; with noobjectthe action is shown. -
The Debugging page still shows a complete
<debug>document for?debug=xml, withclass com.opensymphony.xwork2.ActionSupportin it; the 7.4.0 interceptor fails on theattrelement, and the class names areorg.apache.struts2.ActionSupport. -
The Getting Started page on debugging says that the browser page needs the Struts dojo plugin for its expand and collapse links; the dojo plugin is not among the 17 plugins published for 7.4.0, and the 7.4.0 page works without it (the links use a script in the page).
-
The Development Mode page says that an unknown request field throws an exception; in 7.4.0 it is a log entry, and an explicit
struts.i18n.reloadorstruts.configuration.xml.reloadconstant is honoured over devMode, which the page implies only with its slow-rendering example. -
The Logging page says
struts2-coredeclareslog4j-core; the POM has it only for tests. -
The Security Guide’s Log4j 2 sample configures
com.opensymphony.xwork2, and both it and the Getting Started how-to use a pattern with%C{2}and%F:%L; the Log4j layouts page explains that location information makes Log4j take a snapshot of the stack and walk it, so use a cheaper pattern in production and configureorg.apache.struts2. -
The Config Browser pages carry "2003 - 2015", ask for "Maven 2 data" and print a double slash in the default URL of an action in the root namespace.
Summary
-
struts.devModeis one constant with many effects: reloading, Developer Notification errors, louder warnings, the Problem Report page, the debug tools and the devMode OGNL lists. Off by default; never on in production. -
The debugging interceptor works only in devMode;
commandandbrowserwork in 7.4.0,xmldoes not,xmlandbrowserrun the action, anddebugging.disabledremoves the interceptor from an action. -
<s:debug/>is a block in the page that exists only in devMode; the config browser exists as long as its jar is on the class path and has to be removed or blocked. -
Logging is Log4j 2: add
log4j-coreyourself, raiseorg.apache.struts2subpackages instead of the whole tree, and usemonitorIntervalto change levels without a restart. -
Most "why does it not work" answers are one
WARNorERRORline away: a rejected parameter, a blocked expression, a missing key.
References
-
Struts Core Developer Guide — Development Mode, What does it do?
-
Struts Core Developer Guide — Development Mode, Don’t forget
-
Struts Core Developer Guide — Development Mode, Page rendering is slow
-
Struts Core Developer Guide — Debugging Interceptor, Parameters
-
Struts Core Developer Guide — Debugging Interceptor, Example
-
Struts Core Developer Guide — Logging, Routing Struts logging elsewhere
-
Struts Core Developer Guide — Interceptors, Stacking Interceptors
-
Struts Getting Started — Debugging Struts, Configuration Plugin
-
Struts Getting Started — Debugging Struts, Using the Debugging Interceptor
-
Struts Security Guide — Restrict access to the Config Browser Plugin
-
Apache Log4j 2 — Rolling File Appender, SizeBasedTriggeringPolicy