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 <[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
---
 .../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";

Reply via email to