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


The following commit(s) were added to refs/heads/main by this push:
     new 9b8b0face WW-5733 fix(jasperreports7): stop closing the response 
stream before the report is written (#1923)
9b8b0face is described below

commit 9b8b0faceef7a52f4d0aebda23a5ad7c517fbfe1
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Sep 12 14:29:33 2026 +0200

    WW-5733 fix(jasperreports7): stop closing the response stream before the 
report is written (#1923)
    
    Every exporter provider obtained the servlet output stream in a
    try-with-resources block, so the stream was closed as soon as
    createExporter returned, before JasperReport7Result called
    exporter.exportReport(). Tomcat commits the response on close with the
    bytes written so far, which is none, and drops everything written
    afterwards: every export was an empty 200 with Content-Length: 0. The
    tests did not notice because Spring's MockHttpServletResponse keeps
    accepting writes after close.
    
    The providers now hand the open stream to the exporter and leave it
    alone; the result flushes after exporting and the container closes the
    stream at the end of the request. A response wrapper that refuses
    writes after close guards the ordering for all six formats.
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../export/JasperReport7CsvExporterProvider.java   |  3 +-
 .../export/JasperReport7HtmlExporterProvider.java  |  3 +-
 .../export/JasperReport7PdfExporterProvider.java   |  3 +-
 .../export/JasperReport7RtfExporterProvider.java   |  3 +-
 .../export/JasperReport7XlsxExporterProvider.java  |  3 +-
 .../export/JasperReport7XmlExporterProvider.java   |  3 +-
 .../jasperreports7/JasperReport7ResultTest.java    | 72 ++++++++++++++++++++++
 7 files changed, 84 insertions(+), 6 deletions(-)

diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7CsvExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7CsvExporterProvider.java
index a11b51d00..582d32443 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7CsvExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7CsvExporterProvider.java
@@ -82,7 +82,8 @@ public class JasperReport7CsvExporterProvider implements 
JasperReport7ExporterPr
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseStream = response.getOutputStream()) {
+        try {
+            OutputStream responseStream = response.getOutputStream();
             WriterExporterOutput exporterOutput = new 
SimpleWriterExporterOutput(responseStream);
             exporter.setExporterOutput(exporterOutput);
         } catch (IOException e) {
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7HtmlExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7HtmlExporterProvider.java
index 995b6d946..bc4236d37 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7HtmlExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7HtmlExporterProvider.java
@@ -70,7 +70,8 @@ public class JasperReport7HtmlExporterProvider implements 
JasperReport7ExporterP
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseStream = response.getOutputStream()) {
+        try {
+            OutputStream responseStream = response.getOutputStream();
             SimpleHtmlExporterOutput exporterOutput = new 
SimpleHtmlExporterOutput(responseStream);
             HtmlResourceHandler imageHandler = new 
WebHtmlResourceHandler(request.getContextPath() + imageServletUrl + "%s");
             exporterOutput.setImageHandler(imageHandler);
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7PdfExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7PdfExporterProvider.java
index 24af92ca2..24ec670b5 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7PdfExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7PdfExporterProvider.java
@@ -48,7 +48,8 @@ public class JasperReport7PdfExporterProvider implements 
JasperReport7ExporterPr
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseStream = response.getOutputStream()) {
+        try {
+            OutputStream responseStream = response.getOutputStream();
             OutputStreamExporterOutput exporterOutput = new 
SimpleOutputStreamExporterOutput(responseStream);
             exporter.setExporterOutput(exporterOutput);
         } catch (IOException e) {
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7RtfExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7RtfExporterProvider.java
index 91dc002d7..160bcfc44 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7RtfExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7RtfExporterProvider.java
@@ -48,7 +48,8 @@ public class JasperReport7RtfExporterProvider implements 
JasperReport7ExporterPr
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseStream = response.getOutputStream()) {
+        try {
+            OutputStream responseStream = response.getOutputStream();
             WriterExporterOutput exporterOutput = new 
SimpleWriterExporterOutput(responseStream);
             exporter.setExporterOutput(exporterOutput);
         } catch (IOException e) {
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XlsxExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XlsxExporterProvider.java
index cf1317f6e..49c5a51fe 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XlsxExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XlsxExporterProvider.java
@@ -48,7 +48,8 @@ public class JasperReport7XlsxExporterProvider implements 
JasperReport7ExporterP
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseStream = response.getOutputStream()) {
+        try {
+            OutputStream responseStream = response.getOutputStream();
             OutputStreamExporterOutput exporterOutput = new 
SimpleOutputStreamExporterOutput(responseStream);
             exporter.setExporterOutput(exporterOutput);
         } catch (IOException e) {
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XmlExporterProvider.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XmlExporterProvider.java
index be34c8ebc..b6b9ee152 100644
--- 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XmlExporterProvider.java
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/export/JasperReport7XmlExporterProvider.java
@@ -48,7 +48,8 @@ public class JasperReport7XmlExporterProvider implements 
JasperReport7ExporterPr
         SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
         exporter.setExporterInput(input);
 
-        try (OutputStream responseOutput = response.getOutputStream()) {
+        try {
+            OutputStream responseOutput = response.getOutputStream();
             XmlExporterOutput exporterOutput = new 
SimpleXmlExporterOutput(responseOutput);
             exporter.setExporterOutput(exporterOutput);
         } catch (IOException e) {
diff --git 
a/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java
 
b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java
index d7c3aadc3..630ada4f8 100644
--- 
a/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java
+++ 
b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java
@@ -19,6 +19,10 @@
 package org.apache.struts2.views.jasperreports7;
 
 import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
 import net.sf.jasperreports.engine.JasperCompileManager;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.ActionInvocation;
@@ -29,6 +33,7 @@ import org.apache.struts2.util.ClassLoaderUtil;
 import org.apache.struts2.util.ValueStack;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.sql.Connection;
@@ -303,6 +308,26 @@ public class JasperReport7ResultTest extends 
StrutsTestCase {
         assertThat(response.getContentAsString()).contains("Qux Report");
     }
 
+    public void testExportWritesNothingAfterClosingTheResponseStream() throws 
Exception {
+        for (String format : List.of(JasperReport7Constants.FORMAT_PDF, 
JasperReport7Constants.FORMAT_CSV,
+                JasperReport7Constants.FORMAT_HTML, 
JasperReport7Constants.FORMAT_RTF,
+                JasperReport7Constants.FORMAT_XML, 
JasperReport7Constants.FORMAT_XLSX)) {
+            // given
+            response.setCommitted(false);
+            response.reset();
+            ActionContext.getContext().withServletResponse(new 
StrictCloseResponse(response));
+            result.setDataSource("{#{'firstName':'ignore', 
'lastName':'ignore'}}");
+            result.setReportParameters("#{'title':'Qux'}");
+            result.setFormat(format);
+
+            // when
+            result.execute(this.invocation);
+
+            // then
+            
assertThat(response.getContentAsByteArray()).as(format).isNotEmpty();
+        }
+    }
+
     public void testExportToRtf() throws Exception {
         // given
         result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}");
@@ -386,6 +411,53 @@ public class JasperReport7ResultTest extends 
StrutsTestCase {
         result.setLocation(resource + ".jasper");
     }
 
+    /**
+     * Behaves like a servlet container: once the output stream is closed, 
further writes are lost.
+     */
+    private static class StrictCloseResponse extends 
HttpServletResponseWrapper {
+
+        private ServletOutputStream stream;
+
+        StrictCloseResponse(HttpServletResponse response) {
+            super(response);
+        }
+
+        @Override
+        public ServletOutputStream getOutputStream() throws IOException {
+            if (stream == null) {
+                ServletOutputStream delegate = super.getOutputStream();
+                stream = new ServletOutputStream() {
+                    private boolean closed;
+
+                    @Override
+                    public void write(int b) throws IOException {
+                        if (closed) {
+                            throw new IOException("Stream closed");
+                        }
+                        delegate.write(b);
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                        closed = true;
+                        delegate.close();
+                    }
+
+                    @Override
+                    public boolean isReady() {
+                        return delegate.isReady();
+                    }
+
+                    @Override
+                    public void setWriteListener(WriteListener writeListener) {
+                        delegate.setWriteListener(writeListener);
+                    }
+                };
+            }
+            return stream;
+        }
+    }
+
     private static final List<Map<String, String>> JR_MAP_ARRAY_DATA_SOURCE = 
Stream.<Map<String, String>>of(
             new HashMap<>() {{
                 put("firstName", "Foo");

Reply via email to