This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch 
fix/WW-5535-http-method-interceptor-wildcard-6.8.x
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 784af67339f34fee1f0c0add3e78a8db16aa4425
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Feb 22 17:26:13 2026 +0100

    fix(core): WW-5535 enforce HTTP method annotations for wildcard actions
    
    DefaultActionProxy.resolveMethod() incorrectly set methodSpecified=false
    for config-resolved methods (including wildcard-substituted ones), causing
    HttpMethodInterceptor to skip method-level @HttpPost/@HttpGet annotation
    checks. Move methodSpecified=false inside the inner if block so it only
    applies when truly defaulting to "execute".
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <[email protected]>
---
 .../opensymphony/xwork2/DefaultActionProxy.java    |  2 +-
 .../main/java/org/apache/struts2/ActionProxy.java  |  6 ++-
 .../xwork2/DefaultActionProxyTest.java             | 53 ++++++++++++++++++++--
 ...XmlConfigurationProviderAllowedMethodsTest.java |  2 +-
 .../httpmethod/HttpMethodInterceptorTest.java      | 37 +++++++++++++++
 .../providers/xwork-test-allowed-methods.xml       | 14 +++++-
 6 files changed, 104 insertions(+), 10 deletions(-)

diff --git a/core/src/main/java/com/opensymphony/xwork2/DefaultActionProxy.java 
b/core/src/main/java/com/opensymphony/xwork2/DefaultActionProxy.java
index 4d73813ee..6bc28ed25 100644
--- a/core/src/main/java/com/opensymphony/xwork2/DefaultActionProxy.java
+++ b/core/src/main/java/com/opensymphony/xwork2/DefaultActionProxy.java
@@ -168,8 +168,8 @@ public class DefaultActionProxy implements ActionProxy, 
Serializable {
             this.method = config.getMethodName();
             if (StringUtils.isEmpty(this.method)) {
                 this.method = ActionConfig.DEFAULT_METHOD;
+                methodSpecified = false;
             }
-            methodSpecified = false;
         }
     }
 
