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