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 bf3d66b57593 CAMEL-26218: camel-kserve - a body that is not the 
request of infer, model/ready or model/metadata fails instead of being dropped 
(#29065)
bf3d66b57593 is described below

commit bf3d66b5759360948a6ece3fab76d80cad3a86e6
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 3 12:12:43 2026 +0100

    CAMEL-26218: camel-kserve - a body that is not the request of infer, 
model/ready or model/metadata fails instead of being dropped (#29065)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/components/kserve.json    |   2 +-
 .../camel/catalog/docs/kserve-component.adoc       |   7 +
 .../org/apache/camel/component/kserve/kserve.json  |   2 +-
 .../src/main/docs/kserve-component.adoc            |   7 +
 .../camel/component/kserve/KServeEndpoint.java     |  20 +-
 .../camel/component/kserve/KServeProducer.java     |  50 +++-
 .../component/kserve/KServeProducerBodyTest.java   | 268 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 +
 8 files changed, 353 insertions(+), 16 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/kserve.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/kserve.json
index 12b2d7b13a4b..3575fa2f63d9 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/kserve.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/kserve.json
@@ -39,7 +39,7 @@
     "CamelKServeModelVersion": { "index": 1, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The version of the model used for inference.", 
"constantName": 
"org.apache.camel.component.kserve.KServeConstants#MODEL_VERSION" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "model\/ready", "carriers": [ "any" ] }, { "option": "api", "value": 
"model\/metadata", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/ready", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/live", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/metadata", "carriers": [ "any" ] } ], "description": "infer takes the 
protobuf ModelInferRequest with the input tenso [...]
+    "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "model\/ready", "carriers": [ "null", "pojo" ] }, { "option": "api", 
"value": "model\/metadata", "carriers": [ "null", "pojo" ] }, { "option": 
"api", "value": "server\/ready", "carriers": [ "any" ] }, { "option": "api", 
"value": "server\/live", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/metadata", "carriers": [ "any" ] } ], "description": "infer takes the 
protobuf ModelInferRequest wi [...]
     "producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "infer", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelInferResponse" }, { "option": "api", "value": 
"model\/ready", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelReadyResponse" }, { "option": "api", "value": 
"model\/metadata", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelMetadataResponse" }, { "option": "api", "value": 
"server\/ready", "carr [...]
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kserve-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kserve-component.adoc
index eb4e7efa3173..b6f70ba2e905 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kserve-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kserve-component.adoc
@@ -98,6 +98,13 @@ kserve:<api>[?options]
 | `ServerMetadataResponse` 
footnote:[`inference.GrpcPredictV2.ServerMetadataResponse`]
 |===
 
+The input body of `infer`, `model/ready` and `model/metadata` is the protobuf 
request of the API or its `Builder`.
+The component does not parse any other body that is not empty, such as text, 
JSON, bytes or a `Map`: the exchange
+fails with an `InvalidPayloadException` that names the request type of the 
API, so build the protobuf request before
+the endpoint. Without a body, or with an empty one (a blank `String` or an 
empty `byte[]`), the request is built from
+the options and headers only, which is all that `model/ready` and 
`model/metadata` need. The `server/ready`,
+`server/live` and `server/metadata` APIs do not use the body.
+
 == Examples
 
 === Infer (ModelInfer) API
diff --git 
a/components/camel-ai/camel-kserve/src/generated/resources/META-INF/org/apache/camel/component/kserve/kserve.json
 
b/components/camel-ai/camel-kserve/src/generated/resources/META-INF/org/apache/camel/component/kserve/kserve.json
index 12b2d7b13a4b..3575fa2f63d9 100644
--- 
a/components/camel-ai/camel-kserve/src/generated/resources/META-INF/org/apache/camel/component/kserve/kserve.json
+++ 
b/components/camel-ai/camel-kserve/src/generated/resources/META-INF/org/apache/camel/component/kserve/kserve.json
@@ -39,7 +39,7 @@
     "CamelKServeModelVersion": { "index": 1, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The version of the model used for inference.", 
"constantName": 
"org.apache.camel.component.kserve.KServeConstants#MODEL_VERSION" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "model\/ready", "carriers": [ "any" ] }, { "option": "api", "value": 
"model\/metadata", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/ready", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/live", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/metadata", "carriers": [ "any" ] } ], "description": "infer takes the 
protobuf ModelInferRequest with the input tenso [...]
+    "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "model\/ready", "carriers": [ "null", "pojo" ] }, { "option": "api", 
"value": "model\/metadata", "carriers": [ "null", "pojo" ] }, { "option": 
"api", "value": "server\/ready", "carriers": [ "any" ] }, { "option": "api", 
"value": "server\/live", "carriers": [ "any" ] }, { "option": "api", "value": 
"server\/metadata", "carriers": [ "any" ] } ], "description": "infer takes the 
protobuf ModelInferRequest wi [...]
     "producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": "api", 
"value": "infer", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelInferResponse" }, { "option": "api", "value": 
"model\/ready", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelReadyResponse" }, { "option": "api", "value": 
"model\/metadata", "carriers": [ "pojo" ], "javaType": 
"inference.GrpcPredictV2$ModelMetadataResponse" }, { "option": "api", "value": 
"server\/ready", "carr [...]
   },
   "properties": {
diff --git 
a/components/camel-ai/camel-kserve/src/main/docs/kserve-component.adoc 
b/components/camel-ai/camel-kserve/src/main/docs/kserve-component.adoc
index eb4e7efa3173..b6f70ba2e905 100644
--- a/components/camel-ai/camel-kserve/src/main/docs/kserve-component.adoc
+++ b/components/camel-ai/camel-kserve/src/main/docs/kserve-component.adoc
@@ -98,6 +98,13 @@ kserve:<api>[?options]
 | `ServerMetadataResponse` 
footnote:[`inference.GrpcPredictV2.ServerMetadataResponse`]
 |===
 
+The input body of `infer`, `model/ready` and `model/metadata` is the protobuf 
request of the API or its `Builder`.
+The component does not parse any other body that is not empty, such as text, 
JSON, bytes or a `Map`: the exchange
+fails with an `InvalidPayloadException` that names the request type of the 
API, so build the protobuf request before
+the endpoint. Without a body, or with an empty one (a blank `String` or an 
empty `byte[]`), the request is built from
+the options and headers only, which is all that `model/ready` and 
`model/metadata` need. The `server/ready`,
+`server/live` and `server/metadata` APIs do not use the body.
+
 == Examples
 
 === Infer (ModelInfer) API
diff --git 
a/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeEndpoint.java
 
b/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeEndpoint.java
index 60afb9cb7770..53e41a58c84b 100644
--- 
a/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeEndpoint.java
+++ 
b/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeEndpoint.java
@@ -41,17 +41,23 @@ import org.apache.camel.support.DefaultEndpoint;
              category = { Category.AI }, headersClass = KServeConstants.class)
 @BodyTypes(producerIn = @BodyType(value = Carrier.POJO,
                                   byOption = {
-                                          @BodyType.When(option = "api", value 
= "model/ready", carriers = Carrier.ANY),
-                                          @BodyType.When(option = "api", value 
= "model/metadata", carriers = Carrier.ANY),
+                                          @BodyType.When(option = "api", value 
= "model/ready",
+                                                         carriers = { 
Carrier.NULL, Carrier.POJO }),
+                                          @BodyType.When(option = "api", value 
= "model/metadata",
+                                                         carriers = { 
Carrier.NULL, Carrier.POJO }),
                                           @BodyType.When(option = "api", value 
= "server/ready", carriers = Carrier.ANY),
                                           @BodyType.When(option = "api", value 
= "server/live", carriers = Carrier.ANY),
                                           @BodyType.When(option = "api", value 
= "server/metadata",
                                                          carriers = 
Carrier.ANY) },
-                                  description = "infer takes the protobuf 
ModelInferRequest with the input tensors, or its"
-                                                + " Builder; another body, 
such as text, JSON or bytes, is not read and a"
-                                                + " request without inputs is 
sent. model/ready and model/metadata need no"
-                                                + " body, and read a 
ModelReadyRequest or ModelMetadataRequest when it is"
-                                                + " one; the server apis do 
not use the body. The model comes from the"
+                                  description = "infer takes the protobuf 
ModelInferRequest with the input tensors, or"
+                                                + " its Builder. Another body 
that is not empty, such as text, JSON,"
+                                                + " bytes or a Map, is not 
parsed and fails the exchange with an"
+                                                + " InvalidPayloadException 
that names the request of the api: build the"
+                                                + " protobuf request before 
the endpoint. No body, a blank String or an"
+                                                + " empty byte array builds 
the request from the options and headers"
+                                                + " only, which is all 
model/ready and model/metadata need; they read a"
+                                                + " ModelReadyRequest or 
ModelMetadataRequest when the body is one. The"
+                                                + " server apis do not use the 
body. The model comes from the"
                                                 + " CamelKServeModelName 
header, or else the modelName option"),
            producerOut = @BodyType(value = Carrier.POJO,
                                    byOption = {
diff --git 
a/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeProducer.java
 
b/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeProducer.java
index a09fb65c5221..faf55fdf2d91 100644
--- 
a/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeProducer.java
+++ 
b/components/camel-ai/camel-kserve/src/main/java/org/apache/camel/component/kserve/KServeProducer.java
@@ -22,11 +22,18 @@ import com.google.protobuf.GeneratedMessage;
 import inference.GRPCInferenceServiceGrpc;
 import inference.GrpcPredictV2;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Message;
 import org.apache.camel.support.DefaultProducer;
 
 public class KServeProducer extends DefaultProducer {
 
+    /** The end of the error of an api that reads its request from the body. */
+    private static final String BUILD_THE_REQUEST = ": build the protobuf 
request before the endpoint";
+    /** The end of the error of an api that needs no body. */
+    private static final String OR_NO_BODY
+            = ", and builds the request from the options and headers when 
there is no body, or an empty one";
+
     private final String api;
     private final GRPCInferenceServiceGrpc.GRPCInferenceServiceBlockingStub 
inferenceService;
 
@@ -42,7 +49,7 @@ public class KServeProducer extends DefaultProducer {
     }
 
     @Override
-    public void process(Exchange exchange) {
+    public void process(Exchange exchange) throws Exception {
         GeneratedMessage response = switch (api) {
             case "infer" -> infer(exchange);
             case "model/ready" -> modelReady(exchange);
@@ -55,9 +62,36 @@ public class KServeProducer extends DefaultProducer {
         exchange.getMessage().setBody(response);
     }
 
-    private GrpcPredictV2.ModelInferResponse infer(Exchange exchange) {
+    /**
+     * The body as the request of the api. No body, or an empty one, gives 
null: the request is then built from the
+     * options and headers only. Another body that is not the request and does 
not convert to it, such as text, JSON,
+     * bytes or a Map, fails instead of being dropped, as the request would be 
sent without it.
+     */
+    private <T> T requestOf(Exchange exchange, Class<T> type, String reads) 
throws InvalidPayloadException {
+        Message message = exchange.getMessage();
+        if (isEmpty(message.getBody())) {
+            return null;
+        }
+        T request = message.getBody(type);
+        if (request == null) {
+            throw new InvalidPayloadException(exchange, type, message, "the " 
+ api + " api reads " + reads);
+        }
+        return request;
+    }
+
+    /**
+     * No body, or an empty one that an earlier step can leave, such as a 
blank String or an empty byte array.
+     */
+    private static boolean isEmpty(Object body) {
+        return body == null
+                || (body instanceof String text && text.isBlank())
+                || (body instanceof byte[] bytes && bytes.length == 0);
+    }
+
+    private GrpcPredictV2.ModelInferResponse infer(Exchange exchange) throws 
InvalidPayloadException {
         Message message = exchange.getMessage();
-        GrpcPredictV2.ModelInferRequest request = 
message.getBody(GrpcPredictV2.ModelInferRequest.class);
+        GrpcPredictV2.ModelInferRequest request = requestOf(exchange, 
GrpcPredictV2.ModelInferRequest.class,
+                "a ModelInferRequest with the input tensors, or its Builder" + 
BUILD_THE_REQUEST);
 
         KServeConfiguration configuration = getEndpoint().getConfiguration();
         GrpcPredictV2.ModelInferRequest.Builder builder = 
GrpcPredictV2.ModelInferRequest.newBuilder();
@@ -74,9 +108,10 @@ public class KServeProducer extends DefaultProducer {
         return inferenceService.modelInfer(builder.build());
     }
 
-    private GrpcPredictV2.ModelReadyResponse modelReady(Exchange exchange) {
+    private GrpcPredictV2.ModelReadyResponse modelReady(Exchange exchange) 
throws InvalidPayloadException {
         Message message = exchange.getMessage();
-        GrpcPredictV2.ModelReadyRequest request = 
message.getBody(GrpcPredictV2.ModelReadyRequest.class);
+        GrpcPredictV2.ModelReadyRequest request = requestOf(exchange, 
GrpcPredictV2.ModelReadyRequest.class,
+                "a ModelReadyRequest or its Builder" + OR_NO_BODY);
 
         KServeConfiguration configuration = getEndpoint().getConfiguration();
         GrpcPredictV2.ModelReadyRequest.Builder builder = 
GrpcPredictV2.ModelReadyRequest.newBuilder();
@@ -93,9 +128,10 @@ public class KServeProducer extends DefaultProducer {
         return inferenceService.modelReady(builder.build());
     }
 
-    private GrpcPredictV2.ModelMetadataResponse modelMetadata(Exchange 
exchange) {
+    private GrpcPredictV2.ModelMetadataResponse modelMetadata(Exchange 
exchange) throws InvalidPayloadException {
         Message message = exchange.getMessage();
-        GrpcPredictV2.ModelMetadataRequest request = 
message.getBody(GrpcPredictV2.ModelMetadataRequest.class);
+        GrpcPredictV2.ModelMetadataRequest request = requestOf(exchange, 
GrpcPredictV2.ModelMetadataRequest.class,
+                "a ModelMetadataRequest or its Builder" + OR_NO_BODY);
 
         KServeConfiguration configuration = getEndpoint().getConfiguration();
         GrpcPredictV2.ModelMetadataRequest.Builder builder = 
GrpcPredictV2.ModelMetadataRequest.newBuilder();
diff --git 
a/components/camel-ai/camel-kserve/src/test/java/org/apache/camel/component/kserve/KServeProducerBodyTest.java
 
b/components/camel-ai/camel-kserve/src/test/java/org/apache/camel/component/kserve/KServeProducerBodyTest.java
new file mode 100644
index 000000000000..f2a0d25def7a
--- /dev/null
+++ 
b/components/camel-ai/camel-kserve/src/test/java/org/apache/camel/component/kserve/KServeProducerBodyTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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.kserve;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import inference.GRPCInferenceServiceGrpc;
+import inference.GrpcPredictV2;
+import io.grpc.Grpc;
+import io.grpc.InsecureServerCredentials;
+import io.grpc.Server;
+import io.grpc.stub.StreamObserver;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+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;
+
+/**
+ * The body the producer sends, against a local gRPC server that records the 
requests it receives.
+ */
+class KServeProducerBodyTest extends CamelTestSupport {
+
+    private static final String MODEL = "simple";
+    private static final String JSON = "{\"inputs\": [{\"name\": \"INPUT0\", 
\"data\": [1, 2]}]}";
+
+    private static final BlockingQueue<Object> REQUESTS = new 
LinkedBlockingQueue<>();
+    private static Server server;
+
+    @BeforeAll
+    static void startServer() throws IOException {
+        server = Grpc.newServerBuilderForPort(0, 
InsecureServerCredentials.create())
+                .addService(new RecordingInferenceService())
+                .build()
+                .start();
+    }
+
+    @AfterAll
+    static void stopServer() throws InterruptedException {
+        server.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
+    }
+
+    @BeforeEach
+    void clearRequests() {
+        REQUESTS.clear();
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.getComponent("kserve", 
KServeComponent.class).getConfiguration()
+                .setTarget("localhost:" + server.getPort());
+        return context;
+    }
+
+    static Stream<Arguments> bodiesThatAreNotTheRequest() {
+        byte[] bytes = JSON.getBytes(StandardCharsets.UTF_8);
+        Map<String, Object> map = Map.of("inputs", List.of(Map.of("name", 
"INPUT0", "datatype", "INT32")));
+        return Stream.of(
+                Arguments.of("infer", GrpcPredictV2.ModelInferRequest.class, 
JSON),
+                Arguments.of("infer", GrpcPredictV2.ModelInferRequest.class, 
bytes),
+                Arguments.of("infer", GrpcPredictV2.ModelInferRequest.class, 
map),
+                // the request of another api
+                Arguments.of("infer", GrpcPredictV2.ModelInferRequest.class,
+                        
GrpcPredictV2.ModelReadyRequest.newBuilder().setName(MODEL).build()),
+                Arguments.of("model/ready", 
GrpcPredictV2.ModelReadyRequest.class, MODEL),
+                Arguments.of("model/ready", 
GrpcPredictV2.ModelReadyRequest.class, map),
+                Arguments.of("model/metadata", 
GrpcPredictV2.ModelMetadataRequest.class, MODEL),
+                Arguments.of("model/metadata", 
GrpcPredictV2.ModelMetadataRequest.class, bytes));
+    }
+
+    @ParameterizedTest
+    @MethodSource("bodiesThatAreNotTheRequest")
+    void bodyThatIsNotTheRequestFails(String api, Class<?> requestType, Object 
body) {
+        Exchange exchange = send(api, body);
+
+        InvalidPayloadException e = 
assertInstanceOf(InvalidPayloadException.class, exchange.getException());
+        assertEquals(requestType, e.getType());
+        assertTrue(e.getMessage().contains(requestType.getCanonicalName()), 
e.getMessage());
+        assertTrue(e.getMessage().contains("the " + api + " api reads"), 
e.getMessage());
+        assertTrue(REQUESTS.isEmpty(), "No request must be sent");
+    }
+
+    @Test
+    void inferSendsTheRequestOfTheBody() {
+        GrpcPredictV2.ModelInferRequest request = inferRequest();
+
+        Exchange exchange = send("infer", request);
+
+        assertNull(exchange.getException());
+        GrpcPredictV2.ModelInferRequest sent = 
sent(GrpcPredictV2.ModelInferRequest.class);
+        assertEquals(MODEL, sent.getModelName());
+        assertEquals(request.getInputsList(), sent.getInputsList());
+        assertInstanceOf(GrpcPredictV2.ModelInferResponse.class, 
exchange.getMessage().getBody());
+    }
+
+    @Test
+    void inferSendsTheRequestOfABuilderBody() {
+        GrpcPredictV2.ModelInferRequest.Builder builder = 
inferRequest().toBuilder();
+
+        Exchange exchange = send("infer", builder);
+
+        assertNull(exchange.getException());
+        assertEquals(builder.getInputsList(), 
sent(GrpcPredictV2.ModelInferRequest.class).getInputsList());
+    }
+
+    @Test
+    void inferWithoutBodySendsTheModelOnly() {
+        Exchange exchange = send("infer", null);
+
+        assertNull(exchange.getException());
+        GrpcPredictV2.ModelInferRequest sent = 
sent(GrpcPredictV2.ModelInferRequest.class);
+        assertEquals(MODEL, sent.getModelName());
+        assertEquals(0, sent.getInputsCount());
+    }
+
+    @Test
+    void modelReadyWithoutBodyAsksForTheModelOfTheOptions() {
+        Exchange exchange = send("model/ready", null);
+
+        assertNull(exchange.getException());
+        assertEquals(MODEL, 
sent(GrpcPredictV2.ModelReadyRequest.class).getName());
+        assertInstanceOf(GrpcPredictV2.ModelReadyResponse.class, 
exchange.getMessage().getBody());
+    }
+
+    static Stream<Object> emptyBodies() {
+        return Stream.of("", " \n", new byte[0]);
+    }
+
+    @ParameterizedTest
+    @MethodSource("emptyBodies")
+    void modelReadyWithAnEmptyBodyAsksForTheModelOfTheOptions(Object body) {
+        Exchange exchange = send("model/ready", body);
+
+        assertNull(exchange.getException());
+        assertEquals(MODEL, 
sent(GrpcPredictV2.ModelReadyRequest.class).getName());
+    }
+
+    @Test
+    void modelReadySendsTheRequestOfTheBody() {
+        GrpcPredictV2.ModelReadyRequest request = 
GrpcPredictV2.ModelReadyRequest.newBuilder()
+                .setName("other").setVersion("2").build();
+
+        Exchange exchange = send("model/ready", request);
+
+        assertNull(exchange.getException());
+        assertEquals(request, sent(GrpcPredictV2.ModelReadyRequest.class));
+    }
+
+    @Test
+    void modelMetadataWithoutBodyAsksForTheModelOfTheOptions() {
+        Exchange exchange = send("model/metadata", null);
+
+        assertNull(exchange.getException());
+        assertEquals(MODEL, 
sent(GrpcPredictV2.ModelMetadataRequest.class).getName());
+        assertInstanceOf(GrpcPredictV2.ModelMetadataResponse.class, 
exchange.getMessage().getBody());
+    }
+
+    @Test
+    void modelMetadataSendsTheRequestOfABuilderBody() {
+        GrpcPredictV2.ModelMetadataRequest.Builder builder = 
GrpcPredictV2.ModelMetadataRequest.newBuilder()
+                .setVersion("2");
+
+        Exchange exchange = send("model/metadata", builder);
+
+        assertNull(exchange.getException());
+        GrpcPredictV2.ModelMetadataRequest sent = 
sent(GrpcPredictV2.ModelMetadataRequest.class);
+        assertEquals(MODEL, sent.getName());
+        assertEquals("2", sent.getVersion());
+    }
+
+    @Test
+    void serverReadyDoesNotUseTheBody() {
+        Exchange exchange = send("server/ready", JSON);
+
+        assertNull(exchange.getException());
+        assertInstanceOf(GrpcPredictV2.ServerReadyRequest.class, 
REQUESTS.poll());
+        assertInstanceOf(GrpcPredictV2.ServerReadyResponse.class, 
exchange.getMessage().getBody());
+    }
+
+    private Exchange send(String api, Object body) {
+        return template.send("kserve:" + api + "?modelName=" + MODEL, e -> 
e.getMessage().setBody(body));
+    }
+
+    private static <T> T sent(Class<T> type) {
+        return assertInstanceOf(type, REQUESTS.poll(), "The request the server 
received");
+    }
+
+    private static GrpcPredictV2.ModelInferRequest inferRequest() {
+        return GrpcPredictV2.ModelInferRequest.newBuilder()
+                
.addInputs(GrpcPredictV2.ModelInferRequest.InferInputTensor.newBuilder()
+                        
.setName("INPUT0").setDatatype("INT32").addShape(1).addShape(2)
+                        
.setContents(GrpcPredictV2.InferTensorContents.newBuilder()
+                                .addIntContents(1)
+                                .addIntContents(2)))
+                .build();
+    }
+
+    private static <T> void reply(Object request, T response, 
StreamObserver<T> observer) {
+        REQUESTS.add(request);
+        observer.onNext(response);
+        observer.onCompleted();
+    }
+
+    private static final class RecordingInferenceService extends 
GRPCInferenceServiceGrpc.GRPCInferenceServiceImplBase {
+
+        @Override
+        public void modelInfer(
+                GrpcPredictV2.ModelInferRequest request, 
StreamObserver<GrpcPredictV2.ModelInferResponse> observer) {
+            reply(request, 
GrpcPredictV2.ModelInferResponse.newBuilder().setModelName(request.getModelName()).build(),
+                    observer);
+        }
+
+        @Override
+        public void modelReady(
+                GrpcPredictV2.ModelReadyRequest request, 
StreamObserver<GrpcPredictV2.ModelReadyResponse> observer) {
+            reply(request, 
GrpcPredictV2.ModelReadyResponse.newBuilder().setReady(true).build(), observer);
+        }
+
+        @Override
+        public void modelMetadata(
+                GrpcPredictV2.ModelMetadataRequest request,
+                StreamObserver<GrpcPredictV2.ModelMetadataResponse> observer) {
+            GrpcPredictV2.ModelMetadataResponse response = 
GrpcPredictV2.ModelMetadataResponse.newBuilder()
+                    .setName(request.getName())
+                    .build();
+            reply(request, response, observer);
+        }
+
+        @Override
+        public void serverReady(
+                GrpcPredictV2.ServerReadyRequest request, 
StreamObserver<GrpcPredictV2.ServerReadyResponse> observer) {
+            reply(request, 
GrpcPredictV2.ServerReadyResponse.newBuilder().setReady(true).build(), 
observer);
+        }
+    }
+}
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 fca9a0dfcd2f..092914b318d6 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
@@ -4406,6 +4406,19 @@ A `sessionTimeout` configured on the `milo-client` or 
`milo-browse` component (f
 `camel.component.milo-client.sessionTimeout`) did not reach its endpoints, so 
their sessions used the default of Milo,
 120000 milliseconds. The endpoints now use it, unless they set 
`sessionTimeout` themselves.
 
+=== camel-kserve - a body that is not the request of the api fails
+
+The `kserve` producer converted the body of `infer`, `model/ready` and 
`model/metadata` to the protobuf request of
+the api, and dropped a body that did not convert, such as text, JSON, bytes or 
a `Map`, without an error. The request
+was then sent without it, so `infer` sent a request with no inputs, and 
`model/ready` and `model/metadata` ignored the
+body. Such a body, when it is not empty, now fails the exchange with an 
`InvalidPayloadException` that names the
+request type of the api, before anything is sent. Build the protobuf request 
before the endpoint, or remove a body
+that an earlier step left before calling `model/ready` or `model/metadata`.
+
+No body, a blank `String` or an empty `byte[]` still sends a request built 
from the options and headers, a body that
+is the request or its `Builder` is sent as before, and the `server/ready`, 
`server/live` and `server/metadata` apis
+still do not use the body.
+
 === camel-milo - a write of a null body fails the exchange
 
 A `milo-client` producer no longer writes a `null` body; the exchange now 
fails with an `InvalidPayloadException`,

Reply via email to