Wildcard Mappings & Method Invocation

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, Jakarta Servlet 6.1 and Java 21, with the configuration read against the Struts 6.0 DTD (why 6.0). Everything was verified on 2026-10-08: the facts against the Struts Core Developer Guide, the Getting Started tutorial, the security guide and the 7.4.0 source, and every example by building it into a Maven WAR with struts2-bom 7.4.0 and calling it with curl on a scratch Tomcat 11.0.26. The outputs shown are the outputs of those runs. A wildcard mapping lets one <action> element stand for many URLs, and the placeholder {1} carries the part of the URL that the wildcard matched into the method name, the class name, the results and the parameters. That power is also the page’s main hazard, because a mapping that turns request text into a method name decides which Java methods a stranger can call. The running scenario is the Bookshelf of the previous pages, now with a BookCrudAction that can list, create, edit and delete books, and a maintenance method called purge that must never be reachable from a URL.

Why Wildcards Exist

A conventional mapping names one URL, one class and one method. The Bookshelf with four operations needs four elements that differ only in a method name. A wildcard mapping states the pattern once.

Table 1. The same URL space with four hard-coded mappings and with one wildcard mapping
Approach Configuration Cost

One mapping per URL

createBook, editBook, deleteBook and the list each need their own <action> with method="create", method="edit" and so on

Grows with every new method; easy to forget one; each is explicit and safe

One wildcard mapping

<action name="*Book" method="{1}"> serves createBook, editBook, deleteBook and plain Book

Constant size; every method name that matches the pattern becomes a URL, so the allowed set must be restricted

One regular-expression mapping

<action name="/{type}/content/{title}"> serves any two-segment URL and fills two fields

Needs a different pattern matcher for the whole application; clean URLs without an extension

Wildcard mappings come from Struts 1 and its predecessors (see Struts 1 (Classic) & Migration). The Struts 1 mailreader example that the Core Developer Guide still quotes maps /edit* to Edit{1}Action. In Struts 1 the same need was met with DispatchAction, an action class whose request parameter named the method to call, and with LookupDispatchAction, which looked the method up from a button label. Struts 2 offers three replacements, in the order the framework prefers them: the method attribute with a wildcard, strict method invocation with an allowed list, and, deprecated in practice, dynamic method invocation with the bang character. See the Struts 1 part of the history page for where those ideas came from.

Table 2. Status of each mechanism in Struts 7.4.0 (verified 2026-10-08)
Mechanism Status Default and note

Wildcard patterns with *, ** and {N}

Current

Default pattern matcher (the struts bean, class WildcardHelper); always available

Wildcard method selection (method="{1}")

Current, preferred

Subject to strict method invocation

Strict method invocation (<allowed-methods>, <global-allowed-methods>, @AllowedMethods)

Current, on by default

The DTD default of strict-method-invocation is true; there is no application-wide switch, only a per-package attribute

Regular-expression patterns (struts.patternMatcher = regex)

Current, opt-in

One pattern matcher per application

Named variables in namespaces (struts.patternMatcher = namedVariable)

Current, opt-in

Mutually exclusive with the wildcard and regex matchers

Dynamic method invocation, book!create.action and the method: prefix

Present, off

struts.enable.DynamicMethodInvocation is false in the 7.4.0 default.properties

The action: parameter prefix

Present, off

struts.mapper.action.prefix.enabled is false; the redirect: and redirectAction: prefixes do not exist in the 7.4.0 DefaultActionMapper

How a URL Is Matched

The StrutsPrepareAndExecuteFilter of the servlet filter chain hands the request to the ActionMapper. The default mapper splits the path into a namespace and an action name, removes the extension (action or none, because struts.action.extension is action,, in 7.4.0, so /crud/createBook and /crud/createBook.action are the same request) and, only when dynamic method invocation is on, splits the name at a bang. The configuration then looks the name up inside the namespace. The order of that lookup is the part that people get wrong.

flowchart TD req["GET /crud/createBook.action"] --> map["ActionMapper: namespace /crud, name createBook, extension action"] map --> dmi{"DMI on and name contains a bang?"} dmi -->|"yes"| split["name book, method create from the URL"] dmi -->|"no"| exact{"Exact action name in the namespace?"} split --> exact exact -->|"yes: no wildcard matching at all"| cfg["ActionConfig as written"] exact -->|"no"| wild["Try wildcard patterns in declared order, first match wins"] wild -->|"none matches"| nf["404: There is no Action mapped"] wild -->|"match"| sub["Copy the config and replace each placeholder with the matched text"] sub --> allowed{"Is the resulting method allowed?"} allowed -->|"no"| nf allowed -->|"yes"| proxy["ActionProxy: interceptors, then the method, then the result"] cfg --> proxy

The diagram is the whole rule: a name that has no wildcard is found first and is never compared with the patterns.

The Pattern Tokens

A pattern is an action name that contains a special token. A name without a token is a literal and is matched by plain lookup.

Table 3. Tokens of the default wildcard matcher (class WildcardHelper, 7.4.0)
Token Matches Example

*

Zero or more characters except the slash

