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();
