This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26140-undertow-body-conversion in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9dbd4894bdcfc1c625ca2150304ea637c790603c Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 17:07:56 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 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 1bc188419d61..87fb83e8733f 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 @@ -2336,6 +2336,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
