From 7c1a1042213fa9650e825685da7f21144b0fab64 Mon Sep 17 00:00:00 2001
From: Long9725
Date: Sat, 1 Nov 2025 01:32:53 +0900
Subject: [PATCH 1/4] Add configurable gzip compression level and buffer size
---
java/org/apache/coyote/CompressionConfig.java | 44 ++++++++++++++---
.../org/apache/coyote/LocalStrings.properties | 2 +
.../coyote/http11/AbstractHttp11Protocol.java | 16 ++++++
.../apache/coyote/http11/Http11Processor.java | 5 +-
.../http11/filters/GzipOutputFilter.java | 49 +++++++++++++++++--
.../http11/filters/LocalStrings.properties | 2 +
.../apache/coyote/http2/StreamProcessor.java | 11 ++++-
.../apache/coyote/TestCompressionConfig.java | 39 +++++++++++++++
.../coyote/http11/TestHttp11Processor.java | 37 ++++++++++++++
.../http11/filters/TestGzipOutputFilter.java | 15 ++++++
webapps/docs/config/http.xml | 18 +++++++
11 files changed, 223 insertions(+), 15 deletions(-)
diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java
index c48dee1b1330..ededab01d707 100644
--- a/java/org/apache/coyote/CompressionConfig.java
+++ b/java/org/apache/coyote/CompressionConfig.java
@@ -18,14 +18,10 @@
import java.io.IOException;
import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.StringTokenizer;
+import java.util.*;
import java.util.regex.Pattern;
+import org.apache.coyote.http11.filters.GzipOutputFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.MessageBytes;
@@ -47,7 +43,8 @@ public class CompressionConfig {
"text/javascript,application/javascript,application/json,application/xml";
private String[] compressibleMimeTypes = null;
private int compressionMinSize = 2048;
-
+ private int gzipLevel = -1;
+ private int gzipBufferSize = GzipOutputFilter.DEFAULT_BUFFER_SIZE;
/**
* Set compression level.
@@ -162,6 +159,38 @@ public int getCompressionMinSize() {
return compressionMinSize;
}
+ /**
+ * Set the compression level for gzip.
+ * @param gzipLevel The compression level. Valid values are -1 (default), or 1-9.
+ * -1 uses the default compression level.
+ * 1 gives best speed, 9 gives best compression.
+ */
+ public void setGzipLevel(int gzipLevel) {
+ if (gzipLevel < -1 || gzipLevel > 9) {
+ throw new IllegalArgumentException(sm.getString("compressionConfig.invalidGzipLevel", Integer.valueOf(gzipLevel)));
+ }
+ this.gzipLevel = gzipLevel;
+ }
+
+ public int getGzipLevel() {
+ return gzipLevel;
+ }
+
+ /**
+ * Set the buffer size for gzip compression stream.
+ *
+ * @param gzipBufferSize The buffer size in bytes. Must be positive.
+ */
+ public void setGzipBufferSize(int gzipBufferSize) {
+ if (gzipBufferSize <= 0) {
+ throw new IllegalArgumentException(sm.getString("compressionConfig.invalidGzipBufferSize", Integer.valueOf(gzipBufferSize)));
+ }
+ this.gzipBufferSize = gzipBufferSize;
+ }
+
+ public int getGzipBufferSize() {
+ return gzipBufferSize;
+ }
/**
* Set Minimum size to trigger compression.
@@ -172,7 +201,6 @@ public void setCompressionMinSize(int compressionMinSize) {
this.compressionMinSize = compressionMinSize;
}
-
/**
* Determines if compression should be enabled for the given response and if it is, sets any necessary headers to
* mark it as such.
diff --git a/java/org/apache/coyote/LocalStrings.properties b/java/org/apache/coyote/LocalStrings.properties
index bd4e432e64ba..dffc70a4afe5 100644
--- a/java/org/apache/coyote/LocalStrings.properties
+++ b/java/org/apache/coyote/LocalStrings.properties
@@ -57,6 +57,8 @@ asyncStateMachine.invalidAsyncState=Calling [{0}] is not valid for a request wit
asyncStateMachine.stateChange=Changing async state from [{0}] to [{1}]
compressionConfig.ContentEncodingParseFail=Failed to parse Content-Encoding header when checking to see if compression was already in use
+compressionConfig.invalidGzipLevel=The gzip compression level [{0}] is not valid. Valid values are -1 (default) or 1-9.
+compressionConfig.invalidGzipBufferSize=The gzip buffer size [{0}] is not valid. Must be a positive integer.
continueResponseTiming.invalid=The value [{0}] is not a valid configuration option for continueResponseTiming
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index 0b876e6543f6..c89f4005c66b 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -339,6 +339,22 @@ public void setCompressionMinSize(int compressionMinSize) {
compressionConfig.setCompressionMinSize(compressionMinSize);
}
+ public int getGzipLevel() {
+ return compressionConfig.getGzipLevel();
+ }
+
+ public void setGzipLevel(int gzipLevel) {
+ compressionConfig.setGzipLevel(gzipLevel);
+ }
+
+ public int getGzipBufferSize() {
+ return compressionConfig.getGzipBufferSize();
+ }
+
+ public void setGzipBufferSize(int gzipBufferSize) {
+ compressionConfig.setGzipBufferSize(gzipBufferSize);
+ }
+
public boolean useCompression(Request request, Response response) {
return compressionConfig.useCompression(request, response);
diff --git a/java/org/apache/coyote/http11/Http11Processor.java b/java/org/apache/coyote/http11/Http11Processor.java
index b6c9c5239ceb..a122574106f1 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -190,7 +190,10 @@ public Http11Processor(AbstractHttp11Protocol> protocol, Adapter adapter) {
// Create and add the gzip filters.
// inputBuffer.addFilter(new GzipInputFilter());
- outputBuffer.addFilter(new GzipOutputFilter());
+ GzipOutputFilter gzipOutputFilter = new GzipOutputFilter();
+ gzipOutputFilter.setLevel(protocol.getGzipLevel());
+ gzipOutputFilter.setBufferSize(protocol.getGzipBufferSize());
+ outputBuffer.addFilter(gzipOutputFilter);
pluggableFilterIndex = inputBuffer.getFilters().length;
}
diff --git a/java/org/apache/coyote/http11/filters/GzipOutputFilter.java b/java/org/apache/coyote/http11/filters/GzipOutputFilter.java
index 9ed432aba682..0342936c569b 100644
--- a/java/org/apache/coyote/http11/filters/GzipOutputFilter.java
+++ b/java/org/apache/coyote/http11/filters/GzipOutputFilter.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
+import java.util.zip.Deflater;
import java.util.zip.GZIPOutputStream;
import org.apache.coyote.Response;
@@ -32,7 +33,7 @@
* Gzip output filter.
*/
public class GzipOutputFilter implements OutputFilter {
-
+ public static final int DEFAULT_BUFFER_SIZE = 512;
protected static final Log log = LogFactory.getLog(GzipOutputFilter.class);
private static final StringManager sm = StringManager.getManager(GzipOutputFilter.class);
@@ -56,13 +57,24 @@ public class GzipOutputFilter implements OutputFilter {
*/
protected final OutputStream fakeOutputStream = new FakeOutputStream();
+ /**
+ * Compression level for gzip. Valid values are -1 (default), or 1-9
+ */
+ private int level = Deflater.DEFAULT_COMPRESSION;
+
+ /**
+ * Buffer size for gzip compression stream. Default is Deflater default size 512.
+ */
+ private int bufferSize = DEFAULT_BUFFER_SIZE;
// --------------------------------------------------- OutputBuffer Methods
@Override
public int doWrite(ByteBuffer chunk) throws IOException {
if (compressionStream == null) {
- compressionStream = new GZIPOutputStream(fakeOutputStream, true);
+ compressionStream = new GZIPOutputStream(fakeOutputStream, bufferSize, true) {{
+ this.def.setLevel(level);
+ }};
}
int len = chunk.remaining();
if (chunk.hasArray()) {
@@ -121,7 +133,9 @@ public void setBuffer(HttpOutputBuffer buffer) {
@Override
public void end() throws IOException {
if (compressionStream == null) {
- compressionStream = new GZIPOutputStream(fakeOutputStream, true);
+ compressionStream = new GZIPOutputStream(fakeOutputStream, bufferSize, true) {{
+ this.def.setLevel(level);
+ }};
}
compressionStream.finish();
compressionStream.close();
@@ -135,7 +149,30 @@ public void recycle() {
compressionStream = null;
}
+ /**
+ * Set the compression level for gzip.
+ * @param level The compression level. Valid values are -1 (default), or 1-9.
+ * -1 uses the default compression level.
+ * 1 gives best speed, 9 gives best compression.
+ */
+ public void setLevel(int level) {
+ if (level < -1 || level > 9) {
+ throw new IllegalArgumentException(sm.getString("gzipOutputFilter.invalidLevel", Integer.valueOf(level)));
+ }
+ this.level = level;
+ }
+ /**
+ * Set the buffer size for gzip compression stream.
+ *
+ * @param bufferSize The buffer size in bytes. Must be positive.
+ */
+ public void setBufferSize(int bufferSize) {
+ if (bufferSize <= 0) {
+ throw new IllegalArgumentException(sm.getString("gzipOutputFilter.invalidBufferSize", Integer.valueOf(bufferSize)));
+ }
+ this.bufferSize = bufferSize;
+ }
// ------------------------------------------- FakeOutputStream Inner Class
@@ -157,11 +194,13 @@ public void write(byte[] b, int off, int len) throws IOException {
@Override
public void flush() throws IOException {
- /* NOOP */}
+ /* NOOP */
+ }
@Override
public void close() throws IOException {
- /* NOOP */}
+ /* NOOP */
+ }
}
diff --git a/java/org/apache/coyote/http11/filters/LocalStrings.properties b/java/org/apache/coyote/http11/filters/LocalStrings.properties
index 3650316b88f9..a93adae67c51 100644
--- a/java/org/apache/coyote/http11/filters/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/filters/LocalStrings.properties
@@ -30,5 +30,7 @@ chunkedInputFilter.maxExtension=maxExtensionSize exceeded
chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
gzipOutputFilter.flushFail=Ignored exception while flushing gzip filter
+gzipOutputFilter.invalidLevel=The gzip compression level [{0}] is not valid. Valid values are -1 (default) or 1-9.
+gzipOutputFilter.invalidBufferSize=The gzip buffer size [{0}] is not valid. Must be a positive integer.
inputFilter.maxSwallow=maxSwallowSize exceeded
diff --git a/java/org/apache/coyote/http2/StreamProcessor.java b/java/org/apache/coyote/http2/StreamProcessor.java
index 942ed2014cc6..399defbf3dc2 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -39,6 +39,7 @@
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.Response;
+import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.coyote.http11.filters.GzipOutputFilter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
@@ -211,7 +212,15 @@ static void prepareHeaders(Request coyoteRequest, Response coyoteResponse, boole
if (noSendfile && protocol != null && protocol.useCompression(coyoteRequest, coyoteResponse)) {
// Enable compression. Headers will have been set. Need to configure
// output filter at this point.
- stream.addOutputFilter(new GzipOutputFilter());
+ GzipOutputFilter gzipOutputFilter = new GzipOutputFilter();
+ AbstractHttp11Protocol> http11Protocol = protocol.getHttp11Protocol();
+
+ if (http11Protocol != null) {
+ gzipOutputFilter.setLevel(http11Protocol.getGzipLevel());
+ gzipOutputFilter.setBufferSize(http11Protocol.getGzipBufferSize());
+ }
+
+ stream.addOutputFilter(gzipOutputFilter);
}
// Check to see if a response body is present
diff --git a/test/org/apache/coyote/TestCompressionConfig.java b/test/org/apache/coyote/TestCompressionConfig.java
index ae16c02f8f43..e716d83996d0 100644
--- a/test/org/apache/coyote/TestCompressionConfig.java
+++ b/test/org/apache/coyote/TestCompressionConfig.java
@@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.zip.Deflater;
import org.junit.Assert;
import org.junit.Test;
@@ -110,4 +111,42 @@ public void testUseCompression() throws Exception {
}
}
}
+
+ @Test
+ public void testGzipLevelConfiguration() {
+ CompressionConfig config = new CompressionConfig();
+
+ Assert.assertEquals(-1, config.getGzipLevel());
+
+ config.setGzipLevel(Deflater.BEST_SPEED);
+ Assert.assertEquals(Deflater.BEST_SPEED, config.getGzipLevel());
+
+ config.setGzipLevel(Deflater.BEST_COMPRESSION);
+ Assert.assertEquals(Deflater.BEST_COMPRESSION, config.getGzipLevel());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipLevelLow() {
+ new CompressionConfig().setGzipLevel(-2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipLevelHigh() {
+ new CompressionConfig().setGzipLevel(10);
+ }
+
+ @Test
+ public void testGzipBufferSizeConfiguration() {
+ CompressionConfig config = new CompressionConfig();
+
+ Assert.assertEquals(512, config.getGzipBufferSize());
+
+ config.setGzipBufferSize(1024);
+ Assert.assertEquals(1024, config.getGzipBufferSize());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipBufferSize() {
+ new CompressionConfig().setGzipBufferSize(0);
+ }
}
diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java
index cc00ac4d848e..9cf27034a3a8 100644
--- a/test/org/apache/coyote/http11/TestHttp11Processor.java
+++ b/test/org/apache/coyote/http11/TestHttp11Processor.java
@@ -36,6 +36,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
+import java.util.zip.Deflater;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
@@ -2148,7 +2149,43 @@ public void testEarlyHintsSendErrorWithMessage() throws Exception {
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
}
+ @Test
+ public void testGzipLevel() {
+ Http11NioProtocol protocol = new Http11NioProtocol();
+
+ Assert.assertEquals(-1, protocol.getGzipLevel());
+
+ protocol.setGzipLevel(Deflater.BEST_SPEED);
+ Assert.assertEquals(Deflater.BEST_SPEED, protocol.getGzipLevel());
+
+ protocol.setGzipLevel(Deflater.BEST_COMPRESSION);
+ Assert.assertEquals(Deflater.BEST_COMPRESSION, protocol.getGzipLevel());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipLevelLow() {
+ new Http11NioProtocol().setGzipLevel(-2);
+ }
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipLevelHigh() {
+ new Http11NioProtocol().setGzipLevel(10);
+ }
+
+ @Test
+ public void testGzipBufferSize() {
+ Http11NioProtocol protocol = new Http11NioProtocol();
+
+ Assert.assertEquals(512, protocol.getGzipBufferSize());
+
+ protocol.setGzipBufferSize(1024);
+ Assert.assertEquals(1024, protocol.getGzipBufferSize());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipBufferSize() {
+ new Http11NioProtocol().setGzipBufferSize(0);
+ }
private static class EarlyHintsServlet extends HttpServlet {
diff --git a/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java b/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java
index 7873b0e54773..e24951db242a 100644
--- a/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java
+++ b/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java
@@ -81,4 +81,19 @@ public void testFlushingWithGzip() throws Exception {
// most of the data should have been flushed out
Assert.assertTrue(dataFound.length >= (dataExpected.length - 20));
}
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidLevelLow() {
+ new GzipOutputFilter().setLevel(-2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidLevelHigh() {
+ new GzipOutputFilter().setLevel(10);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidGzipBufferSize() {
+ new GzipOutputFilter().setBufferSize(0);
+ }
}
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 674794a5ef24..970f72d21d7c 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -450,6 +450,24 @@
Units are in bytes.
+
+ If compression is set to "on" or "force" then this
+ attribute may be used to configure the compression level for gzip
+ compression. Valid values are -1 (default compression)
+ or 1 through 9 where 1 gives
+ beset speed and 9 gives best compression ratio. If not
+ specified, this attribute defaults to -1 (JDK default compression).
+
+
+
+ If compression is set to "on" or "force" then this
+ attribute may be used to configure the internal buffer size (in bytes)
+ for the gzip compression stream. Larger buffers can improve compression
+ ratios and throughput for bulk responses, while smaller buffers reduce
+ latency for streaming scenarios. If not specified, this attribute
+ defaults to 512 bytes.
+
+
The number of seconds during which the sockets used by this
Connector will linger when they are closed. The default
From ed6f65548c65e591732cae60a9cede111ad5833e Mon Sep 17 00:00:00 2001
From: Long9725
Date: Sat, 1 Nov 2025 11:10:32 +0900
Subject: [PATCH 2/4] Add OutputFilterFactory for compress extension
---
java/org/apache/coyote/CompressionConfig.java | 59 ++++++++++++++-----
.../coyote/http11/AbstractHttp11Protocol.java | 29 ++++++++-
.../coyote/http11/LocalStrings.properties | 1 +
.../filters/GzipOutputFilterFactory.java | 44 ++++++++++++++
.../http11/filters/OutputFilterFactory.java | 28 +++++++++
.../apache/coyote/TestCompressionConfig.java | 16 ++++-
.../coyote/http11/TestHttp11Processor.java | 55 +++++++++++++++--
7 files changed, 210 insertions(+), 22 deletions(-)
create mode 100644 java/org/apache/coyote/http11/filters/GzipOutputFilterFactory.java
create mode 100644 java/org/apache/coyote/http11/filters/OutputFilterFactory.java
diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java
index ededab01d707..1751041e157d 100644
--- a/java/org/apache/coyote/CompressionConfig.java
+++ b/java/org/apache/coyote/CompressionConfig.java
@@ -43,6 +43,9 @@ public class CompressionConfig {
"text/javascript,application/javascript,application/json,application/xml";
private String[] compressibleMimeTypes = null;
private int compressionMinSize = 2048;
+ private Set noCompressionEncodings = new HashSet(Arrays.asList(
+ "br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd"
+ ));
private int gzipLevel = -1;
private int gzipBufferSize = GzipOutputFilter.DEFAULT_BUFFER_SIZE;
@@ -192,6 +195,31 @@ public int getGzipBufferSize() {
return gzipBufferSize;
}
+ 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 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;
+ }
+
/**
* Set Minimum size to trigger compression.
*
@@ -207,10 +235,11 @@ public void setCompressionMinSize(int compressionMinSize) {
*
* @param request The request that triggered the response
* @param response The response to consider compressing
+ * @param encoding The compression encoding to use (e.g., "gzip", "br", "deflate", "zstd")
*
* @return {@code true} if compression was enabled for the given response, otherwise {@code false}
*/
- public boolean useCompression(Request request, Response response) {
+ public boolean useCompression(Request request, Response response, String encoding) {
// Check if compression is enabled
if (compressionLevel == 0) {
return false;
@@ -238,9 +267,7 @@ public boolean useCompression(Request request, Response response) {
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;
}
@@ -263,9 +290,9 @@ public boolean useCompression(Request request, Response response) {
}
Enumeration headerValues = request.getMimeHeaders().values("TE");
- boolean foundGzip = false;
+ boolean foundEncoding = false;
// TE and accept-encoding seem to have equivalent syntax
- while (!foundGzip && headerValues.hasMoreElements()) {
+ while (!foundEncoding && headerValues.hasMoreElements()) {
List tes;
try {
tes = TE.parse(new StringReader(headerValues.nextElement()));
@@ -275,9 +302,9 @@ public boolean useCompression(Request request, Response response) {
}
for (TE te : tes) {
- if ("gzip".equalsIgnoreCase(te.getEncoding())) {
+ if (encoding.equalsIgnoreCase(te.getEncoding())) {
useTransferEncoding = true;
- foundGzip = true;
+ foundEncoding = true;
break;
}
}
@@ -296,11 +323,11 @@ public boolean useCompression(Request request, Response response) {
// Therefore, set the Vary header to keep proxies happy
ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding");
- // Check if user-agent supports gzip encoding
- // Only interested in whether gzip encoding is supported. Other
+ // Check if user-agent supports the specified encoding
+ // Only interested in whether the encoding is supported. Other
// encodings and weights can be ignored.
headerValues = request.getMimeHeaders().values("accept-encoding");
- while (!foundGzip && headerValues.hasMoreElements()) {
+ while (!foundEncoding && headerValues.hasMoreElements()) {
List acceptEncodings;
try {
acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement()));
@@ -310,15 +337,15 @@ public boolean useCompression(Request request, Response response) {
}
for (AcceptEncoding acceptEncoding : acceptEncodings) {
- if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) {
- foundGzip = true;
+ if (encoding.equalsIgnoreCase(acceptEncoding.getEncoding())) {
+ foundEncoding = true;
break;
}
}
}
}
- if (!foundGzip) {
+ if (!foundEncoding) {
return false;
}
@@ -343,10 +370,10 @@ public boolean useCompression(Request request, Response response) {
response.setContentLength(-1);
if (useTransferEncoding) {
// Configure the transfer encoding for compressed content
- responseHeaders.addValue("Transfer-Encoding").setString("gzip");
+ responseHeaders.addValue("Transfer-Encoding").setString(encoding);
} else {
// Configure the content encoding for compressed content
- responseHeaders.addValue("Content-Encoding").setString("gzip");
+ responseHeaders.addValue("Content-Encoding").setString(encoding);
}
return true;
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index c89f4005c66b..d6d2fc8e1364 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -40,6 +40,8 @@
import org.apache.coyote.Response;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.UpgradeToken;
+import org.apache.coyote.http11.filters.GzipOutputFilterFactory;
+import org.apache.coyote.http11.filters.OutputFilterFactory;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.coyote.http11.upgrade.UpgradeGroupInfo;
import org.apache.coyote.http11.upgrade.UpgradeProcessorExternal;
@@ -59,6 +61,7 @@ public abstract class AbstractHttp11Protocol extends AbstractProtocol {
protected static final StringManager sm = StringManager.getManager(AbstractHttp11Protocol.class);
private final CompressionConfig compressionConfig = new CompressionConfig();
+ private OutputFilterFactory outputFilterFactory = new GzipOutputFilterFactory();
private HttpParser httpParser = null;
@@ -355,9 +358,33 @@ public void setGzipBufferSize(int gzipBufferSize) {
compressionConfig.setGzipBufferSize(gzipBufferSize);
}
+ public String getNoCompressionEncodings() {
+ return compressionConfig.getNoCompressionEncodings();
+ }
+
+ public void setNoCompressionEncodings(String encodings) {
+ compressionConfig.setNoCompressionEncodings(encodings);
+ }
+
+ public OutputFilterFactory getOutputFilterFactory() {
+ return outputFilterFactory;
+ }
+
+ public void setOutputFilterFactory(OutputFilterFactory outputFilterFactory) {
+ this.outputFilterFactory = outputFilterFactory;
+ }
+
+ public void setOutputFilterFactory(String className) {
+ try {
+ Class> clazz = Class.forName(className);
+ this.outputFilterFactory = (OutputFilterFactory) clazz.getDeclaredConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalArgumentException(sm.getString("abstractHttp11Protocol.invalidOutputFilterFactory", className), e);
+ }
+ }
public boolean useCompression(Request request, Response response) {
- return compressionConfig.useCompression(request, response);
+ return compressionConfig.useCompression(request, response, outputFilterFactory.getEncodingName());
}
diff --git a/java/org/apache/coyote/http11/LocalStrings.properties b/java/org/apache/coyote/http11/LocalStrings.properties
index 39c7b89c7df5..f35db3492a56 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -18,6 +18,7 @@ abstractHttp11Protocol.alpnWithNoAlpn=The upgrade handler [{0}] for [{1}] only s
abstractHttp11Protocol.httpUpgradeConfigured=The [{0}] connector has been configured to support HTTP upgrade to [{1}]
abstractHttp11Protocol.upgradeJmxNameFail=Failed to create ObjectName with which to register upgrade protocol in JMX
abstractHttp11Protocol.upgradeJmxRegistrationFail=Failed to register upgrade protocol in JMX
+abstractHttp11Protocol.invalidOutputFilterFactory=The output filter factory class [{0}] could not be loaded or instantiated.
http11processor.fallToDebug=\n\
\ Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
diff --git a/java/org/apache/coyote/http11/filters/GzipOutputFilterFactory.java b/java/org/apache/coyote/http11/filters/GzipOutputFilterFactory.java
new file mode 100644
index 000000000000..6a7713c6e1ac
--- /dev/null
+++ b/java/org/apache/coyote/http11/filters/GzipOutputFilterFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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.http11.filters;
+
+import org.apache.coyote.http11.AbstractHttp11Protocol;
+import org.apache.coyote.http11.OutputFilter;
+
+/**
+ * Factory for creating GzipOutputFilter instances with protocol configuration.
+ * This is the default output filter factory used by Tomcat.
+ */
+public class GzipOutputFilterFactory implements OutputFilterFactory{
+
+ @Override
+ public OutputFilter createFilter(AbstractHttp11Protocol> protocol) {
+ GzipOutputFilter filter = new GzipOutputFilter();
+
+ // Apply configuration from protocol
+ filter.setLevel(protocol.getGzipLevel());
+ filter.setBufferSize(protocol.getGzipBufferSize());
+
+ return filter;
+ }
+
+ @Override
+ public String getEncodingName() {
+ return "gzip";
+ }
+}
diff --git a/java/org/apache/coyote/http11/filters/OutputFilterFactory.java b/java/org/apache/coyote/http11/filters/OutputFilterFactory.java
new file mode 100644
index 000000000000..c36e0efba23e
--- /dev/null
+++ b/java/org/apache/coyote/http11/filters/OutputFilterFactory.java
@@ -0,0 +1,28 @@
+package org.apache.coyote.http11.filters;
+
+import org.apache.coyote.http11.AbstractHttp11Protocol;
+import org.apache.coyote.http11.OutputFilter;
+
+/**
+ * Factory interface for creating output filters.
+ * Allows pluggable compression and transformation filters.
+ */
+public interface OutputFilterFactory {
+
+ /**
+ * Create a new output filter instance configured for the given protocol.
+ *
+ * @param protocol The HTTP protocol instance providing configuration
+ * @return A configured output filter ready for use
+ */
+ OutputFilter createFilter(AbstractHttp11Protocol> protocol);
+
+ /**
+ * Get the encoding name for this filter.
+ * Used for Content-Encoding or Transfer-Encoding headers and
+ * for matching against client Accept-Encoding preferences.
+ *
+ * @return The encoding name (e.g., "gzip", "br", "deflate", "zstd")
+ */
+ String getEncodingName();
+}
diff --git a/test/org/apache/coyote/TestCompressionConfig.java b/test/org/apache/coyote/TestCompressionConfig.java
index e716d83996d0..959af855fd90 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;
import java.util.zip.Deflater;
@@ -92,7 +93,7 @@ public void testUseCompression() throws Exception {
response.getMimeHeaders().addValue("ETag").setString(eTag);
}
- boolean useCompression = compressionConfig.useCompression(request, response);
+ boolean useCompression = compressionConfig.useCompression(request, response, "gzip");
Assert.assertEquals(compress, Boolean.valueOf(useCompression));
if (useTE.booleanValue()) {
@@ -149,4 +150,17 @@ public void testGzipBufferSizeConfiguration() {
public void testInvalidGzipBufferSize() {
new CompressionConfig().setGzipBufferSize(0);
}
+
+ @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");
+ Assert.assertTrue(encodings.contains("br"));
+ Assert.assertFalse(encodings.contains("gzip"));
+ }
}
diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java
index 9cf27034a3a8..2d4b4154731a 100644
--- a/test/org/apache/coyote/http11/TestHttp11Processor.java
+++ b/test/org/apache/coyote/http11/TestHttp11Processor.java
@@ -31,10 +31,7 @@
import java.net.SocketAddress;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.zip.Deflater;
@@ -48,6 +45,8 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import org.apache.coyote.http11.filters.GzipOutputFilterFactory;
+import org.apache.coyote.http11.filters.OutputFilterFactory;
import org.junit.Assert;
import org.junit.Test;
@@ -2187,6 +2186,43 @@ public void testInvalidGzipBufferSize() {
new Http11NioProtocol().setGzipBufferSize(0);
}
+ @Test
+ public void testOutputFilterFactory() {
+ Http11NioProtocol protocol = new Http11NioProtocol();
+
+ // Test default factory is GzipOutputFilterFactory
+ Assert.assertNotNull(protocol.getOutputFilterFactory());
+ Assert.assertTrue(protocol.getOutputFilterFactory() instanceof GzipOutputFilterFactory);
+
+ protocol.setOutputFilterFactory(new MockOutputFilterFactory());
+ Assert.assertNotNull(protocol.getOutputFilterFactory());
+ Assert.assertTrue(protocol.getOutputFilterFactory() instanceof MockOutputFilterFactory);
+ }
+
+ @Test
+ public void testOutputFilterFactoryClassName() {
+ Http11NioProtocol protocol = new Http11NioProtocol();
+
+ protocol.setOutputFilterFactory(MockOutputFilterFactory.class.getName());
+ Assert.assertNotNull(protocol.getOutputFilterFactory());
+ Assert.assertTrue(protocol.getOutputFilterFactory() instanceof MockOutputFilterFactory);
+ }
+
+ @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"));
+ }
+
private static class EarlyHintsServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@@ -2222,4 +2258,15 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
resp.getWriter().write("OK");
}
}
+
+ public static class MockOutputFilterFactory implements OutputFilterFactory {
+
+ public OutputFilter createFilter(AbstractHttp11Protocol> protocol) {
+ return null;
+ }
+
+ public String getEncodingName() {
+ return "";
+ }
+ }
}
From 468fa4db1bfcff2ec9f5d3434f59469a1d1a80cd Mon Sep 17 00:00:00 2001
From: Long9725
Date: Sat, 1 Nov 2025 11:17:33 +0900
Subject: [PATCH 3/4] Maintain CompressionConfig useCompression method
signature
---
java/org/apache/coyote/CompressionConfig.java | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java
index 1751041e157d..8a6beec70820 100644
--- a/java/org/apache/coyote/CompressionConfig.java
+++ b/java/org/apache/coyote/CompressionConfig.java
@@ -229,6 +229,19 @@ public void setCompressionMinSize(int compressionMinSize) {
this.compressionMinSize = compressionMinSize;
}
+ /**
+ * Determines if gzip compression should be enabled for the given response and if it is, sets any necessary headers to
+ * mark it as such.
+ *
+ * @param request The request that triggered the response
+ * @param response The response to consider compressing
+ *
+ * @return {@code true} if compression was enabled for the given response, otherwise {@code false}
+ */
+ public boolean useCompression(Request request, Response response) {
+ return this.useCompression(request, response, "gzip");
+ }
+
/**
* Determines if compression should be enabled for the given response and if it is, sets any necessary headers to
* mark it as such.
From 60ef2f594917dbcc84a1eee514a49655c32a86c2 Mon Sep 17 00:00:00 2001
From: Long9725
Date: Sat, 1 Nov 2025 11:29:16 +0900
Subject: [PATCH 4/4] Add noCompressionEncodings http config docs
---
webapps/docs/config/http.xml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 970f72d21d7c..2039a1832f9d 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -468,6 +468,20 @@
defaults to 512 bytes.
+
+ A comma-separated list of content encodings that indicate
+ already-compressed content. When the response already has a
+ Content-Encoding header with one of these values, compression
+ will not be applied to prevent double compression. This attribute is only
+ used if compression is set to on or
+ force.
+ If not specified, the default values is
+ br,compress,dcb,dcz,deflate,gzip,pack2000-gzip,zstd, which
+ includes all commonly used compression algorithms. This can be customized
+ to support custom compression algorithms when using a custom
+ outputFilterFactory.
+
+
The number of seconds during which the sockets used by this
Connector will linger when they are closed. The default