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

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


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 724d1dabe6a2 CAMEL-25085: camel-support - RestBindingAdvice takes the 
response Content-Type from produces (#26986)
724d1dabe6a2 is described below

commit 724d1dabe6a297fe877058605935260e9d0ffdf8
Author: henrik242 <[email protected]>
AuthorDate: Mon Sep 28 15:45:53 2026 +0200

    CAMEL-25085: camel-support - RestBindingAdvice takes the response 
Content-Type from produces (#26986)
    
    Under json or xml binding, a response without a Content-Type header again
    takes it from the produces of the rest verb, as before Camel 4.18.4 and
    4.22.0 (CAMEL-24001). Those releases used application/json (or xml)
    whenever the binding mode allowed it and marshalled the body, so a verb
    producing text/plain answered with a json-quoted body and a binary body
    failed. When produces lists several media types, the first json (or xml)
    type is used, otherwise the first one. Wildcards such as */* are skipped.
    
    (cherry picked from commit 4767770cff69f14481abc43441ab98545d8ac590)
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
 .../VertxPlatformHttpRestBindingProducesTest.java  |  70 +++++++++++++
 .../FromRestBindingProducesContentTypeTest.java    | 111 +++++++++++++++++++++
 .../camel/support/processor/RestBindingAdvice.java |  28 +++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |   8 ++
 4 files changed, 213 insertions(+), 4 deletions(-)

diff --git 
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingProducesTest.java
 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingProducesTest.java
new file mode 100644
index 000000000000..395a0c637268
--- /dev/null
+++ 
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingProducesTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.platform.http.vertx;
+
+import java.io.ByteArrayInputStream;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.rest.RestBindingMode;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Under json binding, a binary response is written as declared by produces 
instead of being json marshalled.
+ */
+public class VertxPlatformHttpRestBindingProducesTest {
+
+    private static final byte[] PAYLOAD = { 'P', 'K', 3, 4, 0, 1, 2 };
+
+    @Test
+    public void testBinaryBodyIsNotJsonMarshalled() throws Exception {
+        CamelContext context = 
VertxPlatformHttpEngineTest.createCamelContext();
+        try {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    restConfiguration().bindingMode(RestBindingMode.json);
+
+                    rest("/files")
+                            
.get("/bytes").produces("application/octet-stream").to("direct:bytes")
+                            
.get("/stream").produces("application/octet-stream").to("direct:stream");
+
+                    from("direct:bytes").setBody(constant(PAYLOAD));
+                    from("direct:stream").process(e -> 
e.getMessage().setBody(new ByteArrayInputStream(PAYLOAD)));
+                }
+            });
+            context.start();
+
+            for (String path : new String[] { "/files/bytes", "/files/stream" 
}) {
+                byte[] body = given()
+                        .when()
+                        .get(path)
+                        .then()
+                        .statusCode(200)
+                        .header("Content-Type", 
equalTo("application/octet-stream"))
+                        .extract().asByteArray();
+                assertArrayEquals(PAYLOAD, body, path);
+            }
+        } finally {
+            context.stop();
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestBindingProducesContentTypeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestBindingProducesContentTypeTest.java
new file mode 100644
index 000000000000..bd5e2e97768d
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestBindingProducesContentTypeTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.rest;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.rest.RestBindingMode;
+import org.apache.camel.spi.DataFormat;
+import org.apache.camel.spi.DataFormatFactory;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FromRestBindingProducesContentTypeTest extends ContextTestSupport 
{
+
+    @Override
+    protected Registry createCamelRegistry() throws Exception {
+        Registry jndi = super.createCamelRegistry();
+        jndi.bind("dummy-rest", new DummyRestConsumerFactory());
+        // stands in for camel-jackson, marking what it marshals
+        jndi.bind("jackson", (DataFormatFactory) MarkingDataFormat::new);
+        return jndi;
+    }
+
+    @Test
+    public void testBinaryProduces() {
+        assertResponse("seda:get-binary", null, "application/octet-stream", 
"data");
+    }
+
+    @Test
+    public void testPlainProducesWithJsonAccept() {
+        assertResponse("seda:get-plain", "application/json", "text/plain", 
"data");
+    }
+
+    @Test
+    public void testWildcardProducesFallsBackToJson() {
+        assertResponse("seda:get-wildcard", null, "application/json", 
"json:data");
+    }
+
+    @Test
+    public void testMultiValueProducesPicksJson() {
+        assertResponse("seda:get-multi", null, "application/json", 
"json:data");
+    }
+
+    @Test
+    public void testNoProducesFallsBackToJson() {
+        assertResponse("seda:get-none", null, "application/json", "json:data");
+    }
+
+    private void assertResponse(String uri, String accept, String contentType, 
String body) {
+        Exchange out = template.request(uri, exchange -> 
exchange.getIn().setHeader("Accept", accept));
+
+        assertEquals(contentType, 
out.getMessage().getHeader(Exchange.CONTENT_TYPE));
+        assertEquals(body, out.getMessage().getBody(String.class));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
restConfiguration().host("localhost").bindingMode(RestBindingMode.json);
+
+                
rest("/binary").produces("application/octet-stream").get().to("direct:binary");
+                rest("/plain").produces("text/plain").get().to("direct:plain");
+                rest("/wildcard").produces("*/*").get().to("direct:wildcard");
+                
rest("/multi").produces("text/plain,application/json").get().to("direct:multi");
+                rest("/none").get().to("direct:none");
+
+                from("direct:binary").setBody(constant("data"));
+                from("direct:plain").setBody(constant("data"));
+                from("direct:wildcard").setBody(constant("data"));
+                from("direct:multi").setBody(constant("data"));
+                from("direct:none").setBody(constant("data"));
+            }
+        };
+    }
+
+    private static class MarkingDataFormat extends ServiceSupport implements 
DataFormat {
+
+        @Override
+        public void marshal(Exchange exchange, Object graph, OutputStream 
stream) throws Exception {
+            stream.write(("json:" + graph).getBytes());
+        }
+
+        @Override
+        public Object unmarshal(Exchange exchange, InputStream stream) {
+            return stream;
+        }
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
index 62116f707d2f..596f9db9df38 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
@@ -499,16 +499,36 @@ public class RestBindingAdvice extends ServiceSupport 
implements CamelInternalPr
             return;
         }
 
-        // favor json over xml as a concrete single media type
-        if (isJson) {
+        // the verb declares what it produces, so use one of those media 
types, favoring json over xml
+        String produced = contentType != null ? 
selectProducedMediaType(contentType, isXml, isJson) : null;
+        if (produced != null) {
+            exchange.getIn().setHeader(Exchange.CONTENT_TYPE, produced);
+        } else if (isJson) {
             exchange.getIn().setHeader(Exchange.CONTENT_TYPE, 
"application/json");
         } else if (isXml) {
             exchange.getIn().setHeader(Exchange.CONTENT_TYPE, 
"application/xml");
-        } else if (contentType != null) {
-            exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType);
         }
     }
 