diff --git a/core/src/main/java/org/apache/struts2/ActionProxy.java 
b/core/src/main/java/org/apache/struts2/ActionProxy.java
index d5e19e44d..ad800d406 100644
--- a/core/src/main/java/org/apache/struts2/ActionProxy.java
+++ b/core/src/main/java/org/apache/struts2/ActionProxy.java
@@ -93,9 +93,11 @@ public interface ActionProxy {
     String getMethod();
 
     /**
-     * Gets status of the method value's initialization.
+     * Gets status of the method value's initialization. Returns {@code true} 
when the method was explicitly provided
+     * (e.g. via URL parameter, wildcard substitution, or action 
configuration), and {@code false} only when the
+     * framework defaults to {@code "execute"} because no method was specified 
anywhere.
      *
-     * @return true if the method returned by getMethod() is not a default 
initializer value.
+     * @return true if the method returned by getMethod() is not the default 
"execute" fallback.
      */
     boolean isMethodSpecified();
 
diff --git 
a/core/src/test/java/com/opensymphony/xwork2/DefaultActionProxyTest.java 
b/core/src/test/java/com/opensymphony/xwork2/DefaultActionProxyTest.java
index 125c5b4bd..5db2f68cf 100644
--- a/core/src/test/java/com/opensymphony/xwork2/DefaultActionProxyTest.java
+++ b/core/src/test/java/com/opensymphony/xwork2/DefaultActionProxyTest.java
@@ -18,15 +18,14 @@
  */
 package com.opensymphony.xwork2;
 
+import com.opensymphony.xwork2.config.ConfigurationException;
 import com.opensymphony.xwork2.mock.MockActionInvocation;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.config.StrutsXmlConfigurationProvider;
-import org.junit.Test;
 
 public class DefaultActionProxyTest extends StrutsInternalTestCase {
 
-    @Test
-    public void testThorwExceptionOnNotAllowedMethod() throws Exception {
+    public void testThrowExceptionOnNotAllowedMethod() {
         final String filename = 
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
         loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(filename));
         DefaultActionProxy dap = new DefaultActionProxy(new 
MockActionInvocation(), "strict", "Default", "notAllowed", true, true);
@@ -35,8 +34,52 @@ public class DefaultActionProxyTest extends 
StrutsInternalTestCase {
         try {
             dap.prepare();
             fail("Must throw exception!");
-        } catch (Exception e) {
-            assertEquals(e.getMessage(), "Method notAllowed for action Default 
is not allowed!");
+        } catch (ConfigurationException e) {
+            assertEquals("Method notAllowed for action Default is not 
allowed!", e.getMessage());
         }
     }
+
+    public void testMethodSpecifiedWhenPassedExplicitly() {
+        final String filename = 
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(filename));
+        DefaultActionProxy dap = new DefaultActionProxy(new 
MockActionInvocation(), "", "NoMethod", "onPostOnly", true, true);
+        container.inject(dap);
+        dap.prepare();
+
+        assertTrue("Method passed explicitly should be marked as specified", 
dap.isMethodSpecified());
+        assertEquals("onPostOnly", dap.getMethod());
+    }
+
+    public void testMethodSpecifiedWhenResolvedFromConfig() {
+        final String filename = 
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(filename));
+        DefaultActionProxy dap = new DefaultActionProxy(new 
MockActionInvocation(), "", "ConfigMethod", null, true, true);
+        container.inject(dap);
+        dap.prepare();
+
+        assertTrue("Method resolved from action config should be marked as 
specified", dap.isMethodSpecified());
+        assertEquals("onPostOnly", dap.getMethod());
+    }
+
+    public void testMethodNotSpecifiedWhenDefaultingToExecute() {
+        final String filename = 
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(filename));
+        DefaultActionProxy dap = new DefaultActionProxy(new 
MockActionInvocation(), "", "NoMethod", null, true, true);
+        container.inject(dap);
+        dap.prepare();
+
+        assertFalse("Method defaulting to execute should not be marked as 
specified", dap.isMethodSpecified());
+        assertEquals("execute", dap.getMethod());
+    }
+
+    public void testMethodSpecifiedWithWildcardAction() {
+        final String filename = 
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(filename));
+        DefaultActionProxy dap = new DefaultActionProxy(new 
MockActionInvocation(), "", "Wild-onPostOnly", null, true, true);
+        container.inject(dap);
+        dap.prepare();
+
+        assertTrue("Method resolved from wildcard should be marked as 
specified", dap.isMethodSpecified());
+        assertEquals("onPostOnly", dap.getMethod());
+    }
 }
\ No newline at end of file
diff --git 
a/core/src/test/java/com/opensymphony/xwork2/config/providers/XmlConfigurationProviderAllowedMethodsTest.java
 
b/core/src/test/java/com/opensymphony/xwork2/config/providers/XmlConfigurationProviderAllowedMethodsTest.java
index 1ac2315bc..00604757d 100644
--- 
a/core/src/test/java/com/opensymphony/xwork2/config/providers/XmlConfigurationProviderAllowedMethodsTest.java
+++ 
b/core/src/test/java/com/opensymphony/xwork2/config/providers/XmlConfigurationProviderAllowedMethodsTest.java
@@ -42,7 +42,7 @@ public class XmlConfigurationProviderAllowedMethodsTest 
extends ConfigurationTes
         Map actionConfigs = pkg.getActionConfigs();
 
         // assertions
-        assertEquals(5, actionConfigs.size());
+        assertEquals(8, actionConfigs.size());
 
         ActionConfig action = (ActionConfig) actionConfigs.get("Default");
         assertEquals(1, action.getAllowedMethods().size());
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
index 98e78c3fa..f68b01df3 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
@@ -217,6 +217,43 @@ public class HttpMethodInterceptorTest extends 
StrutsInternalTestCase {
         assertEquals(HttpMethod.POST, action.getHttpMethod());
     }
 
+    public void testWildcardResolvedMethodWithPostAnnotationRejectsGet() 
throws Exception {
+        // given
+        HttpMethodsTestAction action = new HttpMethodsTestAction();
+        prepareActionInvocation(action);
+        actionProxy.setMethod("onPostOnly");
+        actionProxy.setMethodSpecified(true);
+
+        invocation.setResultCode("onPostOnly");
+
+        prepareRequest("GET");
+
+        // when
+        String resultName = interceptor.intercept(invocation);
+
+        // then
+        assertEquals("bad-request", resultName);
+    }
+
+    public void testWildcardResolvedMethodWithPostAnnotationAllowsPost() 
throws Exception {
+        // given
+        HttpMethodsTestAction action = new HttpMethodsTestAction();
+        prepareActionInvocation(action);
+        actionProxy.setMethod("onPostOnly");
+        actionProxy.setMethodSpecified(true);
+
+        invocation.setResultCode("onPostOnly");
+
+        prepareRequest("POST");
+
+        // when
+        String resultName = interceptor.intercept(invocation);
+
+        // then
+        assertEquals("onPostOnly", resultName);
+        assertEquals(HttpMethod.POST, action.getHttpMethod());
+    }
+
     private void prepareActionInvocation(Object action) {
         interceptor = new HttpMethodInterceptor();
         invocation = new MockActionInvocation();
diff --git 
a/core/src/test/resources/com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml
 
b/core/src/test/resources/com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml
index 247b4ef27..0c78ff5cf 100644
--- 
a/core/src/test/resources/com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml
+++ 
b/core/src/test/resources/com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml
@@ -29,7 +29,7 @@
         </action>
 
         <action name="Boring">
-            <allowed-methods> </allowed-methods>
+            <allowed-methods></allowed-methods>
         </action>
 
         <action name="Foo">
@@ -43,6 +43,18 @@
         <action name="Baz" method="baz">
             <allowed-methods>foo,bar</allowed-methods>
         </action>
+
+        <action name="Wild-*" class="org.apache.struts2.HttpMethodsTestAction" 
method="{1}">
+            <allowed-methods>regex:.*</allowed-methods>
+        </action>
+
+        <action name="ConfigMethod" 
class="org.apache.struts2.HttpMethodsTestAction" method="onPostOnly">
+            <allowed-methods>regex:.*</allowed-methods>
+        </action>
+
+        <action name="NoMethod" 
class="org.apache.struts2.HttpMethodsTestAction">
+            <allowed-methods>regex:.*</allowed-methods>
+        </action>
     </package>
 
     <package name="strict" strict-method-invocation="true">

Reply via email to