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

lukaszlenart pushed a commit to branch WW-5535-class-annotation-fallback-6x
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 4724b71e791476ee0f9c28daadcf67f310943e33
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed May 20 08:11:52 2026 +0200

    WW-5535 fix(core): enforce class-level HTTP method annotations for 
wildcard-resolved unannotated methods
    
    The WW-5535 change to DefaultActionProxy.resolveMethod() (which made
    wildcard-resolved methods report isMethodSpecified()=true) interacted
    with HttpMethodInterceptor's if/else-if so that the class-level
    annotation branch became unreachable when the resolved method carried
    no method-level annotation:
    
        if (isMethodSpecified()) {
            if (method has annotation) return doIntercept(method);
            // unannotated method falls through silently
        } else if (class has annotation) {
            return doIntercept(class);  // never reached when 
methodSpecified=true
        }
    
    Convert the else-if to a standalone if so the class-level check is
    always evaluated as a fallback. Method-level annotations still take
    precedence — they are checked first and return early.
    
    Adds three tests:
    - testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation:
      GET on a wildcard-resolved unannotated method is rejected when the
      class is @AllowedHttpMethod(POST).
    - testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation:
      POST on the same configuration succeeds.
    - testWildcardResolvedExecuteRejectsGetThroughRealProxy: end-to-end
      via a real DefaultActionProxy with <action name="Wild-*" method="{1}">,
      resolving to ActionSupport.execute().
---
 .../httpmethod/HttpMethodInterceptor.java          |  3 +-
 .../httpmethod/HttpMethodInterceptorTest.java      | 71 ++++++++++++++++++++++
 2 files changed, 73 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
index c0afca1c3..4d033d338 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
@@ -90,7 +90,8 @@ public class HttpMethodInterceptor extends 
AbstractInterceptor {
                     invocation.getProxy().getMethod(), 
AllowedHttpMethod.class.getSimpleName(), request.getMethod());
                 return doIntercept(invocation, method);
             }
-        } else if (AnnotationUtils.isAnnotatedBy(action.getClass(), 
HTTP_METHOD_ANNOTATIONS)) {
+        }
+        if (AnnotationUtils.isAnnotatedBy(action.getClass(), 
HTTP_METHOD_ANNOTATIONS)) {
             LOG.debug("Action: {} annotated with: {}, checking if request: {} 
meets allowed methods!",
                 action, AllowedHttpMethod.class.getSimpleName(), 
request.getMethod());
             return doIntercept(invocation, action.getClass());
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 f68b01df3..edbf56554 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
@@ -19,13 +19,17 @@
 package org.apache.struts2.interceptor.httpmethod;
 
 import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionProxy;
 import com.opensymphony.xwork2.mock.MockActionInvocation;
 import com.opensymphony.xwork2.mock.MockActionProxy;
 import org.apache.struts2.HttpMethodsTestAction;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.TestAction;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
 import org.springframework.mock.web.MockHttpServletRequest;
 
+import java.util.Map;
+
 public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
 
     private HttpMethodInterceptor interceptor;
@@ -254,6 +258,73 @@ public class HttpMethodInterceptorTest extends 
StrutsInternalTestCase {
         assertEquals(HttpMethod.POST, action.getHttpMethod());
     }
 
+    /**
+     * Regression for wildcard-resolved methods with no method-level HTTP 
annotation:
+     * a class-level {@code @AllowedHttpMethod(POST)} must still cause GET to 
be rejected.
+     * Previously the interceptor's {@code if/else-if} structure made the 
class-level
+     * branch unreachable when {@code isMethodSpecified()=true} and the 
resolved method
+     * carried no annotation of its own.
+     */
+    public void 
testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation() throws 
Exception {
+        HttpMethodsTestAction action = new HttpMethodsTestAction();
+        prepareActionInvocation(action);
+        actionProxy.setMethod("execute");
+        actionProxy.setMethodSpecified(true);
+
+        prepareRequest("get");
+
+        String resultName = interceptor.intercept(invocation);
+
+        assertEquals("bad-request", resultName);
+    }
+
+    /**
+     * Counterpart to the above: POST against a wildcard-resolved unannotated 
method must succeed
+     * when the class allows POST via {@code @AllowedHttpMethod(POST)}.
+     */
+    public void 
testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation() 
throws Exception {
+        HttpMethodsTestAction action = new HttpMethodsTestAction();
+        prepareActionInvocation(action);
+        actionProxy.setMethod("execute");
+        actionProxy.setMethodSpecified(true);
+        invocation.setResultCode("success");
+
+        prepareRequest("post");
+
+        String resultName = interceptor.intercept(invocation);
+
+        assertEquals("success", resultName);
+    }
+
+    /**
+     * Exercises the full wildcard resolution path through a real {@link 
com.opensymphony.xwork2.DefaultActionProxy}.
+     * <p>
+     * Config (from xwork-test-allowed-methods.xml):
+     * {@code <action name="Wild-*" class="HttpMethodsTestAction" 
method="{1}">}.
+     * URL {@code Wild-execute} resolves to {@code ActionSupport.execute()} — 
no method-level
+     * HTTP annotation. {@code HttpMethodsTestAction} carries class-level
+     * {@code @AllowedHttpMethod(POST)}, so GET must be rejected end-to-end.
+     */
+    public void testWildcardResolvedExecuteRejectsGetThroughRealProxy() throws 
Exception {
+        loadConfigurationProviders(new StrutsXmlConfigurationProvider(
+            
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml"));
+
+        MockHttpServletRequest request = new MockHttpServletRequest("GET", 
"/Wild-execute");
+        Map<String, Object> extraContext = ActionContext.of()
+            .withServletRequest(request)
+            .getContextMap();
+
+        ActionProxy proxy = actionProxyFactory.createActionProxy("", 
"Wild-execute", null, extraContext);
+
+        assertEquals("execute", proxy.getMethod());
+        assertTrue("Wildcard-resolved method must report 
isMethodSpecified()=true", proxy.isMethodSpecified());
+
+        HttpMethodInterceptor realInterceptor = new HttpMethodInterceptor();
+        String result = realInterceptor.intercept(proxy.getInvocation());
+
+        assertEquals("bad-request", result);
+    }
+
     private void prepareActionInvocation(Object action) {
         interceptor = new HttpMethodInterceptor();
         invocation = new MockActionInvocation();

Reply via email to