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; + } } }
