diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java
index c48dee1b1330..8a6beec70820 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,11 @@ public class CompressionConfig {
"text/javascript,application/javascript,application/json,application/xml";
private String[] compressibleMimeTypes = null;
private int compressionMinSize = 2048;
-
+ private Set 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;
@@ -339,9 +342,49 @@ 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 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/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/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/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/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/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/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/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..959af855fd90 100644
--- a/test/org/apache/coyote/TestCompressionConfig.java
+++ b/test/org/apache/coyote/TestCompressionConfig.java
@@ -17,8 +17,10 @@
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;
import org.junit.Assert;
import org.junit.Test;
@@ -91,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()) {
@@ -110,4 +112,55 @@ 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);
+ }
+
+ @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 cc00ac4d848e..2d4b4154731a 100644
--- a/test/org/apache/coyote/http11/TestHttp11Processor.java
+++ b/test/org/apache/coyote/http11/TestHttp11Processor.java
@@ -31,11 +31,9 @@
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;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
@@ -47,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;
@@ -2148,7 +2148,80 @@ 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);
+ }
+
+ @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 {
@@ -2185,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 "";
+ }
+ }
}
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..2039a1832f9d 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -450,6 +450,38 @@
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.
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