docs/* matches docs/readme but not docs/a/b

**

Zero or more characters including the slash

all/** matches all/a/b and fills {1} with a/b

\x

The backslash escapes the next character, so \* is a literal asterisk

An action name with an asterisk cannot be requested anyway: struts.allowed.action.names accepts only letters, digits and the characters ., _, !, /, -

{N} in an attribute

The text matched by the N-th wildcard, N from 1 to 9

method="{1}"

{0} in an attribute

The whole matched request path

generic mapping, {0} printed generic mapping, anything for /order/anything

Two properties of the matching are easy to forget. First, a wildcard is not greedy: it matches up to the first occurrence of the text that follows it. Second, the loose-match rule: when the pattern ends in a wildcard that follows a literal character, such as Book_*, the pattern is also tried without those last two characters, so Book matches Book_* with an empty {1}. That rule exists to support the old *!* form with an optional part, and it is what lets the Bookshelf call Book and Book_create through one mapping.

The Bookshelf Wildcard Mapping

The scratch application has this action class. execute lists, create, edit and delete are the operations, and purge is the maintenance method that no URL may reach. Setters that carry @StrutsParameter are the only ones that request parameters may fill (the page Parameter Injection and @StrutsParameter covers that annotation); the class below has none that matter yet.

The Bookshelf action: four operations and one method that must stay private to the application
package org.example.bookshelf;

import org.apache.struts2.ActionSupport;

public class BookCrudAction extends ActionSupport {
    private String message = "unset";

    @Override
    public String execute() {
        if ("unset".equals(message)) {
            message = "list";
        }
        return SUCCESS;
    }

    public String create() { message = "create"; return SUCCESS; }
    public String edit()   { message = "edit";   return SUCCESS; }
    public String delete() { message = "delete"; return SUCCESS; }

    /** Maintenance method that was never meant to be reachable from a URL. */
    public String purge()  { message = "purge: every book deleted"; return SUCCESS; }

    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}

The view prints the message field, so the examples below show which method ran. It also prints four fields (type, title, author and id) that are empty in most requests; the outputs on this page keep only the interesting part of the line.

/WEB-INF/views/book.jsp: print the name of the method that ran
<%@ page contentType="text/plain; charset=UTF-8" %><%@ taglib prefix="s" uri="/struts-tags" %>method=<s:property value="message"/> type=<s:property value="type"/> title=<s:property value="title"/> author=<s:property value="author"/> id=<s:property value="id"/>
One wildcard mapping that serves the list, create, edit and delete URLs
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
        "https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
    <package name="crud" extends="struts-default" namespace="/crud">
        <action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
            <result>/WEB-INF/views/book.jsp</result>
            <allowed-methods>create,edit,delete</allowed-methods>
        </action>
    </package>
</struts>
Calling the mapping (output of the scratch run on Tomcat 11.0.26, trimmed to the method= part)
$ curl -s http://localhost:18080/g8wild/crud/createBook.action
method=create
$ curl -s http://localhost:18080/g8wild/crud/editBook.action
method=edit
$ curl -s http://localhost:18080/g8wild/crud/deleteBook.action
method=delete
$ curl -s http://localhost:18080/g8wild/crud/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/crud/purgeBook.action
404

The last request shows the point of the allowed list. purgeBook matches the pattern, the placeholder would turn into purge, and the framework refuses because purge is neither the method attribute of a literal mapping nor on any allowed list. The response is the ordinary "no action" 404, with the text There is no Action mapped for namespace [/crud] and action name [purgeBook].

The URL /crud/Book has nothing in front of Book, so {1} is empty. An empty method becomes execute, and execute is on the default allowed list (see Where the Allowed List Comes From), which is why the plain Book URL lists the books.

Where {1} Can Be Used

The substitution is done by ActionConfigMatcher.convert on a copy of the matched configuration. In 7.4.0 it replaces the placeholders in the following places and in no others.

Table 4. Attributes and elements that accept {N} (from ActionConfigMatcher in the 7.4.0 source; the method, class, parameter, result and location uses were run in the scratch app)
Place Example Effect

method attribute of <action>

method="{1}"

Chooses the Java method; checked against the allowed methods

class attribute of <action>

class="org.example.bookshelf.Edit{1}Action"

Chooses the action class; see A Class Chosen by a Wildcard and the OGNL Allowlist for the allowlist consequence

<param> of the action

<param name="id">{1}</param>

Static parameter, set before request parameters

name of a <result>, and the result type

<result name="{1}">

Maps an outcome from the matched text

<param> of a result

<param name="actionName">{1}</param>

Location, namespace, actionName and every other result parameter

<result> body (the location)

/WEB-INF/views/Edit{1}.jsp

Picks the view

<exception-mapping> name, class, result and parameters

result="{1}"

Chooses the error result

