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

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/11.0.x by this push:
     new ddb1f2894c Do not compress partial content with a content coding 
(#1115)
ddb1f2894c is described below

commit ddb1f2894c7531b7e6ae72a9aaf3369df774bd72
Author: Arturo Bernal <[email protected]>
AuthorDate: Thu Nov 5 20:21:24 2026 +0100

    Do not compress partial content with a content coding (#1115)
    
    The ranges in a Content-Range header field refer to the bytes of the
    representation (RFC 9110, section 14.4). A 206 response whose payload
    was compressed with Content-Encoding: gzip described the range of the
    uncompressed representation but sent a compressed slice of it, which a
    client is unable to reassemble. Do not compress a response that has a
    Content-Range header with a content coding. A transfer coding applies to
    the message rather than to the representation so it is not affected.
    
    Don't set the vary header if the content won't ever be compressed
---
 java/org/apache/coyote/CompressionConfig.java      |   6 +
 .../TestCompressionConfigPartialContent.java       | 134 +++++++++++++++++++++
 2 files changed, 140 insertions(+)

diff --git a/java/org/apache/coyote/CompressionConfig.java 
b/java/org/apache/coyote/CompressionConfig.java
index 6860c8caea..a5660fe7da 100644
--- a/java/org/apache/coyote/CompressionConfig.java
+++ b/java/org/apache/coyote/CompressionConfig.java
@@ -346,6 +346,12 @@ public class CompressionConfig {
         }
 
         if (useContentEncoding && !useTransferEncoding) {
+            // The ranges of a partial response refer to the bytes of the 
representation so it must not be content
+            // encoded. The Content-Range header is not present at the top 
level of a multipart/byteranges response.
+            if (response.getStatus() == 206 || 
responseHeaders.getHeader("Content-Range") != null) {
+                return false;
+            }
+
             // If processing reaches this far, the response might be 
compressed.
             // Therefore, set the Vary header to keep proxies happy
             ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding");
diff --git a/test/org/apache/coyote/TestCompressionConfigPartialContent.java 
b/test/org/apache/coyote/TestCompressionConfigPartialContent.java
new file mode 100644
index 0000000000..9e6dfea95d
--- /dev/null
+++ b/test/org/apache/coyote/TestCompressionConfigPartialContent.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote;
+
+import java.util.Enumeration;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The ranges in a Content-Range header field refer to the bytes of the 
representation (RFC 9110, section 14.4) so a
+ * partial response must not be compressed with a content coding. A transfer 
coding applies to the message rather than
+ * to the representation so it is not affected.
+ */
+public class TestCompressionConfigPartialContent {
+
+    @Test
+    public void testFullResponseContentEncoding() {
+        Response response = new Response();
+
+        Assert.assertTrue(useCompression("accept-encoding", response));
+        Assert.assertEquals("gzip", 
response.getMimeHeaders().getHeader("Content-Encoding"));
+    }
+
+
+    @Test
+    public void testPartialResponseContentEncoding() {
+        Response response = new Response();
+        response.setStatus(206);
+        response.getMimeHeaders().addValue("Content-Range").setString("bytes 
0-99/1000");
+
+        Assert.assertFalse(useCompression("accept-encoding", response));
+        
Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding"));
+    }
+
+
+    @Test
+    public void testMultipartPartialResponseContentEncoding() {
+        // The Content-Range headers are in the body parts. There is none in 
the response headers.
+        Response response = new Response();
+        response.setStatus(206);
+        response.setContentType("multipart/byteranges; boundary=abc");
+
+        Assert.assertFalse(useCompression("accept-encoding", response));
+        
Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding"));
+    }
+
+
+    @Test
+    public void testPartialResponseVary() {
+        Response response = new Response();
+        response.setStatus(206);
+        response.getMimeHeaders().addValue("Content-Range").setString("bytes 
0-99/1000");
+
+        // The response is never compressed so it does not vary with 
Accept-Encoding
+        Assert.assertFalse(useCompression("accept-encoding", response));
+        Assert.assertEquals(0, countVaryAcceptEncoding(response));
+    }
+
+
+    @Test
+    public void testPartialResponseExistingVary() {
+        Response response = new Response();
+        response.setStatus(206);
+        response.getMimeHeaders().addValue("Vary").setString("Accept-Language, 
accept-encoding");
+
+        Assert.assertFalse(useCompression("accept-encoding", response));
+        Assert.assertEquals(1, countVaryAcceptEncoding(response));
+        String vary = response.getMimeHeaders().values("Vary").nextElement();
+        Assert.assertTrue(vary.toLowerCase().contains("accept-language"));
+    }
+
+
+    @Test
+    public void testPartialResponseTransferEncoding() {
+        Response response = new Response();
+        response.setStatus(206);
+        response.getMimeHeaders().addValue("Content-Range").setString("bytes 
0-99/1000");
+
+        Assert.assertTrue(useCompression("TE", response));
+        Assert.assertEquals("gzip", 
response.getMimeHeaders().getHeader("Transfer-Encoding"));
+    }
+
+
+    @Test
+    public void testMultipartPartialResponseTransferEncoding() {
+        Response response = new Response();
+        response.setStatus(206);
+        response.setContentType("multipart/byteranges; boundary=abc");
+
+        Assert.assertTrue(useCompression("TE", response));
+        Assert.assertEquals("gzip", 
response.getMimeHeaders().getHeader("Transfer-Encoding"));
+    }
+
+
+    private static int countVaryAcceptEncoding(Response response) {
+        int count = 0;
+        Enumeration<String> values = response.getMimeHeaders().values("Vary");
+        while (values.hasMoreElements()) {
+            for (String token : values.nextElement().split(",")) {
+                if ("accept-encoding".equalsIgnoreCase(token.trim())) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+
+    private static boolean useCompression(String requestHeaderName, Response 
response) {
+        CompressionConfig compressionConfig = new CompressionConfig();
+        // Skip length and MIME type checks
+        compressionConfig.setCompression("force");
+
+        Request request = new Request();
+        request.getMimeHeaders().addValue(requestHeaderName).setString("gzip");
+
+        return compressionConfig.useCompression(request, response);
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to