Skip to content

Commit 9fed0d1

Browse files
committed
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.
1 parent b54565c commit 9fed0d1

6 files changed

Lines changed: 129 additions & 39 deletions

File tree

java/org/apache/coyote/CompressionConfig.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.StringReader;
2121
import java.util.ArrayList;
22+
import java.util.Arrays;
2223
import java.util.Enumeration;
2324
import java.util.HashSet;
2425
import java.util.List;
@@ -48,6 +49,35 @@ public class CompressionConfig {
4849
private String[] compressibleMimeTypes = null;
4950
private int compressionMinSize = 2048;
5051
private boolean noCompressionStrongETag = true;
52+
private Set<String> noCompressionEncodings =
53+
new HashSet<>(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd"));
54+
55+
56+
public String getNoCompressionEncodings() {
57+
return String.join(",", noCompressionEncodings);
58+
}
59+
60+
61+
/**
62+
* Set the list of content encodings that indicate already-compressed content.
63+
* When content is already encoded with one of these encodings, compression will not be applied
64+
* to prevent double compression.
65+
*
66+
* @param encodings Comma-separated list of encoding names (e.g., "gzip,br.dflate")
67+
*/
68+
public void setNoCompressionEncodings(String encodings) {
69+
Set<String> newEncodings = new HashSet<>();
70+
if (encodings != null && !encodings.isEmpty()) {
71+
StringTokenizer tokens = new StringTokenizer(encodings, ",");
72+
while (tokens.hasMoreTokens()) {
73+
String token = tokens.nextToken().trim();
74+
if(!token.isEmpty()) {
75+
newEncodings.add(token);
76+
}
77+
}
78+
}
79+
this.noCompressionEncodings = newEncodings;
80+
}
5181

5282

5383
/**
@@ -242,9 +272,7 @@ public boolean useCompression(Request request, Response response) {
242272
if (tokens.contains("identity")) {
243273
// If identity, do not do content modifications
244274
useContentEncoding = false;
245-
} else if (tokens.contains("br") || tokens.contains("compress") || tokens.contains("dcb") ||
246-
tokens.contains("dcz") || tokens.contains("deflate") || tokens.contains("gzip") ||
247-
tokens.contains("pack200-gzip") || tokens.contains("zstd")) {
275+
} else if (noCompressionEncodings.stream().anyMatch(tokens::contains)) {
248276
// Content should not be compressed twice
249277
return false;
250278
}

java/org/apache/coyote/http11/AbstractHttp11Protocol.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ public void setCompressionMinSize(int compressionMinSize) {
427427
}
428428

429429

430+
public String getNoCompressionEncodings() {
431+
return compressionConfig.getNoCompressionEncodings();
432+
}
433+
434+
public void setNoCompressionEncodings(String encodings) {
435+
compressionConfig.setNoCompressionEncodings(encodings);
436+
}
437+
438+
430439
@Deprecated
431440
public boolean getNoCompressionStrongETag() {
432441
return compressionConfig.getNoCompressionStrongETag();

test/org/apache/coyote/TestCompressionConfig.java

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.coyote;
1818

1919
import java.util.ArrayList;
20+
import java.util.Arrays;
2021
import java.util.Collection;
2122
import java.util.List;
2223

@@ -33,35 +34,39 @@ public class TestCompressionConfig {
3334
public static Collection<Object[]> parameters() {
3435
List<Object[]> parameterSets = new ArrayList<>();
3536

36-
parameterSets.add(new Object[] { new String[] { }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
37-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
38-
parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
39-
parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
40-
parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
41-
parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
42-
43-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
44-
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
45-
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
46-
47-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
48-
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
49-
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
50-
51-
parameterSets.add(new Object[] { new String[] { }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
52-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
53-
parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
54-
parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
55-
parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
56-
parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
57-
58-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
59-
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
60-
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
61-
62-
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
63-
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
64-
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
37+
parameterSets.add(new Object[] { new String[] {}, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
38+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
39+
parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
40+
parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
41+
parameterSets
42+
.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
43+
parameterSets
44+
.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
45+
46+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
47+
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.FALSE });
48+
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.FALSE });
49+
50+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
51+
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
52+
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE });
53+
54+
parameterSets.add(new Object[] { new String[] {}, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
55+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
56+
parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
57+
parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
58+
parameterSets
59+
.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
60+
parameterSets
61+
.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
62+
63+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
64+
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE, Boolean.TRUE });
65+
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE, Boolean.TRUE });
66+
67+
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
68+
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
69+
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE });
6570

6671
return parameterSets;
6772
}
@@ -120,4 +125,18 @@ public void testUseCompression() throws Exception {
120125
}
121126
}
122127
}
128+
129+
130+
@Test
131+
public void testNoCompressionEncodings() {
132+
CompressionConfig config = new CompressionConfig();
133+
String encodings = config.getNoCompressionEncodings();
134+
Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd")
135+
.stream().anyMatch(encodings::contains));
136+
137+
config.setNoCompressionEncodings("br");
138+
String newEncodings = config.getNoCompressionEncodings();
139+
Assert.assertTrue(newEncodings.contains("br"));
140+
Assert.assertFalse(newEncodings.contains("gzip"));
141+
}
123142
}

test/org/apache/coyote/http11/TestHttp11Processor.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.nio.CharBuffer;
3333
import java.nio.charset.StandardCharsets;
3434
import java.util.ArrayList;
35+
import java.util.Arrays;
3536
import java.util.HashMap;
3637
import java.util.List;
3738
import java.util.Map;
@@ -421,7 +422,7 @@ public void testChunking11NoContentLength() throws Exception {
421422
tomcat.start();
422423

423424
ByteChunk responseBody = new ByteChunk();
424-
Map<String, List<String>> responseHeaders = new HashMap<>();
425+
Map<String,List<String>> responseHeaders = new HashMap<>();
425426
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
426427

427428
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
@@ -445,7 +446,7 @@ public void testNoChunking11NoContentLengthConnectionClose() throws Exception {
445446
tomcat.start();
446447

447448
ByteChunk responseBody = new ByteChunk();
448-
Map<String, List<String>> responseHeaders = new HashMap<>();
449+
Map<String,List<String>> responseHeaders = new HashMap<>();
449450
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
450451

451452
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
@@ -854,11 +855,11 @@ public void testBug59310() throws Exception {
854855
tomcat.start();
855856

856857
ByteChunk getBody = new ByteChunk();
857-
Map<String, List<String>> getHeaders = new HashMap<>();
858+
Map<String,List<String>> getHeaders = new HashMap<>();
858859
int getStatus = getUrl("http://localhost:" + getPort() + "/test", getBody, getHeaders);
859860

860861
ByteChunk headBody = new ByteChunk();
861-
Map<String, List<String>> headHeaders = new HashMap<>();
862+
Map<String,List<String>> headHeaders = new HashMap<>();
862863
int headStatus = getUrl("http://localhost:" + getPort() + "/test", headBody, headHeaders);
863864

864865
Assert.assertEquals(HttpServletResponse.SC_OK, getStatus);
@@ -997,7 +998,7 @@ public void testBug61086() throws Exception {
997998
tomcat.start();
998999

9991000
ByteChunk responseBody = new ByteChunk();
1000-
Map<String, List<String>> responseHeaders = new HashMap<>();
1001+
Map<String,List<String>> responseHeaders = new HashMap<>();
10011002
int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders);
10021003

10031004
Assert.assertEquals(HttpServletResponse.SC_RESET_CONTENT, rc);
@@ -2149,7 +2150,6 @@ public void testEarlyHintsSendErrorWithMessage() throws Exception {
21492150
}
21502151

21512152

2152-
21532153
private static class EarlyHintsServlet extends HttpServlet {
21542154

21552155
private static final long serialVersionUID = 1L;
@@ -2165,6 +2165,7 @@ private static class EarlyHintsServlet extends HttpServlet {
21652165
this.useSendError = useSendError;
21662166
this.errorString = errorString;
21672167
}
2168+
21682169
@Override
21692170
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
21702171
resp.addHeader("Link", "</style.css>; rel=preload; as=style");
@@ -2185,4 +2186,19 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
21852186
resp.getWriter().write("OK");
21862187
}
21872188
}
2188-
}
2189+
2190+
2191+
@Test
2192+
public void testNoCompressionEncodings() {
2193+
Http11NioProtocol protocol = new Http11NioProtocol();
2194+
String encodings = protocol.getNoCompressionEncodings();
2195+
Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd")
2196+
.stream().anyMatch(encodings::contains));
2197+
2198+
protocol.setNoCompressionEncodings("br");
2199+
2200+
String newEncodings = protocol.getNoCompressionEncodings();
2201+
Assert.assertTrue(newEncodings.contains("br"));
2202+
Assert.assertFalse(newEncodings.contains("gzip"));
2203+
}
2204+
}

webapps/docs/changelog.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@
111111
<bug>69938</bug>: Avoid changing the closed state of TLS channel when
112112
resetting it after close. (remm)
113113
</fix>
114+
<add>
115+
Add an HTTP configuration setting, <code>noCompressionEncodings</code>,
116+
that can be used to control which content encodings will not be
117+
compressed when compression is enabled. Based on pull request
118+
<pr>914</pr> by Long9725. (markt)
119+
</add>
114120
</changelog>
115121
</subsection>
116122
<subsection name="Jasper">

webapps/docs/config/http.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,18 @@
682682
used.</p>
683683
</attribute>
684684

685+
<attribute name="noCompressionEncodings" requried="false">
686+
<p>A comma-separated list of content encodings that indicate
687+
already-compressed content. When the response already has a
688+
<code>Content-Encoding</code> header with one of these values, compression
689+
will not be applied to prevent double compression. This attribute is only
690+
used if <strong>compression</strong> is set to <code>on</code> or
691+
<code>force</code>.</p>
692+
<p>If not specified, the default values is
693+
<code>br,compress,dcb,dcz,deflate,gzip,pack2000-gzip,zstd</code>, which
694+
includes all commonly used compression algorithms.</p>
695+
</attribute>
696+
685697
<attribute name="noCompressionStrongETag" required="false">
686698
<p>This flag configures whether resources with a strong ETag will be
687699
considered for compression. If <code>true</code>, resources with a strong

0 commit comments

Comments
 (0)