{1} in the class, the result location and a static parameter
<package name="admin" extends="struts-default" namespace="/admin">
    <action name="edit*" class="org.example.bookshelf.Edit{1}Action">
        <result>/WEB-INF/views/Edit{1}.jsp</result>
    </action>
    <action name="book/*" class="org.example.bookshelf.BookCrudAction">
        <param name="id">{1}</param>
        <param name="message">book/{1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run (the first line needs the package on the allowlist, see A Class Chosen by a Wildcard and the OGNL Allowlist)
$ curl -s http://localhost:18080/g8wildal/admin/editAuthor
EditAuthor view: EditAuthorAction ran, id=null
$ curl -s http://localhost:18080/g8wild/admin/book/42
method=book/42 id=42

A result can be a whole redirect target. The mapping below sends /go/toabout to the action about of the /pages namespace.

{1} inside a redirectAction result
<package name="go" extends="struts-default" namespace="/go">
    <action name="to*">
        <result type="redirectAction">
            <param name="namespace">/pages</param>
            <param name="actionName">{1}</param>
        </result>
    </action>
</package>
The redirect answered by the scratch app
$ curl -s -i http://localhost:18080/g8wild/go/toabout | grep -i "HTTP/\|location"
HTTP/1.1 302
Location: /g8wild/pages/about.action

A mapping with no class at all, the last of these placeholder uses, is the catch-all that serves static-looking pages by name. The framework fills in ActionSupport as the default class, and the result location is built from the matched name.

A catch-all that maps /pages/about to /WEB-INF/views/about.jsp
<package name="pages" extends="struts-default" namespace="/pages">
    <action name="*">
        <result>/WEB-INF/views/{1}.jsp</result>
    </action>
</package>
Output of the scratch run
$ curl -s http://localhost:18080/g8wild/pages/about
page about rendered
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/pages/secret
404

A single * cannot contain a slash, so /pages/../x cannot be built from the name, and the views stay under WEB-INF. A ** in the same position would remove that protection, so do not use it in a result location. The catch-all belongs at the end of the configuration of its namespace, as the documentation says, because anything declared after it that it also matches is never reached (see Which Pattern Wins).

Once a wildcard mapping exists, the framework treats the expanded name as an action of its own. The Core Developer Guide says that such a "virtual" mapping can have its own validations, message resources and type converters, just as a hand-written mapping can.

Which Pattern Wins

An exact name always beats a pattern, and a request that matches an exact name never reaches the wildcard matcher. Among several patterns that match, the order of declaration decides, and here the official documentation and the framework disagree.

Table 5. What the sources say about patterns that overlap
Source Statement

Core Developer Guide, Wildcard Mappings

"If more than one pattern matches the last one wins, so less specific patterns must appear before more specific ones"

AbstractMatcher, 7.4.0 source

"Patterns will be matched in the order they were added. The first match wins, so more specific patterns should be defined before less specific patterns"; the loop stops at the first match

The scratch run

The first declared pattern that matches wins, so the guide’s sentence is wrong for 7.4.0

Two packages with the same two patterns in opposite orders show it.

The generic pattern first, then the specific one (namespace /order), and the reverse (namespace /order2)
<package name="order" extends="struts-default" namespace="/order">
    <action name="*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">generic mapping, {0}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="book*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">book* mapping, {1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="bookshelf" class="org.example.bookshelf.BookCrudAction">
        <param name="message">exact mapping</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
<package name="order2" extends="struts-default" namespace="/order2">
    <action name="book*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">book* mapping, {1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">generic mapping, {0}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run
$ curl -s http://localhost:18080/g8wild/order/bookcase
method=generic mapping, bookcase
$ curl -s http://localhost:18080/g8wild/order/bookshelf
method=exact mapping
$ curl -s http://localhost:18080/g8wild/order2/bookcase
method=book* mapping, case
$ curl -s http://localhost:18080/g8wild/order2/anything
method=generic mapping, anything

So the rule to apply is the one in the framework’s own javadoc: write the specific pattern first and the catch-all last. Physical order is a property of XML; the Convention plugin has no such order, because annotated classes are found by scanning. Since Struts 7.3.0 the names of annotated wildcard actions are sorted most specific first before they are registered (WW-3784), with these keys in order: fewer ** tokens, fewer wildcard tokens (a *, a ** run or a {var} group), more literal characters, then alphabetical order as a tie-breaker. Struts 7.3.0 applied the ** key third and 7.4.0 moves it to the first place, so a/*/* is registered before a/** (WW-5743). XML mappings are unaffected and keep their declared order.

Non-Greedy Matching and Several Wildcards

A wildcard stops at the first occurrence of what follows it.

A mapping that works for ListAccounts and fails for ListSponsors, and two wildcards in one name
<package name="star" extends="struts-default" namespace="/star">
    <action name="docs/*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">docs/* got {1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="all/**" class="org.example.bookshelf.BookCrudAction">
        <param name="message">all/** got {1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="List*s" class="org.example.bookshelf.BookCrudAction">
        <param name="message">List*s got {1}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
    <action name="two/*/*" class="org.example.bookshelf.BookCrudAction">
        <param name="message">two got {1} and {2}</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run (slashes in action names need the constants of Slashes in Action Names)
$ curl -s http://localhost:18080/g8wild/star/docs/readme
method=docs/* got readme
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/star/docs/a/b
404
$ curl -s http://localhost:18080/g8wild/star/all/a/b
method=all/** got a/b
$ curl -s http://localhost:18080/g8wild/star/ListAccounts
method=List*s got Account
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/star/ListSponsors
404
$ curl -s http://localhost:18080/g8wild/star/two/a/b
method=two got a and b

ListSponsors fails for the reason the guide gives: the pattern List*s stops at the first s, so {1} becomes Spon and the configuration that results is ListSpons, which does not exist. Where a name can legitimately contain the following letter, use the regular-expression matcher of Regular-Expression Patterns.

Prefix and Postfix Styles

The Core Developer Guide shows two naming styles for wildcard methods: the method as a prefix of the action name (*Book, so createBook) and the method as a postfix after an underscore (Book_*, so Book_create). The postfix style reads like a URL with a verb and is the one the loose-match rule was written for.

The postfix style
<package name="postfix" extends="struts-default" namespace="/postfix">
    <action name="Book_*" class="org.example.bookshelf.BookCrudAction" method="{1}">
        <result>/WEB-INF/views/book.jsp</result>
        <allowed-methods>create,edit</allowed-methods>
    </action>
</package>
Output of the scratch run
$ curl -s http://localhost:18080/g8wild/postfix/Book_create
method=create
$ curl -s http://localhost:18080/g8wild/postfix/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/postfix/Book_purge
404

Slashes in Action Names

By default an action name has no slash: /admin/book/42 is read as namespace /admin/book, name 42. The Core Developer Guide’s section "Parameters after the action name" shows how to keep the rest of the path in the name. Two constants are needed.

The two constants that make book/* and all/** possible
<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>

The second constant matters because struts.mapper.alwaysSelectFullNamespace is true in the 7.4.0 default.properties (a change of the 6 to 7 line). With it set to true the mapper takes the whole path before the last slash as the namespace and never looks for a shorter namespace that has a longer action name.

The same URLs with and without the constants (output of the scratch runs g8wild and g8wildns)
$ curl -s http://localhost:18080/g8wild/admin/book/42
method=book/42 id=42
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildns/admin/book/42
404
$ curl -s http://localhost:18080/g8wildns/admin/book/42 | grep -o "There is no Action[^.]*"
There is no Action mapped for namespace [/admin] and action name [42] associated with context path [/g8wildns]
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/admin/book/42/x
404

The constant struts.enable.SlashesInActionNames changes the whole application, not one package. The Core Developer Guide links the side effects to WW-1383, and the validation-file naming for names with slashes is covered on the page Validation. The related constant struts.allowed.action.names holds the regular expression that an action name must match; the default is [a-zA-Z0-9._!/\-]*, and a name that does not match is answered with the default action.

Strict Method Invocation

Strict method invocation (SMI) is the rule that decides which Java methods a request may run. It replaces the older "strict DMI" and applies to every way of selecting a method: a method attribute with placeholders, a bang URL and a method: parameter. The check is made by ActionConfig.isAllowedMethod: a method is allowed when it is the method written in the mapping, when it equals execute for a mapping without a method, or when it is on the allowed list of the action.

Where the Allowed List Comes From

Table 6. Sources of allowed methods (Core Developer Guide, Action Configuration; behaviour verified on 7.4.0)
Source Scope Note

The method attribute of the mapping

That mapping

A literal such as method="edit" needs no other entry

<allowed-methods> inside <action>

That action

A comma-separated list; it must come after <result>, <param>, <interceptor-ref> and <exception-mapping>

<global-allowed-methods> inside <package>

Every action of the package

The struts-default package declares execute,input,back,cancel,browse,save,delete,list,index, and a package that extends it keeps those names (verified, see Global Lists, Regular Expressions and Annotations)

@AllowedMethods on an action class

That class

Convention plugin annotation; takes an array of names

@AllowedMethods in package-info.java

Every action of that Java package

Same annotation

regex: prefix in any list

As the list it appears in

The text after the prefix is a Java regular expression that must match the whole method name, as in the example of Global Lists, Regular Expressions and Annotations

strict-method-invocation="false" on <package>

The package

Any method whose name matches ([A-Za-z0-9_$]*); see What Switching SMI Off Allows

Three states exist. With SMI on and no list for an action, only the global list and the mapping’s own method are allowed. With SMI on and a list, the list is added to those. With SMI off, any method name matching the regular expression is allowed.

Three Packages for the Same Mapping

The crud package of The Bookshelf Wildcard Mapping, the same wildcard with no list at all, and with SMI turned off
<package name="bare" extends="struts-default" namespace="/bare">
    <action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
<package name="open" extends="struts-default" namespace="/open" strict-method-invocation="false">
    <action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/bare/createBook
404
$ curl -s http://localhost:18080/g8wild/bare/deleteBook
method=delete
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/bare/listBook
500
$ curl -s http://localhost:18080/g8wild/open/purgeBook
method=purge: every book deleted

The four results teach different things.

  • bare/createBook is refused: with no list, create is not allowed, because the package declares no <allowed-methods> and create is not on the default list.

  • bare/deleteBook works with no entry anywhere, because delete is on the global list that struts-default declares. A method called delete is allowed by default in every package that extends struts-default.

  • bare/listBook is also allowed by the global list, and then fails with HTTP 500 and the message org.example.bookshelf.BookCrudAction.list(), a NoSuchMethodException, because the class has no list method. Being allowed is not the same as existing.

  • open/purgeBook calls the maintenance method. The package turned the protection off, and the mapping {1} became the pattern ([A-Za-z0-9_$]*). Do not do this to a package whose action has methods that are not meant for URLs.

What Switching SMI Off Allows

The default regular expression ([A-Za-z0-9_$]*) allows every public method whose name uses those characters, including getters. A getter returns a String in the Bookshelf (getMessage), and the framework treats the returned String as a result code. The request below executes getMessage in the unprotected package, and the response is the framework complaining that unset is not a result.

A getter reached as an action method in the package with SMI off (scratch run g8wildsmi)
$ curl -s http://localhost:18080/g8wildsmi/open/getMessageBook | grep -o "No result defined[^<]*"
No result defined for action org.example.bookshelf.BookCrudAction and result unset

If a package must run with SMI off, narrow the expression with the constant struts.strictMethodInvocation.methodRegex. With the value ([a-z]*) the same request is refused, because getMessage has an upper-case letter, while purge and create still pass.

Narrowing the method name expression
<constant name="struts.strictMethodInvocation.methodRegex" value="([a-z]*)"/>
Output of the scratch run g8wild that sets the constant (g8wildsmi is the same configuration without it)
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/open/getMessageBook
404
$ curl -s http://localhost:18080/g8wild/open/createBook
method=create

The better position is never to turn SMI off. A restricted wildcard mapping, with the three method names in <allowed-methods>, can be reasoned about; a package-wide regular expression cannot.

Global Lists, Regular Expressions and Annotations

A package can add to the global list, and a list entry can be a regular expression when it starts with regex:.

A package-wide list with a regular expression
<package name="regex" extends="struts-default" namespace="/regex">
    <global-allowed-methods>regex:(create|edit)</global-allowed-methods>
    <action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run
$ curl -s http://localhost:18080/g8wild/regex2/createBook
method=create
$ curl -s http://localhost:18080/g8wild/regex2/editBook
method=edit
$ curl -s http://localhost:18080/g8wild/regex2/deleteBook
method=delete
$ curl -s http://localhost:18080/g8wild/regex2/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/regex2/purgeBook
404

The list of the package adds to the default list instead of replacing it: deleteBook and the plain Book (which runs execute) still work although the package names only the regular expression. The 7.4.0 default list is therefore in force for every package that extends struts-default.

The Convention plugin offers the same control with an annotation, either on the class or in package-info.java. The class below is in the package org.example.bookshelf.action, which the plugin scans.

A Convention action that allows the extra method archive for dynamic invocation
package org.example.bookshelf.action;

import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.AllowedMethods;
import org.apache.struts2.convention.annotation.Result;

@AllowedMethods({"archive"})
@Action(value = "shelf", results = @Result(name = "success", location = "/WEB-INF/views/book.jsp"))
public class ShelfAction extends org.example.bookshelf.BookCrudAction {
    public String archive() {
        setMessage("archive");
        return SUCCESS;
    }
}

The effect shows only when a method can be selected from the URL, so the output is in Dynamic Method Invocation.

Ambiguous Action Methods

The security guide has a section, "Ambiguous Action Methods", about actions with a deep class hierarchy that reuse a method name. Its example has save() in three classes, one of them overloaded with a Double parameter, and says that OGNL cannot decide which method to call (WW-4405).

The guide’s hierarchy, mapped as a Bookshelf action
public abstract class AbstractAction extends ActionSupport {
    protected void save(Double value) {
        // some logic
    }
}

public class BaseAction extends AbstractAction {
    public String save() throws Exception {
        save(Double.MAX_VALUE);
        return SUCCESS;
    }
}

public class RealAction extends BaseAction {
    @Override
    public String save() throws Exception {
        super.save();
        return SUCCESS;
    }
}
The mapping, with a result that answers 204 and the output of the scratch run
<package name="amb" extends="struts-default" namespace="/amb">
    <action name="save" class="org.example.bookshelf.amb.RealAction">
        <result type="httpheader"><param name="status">204</param></result>
    </action>
</package>
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/amb/save.action
204

On 7.4.0 the example works. The JIRA issue WW-4405 is closed as fixed in 2.5, so the section of the guide is historical: renaming the method, as the guide advises, is still harmless, but it is no longer needed to make the request work.

The DTD Order of allowed-methods

The DTD declares the content of <action> as ((param|result|interceptor-ref|exception-mapping)*,allowed-methods?). The element <allowed-methods> must therefore come last. An application whose element is first does not start. Tomcat reports the failure of the filter, with the cause below, and every request then answers 404.

The startup failure when <allowed-methods> precedes <result> (first scratch build)
Caused by: The content of element type "action" must match
"((param|result|interceptor-ref|exception-mapping)*,allowed-methods?)".

Dynamic Method Invocation

Dynamic method invocation (DMI) lets the caller name the method in the URL. The bang form book!create.action runs the book mapping with the method create. The parameter form sends a request parameter whose name starts with method:, as in book.action?method:create=x, and the value is ignored. Struts uses the second form for buttons: <s:submit method="create"/> renders as name="method:create".

The Core Developer Guide explains why the framework does not like it. The code scans the action name for a bang and then tricks the framework into calling another method while the configuration is that of execute, including its validation. The framework "believes" it is running execute. The wildcard method feature is implemented differently: the framework behaves as if the mapping Category!create had been written in the file, so the virtual mapping has its own validations, messages and type converters. For that reason the guide says wildcard method selection is preferred. A DMI request also reaches a method that no mapping mentions, and the security guide recommends disabling DMI to avoid XSS in the automatically generated error pages that contain the action name.

Table 7. The state in Struts 7.4.0
Switch Effect

struts.enable.DynamicMethodInvocation

false by default. When false, book!create.action is not split at the bang and the name book!create is looked up literally, so the answer is the 404 "no Action mapped" for [book!create]. The method: prefix is ignored

struts.mapper.action.prefix.enabled

false by default. The action: prefix, as in name="action:other", is ignored until it is true; action:name!method additionally needs DMI

struts.mapper.action.prefix.crossNamespaces

false by default. Blocks action: from reaching other namespaces

With DMI Off

The exact namespace of the scratch app (DMI off) and what the bang and the method: button do there
<package name="exact" extends="struts-default" namespace="/exact">
    <action name="book" class="org.example.bookshelf.BookCrudAction">
        <result>/WEB-INF/views/book.jsp</result>
        <allowed-methods>create</allowed-methods>
    </action>
    <action name="other" class="org.example.bookshelf.BookCrudAction">
        <param name="message">other action ran</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run g8wild (defaults: DMI off, action prefix off)
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g8wild/exact/book!create.action'
404
$ curl -s -d 'method:create=Create' http://localhost:18080/g8wild/exact/book.action
method=list
$ curl -s -d 'action:other=Other' http://localhost:18080/g8wild/exact/book.action
method=list

With DMI On, Strict Method Invocation Still Applies

DMI is not removed from Struts 7.4.0, and an application that still relies on it has to turn it on. SMI then decides which methods the URL may name. The scratch application g8wilddmi sets both switches on and uses these mappings.

A scratch configuration with DMI and the action prefix switched on
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.mapper.action.prefix.enabled" value="true"/>
<package name="dmi" extends="struts-default" namespace="/">
    <action name="book" class="org.example.bookshelf.BookCrudAction">
        <result>/WEB-INF/views/book.jsp</result>
        <allowed-methods>create,edit</allowed-methods>
    </action>
    <action name="other" class="org.example.bookshelf.BookCrudAction">
        <param name="message">other action ran</param>
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
<package name="dmiopen" extends="struts-default" namespace="/open" strict-method-invocation="false">
    <action name="book" class="org.example.bookshelf.BookCrudAction">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run g8wilddmi
$ curl -s 'http://localhost:18080/g8wilddmi/book!create.action'
method=create
$ curl -s 'http://localhost:18080/g8wilddmi/book!delete.action'
method=delete
$ curl -s 'http://localhost:18080/g8wilddmi/book!purge.action' | grep -o "Method purge[^<]*"
Method purge for action book is not allowed!
$ curl -s 'http://localhost:18080/g8wilddmi/book.action?method:create=x'
method=create
$ curl -s 'http://localhost:18080/g8wilddmi/other!create.action' | grep -o "Method create[^<]*"
Method create for action other is not allowed!
$ curl -s -d 'action:other=Other' http://localhost:18080/g8wilddmi/book.action
method=other action ran
$ curl -s 'http://localhost:18080/g8wilddmi/open/book!purge.action'
method=purge: every book deleted
$ curl -s 'http://localhost:18080/g8wilddmi/shelf!archive.action'
method=archive
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g8wilddmi/shelf!purge.action'
404

Read the results in order. create is allowed by the list of book; delete is allowed by the global default list; purge is refused with the message Method purge for action book is not allowed!, which is a different 404 text from the wildcard case because the action was found. other has no list, so create is refused. The action: prefix runs the other mapping when the constant allows it. The package with SMI off lets purge through exactly as the wildcard variant did. The Convention action accepts archive because of @AllowedMethods, and refuses purge, which the subclass inherits as a public method.

Multi-Button Forms Without DMI

The common reason for DMI was a form with several buttons. A form tag of Struts renders the method and action attributes of <s:submit> as button names with a prefix, and both prefixes are inert by default, so the buttons stop doing anything.

A form with a method button, an action button and a plain HTML5 formaction button
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:form action="book" namespace="/exact" theme="simple">
    <s:submit value="Create" method="create"/>
    <s:submit value="Other" action="other" namespace="/exact"/>
    <button type="submit" formaction="<s:url action='book' namespace='/exact'/>">Plain</button>
</s:form>
The rendered HTML (the session id of the scratch run is removed)
<form id="book" name="book" action="/g8wild/exact/book.action" method="post">
    <input type="submit" value="Create" id="book__create" name="method:create"/>
    <input type="submit" value="Other" id="book_other" name="action:other" namespace="/exact"/>
    <button type="submit" formaction="/g8wild/exact/book.action">Plain</button>
</form>

The first two buttons send method:create and action:other, which the application above ignores. The replacement without DMI is one form per operation, or a formaction attribute on an HTML button that points at a wildcard URL such as createBook.action. Both use ordinary mappings with an allowed list.

Recommendation

Keep struts.enable.DynamicMethodInvocation at false. The security guide’s checklist says so (the page Security Hardening collects it), and the tutorial in the Getting Started guide gives the same advice. Convert any action!method link or method: button to a wildcard mapping with an allowed list. State-changing methods such as create, edit and delete belong behind POST and a token (see Double-Submit Tokens & Wait Pages), not behind a link that can be followed from another site; the examples on this page use GET only because curl makes the outputs short.

Regular-Expression Patterns

The default matcher knows only * and **. When the URL should carry named values, with no file extension and with real regular expressions, set struts.patternMatcher to regex. The Core Developer Guide’s "Advanced Wildcards" section lists four constants.

Table 8. The constants of the regex matcher (Core Developer Guide, Advanced Wildcards)
Constant Value Reason

struts.enable.SlashesInActionNames

true

The patterns contain slashes

struts.mapper.alwaysSelectFullNamespace

false

So that the namespace is the shortest match, /

struts.patternMatcher

regex

Selects RegexPatternMatcher

struts.matcher.appendNamedParameters

true

"Required from 2.5.25+"; the default in the 7.4.0 DefaultConfiguration is already true

The official example project wildcard-regex of the struts-examples repository sets struts.patternMatcher to regex and uses the 6.5 DTD. A name can contain {FIELD_NAME}, which matches (.*?) and fills the field of that name, or {FIELD_NAME:REGEX} with a Java regular expression. The groups can still be read as {1}, {2}, and {0} is the whole input.

A scratch application with the regex matcher and four patterns
<struts>
    <constant name="struts.enable.SlashesInActionNames" value="true"/>
    <constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>
    <constant name="struts.patternMatcher" value="regex"/>
    <constant name="struts.matcher.appendNamedParameters" value="true"/>
    <package name="books" extends="struts-default" namespace="/">
        <action name="/{type}/content/{title}" class="org.example.bookshelf.BookCrudAction">
            <result>/WEB-INF/views/book.jsp</result>
        </action>
        <action name="/{type}/{author:.+}/list" class="org.example.bookshelf.BookCrudAction">
            <result>/WEB-INF/views/book.jsp</result>
        </action>
        <action name="/isbn/{ISBN}/content" class="org.example.bookshelf.BookCrudAction">
            <param name="message">isbn {1}</param>
            <result>/WEB-INF/views/book.jsp</result>
        </action>
        <action name="/year/{year:[0-9]+}" class="org.example.bookshelf.BookCrudAction">
            <param name="message">year {1}</param>
            <result>/WEB-INF/views/book.jsp</result>
        </action>
    </package>
</struts>
The action fields the scratch class fills (annotated setters, as everywhere else on this page)
public String getType() { return type; }
public String getTitle() { return title; }
public String getAuthor() { return author; }

@StrutsParameter public void setType(String type) { this.type = type; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
@StrutsParameter public void setAuthor(String author) { this.author = author; }
Output of the scratch run g8wildrx (the view prints the method and the three fields)
$ curl -s http://localhost:18080/g8wildrx/fiction/content/Frankenstein
method=list type=fiction title=Frankenstein author=
$ curl -s http://localhost:18080/g8wildrx/philosophy/AynRand/list
method=list type=philosophy title= author=AynRand
$ curl -s http://localhost:18080/g8wildrx/isbn/978-0441172719/content
method=isbn 978-0441172719
$ curl -s http://localhost:18080/g8wildrx/year/1965
method=year 1965
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildrx/year/abc
404
$ curl -s http://localhost:18080/g8wildrx/fiction/content/a/b
method=list type=fiction title=a/b author=

Four properties of this matcher are not in the guide, and the scratch runs showed them.

  • A {title} with no regular expression matches any text including slashes, so /fiction/content/a/b gives the title a/b. Add a regular expression such as {title:[^/]+} where that is wrong.

  • The * and ** tokens do not exist in this mode. The matcher treats a name with no { as a literal, so every wildcard mapping of the earlier sections would stop working if struts.patternMatcher were switched on its own.

  • A pattern whose regular expression contains braces, such as {year:[0-9]{4}}, stops the application from starting. The matcher ends the expression at the first closing brace, builds the regular expression year/([0-9](.*?) and Tomcat reports PatternSyntaxException: Unclosed group. Write [0-9]+ or \d+, or repeat the class.

  • Only one pattern matcher exists per application. A project that needs wildcards in some packages and regular expressions in others needs a custom PatternMatcher, as the guide says.

Named Variables in Namespaces

The third matcher puts variables in the namespace. The guide’s example is @Namespace("/users/{userID}") on an action class, which makes /users/10/detail run DetailsAction with the field userID set to 10. The XML form is a package namespace.

A namespace with a variable, in XML (the guide’s text uses an annotation whose Java is not valid, see the errata of this page)
<constant name="struts.patternMatcher" value="namedVariable"/>
<package name="shelves" extends="struts-default" namespace="/authors/{id}">
    <action name="books" class="org.example.bookshelf.BookCrudAction">
        <result>/WEB-INF/views/book.jsp</result>
    </action>
</package>
Output of the scratch run g8wildnv
$ curl -s http://localhost:18080/g8wildnv/authors/10/books
method=list id=10
$ curl -s http://localhost:18080/g8wildnv/authors/abc/books
method=list id=abc
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildnv/authors/books
404

The guide lists the limits. The named-variable matcher and the wildcard matcher cannot be used together, so a * in an action name stops working. Some tags write the literal namespace with the braces into the HTML (/{user}/2w) instead of the namespace of the request when they guess a namespace from the current action: <s:form>, <s:url> and action= attributes. The workaround is a plain HTML tag with an explicit URL. A custom ActionMapper can do the same job, and the default mapper is the class that calls the pattern matcher.

Path Values and Parameter Binding

A value that the pattern extracts reaches the action in one of two ways, and the two ways have different security rules.

  1. As <param> elements. A {1} in <param name="id"> is a static parameter of the mapping. The staticParams interceptor sets it before the parameters interceptor runs and it does not need @StrutsParameter.

  2. As named values ({type} in the regex matcher, {id} in a namespace). In 7.4.0 these are appended to the static parameters of the mapping (struts.matcher.appendNamedParameters is true), so they also bind without an annotation.

Both ways are different from a query-string parameter, which binds only through a setter annotated with @StrutsParameter. The scratch app proves the difference and a consequence.

A class with an unannotated setter, mapped by a path variable
public class UnannotatedBookAction extends ActionSupport {
    private String type;
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public String getMessage() { return "type=" + type; }
}
The regex-matcher mapping and the output of the scratch run g8wildrx
<action name="/plain/{type}" class="org.example.bookshelf.UnannotatedBookAction">
    <result>/WEB-INF/views/book.jsp</result>
</action>
$ curl -s 'http://localhost:18080/g8wildrx/plain/fiction?type=evil'
method=type=fiction type=fiction
$ curl -s 'http://localhost:18080/g8wildrx/fiction/content/Dune?type=evil'
method=list type=evil title=Dune

The first request shows that the path variable binds although the setter is not annotated, and that the query parameter type does not replace it, because the setter is not annotated. The second shows the other side: the setter of BookCrudAction is annotated, so a query parameter with the same name overrides the value that came from the path. Treat a path variable like any other input. Validate it, and do not assume it is the value the route says it is.

A Class Chosen by a Wildcard and the OGNL Allowlist

A wildcard can build the class name, as in class="org.example.bookshelf.Edit{1}Action", and the default configuration comment of Struts 7 still suggests <action name="*/*" method="{2}" class="actions.{1}">. In Struts 7 that mapping fails by default. The OGNL allowlist (struts.allowlist.enable is on by default from 7.0) allows the classes that the configuration names, and a class that appears only through a placeholder is not named anywhere. The framework calls execute() through OGNL and the call is blocked.

The /admin/editAuthor request in the default scratch app, where EditAuthorAction is reached only through {1}
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/admin/editAuthor
500
$ curl -s http://localhost:18080/g8wild/admin/editAuthor | grep -o "NoSuchMethodException[^<]*"
NoSuchMethodException: org.example.bookshelf.EditAuthorAction.execute()

EditBookAction in the same package worked because a different mapping (direct) names it. Adding the package to the allowlist fixes the other class.

Allowlisting the package of the classes that wildcards choose
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf"/>
The same request after the change (scratch run g8wildal)
$ curl -s http://localhost:18080/g8wildal/admin/editAuthor
EditAuthor view: EditAuthorAction ran, id=null

The failure shows as an HTTP 500 and a NoSuchMethodException for a method that exists. An application that logs at WARN level also shows a warning from SecurityMemberAccess that names the blocked class and recommends struts.allowlist.classes or struts.allowlist.packageNames, which is the clue to read. The better design avoids the problem: a mapping whose class comes from request text means that the request chooses which Java class to run, so allowlist a small package of such classes and keep other classes out of it.

Wildcards in the Convention Plugin

The Convention plugin (see Convention Plugin & Annotations) builds mappings from annotations. A wildcard name in @Action is a wildcard mapping like any other, and the plugin’s registration order has no relation to the source order of the classes. That was a bug until 7.3.0: a broad pattern registered before a narrower one that it also matches could shadow it, in an order that differed between JVMs (WW-3784). The sort rule of Which Pattern Wins is the fix, and 7.4.0 corrects the position of the ** key (WW-5743).

Pitfalls and Error Names

Table 9. Wildcard and method-invocation pitfalls (rows marked "observed" were reproduced on Struts 7.4.0 with Tomcat 11.0.26)
Symptom or error Cause Fix

404 There is no Action mapped for namespace [/crud] and action name [purgeBook] (observed)

The method that the placeholder produced is not allowed

List the method in <allowed-methods> or the global list, or accept the 404 as the intended protection

404 There is no Action mapped for namespace [/exact] and action name [book!create] (observed)

DMI is off, so the bang name is looked up literally

Use a wildcard mapping, or turn DMI on knowingly

404 Method purge for action book is not allowed! (observed)

DMI is on and SMI refuses the method

Add the method to the action’s list, or stop using DMI

HTTP 500 NoSuchMethodException: ...BookCrudAction.list() (observed)

The name is on the global allowed list but the class has no such method

Add the method, or remove the placeholder value that selects it

HTTP 500 NoSuchMethodException: ...EditAuthorAction.execute() (observed)

The class is chosen by {1} and is not on the OGNL allowlist

struts.allowlist.packageNames or struts.allowlist.classes

Application does not start, "The content of element type action must match" (observed)

<allowed-methods> is before <result> in the element

Put <allowed-methods> last in <action>

Application does not start, PatternSyntaxException: Unclosed group (observed)

A regex segment with braces such as {year:[0-9]{4}} in the regex matcher

Write the class without a repetition count

The generic mapping answers for a URL that a specific mapping below it also matches (observed)

The first matching pattern wins, contrary to the Core Developer Guide’s text

Declare the specific pattern first and the catch-all last

404 There is no Action mapped ... action name [42] for /admin/book/42 (observed)

Slashes in action names are off, or the full namespace is selected

Set the two constants of Slashes in Action Names

ListSponsors does not match List*s (observed)

The wildcard is not greedy and stops at the first s

Use List* with a method check, or the regex matcher with an explicit class

* mappings stop working after enabling regex or namedVariable

Only one pattern matcher is active, and the regex matcher treats names without { as literals

Convert the patterns, or keep the default matcher

A getter runs as an action method and the page reports No result defined ... result unset (observed)

A package with SMI off and the default method regex

Keep SMI on, or narrow struts.strictMethodInvocation.methodRegex

A method intended for maintenance, such as purge, answers a URL (observed)

A package with strict-method-invocation="false"

Delete the attribute and list the allowed methods

A button with <s:submit method="create"/> does nothing (observed)

It renders method:create, which only DMI honours

Use separate forms or formaction

A path variable is replaced by a query parameter of the same name (observed)

The setter is annotated, so the parameters interceptor runs after the static parameters

Use a different property for the query parameter, or validate the final value

<s:form> writes /{user}/2w into the HTML

A named-variable namespace and a tag that guesses the namespace

Write the URL by hand or use a custom ActionMapper

The 404 page shows the raw action name

Struts does not escape action names in its automatically generated error pages

Define a global error page, or disable DMI (S2-006 in the security guide)

What Changed Recently

Table 10. Changes that touch wildcards and method invocation (versions from the Struts documentation and 7.4.0 release notes)
Version Change

2.5

Strict method invocation replaces the older strict DMI (the Core Developer Guide dates the earlier strict DMI to 2.3, the security guide dates strict method invocation to 2.5); strict-method-invocation defaults to true in struts-default; <allowed-methods>, <global-allowed-methods> and the Convention annotation @AllowedMethods arrive. WW-4405 (an ArrayIndexOutOfBoundsException with an overloaded method in a class hierarchy) is fixed in this line, although the security guide still has the section "Ambiguous Action Methods" for it

2.5.25

The regex matcher needs struts.matcher.appendNamedParameters, which is true by default in 7.4.0

6.0

The struts-6.0.dtd is the DTD that 7.4.0 configurations still declare (struts.xml)

7.0

The OGNL allowlist is on by default; struts.mapper.alwaysSelectFullNamespace is true by default; packages moved to org.apache.struts2.* (ActionSupport, @StrutsParameter and the Convention annotations are imported from there)

7.3.0

Annotated wildcard actions are sorted most specific first before registration (WW-3784)

7.4.0

The path-spanning ** key moves to the first place of that sort (WW-5743); Struts 7.4.0 released 2026-10-02; struts.enable.DynamicMethodInvocation remains false

Where the Books Are Outdated

Struts in Action (Manning, 2002, Struts 1) and Struts 2 in Action (Manning, 2008, Struts 2.0) are listed in the section bibliography. They teach the same problem, and the answers have moved.

Table 11. Material from the books and its place in Struts 7.4.0
The book’s material Today

Struts 1 DispatchAction: a request parameter names the method, LookupDispatchAction maps a button label to it

Both are gone with Struts 1, which has been end of life since 2013 and has unfixed vulnerabilities. The replacement is a wildcard mapping with method="{1}" and an allowed list

The Struts 1 mapping path /editSubscription with a .do extension and ActionForward

A name without a slash, the action,, extension and a result; there is no .do unless you configure struts.action.extension

Dynamic method invocation action!method as the first choice, on by default

Off by default, and every method it names must be allowed. The security guide and the tutorial recommend turning it off

<s:submit method="x"> and <s:submit action="y"> for multi-button forms

Both render prefixes that are off by default; use separate forms or formaction

com.opensymphony.xwork2.* for actions and struts.properties for constants

org.apache.struts2.* and <constant> in struts.xml

Wildcard examples with class="actions.{1}"

The OGNL allowlist blocks the class unless the package is allowlisted

No method allow list: every public method of an action was callable

Strict method invocation, on by default since 2.5

References