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

lukaszlenart pushed a commit to branch 
fix/WW-5602-streamresult-contentcharset-handling
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 04c108a41909c999f915a7cd79a6e5f7519c1e71
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Jan 4 17:49:52 2026 +0100

    fix(core): WW-5602 fix StreamResult contentCharSet handling
    
    Evaluates contentCharSet expression before emptiness check to prevent
    malformed content-type headers when expression evaluates to null.
    
    - Parse contentCharSet expression first, then check if result is empty
    - Use StringUtils.isNotEmpty() for proper null/empty validation
    - Use setCharacterEncoding() instead of appending to content-type string
    - Add test for null-evaluating charset expressions
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <[email protected]>
---
 CLAUDE.md                                                |  1 +
 .../java/org/apache/struts2/result/StreamResult.java     | 16 +++++++++-------
 .../java/org/apache/struts2/result/StreamResultTest.java | 14 ++++++++++++++
 3 files changed, 24 insertions(+), 7 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3710f0354..408f7c02d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -148,6 +148,7 @@ Each plugin is a separate Maven module with:
 ### Important Notes
 - **Version**: Currently 6.7.5-SNAPSHOT (release branch: 
`release/struts-6-7-x`)
 - **Java Compatibility**: Compiled for Java 8, tested through Java 21
+- **Servlet API**: Uses javax.servlet (Java EE), NOT Jakarta EE 
(jakarta.servlet)
 - **Security**: Always validate inputs and follow OWASP guidelines
 - **Performance**: Leverage built-in caching (OGNL expressions, templates)
 - **Deprecation**: Some legacy XWork components marked for removal
diff --git a/core/src/main/java/org/apache/struts2/result/StreamResult.java 
b/core/src/main/java/org/apache/struts2/result/StreamResult.java
index 9324d5bb5..235152e98 100644
--- a/core/src/main/java/org/apache/struts2/result/StreamResult.java
+++ b/core/src/main/java/org/apache/struts2/result/StreamResult.java
@@ -21,6 +21,7 @@ package org.apache.struts2.result;
 import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -223,7 +224,7 @@ public class StreamResult extends StrutsResultSupport {
 
             if (inputStream == null) {
                 String msg = ("Can not find a java.io.InputStream with the 
name [" + parsedInputName + "] in the invocation stack. " +
-                    "Check the <param name=\"inputName\"> tag specified for 
this action is correct, not excluded and accepted.");
+                        "Check the <param name=\"inputName\"> tag specified 
for this action is correct, not excluded and accepted.");
                 LOG.error(msg);
                 throw new IllegalArgumentException(msg);
             }
@@ -231,11 +232,12 @@ public class StreamResult extends StrutsResultSupport {
 
             HttpServletResponse oResponse = 
invocation.getInvocationContext().getServletResponse();
 
-            LOG.debug("Set the content type: {};charset{}", contentType, 
contentCharSet);
-            if (contentCharSet != null && !contentCharSet.equals("")) {
-                oResponse.setContentType(conditionalParse(contentType, 
invocation) + ";charset=" + conditionalParse(contentCharSet, invocation));
-            } else {
-                oResponse.setContentType(conditionalParse(contentType, 
invocation));
+            LOG.debug("Set the content type: {};charset={}", contentType, 
contentCharSet);
+            String parsedContentType = conditionalParse(contentType, 
invocation);
+            String parsedContentCharSet = conditionalParse(contentCharSet, 
invocation);
+            oResponse.setContentType(parsedContentType);
+            if (StringUtils.isNotEmpty(parsedContentCharSet)) {
+                oResponse.setCharacterEncoding(parsedContentCharSet);
             }
 
             LOG.debug("Set the content length: {}", contentLength);
@@ -267,7 +269,7 @@ public class StreamResult extends StrutsResultSupport {
             oOutput = oResponse.getOutputStream();
 
             LOG.debug("Streaming result [{}] type=[{}] length=[{}] 
content-disposition=[{}] charset=[{}]",
-                inputName, contentType, contentLength, contentDisposition, 
contentCharSet);
+                    inputName, contentType, contentLength, contentDisposition, 
contentCharSet);
 
             LOG.debug("Streaming to output buffer +++ START +++");
             byte[] oBuff = new byte[bufferSize];
diff --git a/core/src/test/java/org/apache/struts2/result/StreamResultTest.java 
b/core/src/test/java/org/apache/struts2/result/StreamResultTest.java
index a02781812..a7621e8e9 100644
--- a/core/src/test/java/org/apache/struts2/result/StreamResultTest.java
+++ b/core/src/test/java/org/apache/struts2/result/StreamResultTest.java
@@ -120,6 +120,16 @@ public class StreamResultTest extends 
StrutsInternalTestCase {
         assertEquals("inline", response.getHeader("Content-disposition"));
     }
 
+    public void testStreamResultWithNullCharSetExpression() throws Exception {
+        result.setParse(true);
+        result.setInputName("streamForImage");
+        result.setContentCharSet("${nullCharSetMethod}");
+
+        result.doExecute("helloworld", mai);
+
+        assertEquals("text/plain", response.getContentType());
+    }
+
     public void testAllowCacheDefault() throws Exception {
         result.setInputName("streamForImage");
 
@@ -310,6 +320,10 @@ public class StreamResultTest extends 
StrutsInternalTestCase {
         public String getContentCharSetMethod() {
             return "UTF-8";
         }
+
+        public String getNullCharSetMethod() {
+            return null;
+        }
     }
 
 }

Reply via email to