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

fmariani pushed a commit to branch camel-4.14.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.14.x by this push:
     new ce34abd753a0 CAMEL-23249: preserve Response headers during stream 
cache conversion
ce34abd753a0 is described below

commit ce34abd753a0f750fc30e5b0e100c1c247560bb7
Author: Croway <[email protected]>
AuthorDate: Thu Apr 9 17:36:35 2026 +0200

    CAMEL-23249: preserve Response headers during stream cache conversion
    
    When stream caching converts a JAX-RS Response to StreamCache, the
    Content-Type and other response metadata headers were lost because only
    the HTTP status code was saved before the conversion.
    
    This fix preserves the Content-Type via Response.getMediaType() and
    other headers via the PROTOCOL_HEADERS map. It also removes the
    containsKey guard in populateViaResponse that prevented the explicit
    Content-Type from overriding CXF's default */* value.
---
 .../camel/component/cxf/jaxrs/CxfConverter.java    |  29 +++++
 .../component/cxf/jaxrs/DefaultCxfRsBinding.java   |  13 +-
 .../cxf/jaxrs/CxfRsStreamCacheContentTypeTest.java | 136 +++++++++++++++++++++
 3 files changed, 172 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfConverter.java
 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfConverter.java
index 7815650c7748..6029ded03c30 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfConverter.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfConverter.java
@@ -20,7 +20,10 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
 
 import jakarta.ws.rs.core.Response;
 import jakarta.xml.soap.SOAPException;
@@ -139,6 +142,32 @@ public final class CxfConverter {
             exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 
status);
         }
 
+        // Preserve response metadata headers (e.g. Content-Type) before the 
Response object
+        // is consumed by the conversion. Without this, headers set via 
Response.type() or
+        // Response.header() are lost when the body becomes a StreamCache 
(CAMEL-23249).
+        jakarta.ws.rs.core.MediaType mediaType = response.getMediaType();
+        if (mediaType != null) {
+            exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
mediaType.toString());
+        }
+        // Save other response headers (e.g. custom headers) into the 
PROTOCOL_HEADERS map
+        // so they are propagated to the CXF outMessage by populateViaResponse.
+        jakarta.ws.rs.core.MultivaluedMap<String, Object> metadata = 
response.getMetadata();
+        if (metadata != null && !metadata.isEmpty()) {
+            Map<String, Object> protocolHeaders = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+            for (String headerName : metadata.keySet()) {
+                if ("Content-Type".equalsIgnoreCase(headerName)) {
+                    continue;
+                }
+                String headerValue = response.getHeaderString(headerName);
+                if (headerValue != null) {
+                    protocolHeaders.put(headerName, 
Arrays.asList(headerValue));
+                }
+            }
+            if (!protocolHeaders.isEmpty()) {
+                
exchange.getMessage().setHeader(org.apache.cxf.message.Message.PROTOCOL_HEADERS,
 protocolHeaders);
+            }
+        }
+
         // Convert the body (entity) to an InputStream
         InputStream is = toInputStream(response, exchange);
         //If there is no entity body, provide an empty stream
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
index c6955b686c98..12092b5a78c7 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
@@ -106,16 +106,17 @@ public class DefaultCxfRsBinding implements CxfRsBinding, 
HeaderFilterStrategyAw
             cxfExchange.put(org.apache.cxf.message.Message.RESPONSE_CODE,
                     response.getHeader(CxfConstants.HTTP_RESPONSE_CODE, 
Integer.class));
         }
