Skip to content

Commit 04c108a

Browse files
lukaszlenartclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 7f96a4f commit 04c108a

File tree

3 files changed

+24
-7
lines changed

3 files changed

+24
-7
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Each plugin is a separate Maven module with:
148148
### Important Notes
149149
- **Version**: Currently 6.7.5-SNAPSHOT (release branch: `release/struts-6-7-x`)
150150
- **Java Compatibility**: Compiled for Java 8, tested through Java 21
151+
- **Servlet API**: Uses javax.servlet (Java EE), NOT Jakarta EE (jakarta.servlet)
151152
- **Security**: Always validate inputs and follow OWASP guidelines
152153
- **Performance**: Leverage built-in caching (OGNL expressions, templates)
153154
- **Deprecation**: Some legacy XWork components marked for removal

core/src/main/java/org/apache/struts2/result/StreamResult.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.opensymphony.xwork2.ActionInvocation;
2222
import com.opensymphony.xwork2.inject.Inject;
2323
import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker;
24+
import org.apache.commons.lang3.StringUtils;
2425
import org.apache.logging.log4j.LogManager;
2526
import org.apache.logging.log4j.Logger;
2627

@@ -223,19 +224,20 @@ protected void doExecute(String finalLocation, ActionInvocation invocation) thro
223224

224225
if (inputStream == null) {
225226
String msg = ("Can not find a java.io.InputStream with the name [" + parsedInputName + "] in the invocation stack. " +
226-
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
227+
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
227228
LOG.error(msg);
228229
throw new IllegalArgumentException(msg);
229230
}
230231

231232

232233
HttpServletResponse oResponse = invocation.getInvocationContext().getServletResponse();
233234

234-
LOG.debug("Set the content type: {};charset{}", contentType, contentCharSet);
235-
if (contentCharSet != null && !contentCharSet.equals("")) {
236-
oResponse.setContentType(conditionalParse(contentType, invocation) + ";charset=" + conditionalParse(contentCharSet, invocation));
237-
} else {
238-
oResponse.setContentType(conditionalParse(contentType, invocation));
235+
LOG.debug("Set the content type: {};charset={}", contentType, contentCharSet);
236+
String parsedContentType = conditionalParse(contentType, invocation);
237+
String parsedContentCharSet = conditionalParse(contentCharSet, invocation);
238+
oResponse.setContentType(parsedContentType);
239+
if (StringUtils.isNotEmpty(parsedContentCharSet)) {
240+
oResponse.setCharacterEncoding(parsedContentCharSet);
239241
}
240242

241243
LOG.debug("Set the content length: {}", contentLength);
@@ -267,7 +269,7 @@ protected void doExecute(String finalLocation, ActionInvocation invocation) thro
267269
oOutput = oResponse.getOutputStream();
268270

269271
LOG.debug("Streaming result [{}] type=[{}] length=[{}] content-disposition=[{}] charset=[{}]",
270-
inputName, contentType, contentLength, contentDisposition, contentCharSet);
272+
inputName, contentType, contentLength, contentDisposition, contentCharSet);
271273

272274
LOG.debug("Streaming to output buffer +++ START +++");
273275
byte[] oBuff = new byte[bufferSize];

core/src/test/java/org/apache/struts2/result/StreamResultTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ public void testStreamResultWithCharSet2() throws Exception {
120120
assertEquals("inline", response.getHeader("Content-disposition"));
121121
}
122122

123+
public void testStreamResultWithNullCharSetExpression() throws Exception {
124+
result.setParse(true);
125+
result.setInputName("streamForImage");
126+
result.setContentCharSet("${nullCharSetMethod}");
127+
128+
result.doExecute("helloworld", mai);
129+
130+
assertEquals("text/plain", response.getContentType());
131+
}
132+
123133
public void testAllowCacheDefault() throws Exception {
124134
result.setInputName("streamForImage");
125135

@@ -310,6 +320,10 @@ public String getStreamForImageAsExpression() {
310320
public String getContentCharSetMethod() {
311321
return "UTF-8";
312322
}
323+
324+
public String getNullCharSetMethod() {
325+
return null;
326+
}
313327
}
314328

315329
}

0 commit comments

Comments
 (0)