This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 57b551577f556aecf2ec488b6c8d1a7fbf93c4f7 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 2a2fe53dea..8281689b87 100644 --- a/java/org/apache/catalina/ssi/SSIServlet.java +++ b/java/org/apache/catalina/ssi/SSIServlet.java @@ -211,8 +211,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 28e8fa6337..d1363782c3 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]
