This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 363ec9f0d3c8a9e8518c05473ababc419efae608
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]

Reply via email to