This is an automated email from the ASF dual-hosted git repository.
fmariani pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 04e15705c75b CAMEL-23249: preserve Response headers during stream
cache conversion
04e15705c75b is described below
commit 04e15705c75bc27144f640b295acf4d1d88ff925
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 6d9343282ad0..9be2aa4e556f 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 ef462b3162db..b62e53dc854a 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..50f19f16129f
--- /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.junit6.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;
+ });
+ }
+ }
+}