-        if (response.getHeader(CxfConstants.CONTENT_TYPE) != null
-                && 
!cxfExchange.containsKey(org.apache.cxf.message.Message.CONTENT_TYPE)) {
+        if (response.getHeader(CxfConstants.CONTENT_TYPE) != null) {
+            // Set the Content-Type on the CXF exchange unconditionally when 
it comes from the Camel
+            // message, because CXF pre-sets a default Content-Type of */* 
which would otherwise
+            // prevent the explicit value from being applied (CAMEL-23249).
             if (!ObjectHelper.isEmpty(cxfExchange) && 
!ObjectHelper.isEmpty(cxfExchange.getOutMessage())) {
                 
cxfExchange.getOutMessage().putIfAbsent(CxfConstants.PROTOCOL_HEADERS,
                         new TreeMap<>(String.CASE_INSENSITIVE_ORDER));
-            }
-            final Map<String, List<String>> cxfHeaders = CastUtils
-                    .cast((Map<?, ?>) 
cxfExchange.getOutMessage().get(CxfConstants.PROTOCOL_HEADERS));
 
-            if (!cxfHeaders.containsKey(CxfConstants.CONTENT_TYPE)) {
+                final Map<String, List<String>> cxfHeaders = CastUtils
+                        .cast((Map<?, ?>) 
cxfExchange.getOutMessage().get(CxfConstants.PROTOCOL_HEADERS));
+
                 List<String> a = Arrays.asList((String) 
response.getHeader(CxfConstants.CONTENT_TYPE));
                 cxfHeaders.put(CxfConstants.CONTENT_TYPE, a);
                 cxfExchange.getOutMessage().put(CxfConstants.CONTENT_TYPE, 
response.getHeader(CxfConstants.CONTENT_TYPE));
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsStreamCacheContentTypeTest.java
 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsStreamCacheContentTypeTest.java
new file mode 100644
index 000000000000..b723377dfd35
--- /dev/null
+++ 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsStreamCacheContentTypeTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.camel.component.cxf.jaxrs;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.cxf.common.CXFTestSupport;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * CAMEL-23249: CXF RS Rest service loses content-type with stream caching 
enabled.
+ *
+ * When a CXF RS consumer route returns a JAX-RS {@link Response} with an 
explicit content type set via
+ * {@code Response.type(MediaType)}, and stream caching is enabled (the 
default in Camel 4.x), the content-type header
+ * is lost because {@code CxfConverter.toStreamCache(Response, Exchange)} only 
preserves the HTTP status code but not
+ * the response metadata headers.
+ *
+ * @see <a 
href="https://issues.apache.org/jira/browse/CAMEL-23249";>CAMEL-23249</a>
+ * @see <a 
href="https://github.com/driseley/cxf-responsecaching-test";>Reproducer 
project</a>
+ */
+public class CxfRsStreamCacheContentTypeTest extends CamelTestSupport {
+
+    private static final String PUT_REQUEST = 
"<Customer><name>Mary</name><id>123</id></Customer>";
+    private static final String JSON_RESPONSE = 
"{\"name\":\"Mary\",\"id\":123}";
+    private static final String CONTEXT = "/CxfRsStreamCacheContentTypeTest";
+    private static final int PORT = CXFTestSupport.getPort5();
+    private static final String CXT = PORT + CONTEXT;
+
+    private static final String CXF_RS_ENDPOINT_URI
+            = "cxfrs://http://localhost:"; + CXT
+              + 
"/rest?resourceClasses=org.apache.camel.component.cxf.jaxrs.testbean.CustomerService";
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from(CXF_RS_ENDPOINT_URI)
+                        .process(exchange -> {
+                            // Return a Response with explicit JSON content 
type and a custom header,
+                            // mimicking the reproducer's 
EchoServiceImpl.echoVariable()
+                            Response resp = Response
+                                    .status(Response.Status.OK)
+                                    .type(MediaType.APPLICATION_JSON)
+                                    .header("X-Custom-Header", "custom-value")
+                                    .entity(JSON_RESPONSE)
+                                    .build();
+                            exchange.getMessage().setBody(resp);
+                        })
+                        // The log() EIP triggers stream cache conversion of 
the Response body.
+                        // After this point the body is a StreamCache, not a 
Response.
+                        .log("Response body: ${body}");
+            }
+        };
+    }
+
+    /**
+     * Verifies that the content-type set via {@code 
Response.type(MediaType.APPLICATION_JSON)} is preserved in the HTTP
+     * response when stream caching converts the {@link Response} to a {@code 
StreamCache}.
+     *
+     * This test reproduces the exact scenario from CAMEL-23249: a CXF RS 
consumer route returns a JAX-RS Response with
+     * an explicit content type, but the client receives 
"application/octet-stream" instead.
+     */
+    @Test
+    public void testContentTypePreservedAfterStreamCacheConversion() throws 
Exception {
+        HttpPut put = new HttpPut("http://localhost:"; + CXT + 
"/rest/customerservice/customers");
+        StringEntity entity = new StringEntity(PUT_REQUEST, 
ContentType.parse("text/xml; charset=ISO-8859-1"));
+        put.setEntity(entity);
+
+        try (CloseableHttpClient httpclient = 
HttpClientBuilder.create().build()) {
+            httpclient.execute(put, response -> {
+                assertEquals(200, response.getCode(), "HTTP status should be 
200");
+
+                assertNotNull(response.getFirstHeader("Content-Type"),
+                        "Content-Type header should be present in response");
+                String contentType = 
response.getFirstHeader("Content-Type").getValue();
+                assertEquals("application/json", contentType,
+                        "Content-Type should be application/json but was: " + 
contentType);
+
+                String body = EntityUtils.toString(response.getEntity());
+                assertEquals(JSON_RESPONSE, body);
+
+                return null;
+            });
+        }
+    }
+
+    /**
+     * Verifies that custom response headers set via {@code 
Response.header(name, value)} are also preserved when stream
+     * caching converts the {@link Response} to a {@code StreamCache}.
+     */
+    @Test
+    public void testCustomHeaderPreservedAfterStreamCacheConversion() throws 
Exception {
+        HttpPut put = new HttpPut("http://localhost:"; + CXT + 
"/rest/customerservice/customers");
+        StringEntity entity = new StringEntity(PUT_REQUEST, 
ContentType.parse("text/xml; charset=ISO-8859-1"));
+        put.setEntity(entity);
+
+        try (CloseableHttpClient httpclient = 
HttpClientBuilder.create().build()) {
+            httpclient.execute(put, response -> {
+                assertEquals(200, response.getCode(), "HTTP status should be 
200");
+
+                assertNotNull(response.getFirstHeader("X-Custom-Header"),
+                        "X-Custom-Header should be present in response");
+                assertEquals("custom-value", 
response.getFirstHeader("X-Custom-Header").getValue(),
+                        "Custom header value should be preserved after stream 
cache conversion");
+
+                return null;
+            });
+        }
+    }
+}

Reply via email to