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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 2a4d1aa8d2c83c65c763f72f7761947dcc441abb
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Nov 2 08:09:35 2026 +0100

    WW-4164 fix(core): skip extension resolution when nothing declares 
extensions
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../dispatcher/mapper/ActionExtensionResolver.java | 33 ++++++++++++++++++++--
 .../mapper/ActionExtensionResolverTest.java        |  8 ++++++
 2 files changed, 39 insertions(+), 2 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
index b4d450cd8..574fffbbe 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
@@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.config.Configuration;
+import org.apache.struts2.config.RuntimeConfiguration;
 import org.apache.struts2.config.entities.ActionConfig;
 import org.apache.struts2.inject.Inject;
 
@@ -37,6 +38,7 @@ public class ActionExtensionResolver {
 
     private Configuration configuration;
     private List<String> extensions;
+    private volatile DeclarationState declarationState;
 
     /**
      * @param configuration the configuration used to look up the target action
@@ -60,7 +62,8 @@ public class ActionExtensionResolver {
      * Sets the extension of {@code target} to the current request's extension 
when the target accepts it, otherwise
      * to the target's first accepted extension, which is also used when the 
current request has no extension, as in
      * {@link DefaultActionMapper#lookupExtension(String)}. Leaves {@code 
target} unchanged when it already has an extension,
-     * when the target action is unknown, or when neither the target nor the 
global constant declares extensions.
+     * when no action in the configuration declares extensions, when the 
target action is unknown, or when neither the
+     * target nor the global constant declares extensions.
      *
      * @param target the mapping of the action the URL points at
      * @since 7.5.0
@@ -69,7 +72,11 @@ public class ActionExtensionResolver {
         if (target.getExtension() != null) {
             return;
         }
-        ActionConfig actionConfig = configuration.getRuntimeConfiguration()
+        RuntimeConfiguration runtimeConfiguration = 
configuration.getRuntimeConfiguration();
+        if (!anyActionDeclaresExtensions(runtimeConfiguration)) {
+            return;
+        }
+        ActionConfig actionConfig = runtimeConfiguration
             .getActionConfig(target.getNamespace(), 
StringUtils.substringBefore(target.getName(), "!"));
         if (actionConfig == null) {
             return;
@@ -82,6 +89,18 @@ public class ActionExtensionResolver {
         target.setExtension(current == null ? effective.get(0) : 
ActionExtensions.select(effective, current));
     }
 
+    private boolean anyActionDeclaresExtensions(RuntimeConfiguration 
runtimeConfiguration) {
+        DeclarationState cached = declarationState;
+        if (cached != null && cached.runtimeConfiguration == 
runtimeConfiguration) {
+            return cached.anyDeclared;
+        }
+        boolean anyDeclared = 
runtimeConfiguration.getActionConfigs().values().stream()
+            .flatMap(actionConfigs -> actionConfigs.values().stream())
+            .anyMatch(actionConfig -> actionConfig.getExtensions() != null);
+        declarationState = new DeclarationState(runtimeConfiguration, 
anyDeclared);
+        return anyDeclared;
+    }
+
     private String currentExtension() {
         ActionContext context = ActionContext.getContext();
         if (context == null || context.getActionMapping() == null) {
@@ -89,4 +108,14 @@ public class ActionExtensionResolver {
         }
         return context.getActionMapping().getExtension();
     }
+
+    private static final class DeclarationState {
+        private final RuntimeConfiguration runtimeConfiguration;
+        private final boolean anyDeclared;
+
+        private DeclarationState(RuntimeConfiguration runtimeConfiguration, 
boolean anyDeclared) {
+            this.runtimeConfiguration = runtimeConfiguration;
+            this.anyDeclared = anyDeclared;
+        }
+    }
 }
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
index 061302865..c1626f46a 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
@@ -102,6 +102,14 @@ public class ActionExtensionResolverTest extends 
StrutsInternalTestCase {
         assertThat(resolve("/plain", "undeclared")).isNull();
     }
 
+    public void testNoDeclarationsAnywhereLeavesMappingUntouched() {
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider("org/apache/struts2/config/providers/xwork-test-actions.xml"));
+        resolver.setConfiguration(configuration);
+        withCurrentExtension("xyz");
+
+        assertThat(resolve("", "Bar")).isNull();
+    }
+
     private String resolve(String namespace, String name) {
         ActionMapping target = new ActionMapping(name, namespace, null, null);
         resolver.applyTo(target);

Reply via email to