This is an automated email from the ASF dual-hosted git repository.
ffang pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
new c090a51b066 CXF-7404: Always set Vary: Accept-Encoding on responses of
the GZIP f… (#3615)
c090a51b066 is described below
commit c090a51b0667b3a8de80f9b5c6d790c3624b3f68
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Wed Oct 21 10:36:48 2026 -0400
CXF-7404: Always set Vary: Accept-Encoding on responses of the GZIP f…
(#3615)
* CXF-7404: Always set Vary: Accept-Encoding on responses of the GZIP
feature
GZIPOutInterceptor only added Vary: Accept-Encoding when it actually
compressed a response. A response to a client that did not accept gzip
was sent uncompressed without Vary, although the content coding was
selected based on the Accept-Encoding request header. RFC 9110 (section
12.5.5) recommends sending Vary in that case. Without it, a shared
cache may store the uncompressed response and serve it to clients that
accept gzip.
Vary is now also added to responses that are not compressed because
the client does not accept gzip, or because they are below the
compression threshold. Like Content-Encoding, the header is added when
the body is written, so that it is not lost if the protocol headers are
replaced later (e.g. by the JAX-RS response headers). If gzip is not
permitted, the output stream is wrapped without buffering, so flushed
streaming responses are not delayed. Responses with a content type that
is never compressed (supportedPayloadContentTypes) and client requests
are unchanged.
* CXF-7404: Document Vary header behavior in GZIP interceptor
(cherry picked from commit 42fc903a5b5ebb7013ac5987ce68405b0cd5596f)
---
.../transport/common/gzip/GZIPOutInterceptor.java | 48 +++++++--
.../common/gzip/GZIPAcceptEncodingTest.java | 110 ++++++++++++++++++++-
.../systest/jaxrs/JAXRSClientServerBookTest.java | 38 ++++++-
3 files changed, 184 insertions(+), 12 deletions(-)
diff --git
a/core/src/main/java/org/apache/cxf/transport/common/gzip/GZIPOutInterceptor.java
b/core/src/main/java/org/apache/cxf/transport/common/gzip/GZIPOutInterceptor.java
index 6f6e1eaccbd..669b6289848 100644
---
a/core/src/main/java/org/apache/cxf/transport/common/gzip/GZIPOutInterceptor.java
+++
b/core/src/main/java/org/apache/cxf/transport/common/gzip/GZIPOutInterceptor.java
@@ -39,6 +39,7 @@ import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.MessageSenderInterceptor;
import org.apache.cxf.io.AbstractThresholdOutputStream;
+import org.apache.cxf.io.AbstractWrappedOutputStream;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
@@ -56,7 +57,10 @@ import org.apache.cxf.phase.Phase;
* see {@link GZIPInInterceptor}. This interceptor supports a compression
* {@link #threshold} (default 1kB) - messages smaller than this threshold will
* not be compressed. To force compression of all messages, set the threshold
to
- * 0. This class was originally based on one of the CXF samples
+ * 0. For server responses with a payload content type eligible for
compression,
+ * this interceptor always adds {@code Vary: Accept-Encoding}, even when the
+ * response is not compressed.
+ * This class was originally based on one of the CXF samples
* (configuration_interceptor).
*/
public class GZIPOutInterceptor extends AbstractPhaseInterceptor<Message> {
@@ -152,6 +156,26 @@ public class GZIPOutInterceptor extends
AbstractPhaseInterceptor<Message> {
use == UseGzip.FORCE,
message);
message.setContent(OutputStream.class, cs);
+ } else if (!MessageUtils.isRequestor(message) &&
isSupportedPayloadContentType(message)) {
+ // the response is not compressed, but it would have been if the
client accepted
+ // gzip, so it still needs the Vary header
+ OutputStream os = message.getContent(OutputStream.class);
+ if (os != null) {
+ message.setContent(OutputStream.class, new
AbstractWrappedOutputStream(os) {
+ @Override
+ protected void onFirstWrite() {
+ addHeader(message, "Vary", "Accept-Encoding");
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!written) {
+ onFirstWrite();
+ }
+ super.close();
+ }
+ });
+ }
}
}
@@ -171,8 +195,7 @@ public class GZIPOutInterceptor extends
AbstractPhaseInterceptor<Message> {
*/
public UseGzip gzipPermitted(Message message) {
UseGzip permitted = UseGzip.NO;
- if (supportedPayloadContentTypes != null &&
message.containsKey(Message.CONTENT_TYPE)
- &&
!supportedPayloadContentTypes.contains(message.get(Message.CONTENT_TYPE))) {
+ if (!isSupportedPayloadContentType(message)) {
return permitted;
}
if (MessageUtils.isRequestor(message)) {
@@ -272,6 +295,11 @@ public class GZIPOutInterceptor extends
AbstractPhaseInterceptor<Message> {
return permitted;
}
+ private boolean isSupportedPayloadContentType(Message message) {
+ return supportedPayloadContentTypes == null ||
!message.containsKey(Message.CONTENT_TYPE)
+ ||
supportedPayloadContentTypes.contains(message.get(Message.CONTENT_TYPE));
+ }
+
static class GZipThresholdOutputStream extends
AbstractThresholdOutputStream {
Message message;
@@ -291,8 +319,8 @@ public class GZIPOutInterceptor extends
AbstractPhaseInterceptor<Message> {
@Override
public void thresholdNotReached() {
- //nothing
LOG.fine("Message is smaller than compression threshold, not
compressing.");
+ addVaryHeader();
}
@Override
@@ -301,15 +329,19 @@ public class GZIPOutInterceptor extends
AbstractPhaseInterceptor<Message> {
// Set the Content-Encoding HTTP header
String enc = (String)message.get(GZIP_ENCODING_KEY);
addHeader(message, "Content-Encoding", enc);
- // if this is a response message, add the Vary header
- if (!Boolean.TRUE.equals(message.get(Message.REQUESTOR_ROLE))) {
- addHeader(message, "Vary", "Accept-Encoding");
- }
+ addVaryHeader();
// gzip the result
GZIPOutputStream zipOutput = new GZIPOutputStream(wrappedStream);
wrappedStream = zipOutput;
}
+
+ private void addVaryHeader() {
+ // if this is a response message, add the Vary header, whether it
is compressed or not
+ if (!MessageUtils.isRequestor(message)) {
+ addHeader(message, "Vary", "Accept-Encoding");
+ }
+ }
}
/**
diff --git
a/core/src/test/java/org/apache/cxf/transport/common/gzip/GZIPAcceptEncodingTest.java
b/core/src/test/java/org/apache/cxf/transport/common/gzip/GZIPAcceptEncodingTest.java
index a529639c7c7..e96bd142913 100644
---
a/core/src/test/java/org/apache/cxf/transport/common/gzip/GZIPAcceptEncodingTest.java
+++
b/core/src/test/java/org/apache/cxf/transport/common/gzip/GZIPAcceptEncodingTest.java
@@ -22,11 +22,13 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.InterceptorChain;
@@ -41,6 +43,8 @@ import org.junit.Test;
import static
org.apache.cxf.transport.common.gzip.GZIPOutInterceptor.UseGzip.FORCE;
import static
org.apache.cxf.transport.common.gzip.GZIPOutInterceptor.UseGzip.YES;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
@@ -115,6 +119,106 @@ public class GZIPAcceptEncodingTest {
interceptor.handleMessage(outMessage);
}
+ @Test
+ public void testVaryWhenNotCompressed() throws Exception {
+ // the response is not compressed, but would have been for a client
accepting gzip
+ setAcceptEncoding("identity");
+ writeResponse(2048);
+ assertNull(getOutHeaders().get("Content-Encoding"));
+ assertVary("Accept-Encoding");
+ }
+
+ @Test
+ public void testVaryWithoutAcceptEncoding() throws Exception {
+ setRequestHeaders(new TreeMap<>(String.CASE_INSENSITIVE_ORDER));
+ writeResponse(2048);
+ assertVary("Accept-Encoding");
+ }
+
+ @Test
+ public void testVaryWithoutBody() throws Exception {
+ setAcceptEncoding("identity");
+ interceptor.handleMessage(outMessage);
+ outMessage.getContent(OutputStream.class).close();
+ assertVary("Accept-Encoding");
+ assertNull(getOutHeaders().get("Content-Encoding"));
+ }
+
+ @Test
+ public void testVaryWithoutBodyOrAcceptEncoding() throws Exception {
+ setRequestHeaders(new TreeMap<>(String.CASE_INSENSITIVE_ORDER));
+ interceptor.handleMessage(outMessage);
+ // JAX-RS can replace the response headers after this interceptor runs.
+ Map<String, List<String>> headers = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ headers.put("Vary", Collections.singletonList("Accept"));
+ outMessage.put(Message.PROTOCOL_HEADERS, headers);
+ outMessage.getContent(OutputStream.class).close();
+ assertVary("Accept", "Accept-Encoding");
+ assertNull(getOutHeaders().get("Content-Encoding"));
+ }
+
+ @Test
+ public void testVaryWhenCompressed() throws Exception {
+ setAcceptEncoding("gzip");
+ writeResponse(2048);
+ assertEquals(Collections.singletonList("gzip"),
getOutHeaders().get("Content-Encoding"));
+ assertVary("Accept-Encoding");
+ }
+
+ @Test
+ public void testVaryBelowThreshold() throws Exception {
+ setAcceptEncoding("gzip");
+ writeResponse(16);
+ assertNull(getOutHeaders().get("Content-Encoding"));
+ assertVary("Accept-Encoding");
+ }
+
+ @Test
+ public void testVaryAppended() throws Exception {
+ Map<String, List<String>> outHeaders = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ outHeaders.put("Vary", Collections.singletonList("Accept"));
+ outMessage.put(Message.PROTOCOL_HEADERS, outHeaders);
+ setAcceptEncoding("identity");
+ writeResponse(2048);
+ assertVary("Accept", "Accept-Encoding");
+ }
+
+ @Test
+ public void testNoVaryForUnsupportedContentType() throws Exception {
+ // a response that is never compressed does not depend on
Accept-Encoding
+
interceptor.setSupportedPayloadContentTypes(Collections.singleton("text/xml"));
+ outMessage.put(Message.CONTENT_TYPE, "image/png");
+ setAcceptEncoding("gzip");
+ writeResponse(2048);
+ assertNull(getOutHeaders());
+ }
+
+ @Test
+ public void testNoVaryForRequest() throws Exception {
+ outMessage.put(Message.REQUESTOR_ROLE, Boolean.TRUE);
+ outMessage.put(GZIPOutInterceptor.USE_GZIP_KEY, YES);
+ writeResponse(2048);
+ assertEquals(Collections.singletonList("gzip"),
getOutHeaders().get("Content-Encoding"));
+ assertNull(getOutHeaders().get("Vary"));
+ }
+
+ private void writeResponse(int size) throws Exception {
+ interceptor.handleMessage(outMessage);
+ try (OutputStream os = outMessage.getContent(OutputStream.class)) {
+ os.write(new byte[size]);
+ }
+ }
+
+ private void assertVary(String... expected) {
+ Map<String, List<String>> headers = getOutHeaders();
+ assertNotNull("Response headers should include Vary", headers);
+ assertEquals(Arrays.asList(expected), headers.get("Vary"));
+ }
+
+ private Map<String, List<String>> getOutHeaders() {
+ return CastUtils.cast((Map<?,
?>)outMessage.get(Message.PROTOCOL_HEADERS));
+ }
+
private void singleTest(String encoding,
GZIPOutInterceptor.UseGzip expectedUseGzip, String
expectedGzipEncoding)
throws Exception {
@@ -132,6 +236,10 @@ public class GZIPAcceptEncodingTest {
= new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
protocolHeaders.put(HttpHeaderHelper.getHeaderKey(HttpHeaderHelper.ACCEPT_ENCODING),
Collections
.singletonList(enc));
+ setRequestHeaders(protocolHeaders);
+ }
+
+ private void setRequestHeaders(Map<String, List<String>> protocolHeaders) {
inMessage.put(Message.PROTOCOL_HEADERS, protocolHeaders);
}
-}
\ No newline at end of file
+}
diff --git
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerBookTest.java
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerBookTest.java
index b6b1949cb67..604a1dcc074 100644
---
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerBookTest.java
+++
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/JAXRSClientServerBookTest.java
@@ -213,14 +213,46 @@ public class JAXRSClientServerBookTest extends
AbstractBusClientServerTestBase {
WebClient wc = WebClient.create(address);
wc.acceptEncoding("gzip,deflate");
wc.encoding("gzip");
- InputStream r = wc.get(InputStream.class);
- assertNotNull(r);
- GZIPInputStream in = new GZIPInputStream(r);
+ Response r = wc.get();
+ assertEquals("gzip", r.getHeaderString("Content-Encoding"));
+ assertEquals("Accept-Encoding", r.getHeaderString("Vary"));
+ GZIPInputStream in = new
GZIPInputStream(r.readEntity(InputStream.class));
String s = IOUtils.toString(in);
in.close();
assertTrue(s, s.contains("id>124"));
}
+ @Test
+ public void testGetBookQueryNoGZIP() throws Exception {
+ String address = "http://localhost:" + PORT + "/bookstore/";
+ WebClient wc = WebClient.create(address);
+ wc.acceptEncoding("identity");
+ Response r = wc.get();
+ assertNull(r.getHeaderString("Content-Encoding"));
+ // the response would have been compressed if the client had accepted
gzip
+ assertEquals("Accept-Encoding", r.getHeaderString("Vary"));
+ String s = r.readEntity(String.class);
+ assertTrue(s, s.contains("id>124"));
+ }
+
+ @Test
+ public void testGetBookNotModifiedVary() throws Exception {
+ String address = "http://localhost:" + PORT +
"/bookstore/books/response3/123";
+ for (String encoding : new String[] {"identity", null, "gzip"}) {
+ WebClient wc = WebClient.create(address).accept("application/xml");
+ if (encoding != null) {
+ wc.acceptEncoding(encoding);
+ }
+ wc.header("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT");
+ try (Response response = wc.get()) {
+ assertEquals(304, response.getStatus());
+ assertEquals("Accept-Encoding",
response.getHeaderString("Vary"));
+ assertNull(response.getHeaderString("Content-Encoding"));
+ assertFalse(response.hasEntity());
+ }
+ }
+ }
+
@Test
public void testGetBookQueryDefault() throws Exception {
String address = "http://localhost:" + PORT +
"/bookstore/books/query/default";