+    private static String selectProducedMediaType(String produces, boolean 
isXml, boolean isJson) {
+        String kind = isJson ? "json" : isXml ? "xml" : null;
+        String first = null;
+        for (String type : produces.split(",")) {
+            type = type.trim();
+            // a wildcard cannot be the Content-Type of a response
+            if (type.contains("*")) {
+                continue;
+            }
+            if (kind != null && 
type.toLowerCase(Locale.ENGLISH).contains(kind)) {
+                return type;
+            }
+            if (first == null) {
+                first = type;
+            }
+        }
+        return first;
+    }
+
     private void setCORSHeaders(Exchange exchange) {
         // add the CORS headers after routing, but before the consumer writes 
the response
         Message msg = exchange.getMessage();
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index b3b24f7dca1b..8e8789842e2b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -13,6 +13,14 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading from 4.18.4 to 4.18.5
 
+=== camel-core - Rest DSL response Content-Type
+
+Under json or xml binding, a response without a Content-Type header again 
takes it from the `produces` of the rest
+verb, as before Camel 4.18.4 and 4.22.0. Those releases used 
`application/json` (or `application/xml`) whenever the
+binding mode allowed it and marshalled the body, so a verb producing 
`text/plain` answered with a json-quoted body and
+a binary body failed. When `produces` lists several media types, the first 
json (or xml) type is used, otherwise the
+first one. Wildcards such as `*/*` are skipped, falling back to 
`application/json` or `application/xml`.
+
 === camel-hazelcast - ReplicatedHazelcastAggregationRepository now applies the 
default serialization filter
 
 `ReplicatedHazelcastAggregationRepository` now applies the same default

Reply via email to