This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 638759b26eb2ae7b5760232ced65b1147ec585ef Author: Mark Thomas <[email protected]> AuthorDate: Fri Feb 6 18:55:30 2026 +0000 Add an HTTP configuration setting, noCompressionEncodings This can be used to control which content encodings will not be compressed when compression is enabled. Based on pull request #914 by Long9725. --- java/org/apache/coyote/CompressionConfig.java | 34 ++++++++++++-- .../coyote/http11/AbstractHttp11Protocol.java | 9 ++++ test/org/apache/coyote/TestCompressionConfig.java | 54 ++++++++++++++-------- .../apache/coyote/http11/TestHttp11Processor.java | 30 +++++++++--- webapps/docs/changelog.xml | 6 +++ webapps/docs/config/http.xml | 12 +++++ 6 files changed, 116 insertions(+), 29 deletions(-) diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java index c48dee1b13..2cf718936b 100644 --- a/java/org/apache/coyote/CompressionConfig.java +++ b/java/org/apache/coyote/CompressionConfig.java @@ -19,6 +19,7 @@ package org.apache.coyote; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.List; @@ -47,6 +48,35 @@ public class CompressionConfig { "text/javascript,application/javascript,application/json,application/xml"; private String[] compressibleMimeTypes = null; private int compressionMinSize = 2048; + private Set<String> noCompressionEncodings = + new HashSet<>(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd")); + + + public String getNoCompressionEncodings() { + return String.join(",", noCompressionEncodings); + } + + + /** + * Set the list of content encodings that indicate already-compressed content. + * When content is already encoded with one of these encodings, compression will not be applied + * to prevent double compression. + * + * @param encodings Comma-separated list of encoding names (e.g., "gzip,br.dflate") + */ + public void setNoCompressionEncodings(String encodings) { + Set<String> newEncodings = new HashSet<>(); + if (encodings != null && !encodings.isEmpty()) { + StringTokenizer tokens = new StringTokenizer(encodings, ","); + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken().trim(); + if(!token.isEmpty()) { + newEncodings.add(token); + } + } + } + this.noCompressionEncodings = newEncodings; + } /** @@ -210,9 +240,7 @@ public class CompressionConfig { if (tokens.contains("identity")) { // If identity, do not do content modifications useContentEncoding = false; - } else if (tokens.contains("br") || tokens.contains("compress") || tokens.contains("dcb") || - tokens.contains("dcz") || tokens.contains("deflate") || tokens.contains("gzip") || - tokens.contains("pack200-gzip") || tokens.contains("zstd")) { + } else if (noCompressionEncodings.stream().anyMatch(tokens::contains)) { // Content should not be compressed twice return false; } diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java index 3b778a5057..f5ee4821d3 100644 --- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java +++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java @@ -340,6 +340,15 @@ public abstract class AbstractHttp11Protocol<S> extends AbstractProtocol<S> { } + public String getNoCompressionEncodings() { + return compressionConfig.getNoCompressionEncodings(); + } + + public void setNoCompressionEncodings(String encodings) { + compressionConfig.setNoCompressionEncodings(encodings); + } + + public boolean useCompression(Request request, Response response) { return compressionConfig.useCompression(request, response); } diff --git a/test/org/apache/coyote/TestCompressionConfig.java b/test/org/apache/coyote/TestCompressionConfig.java index ae16c02f8f..be336ec7e6 100644 --- a/test/org/apache/coyote/TestCompressionConfig.java +++ b/test/org/apache/coyote/TestCompressionConfig.java @@ -17,6 +17,7 @@ package org.apache.coyote; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -33,29 +34,30 @@ public class TestCompressionConfig { public static Collection<Object[]> parameters() { List<Object[]> parameterSets = new ArrayList<>(); - parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] {}, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] {}, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "foobar;foo=bar, gzip;bla=\"quoted\"" }, "XX", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foobar;foo=bar, gzip;bla=\"quoted\"" }, "XX", Boolean.TRUE, + Boolean.TRUE }); return parameterSets; } @@ -110,4 +112,18 @@ public class TestCompressionConfig { } } } + + + @Test + public void testNoCompressionEncodings() { + CompressionConfig config = new CompressionConfig(); + String encodings = config.getNoCompressionEncodings(); + Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd") + .stream().anyMatch(encodings::contains)); + + config.setNoCompressionEncodings("br"); + String newEncodings = config.getNoCompressionEncodings(); + Assert.assertTrue(newEncodings.contains("br")); + Assert.assertFalse(newEncodings.contains("gzip")); + } } diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java index cc00ac4d84..fd77108f0d 100644 --- a/test/org/apache/coyote/http11/TestHttp11Processor.java +++ b/test/org/apache/coyote/http11/TestHttp11Processor.java @@ -32,6 +32,7 @@ import java.net.SocketAddress; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -421,7 +422,7 @@ public class TestHttp11Processor extends TomcatBaseTest { tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map<String, List<String>> responseHeaders = new HashMap<>(); + Map<String,List<String>> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, rc); @@ -445,7 +446,7 @@ public class TestHttp11Processor extends TomcatBaseTest { tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map<String, List<String>> responseHeaders = new HashMap<>(); + Map<String,List<String>> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, rc); @@ -854,11 +855,11 @@ public class TestHttp11Processor extends TomcatBaseTest { tomcat.start(); ByteChunk getBody = new ByteChunk(); - Map<String, List<String>> getHeaders = new HashMap<>(); + Map<String,List<String>> getHeaders = new HashMap<>(); int getStatus = getUrl("http://localhost:" + getPort() + "/test", getBody, getHeaders); ByteChunk headBody = new ByteChunk(); - Map<String, List<String>> headHeaders = new HashMap<>(); + Map<String,List<String>> headHeaders = new HashMap<>(); int headStatus = getUrl("http://localhost:" + getPort() + "/test", headBody, headHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, getStatus); @@ -997,7 +998,7 @@ public class TestHttp11Processor extends TomcatBaseTest { tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map<String, List<String>> responseHeaders = new HashMap<>(); + Map<String,List<String>> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_RESET_CONTENT, rc); @@ -2149,7 +2150,6 @@ public class TestHttp11Processor extends TomcatBaseTest { } - private static class EarlyHintsServlet extends HttpServlet { private static final long serialVersionUID = 1L; @@ -2165,6 +2165,7 @@ public class TestHttp11Processor extends TomcatBaseTest { this.useSendError = useSendError; this.errorString = errorString; } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.addHeader("Link", "</style.css>; rel=preload; as=style"); @@ -2185,4 +2186,19 @@ public class TestHttp11Processor extends TomcatBaseTest { resp.getWriter().write("OK"); } } -} + + + @Test + public void testNoCompressionEncodings() { + Http11NioProtocol protocol = new Http11NioProtocol(); + String encodings = protocol.getNoCompressionEncodings(); + Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd") + .stream().anyMatch(encodings::contains)); + + protocol.setNoCompressionEncodings("br"); + + String newEncodings = protocol.getNoCompressionEncodings(); + Assert.assertTrue(newEncodings.contains("br")); + Assert.assertFalse(newEncodings.contains("gzip")); + } +} \ No newline at end of file diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 83d23edd62..a470fac154 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -227,6 +227,12 @@ <bug>69938</bug>: Avoid changing the closed state of TLS channel when resetting it after close. (remm) </fix> + <add> + Add an HTTP configuration setting, <code>noCompressionEncodings</code>, + that can be used to control which content encodings will not be + compressed when compression is enabled. Based on pull request + <pr>914</pr> by Long9725. (markt) + </add> </changelog> </subsection> <subsection name="Jasper"> diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index deb1ed5c3c..443183508f 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -651,6 +651,18 @@ used.</p> </attribute> + <attribute name="noCompressionEncodings" requried="false"> + <p>A comma-separated list of content encodings that indicate + already-compressed content. When the response already has a + <code>Content-Encoding</code> header with one of these values, compression + will not be applied to prevent double compression. This attribute is only + used if <strong>compression</strong> is set to <code>on</code> or + <code>force</code>.</p> + <p>If not specified, the default values is + <code>br,compress,dcb,dcz,deflate,gzip,pack2000-gzip,zstd</code>, which + includes all commonly used compression algorithms.</p> + </attribute> + <attribute name="noCompressionUserAgents" required="false"> <p>The value is a regular expression (using <code>java.util.regex</code>) matching the <code>user-agent</code> header of HTTP clients for which --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
