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 62c8f6e00fca CAMEL-26140: camel-undertow - send a number as text, fail
on a Map or object body, read a DELETE body, null body for a 204 (#28910)
62c8f6e00fca is described below
commit 62c8f6e00fcac270e7f23acf7206229fcca17798
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:42:18 2026 +0100
CAMEL-26140: camel-undertow - send a number as text, fail on a Map or
object body, read a DELETE body, null body for a 204 (#28910)
… object body, read a DELETE body, null body for a 204
The consumer reply and the producer request converted the body with the type
converter to a ByteBuffer: a number became its 4 or 8 binary bytes, and a
Map,
List or object did not convert, so the consumer answered an empty 200 (with
a
WARN) and the producer sent an empty POST. A number, boolean, character or
enum
is now sent as its text. A body that does not convert makes the consumer
answer
500, and the producer fail a POST, PUT or PATCH, with a message that says
what
the body is and the marshal that writes it (BodyFacts, as camel-http and
camel-platform-http-vertx do). The consumer now reads the body of any method
that has one, such as a DELETE, and the producer gives a null body for a
204,
a 304 or the response to a HEAD, as camel-http does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/docs/undertow-component.adoc | 11 ++
.../src/main/docs/undertow-component.adoc | 11 ++
.../undertow/DefaultUndertowHttpBinding.java | 30 ++-
.../camel/component/undertow/UndertowConsumer.java | 23 ++-
.../camel/component/undertow/UndertowHelper.java | 45 +++++
.../camel/component/undertow/UndertowProducer.java | 30 +--
.../undertow/UndertowBodyConversionTest.java | 218 +++++++++++++++++++++
.../undertow/UndertowSwitchingStatus204Test.java | 6 +-
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 15 ++
9 files changed, 363 insertions(+), 26 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/undertow-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/undertow-component.adoc
index c35012a58843..1f372e399d33 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/undertow-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/undertow-component.adoc
@@ -189,6 +189,17 @@ For example, given a client request with the URL,
`\http://myserver/myserver?orderid=123`, the exchange will contain a
header named `orderid` with the value `123`.
+=== Message body
+
+The consumer reads the body of a request of any method that has one (a
Content-Length above 0 or a chunked body).
+
+The body of a reply (consumer) or a request (producer) is sent as bytes. Text
is written in the charset that the
+`Content-Type` declares (UTF-8 otherwise), and a number, a boolean, a
character or an enum as its text. A Map, a List
+or an object must be marshalled first (for example with `marshal().json()`):
the consumer answers `500` for such a
+reply, and the producer fails the exchange for a POST, PUT or PATCH with such
a body.
+
+The producer gives a null body for a `204` or `304` response and for the
response to a HEAD request.
+
=== Using localhost as host
When you specify `localhost` in a URL, Camel exposes the endpoint only
diff --git a/components/camel-undertow/src/main/docs/undertow-component.adoc
b/components/camel-undertow/src/main/docs/undertow-component.adoc
index c35012a58843..1f372e399d33 100644
--- a/components/camel-undertow/src/main/docs/undertow-component.adoc
+++ b/components/camel-undertow/src/main/docs/undertow-component.adoc
@@ -189,6 +189,17 @@ For example, given a client request with the URL,
`\http://myserver/myserver?orderid=123`, the exchange will contain a
header named `orderid` with the value `123`.
+=== Message body
+
+The consumer reads the body of a request of any method that has one (a
Content-Length above 0 or a chunked body).
+
+The body of a reply (consumer) or a request (producer) is sent as bytes. Text
is written in the charset that the
+`Content-Type` declares (UTF-8 otherwise), and a number, a boolean, a
character or an enum as its text. A Map, a List
+or an object must be marshalled first (for example with `marshal().json()`):
the consumer answers `500` for such a
+reply, and the producer fails the exchange for a POST, PUT or PATCH with such
a body.
+
+The producer gives a null body for a `204` or `304` response and for the
response to a HEAD request.
+
=== Using localhost as host
When you specify `localhost` in a URL, Camel exposes the endpoint only
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHttpBinding.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHttpBinding.java
index 5890436be2ed..484be77762a5 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHttpBinding.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHttpBinding.java
@@ -42,6 +42,7 @@ import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
+import io.undertow.util.StatusCodes;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.TypeConverter;
@@ -149,10 +150,9 @@ public class DefaultUndertowHttpBinding implements
UndertowHttpBinding {
});
result.setBody(body);
} else {
- //extract body by myself if undertow parser didn't handle and the
method is allowed to have one
+ //extract body by myself if undertow parser didn't handle and the
request has one
//body is extracted as byte[] then auto TypeConverter kicks in
- if (Methods.POST.equals(httpExchange.getRequestMethod()) ||
Methods.PUT.equals(httpExchange.getRequestMethod())
- || Methods.PATCH.equals(httpExchange.getRequestMethod())) {
+ if (hasRequestBody(httpExchange)) {
StreamSourceChannel source = httpExchange.getRequestChannel();
if (useStreaming) {
result.setBody(new ChannelInputStream(source));
@@ -166,6 +166,19 @@ public class DefaultUndertowHttpBinding implements
UndertowHttpBinding {
return result;
}
+ /**
+ * Whether the request has a body: a POST, PUT or PATCH, as before, or a
request of any other method (such as a
+ * DELETE) that sends a Content-Length above 0 or a chunked body.
+ */
+ private static boolean hasRequestBody(HttpServerExchange httpExchange) {
+ HttpString method = httpExchange.getRequestMethod();
+ if (Methods.POST.equals(method) || Methods.PUT.equals(method) ||
Methods.PATCH.equals(method)) {
+ return true;
+ }
+ return httpExchange.getRequestContentLength() > 0
+ ||
httpExchange.getRequestHeaders().contains(Headers.TRANSFER_ENCODING);
+ }
+
@Override
public Message toCamelMessage(ClientExchange clientExchange, Exchange
exchange) throws Exception {
Message result = new DefaultMessage(exchange.getContext());
@@ -174,7 +187,10 @@ public class DefaultUndertowHttpBinding implements
UndertowHttpBinding {
populateCamelHeaders(clientExchange.getResponse(),
result.getHeaders(), exchange);
StreamSourceChannel source = clientExchange.getResponseChannel();
- if (useStreaming) {
+ if (hasNoResponseBody(clientExchange)) {
+ // a 204, a 304 or the response to a HEAD has no body (RFC 9110),
so there is no body rather than an empty one
+ result.setBody(null);
+ } else if (useStreaming) {
// client connection can be closed only after input stream is
fully read
result.setBody(new ChannelInputStream(source) {
@Override
@@ -193,6 +209,12 @@ public class DefaultUndertowHttpBinding implements
UndertowHttpBinding {
return result;
}
+ private static boolean hasNoResponseBody(ClientExchange clientExchange) {
+ int code = clientExchange.getResponse().getResponseCode();
+ return code == StatusCodes.NO_CONTENT || code ==
StatusCodes.NOT_MODIFIED
+ ||
Methods.HEAD.equals(clientExchange.getRequest().getMethod());
+ }
+
@Override
public void populateCamelHeaders(HttpServerExchange httpExchange,
Map<String, Object> headersMap, Exchange exchange)
throws Exception {
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
index b55270502a84..6f5072afa2aa 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
@@ -230,6 +230,14 @@ public class UndertowConsumer extends DefaultConsumer
implements HttpHandler, Su
getProcessor().process(camelExchange);
sendResponse(httpExchange, camelExchange);
} catch (Exception e) {
+ if (!httpExchange.isResponseStarted()) {
+ // the reply could not be written, such as a body that cannot
be converted: answer 500 rather than end
+ // the exchange with the status code of the reply and an empty
body
+ httpExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
+ httpExchange.getResponseHeaders().remove(Headers.CONTENT_TYPE);
+ httpExchange.getResponseHeaders().put(Headers.CONTENT_LENGTH,
0);
+ httpExchange.endExchange();
+ }
getExceptionHandler().handleException(e);
} finally {
doneUoW(camelExchange);
@@ -287,15 +295,14 @@ public class UndertowConsumer extends DefaultConsumer
implements HttpHandler, Su
IOHelper.copy(input, output, IOHelper.DEFAULT_BUFFER_SIZE,
true);
}
} else {
- ByteBuffer bodyAsByteBuffer = null;
- if (body instanceof String text) {
- // write the text in the charset that the response
Content-Type declares, if any
- bodyAsByteBuffer = UndertowHelper.toByteBuffer(text,
-
httpExchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE));
- }
+ // write text in the charset that the response Content-Type
declares, if any, and a number as its text
+ String contentType =
httpExchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE);
+ TypeConverter tc =
getEndpoint().getCamelContext().getTypeConverter();
+ ByteBuffer bodyAsByteBuffer = UndertowHelper.toByteBuffer(tc,
body, contentType);
if (bodyAsByteBuffer == null) {
- TypeConverter tc =
getEndpoint().getCamelContext().getTypeConverter();
- bodyAsByteBuffer = tc.mandatoryConvertTo(ByteBuffer.class,
body);
+ // a Map, a List or an object must be marshalled first
+ throw new NoTypeConversionAvailableException(
+ UndertowHelper.notAnHttpBody(camelExchange,
"response", contentType), body, ByteBuffer.class);
}
httpExchange.getResponseSender().send(bodyAsByteBuffer);
}
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
index 198ec9253f45..a235fb714222 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
@@ -25,6 +25,8 @@ import java.util.Map;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.apache.camel.Exchange;
+import org.apache.camel.TypeConverter;
+import org.apache.camel.support.BodyFacts;
import org.apache.camel.util.CollectionHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
@@ -228,6 +230,49 @@ public final class UndertowHelper {
}
}
+ /**
+ * Converts a body to the bytes to send over HTTP. Text and a single value
(a number, a boolean, a character or an
+ * enum) are written as text, in the charset that the Content-Type
declares (UTF-8 otherwise): the type converter
+ * would write a number in its binary form. Other bodies are converted by
the type converter.
+ *
+ * @param tc the type converter
+ * @param body the body, not <tt>null</tt>
+ * @param contentType the Content-Type of the request or response being
sent, may be <tt>null</tt>
+ * @return the bytes, or <tt>null</tt> when the body cannot be
converted, such as a Map, a List or an
+ * object, which must be marshalled first
+ */
+ public static ByteBuffer toByteBuffer(TypeConverter tc, Object body,
String contentType) {
+ Object value = body;
+ if (body instanceof CharSequence || body instanceof Number || body
instanceof Boolean
+ || body instanceof Character || body instanceof Enum<?>) {
+ value = body.toString();
+ }
+ ByteBuffer answer = null;
+ if (value instanceof String text) {
+ answer = toByteBuffer(text, contentType);
+ }
+ if (answer == null) {
+ answer = tc.tryConvertTo(ByteBuffer.class, value);
+ }
+ return answer;
+ }
+
+ /**
+ * Why the body cannot be sent as the body of an HTTP request or response:
what the body is, the step that set it,
+ * and the marshal that writes it, in the data format that the
Content-Type names (as camel-http and
+ * camel-platform-http say it).
+ *
+ * @param exchange the exchange
+ * @param what "request" or "response"
+ * @param contentType the Content-Type of the request or response being
sent, may be <tt>null</tt>
+ * @return the message
+ */
+ public static String notAnHttpBody(Exchange exchange, String what, String
contentType) {
+ String message = BodyFacts.describe(exchange) + ", which is not an
HTTP " + what + " body";
+ String hint = BodyFacts.hint(exchange, BodyFacts.Need.BYTES,
contentType);
+ return hint != null ? message + ": " + hint : message;
+ }
+
/**
* Gets the charset parameter of a Content-Type. The parameter name is
case-insensitive (RFC 9110), as in the other
* HTTP components. Unlike {@link
IOHelper#getCharsetNameFromContentType(String)}, there is no default: a
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
index 4c973b563228..6ede7c60a62c 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
@@ -36,8 +36,10 @@ import io.undertow.server.DefaultByteBufferPool;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
+import io.undertow.util.Methods;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Message;
import org.apache.camel.TypeConverter;
import org.apache.camel.component.undertow.handlers.CamelWebSocketHandler;
@@ -167,19 +169,23 @@ public class UndertowProducer extends
DefaultAsyncProducer {
request, (InputStream) body);
} else {
ByteBuffer bodyAsByte = null;
- if (body instanceof String text) {
- // write the text in the charset that the request Content-Type
declares, if any
- bodyAsByte = UndertowHelper.toByteBuffer(text,
requestHeaders.getFirst(Headers.CONTENT_TYPE));
- }
- if (bodyAsByte == null) {
+ if (body != null) {
+ // write text in the charset that the request Content-Type
declares, if any, and a number as its text
+ final String contentType =
requestHeaders.getFirst(Headers.CONTENT_TYPE);
final TypeConverter tc =
endpoint.getCamelContext().getTypeConverter();
- bodyAsByte = tc.tryConvertTo(ByteBuffer.class, body);
- }
-
- // As tryConvertTo is used to convert the body, we should do null
check
- // or the call bodyAsByte.remaining() may throw an NPE
- if (body != null && bodyAsByte != null) {
- requestHeaders.put(Headers.CONTENT_LENGTH,
bodyAsByte.remaining());
+ bodyAsByte = UndertowHelper.toByteBuffer(tc, body,
contentType);
+ if (bodyAsByte != null) {
+ requestHeaders.put(Headers.CONTENT_LENGTH,
bodyAsByte.remaining());
+ } else if (Methods.POST.equals(method) ||
Methods.PUT.equals(method) || Methods.PATCH.equals(method)) {
+ // a Map, a List or an object must be marshalled first,
rather than be sent as an empty request
+ // (a GET or a DELETE sends no body it cannot convert, as
before)
+ final Message in = camelExchange.getIn();
+ camelExchange.setException(new InvalidPayloadException(
+ camelExchange, ByteBuffer.class, in,
+ UndertowHelper.notAnHttpBody(camelExchange,
"request", contentType)));
+ callback.done(true);
+ return true;
+ }
}
if (streaming) {
diff --git
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowBodyConversionTest.java
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowBodyConversionTest.java
new file mode 100644
index 000000000000..3023013322ee
--- /dev/null
+++
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowBodyConversionTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.undertow;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A number is sent as its text, a Map, a List or an object fails clearly
rather than be sent as an empty body, a body
+ * is read for any method, and a 204 response has no body.
+ */
+public class UndertowBodyConversionTest extends BaseUndertowTest {
+
+ @Test
+ public void testReplyNumberIsText() throws Exception {
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("number")).GET().build());
+
+ assertEquals(200, response.statusCode());
+ assertEquals("42", response.body());
+ }
+
+ @Test
+ public void testReplyBooleanIsText() throws Exception {
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("boolean")).GET().build());
+
+ assertEquals(200, response.statusCode());
+ assertEquals("true", response.body());
+ }
+
+ @Test
+ public void testReplyMapIs500() throws Exception {
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("map")).GET().build());
+
+ assertEquals(500, response.statusCode());
+ assertEquals("", response.body());
+ }
+
+ @Test
+ public void testReplyListIs500() throws Exception {
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("list")).GET().build());
+
+ assertEquals(500, response.statusCode());
+ }
+
+ @Test
+ public void testReplyObjectIs500() throws Exception {
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("object")).GET().build());
+
+ assertEquals(500, response.statusCode());
+ }
+
+ @Test
+ public void testProducerSendsNumberAsText() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedBodiesReceived("42");
+
+ template.sendBody("undertow:http://localhost:{{port}}/received", 42);
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Test
+ public void testProducerFailsOnMap() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedMessageCount(0);
+
+ Exchange out =
template.send("undertow:http://localhost:{{port}}/received",
+ exchange -> exchange.getIn().setBody(Map.of("id", 1)));
+
+ InvalidPayloadException cause =
assertInstanceOf(InvalidPayloadException.class, out.getException());
+ assertTrue(cause.getMessage().contains("which is not an HTTP request
body: marshal it first"), cause.getMessage());
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Test
+ public void testProducerFailsOnMapWithJsonContentType() {
+ Exchange out =
template.send("undertow:http://localhost:{{port}}/received", exchange -> {
+ exchange.getIn().setBody(List.of("a", "b"));
+ exchange.getIn().setHeader(Exchange.CONTENT_TYPE,
"application/json");
+ });
+
+ InvalidPayloadException cause =
assertInstanceOf(InvalidPayloadException.class, out.getException());
+ assertTrue(cause.getMessage().contains("marshal it first (marshal:
json"), cause.getMessage());
+ }
+
+ @Test
+ public void testProducerGetIgnoresMap() throws Exception {
+ // a GET sends no body it cannot convert, as before
+ Exchange out =
template.send("undertow:http://localhost:{{port}}/number", exchange -> {
+ exchange.getIn().setBody(Map.of("id", 1));
+ exchange.getIn().setHeader(Exchange.HTTP_METHOD, "GET");
+ });
+
+ assertNull(out.getException());
+ assertEquals("42", out.getMessage().getBody(String.class));
+ }
+
+ @Test
+ public void testDeleteWithBody() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedBodiesReceived("remove me");
+
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("received"))
+ .method("DELETE", HttpRequest.BodyPublishers.ofString("remove
me", UTF_8)).build());
+
+ assertEquals(200, response.statusCode());
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Test
+ public void testDeleteWithoutBody() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedMessageCount(1);
+
+ HttpResponse<String> response =
send(HttpRequest.newBuilder(uri("received")).DELETE().build());
+
+ assertEquals(200, response.statusCode());
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(mock.getExchanges().get(0).getIn().getBody());
+ }
+
+ @Test
+ public void testProducerNoContentHasNullBody() {
+ Exchange out =
template.request("undertow:http://localhost:{{port}}/noContent", null);
+
+ assertNull(out.getException());
+ assertEquals(204,
out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(out.getMessage().getBody());
+ }
+
+ @Test
+ public void testProducerHeadHasNullBody() {
+ Exchange out =
template.request("undertow:http://localhost:{{port}}/number",
+ exchange -> exchange.getIn().setHeader(Exchange.HTTP_METHOD,
"HEAD"));
+
+ assertNull(out.getException());
+ assertEquals(200,
out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(out.getMessage().getBody());
+ }
+
+ private URI uri(String path) {
+ return URI.create("http://localhost:" + getPort() + "/" + path);
+ }
+
+ private static HttpResponse<String> send(HttpRequest request) throws
Exception {
+ return HttpClient.newHttpClient().send(request,
HttpResponse.BodyHandlers.ofString());
+ }
+
+ public static class Order {
+ private final int id = 1;
+
+ public int getId() {
+ return id;
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("undertow:http://localhost:{{port}}/number")
+ .setBody(constant(42));
+
+ from("undertow:http://localhost:{{port}}/boolean")
+ .setBody(constant(true));
+
+ from("undertow:http://localhost:{{port}}/map")
+ .setBody(constant(Map.of("id", 1)));
+
+ from("undertow:http://localhost:{{port}}/list")
+ .setBody(constant(List.of("a", "b")));
+
+ from("undertow:http://localhost:{{port}}/object")
+ .setBody(constant(new Order()));
+
+ from("undertow:http://localhost:{{port}}/received")
+ .convertBodyTo(String.class)
+ .to("mock:received")
+ .setBody(constant("OK"));
+
+ from("undertow:http://localhost:{{port}}/noContent")
+ .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(204))
+ .setBody(constant(""));
+ }
+ };
+ }
+}
diff --git
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowSwitchingStatus204Test.java
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowSwitchingStatus204Test.java
index 65ff92b4cbb4..0d7cb74820a9 100644
---
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowSwitchingStatus204Test.java
+++
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowSwitchingStatus204Test.java
@@ -51,7 +51,8 @@ public class UndertowSwitchingStatus204Test extends
BaseUndertowTest {
Message msg = outExchange.getMessage();
assertEquals(204, msg.getHeader(Exchange.HTTP_RESPONSE_CODE));
- assertEquals("", msg.getBody(String.class));
+ // a 204 has no body
+ assertNull(msg.getBody());
}
@Test
@@ -61,7 +62,8 @@ public class UndertowSwitchingStatus204Test extends
BaseUndertowTest {
Message msg = outExchange.getMessage();
assertEquals(204, msg.getHeader(Exchange.HTTP_RESPONSE_CODE));
- assertEquals("", msg.getBody(String.class));
+ // a 204 has no body
+ assertNull(msg.getBody());
}
@Test
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index f6c816a7f2f8..f70dd6e55b38 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2428,6 +2428,21 @@ with a `vertx-http:` producer and `bridgeEndpoint=true`,
now sends a request for
`removeHeader(Exchange.HTTP_PATH)`. The path comes from the client request,
and the producer sends it with the query
of the endpoint (`key=1` in the example): a proxy route should validate or
restrict the path, or remove the header.
+=== camel-undertow - the bodies of requests and responses
+
+The undertow consumer and producer change how they send and read a message
body:
+
+* A number, a boolean, a character or an enum is sent as its text:
`setBody(constant(42))` sends `42` and no longer
+ the 4 bytes of the binary form of the number. A boolean or a character was
not sent at all before.
+* A Map, a List or an object that does not convert to bytes is no longer sent
as an empty body. The consumer answers
+ `500` instead of an empty `200`, and logs what the body is and the marshal
that writes it. The producer fails the
+ exchange with an `InvalidPayloadException` for a POST, PUT or PATCH instead
of sending an empty request; for the
+ other methods, such as a GET, it sends no body as before. Marshal the body
first, for example with `marshal().json()`.
+* The consumer reads the body of a request of any method that has one (a
Content-Length above 0 or a chunked body),
+ such as a DELETE. Before, only the body of a POST, PUT or PATCH was read,
and the body was null for the others.
+* The producer gives a null body for a `204` or `304` response and for the
response to a HEAD request, instead of an
+ empty `byte[]`, as camel-http does. A `200` response with an empty body is
still an empty `byte[]`.
+
=== camel-dapr - the saveBulk and executeTransaction state operations are sent
The `saveBulk` and `executeTransaction` operations of `dapr:state` built their
request but never sent it to Dapr, so