This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 69546d9f85b60ecd7278f812aad6f25354d444e0 Author: opencode <[email protected]> AuthorDate: Thu Oct 8 14:46:53 2026 +0200 Keep the Last-Modified header for large unbuffered SSI documents In the default unbuffered mode, SSIServlet streams the processed output directly to the response writer. Documents larger than the response buffer commit the response while processing, so the Last-Modified header that was set after processing completed was silently ignored and large SSI pages were served without it. Set the header from the source resource's last modified time before the output is generated, and upgrade it to the value aggregated from the directives afterwards only when it is newer and the response has not been committed yet. This keeps the previous behavior for buffered processing and responses that stay uncommitted. --- java/org/apache/catalina/ssi/SSIServlet.java | 11 ++++-- test/org/apache/catalina/ssi/TestSsiServlet.java | 45 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/java/org/apache/catalina/ssi/SSIServlet.java b/java/org/apache/catalina/ssi/SSIServlet.java index 341799d286..d4f4993650 100644 --- a/java/org/apache/catalina/ssi/SSIServlet.java +++ b/java/org/apache/catalina/ssi/SSIServlet.java @@ -209,8 +209,15 @@ public class SSIServlet extends HttpServlet { } try (BufferedReader bufferedReader = new BufferedReader(isr)) { - long lastModified = ssiProcessor.process(bufferedReader, resourceInfo.getLastModified(), printWriter); - if (lastModified > 0) { + // Set the header from the source before the output is generated. In the + // unbuffered mode a response larger than the response buffer is committed + // while processing, after which setting the header is silently ignored. + long sourceLastModified = resourceInfo.getLastModified(); + if (sourceLastModified > 0) { + res.setDateHeader("last-modified", sourceLastModified); + } + long lastModified = ssiProcessor.process(bufferedReader, sourceLastModified, printWriter); + if (lastModified > sourceLastModified && !res.isCommitted()) { res.setDateHeader("last-modified", lastModified); } if (stringWriter != null) { diff --git a/test/org/apache/catalina/ssi/TestSsiServlet.java b/test/org/apache/catalina/ssi/TestSsiServlet.java index e20d9660c3..fecad6f643 100644 --- a/test/org/apache/catalina/ssi/TestSsiServlet.java +++ b/test/org/apache/catalina/ssi/TestSsiServlet.java @@ -17,6 +17,9 @@ package org.apache.catalina.ssi; import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; @@ -72,4 +75,46 @@ public class TestSsiServlet extends TomcatBaseTest { Assert.assertFalse(body.contains("(none)")); } + + + @Test + public void testLargeDocumentLastModifiedHeader() throws Exception { + // A document larger than the response buffer commits the unbuffered + // response while the output is being generated. The Last-Modified + // header must be set before that happens. + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getTemporaryDirectory(), "ssi-large"); + Assert.assertTrue(appDir.mkdirs() || appDir.isDirectory()); + addDeleteOnTearDown(appDir); + File doc = new File(appDir, "large.shtml"); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(doc), StandardCharsets.ISO_8859_1)) { + writer.write("X".repeat(20000)); + writer.write("<!--#flastmod file=\"large.shtml\" -->TAIL"); + } + + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Tomcat.addServlet(ctxt, "ssi", new SSIServlet()); + ctxt.addServletMapping("*.shtml", "ssi"); + + tomcat.start(); + + Map<String,List<String>> resHeaders = new HashMap<>(); + String path = "http://localhost:" + getPort() + "/large.shtml"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String body = new String(out.getBuffer(), 0, out.getLength(), StandardCharsets.ISO_8859_1); + Assert.assertTrue(body.endsWith("TAIL")); + // Headers are case-insensitive. SSIServlet uses the lower-case name. + String lastModified = null; + for (Map.Entry<String,List<String>> header : resHeaders.entrySet()) { + if ("Last-Modified".equalsIgnoreCase(header.getKey())) { + Assert.assertEquals(1, header.getValue().size()); + lastModified = header.getValue().get(0); + } + } + Assert.assertNotNull(lastModified); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
