This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 f94a58d05e10 CAMEL-25769: camel-core - the REST binding answers an
object as JSON when the request had a Content-Type the binding does not write
(#28101)
f94a58d05e10 is described below
commit f94a58d05e1066c4427c34e258fca4f995867744
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 21:24:28 2026 +0200
CAMEL-25769: camel-core - the REST binding answers an object as JSON when
the request had a Content-Type the binding does not write (#28101)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
...tformHttpRestBindingRequestContentTypeTest.java | 102 +++++++++++++++++++++
.../camel/support/processor/RestBindingAdvice.java | 36 ++++++++
2 files changed, 138 insertions(+)
diff --git
a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingRequestContentTypeTest.java
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingRequestContentTypeTest.java
new file mode 100644
index 000000000000..9a1c23ebc0fe
--- /dev/null
+++
b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpRestBindingRequestContentTypeTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.rest.RestBindingMode;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * CAMEL-25769: the Content-Type of a request describes its body, not the
answer. Under json binding, a GET sent with
+ * Content-Type text/plain gets its object answer as JSON, as a GET without
one does.
+ */
+public class VertxPlatformHttpRestBindingRequestContentTypeTest {
+
+ private CamelContext context;
+
+ @BeforeEach
+ public void startCamel() throws Exception {
+ context = VertxPlatformHttpEngineTest.createCamelContext();
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().bindingMode(RestBindingMode.json);
+ rest("/orders")
+ .get("/map").to("direct:map")
+ .get("/text").to("direct:text")
+ .get("/csv").to("direct:csv");
+ from("direct:map").setBody(constant(Map.of("orderId",
"ORD-1002", "customer", "C-207")));
+ from("direct:text").setBody(constant("plain answer"));
+ from("direct:csv").setHeader(Exchange.CONTENT_TYPE,
constant("text/csv")).setBody(constant("ORD-1002;C-207"));
+ }
+ });
+ VertxPlatformHttpEngineTest.startCamelContext(context);
+ }
+
+ @AfterEach
+ public void stopCamel() {
+ context.stop();
+ }
+
+ @Test
+ public void testAnObjectAnswerIsJsonWhateverTheContentTypeOfTheRequest() {
+ // was a 500: the Map could not be written as text/plain
+ given().contentType("text/plain;charset=UTF-8")
+ .when().get("/orders/map")
+ .then().statusCode(200)
+ .header("Content-Type", containsString("application/json"))
+ .body("orderId", equalTo("ORD-1002"));
+ given().contentType("application/json")
+ .when().get("/orders/map")
+ .then().statusCode(200)
+ .header("Content-Type", containsString("application/json"))
+ .body("orderId", equalTo("ORD-1002"));
+ given()
+ .when().get("/orders/map")
+ .then().statusCode(200)
+ .header("Content-Type", containsString("application/json"))
+ .body("orderId", equalTo("ORD-1002"));
+ }
+
+ @Test
+ public void testATextAnswerToATextRequestStaysText() {
+ given().contentType("text/plain;charset=UTF-8")
+ .when().get("/orders/text")
+ .then().statusCode(200)
+ .header("Content-Type", containsString("text/plain"))
+ .body(equalTo("plain answer"));
+ }
+
+ @Test
+ public void testTheContentTypeTheRouteSetsWins() {
+ given().contentType("text/plain;charset=UTF-8")
+ .when().get("/orders/csv")
+ .then().statusCode(200)
+ .header("Content-Type", containsString("text/csv"))
+ .body(equalTo("ORD-1002;C-207"));
+ }
+}
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 7371f6832412..3edb96b6fc33 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
@@ -16,6 +16,9 @@
*/
package org.apache.camel.support.processor;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
@@ -27,6 +30,7 @@ import org.apache.camel.CamelContext;
import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.StreamCache;
import org.apache.camel.spi.CamelInternalProcessorAdvice;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataType;
@@ -58,6 +62,7 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
private static final String STATE_KEY_DO_MARSHAL = "doMarshal";
private static final String STATE_KEY_ACCEPT = "accept";
+ private static final String STATE_KEY_REQUEST_CONTENT_TYPE =
"requestContentType";
private static final String STATE_JSON = "json";
private static final String STATE_XML = "xml";
@@ -196,6 +201,8 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
boolean isJson = false;
String contentType = ExchangeHelper.getContentType(exchange);
+ // the answer is bound with the Content-Type of the binding when the
route leaves this one (CAMEL-25769)
+ state.put(STATE_KEY_REQUEST_CONTENT_TYPE, contentType);
if (contentType != null) {
isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
@@ -415,6 +422,9 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
return;
}
+ // the Content-Type of the request describes its body, not the answer
(a GET has no body)
+ dropRequestContentType(exchange, state, isXml, isJson);
+
// ensure there is a content type header (even if binding is off)
ensureHeaderContentType(produces, isXml, isJson, exchange);
@@ -514,6 +524,32 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
}
}
+ /**
+ * Removes the Content-Type of the request when the route did not set
another, the binding does not write that type,
+ * and the answer is an object (a Map, a List, a POJO): the binding then
sets the Content-Type of its mode and
+ * marshals the answer, as for a request without a Content-Type. Before, a
GET sent with Content-Type text/plain got
+ * a 500, as the object could not be written as text/plain (CAMEL-25769).
A request type the binding writes (json
+ * for a json binding) is kept as before, and so is a body that is already
text, bytes or a stream, which the route
+ * may answer as such.
+ */
+ private static void dropRequestContentType(
+ Exchange exchange, Map<String, Object> state, boolean isXml,
boolean isJson) {
+ Object requestContentType = state.get(STATE_KEY_REQUEST_CONTENT_TYPE);
+ if (requestContentType == null ||
!requestContentType.equals(ExchangeHelper.getContentType(exchange))) {
+ return;
+ }
+ String type =
requestContentType.toString().toLowerCase(Locale.ENGLISH);
+ if (isJson && type.contains("json") || isXml && type.contains("xml")) {
+ return;
+ }
+ Object body = exchange.getMessage().getBody();
+ if (body instanceof CharSequence || body instanceof byte[] || body
instanceof InputStream
+ || body instanceof Reader || body instanceof StreamCache ||
body instanceof ByteBuffer) {
+ return;
+ }
+ exchange.getMessage().removeHeader(Exchange.CONTENT_TYPE);
+ }
+
private void ensureHeaderContentType(String contentType, boolean isXml,
boolean isJson, Exchange exchange) {
String type = ExchangeHelper.getContentType(exchange);
if (type != null) {