From 2397fe13c52ba6bf177e453a113e816513e3620d Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 12 Sep 2026 12:06:24 +0200 Subject: [PATCH] WW-5733 fix(jasperreports7): stop closing the response stream before the report is written 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) --- .../JasperReport7CsvExporterProvider.java | 3 +- .../JasperReport7HtmlExporterProvider.java | 3 +- .../JasperReport7PdfExporterProvider.java | 3 +- .../JasperReport7RtfExporterProvider.java | 3 +- .../JasperReport7XlsxExporterProvider.java | 3 +- .../JasperReport7XmlExporterProvider.java | 3 +- .../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 a11b51d005..582d32443f 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 JRCsvExporter createExporter(ActionInvocation invocation, JasperPrint jas 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 995b6d9461..bc4236d375 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 HtmlExporter createExporter(ActionInvocation invocation, JasperPrint jasp 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 24af92ca28..24ec670b51 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 JRPdfExporter createExporter(ActionInvocation invocation, JasperPrint jas 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 91dc002d70..160bcfc44f 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 JRRtfExporter createExporter(ActionInvocation invocation, JasperPrint jas 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 cf1317f6e4..49c5a51fea 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 JRXlsxExporter createExporter(ActionInvocation invocation, JasperPrint ja 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 be34c8ebc6..b6b9ee1522 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 JRXmlExporter createExporter(ActionInvocation invocation, JasperPrint jas 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 d7c3aadc3e..630ada4f8b 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.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 void testFormatLookupIsCaseInsensitive() throws Exception { 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 @@ private void compileAndUseReport(String jrxml) throws Exception { 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> JR_MAP_ARRAY_DATA_SOURCE = Stream.>of( new HashMap<>() {{ put("firstName", "Foo");