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

Reply via email to