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`,