This is an automated email from the ASF dual-hosted git repository.
remm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 2eb8e19552 Update with transfer-encoding support
2eb8e19552 is described below
commit 2eb8e19552ec78cc922cba2f1cbd05940e8b6951
Author: remm <[email protected]>
AuthorDate: Thu Feb 20 18:57:39 2025 +0100
Update with transfer-encoding support
Although there is no client browser support, and there may never be any,
Transfer-Encoding is the right way to do compression the way Tomcat does
it in GzipOutputFilter.
This will be used if the client sends a TE: gzip header.
---
java/org/apache/coyote/CompressionConfig.java | 75 ++++++++++++++------
java/org/apache/tomcat/util/http/parser/TE.java | 84 +++++++++++++++++++++++
test/org/apache/coyote/TestCompressionConfig.java | 59 ++++++++++++----
webapps/docs/changelog.xml | 5 ++
4 files changed, 189 insertions(+), 34 deletions(-)
diff --git a/java/org/apache/coyote/CompressionConfig.java
b/java/org/apache/coyote/CompressionConfig.java
index d4b27f3a5d..3545db1a52 100644
--- a/java/org/apache/coyote/CompressionConfig.java
+++ b/java/org/apache/coyote/CompressionConfig.java
@@ -32,6 +32,7 @@ import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.ResponseUtil;
import org.apache.tomcat.util.http.parser.AcceptEncoding;
+import org.apache.tomcat.util.http.parser.TE;
import org.apache.tomcat.util.http.parser.TokenList;
import org.apache.tomcat.util.res.StringManager;
@@ -192,6 +193,8 @@ public class CompressionConfig {
return false;
}
+ boolean useTE = false;
+
MimeHeaders responseHeaders = response.getMimeHeaders();
// Check if content is not already compressed
@@ -230,40 +233,63 @@ public class CompressionConfig {
}
}
- // Check if the resource has a strong ETag
- String eTag = responseHeaders.getHeader("ETag");
- if (eTag != null && !eTag.trim().startsWith("W/")) {
- // Has an ETag that doesn't start with "W/..." so it must be a
- // strong ETag
- 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");
-
- // Check if user-agent supports gzip encoding
- // Only interested in whether gzip encoding is supported. Other
- // encodings and weights can be ignored.
- Enumeration<String> headerValues =
request.getMimeHeaders().values("accept-encoding");
+ Enumeration<String> headerValues =
request.getMimeHeaders().values("TE");
boolean foundGzip = false;
+ // TE and accept-encoding seem to have equivalent syntax
while (!foundGzip && headerValues.hasMoreElements()) {
- List<AcceptEncoding> acceptEncodings = null;
+ List<TE> tes = null;
try {
- acceptEncodings = AcceptEncoding.parse(new
StringReader(headerValues.nextElement()));
+ tes = TE.parse(new StringReader(headerValues.nextElement()));
} catch (IOException ioe) {
// If there is a problem reading the header, disable
compression
return false;
}
- for (AcceptEncoding acceptEncoding : acceptEncodings) {
- if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) {
+ for (TE te : tes) {
+ if ("gzip".equalsIgnoreCase(te.getEncoding())) {
+ useTE = true;
foundGzip = true;
break;
}
}
}
+ // Check if the resource has a strong ETag
+ String eTag = responseHeaders.getHeader("ETag");
+ if (!useTE && eTag != null && !eTag.trim().startsWith("W/")) {
+ // Has an ETag that doesn't start with "W/..." so it must be a
+ // strong ETag
+ return false;
+ }
+
+ if (!useTE) {
+ // If processing reaches this far, the response might be
compressed.
+ // 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
+ // encodings and weights can be ignored.
+ headerValues = request.getMimeHeaders().values("accept-encoding");
+ foundGzip = false;
+ while (!foundGzip && headerValues.hasMoreElements()) {
+ List<AcceptEncoding> acceptEncodings = null;
+ try {
+ acceptEncodings = AcceptEncoding.parse(new
StringReader(headerValues.nextElement()));
+ } catch (IOException ioe) {
+ // If there is a problem reading the header, disable
compression
+ return false;
+ }
+
+ for (AcceptEncoding acceptEncoding : acceptEncodings) {
+ if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding()))
{
+ foundGzip = true;
+ break;
+ }
+ }
+ }
+ }
+
if (!foundGzip) {
return false;
}
@@ -287,8 +313,13 @@ public class CompressionConfig {
// Compressed content length is unknown so mark it as such.
response.setContentLength(-1);
- // Configure the content encoding for compressed content
- responseHeaders.setValue("Content-Encoding").setString("gzip");
+ if (useTE) {
+ // Configure the transfer encoding for compressed content
+ responseHeaders.addValue("Transfer-Encoding").setString("gzip");
+ } else {
+ // Configure the content encoding for compressed content
+ responseHeaders.setValue("Content-Encoding").setString("gzip");
+ }
return true;
}
diff --git a/java/org/apache/tomcat/util/http/parser/TE.java
b/java/org/apache/tomcat/util/http/parser/TE.java
new file mode 100644
index 0000000000..b2792448fa
--- /dev/null
+++ b/java/org/apache/tomcat/util/http/parser/TE.java
@@ -0,0 +1,84 @@
+/*
+ * 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.tomcat.util.http.parser;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TE {
+
+ private final String encoding;
+ private final Map<String,String> parameters;
+
+ protected TE(String encoding, Map<String,String> parameters) {
+ this.encoding = encoding;
+ this.parameters = parameters;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public Map<String,String> getParameters() {
+ return parameters;
+ }
+
+
+ public static List<TE> parse(StringReader input) throws IOException {
+
+ List<TE> result = new ArrayList<>();
+
+ do {
+ String encoding = HttpParser.readToken(input);
+ if (encoding == null) {
+ // Invalid encoding, skip to the next one
+ HttpParser.skipUntil(input, 0, ',');
+ continue;
+ }
+
+ if (encoding.length() == 0) {
+ // No more data to read
+ break;
+ }
+
+ Map<String,String> parameters = null;
+
+ // See if a quality has been provided
+ while (HttpParser.skipConstant(input, ";") == SkipResult.FOUND) {
+ String name = HttpParser.readToken(input);
+ String value = null;
+ if (HttpParser.skipConstant(input, "=") == SkipResult.FOUND) {
+ value = HttpParser.readTokenOrQuotedString(input, true);
+ }
+ if (name != null && value != null) {
+ if (parameters == null) {
+ parameters = new HashMap<>();
+ }
+ parameters.put(name, value);
+ }
+ }
+
+ result.add(new TE(encoding, parameters));
+ } while (true);
+
+ return result;
+ }
+}
diff --git a/test/org/apache/coyote/TestCompressionConfig.java
b/test/org/apache/coyote/TestCompressionConfig.java
index f5dc0379e0..ae16c02f8f 100644
--- a/test/org/apache/coyote/TestCompressionConfig.java
+++ b/test/org/apache/coyote/TestCompressionConfig.java
@@ -29,20 +29,33 @@ import org.junit.runners.Parameterized.Parameter;
@RunWith(Parameterized.class)
public class TestCompressionConfig {
- @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag
[{1}], compress[{2}]")
+ @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag
[{1}], compress[{2}], useTE[{3}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
- parameterSets.add(new Object[] { new String[] { }, null,
Boolean.FALSE });
- parameterSets.add(new Object[] { new String[] { "gzip" }, null,
Boolean.TRUE });
- parameterSets.add(new Object[] { new String[] { "xgzip" }, null,
Boolean.FALSE });
- parameterSets.add(new Object[] { new String[] { "<>gzip" }, null,
Boolean.FALSE });
- parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null,
Boolean.TRUE });
- parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null,
Boolean.TRUE });
+ 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 });
- parameterSets.add(new Object[] { new String[] { "gzip" }, "W/",
Boolean.TRUE });
- parameterSets.add(new Object[] { new String[] { "gzip" }, "XX",
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[] { "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 });
return parameterSets;
}
@@ -53,6 +66,8 @@ public class TestCompressionConfig {
public String eTag;
@Parameter(2)
public Boolean compress;
+ @Parameter(3)
+ public Boolean useTE;
@Test
public void testUseCompression() throws Exception {
@@ -65,14 +80,34 @@ public class TestCompressionConfig {
Response response = new Response();
for (String header : headers) {
-
request.getMimeHeaders().addValue("accept-encoding").setString(header);
+ if (useTE.booleanValue()) {
+ request.getMimeHeaders().addValue("TE").setString(header);
+ } else {
+
request.getMimeHeaders().addValue("accept-encoding").setString(header);
+ }
}
if (eTag != null) {
response.getMimeHeaders().addValue("ETag").setString(eTag);
}
+ boolean useCompression = compressionConfig.useCompression(request,
response);
+ Assert.assertEquals(compress, Boolean.valueOf(useCompression));
- Assert.assertEquals(compress,
Boolean.valueOf(compressionConfig.useCompression(request, response)));
+ if (useTE.booleanValue()) {
+
Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding"));
+ if (useCompression) {
+ Assert.assertEquals("gzip",
response.getMimeHeaders().getHeader("Transfer-Encoding"));
+ } else {
+
Assert.assertNull(response.getMimeHeaders().getHeader("Transfer-Encoding"));
+ }
+ } else {
+
Assert.assertNull(response.getMimeHeaders().getHeader("Transfer-Encoding"));
+ if (useCompression) {
+ Assert.assertEquals("gzip",
response.getMimeHeaders().getHeader("Content-Encoding"));
+ } else {
+
Assert.assertNull(response.getMimeHeaders().getHeader("Content-Encoding"));
+ }
+ }
}
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index be1b259ff8..e24c2e8cc3 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -179,6 +179,11 @@
compressed using <code>compress</code>, <code>deflate</code> or
<code>zstd</code>. (remm)
</fix>
+ <update>
+ Use <code>Transfer-Encoding</code> for compression rather than
+ <code>Content-Encoding</code> if the client submits a <code>TE</code>
+ header containing <code>gzip</code>. (remm)
+ </update>
</changelog>
</subsection>
<subsection name="Jasper">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]