You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by ff...@apache.org on 2026/10/21 14:44:59 UTC

(cxf) branch main updated: CXF-7404: Always set Vary: Accept-Encoding on responses of the GZIP f… (#3615)

This is an automated email from the ASF dual-hosted git repository.

ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new 42fc903a5b5 CXF-7404: Always set Vary: Accept-Encoding on responses of the GZIP f… (#3615)
42fc903a5b5 is described below

commit 42fc903a5b5ebb7013ac5987ce68405b0cd5596f
Author: Freeman(Yue) Fang <fr...@gmail.com>
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
---
 .../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 8fb2e885b62..4d6cb3eaad0 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
@@ -212,14 +212,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";