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 de36cecfda63 CAMEL-26217: camel-tensorflow-serving - a body that is
not the request of the api fails instead of being dropped, and model-metadata
asks for signature_def once (#29064)
de36cecfda63 is described below
commit de36cecfda632092d5ee9fe827378ec73bf01648
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 3 12:12:53 2026 +0100
CAMEL-26217: camel-tensorflow-serving - a body that is not the request of
the api fails instead of being dropped, and model-metadata asks for
signature_def once (#29064)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../catalog/components/tensorflow-serving.json | 2 +-
.../catalog/docs/tensorflow-serving-component.adoc | 9 +
.../tensorflow/serving/tensorflow-serving.json | 2 +-
.../main/docs/tensorflow-serving-component.adoc | 9 +
.../serving/TensorFlowServingEndpoint.java | 25 +-
.../serving/TensorFlowServingProducer.java | 71 ++++-
.../serving/TensorFlowServingProducerBodyTest.java | 326 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 16 +
8 files changed, 433 insertions(+), 27 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/tensorflow-serving.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/tensorflow-serving.json
index d7692d3f6acd..43fbf4183aac 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/tensorflow-serving.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/tensorflow-serving.json
@@ -43,7 +43,7 @@
"CamelTensorFlowServingSignatureName": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A named signature to evaluate. If
unspecified, the default signature will be used.", "constantName":
"org.apache.camel.component.tensorflow.serving.TensorFlowServingConstants#SIGNATURE_NAME"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "any" ] }, { "option": "api", "value":
"model-metadata", "carriers": [ "any" ] } ], "description": "The protobuf
request of the api, or its Builder: a PredictRequest for predict, a
ClassificationRequest or RegressionRequest, or the Input of one, for classify
and regress. Another body, such as text, JSON or bytes, is not read: an empty
request is sent and the server rejects [...]
+ "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "null", "pojo" ] }, { "option": "api",
"value": "model-metadata", "carriers": [ "null", "pojo" ] } ], "description":
"The protobuf request of the api, or its Builder: a PredictRequest for predict,
a ClassificationRequest or RegressionRequest, or the Input of one, for classify
and regress. Another body that is not empty, such as text, JSON, bytes or a
Map, is not parsed and [...]
"producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.GetModelStatus$GetModelStatusResponse" }, { "option":
"api", "value": "model-metadata", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.GetModelMetadata$GetModelMetadataResponse" }, { "option":
"api", "value": "classify", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.Classification$ClassificationResponse" }, { "op [...]
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tensorflow-serving-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tensorflow-serving-component.adoc
index 0f3d5b3b7e32..6b88daf4333b 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tensorflow-serving-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tensorflow-serving-component.adoc
@@ -102,6 +102,12 @@ tensorflow-serving:<api>[?options]
| `PredictResponse` footnote:[`tensorflow.serving.Predict.PredictResponse`]
|===
+The input body is the protobuf request of the API or its `Builder`, and for
`classify` and `regress` also an `Input`
+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-status` and `model-metadata` need.
+
== Examples
=== Model status API
@@ -210,6 +216,9 @@ YAML::
Currently, TensorFlow Serving only supports `signature_def` as the metadata
field.
See:
https://github.com/tensorflow/serving/blob/2.18.0/tensorflow_serving/apis/get_model_metadata.proto#L26-L28[get_model_metadata.proto]
+The component asks for `signature_def`, unless the `GetModelMetadataRequest`
in the body names the metadata fields
+itself.
+
.Fetch model metadata
[tabs]
diff --git
a/components/camel-ai/camel-tensorflow-serving/src/generated/resources/META-INF/org/apache/camel/component/tensorflow/serving/tensorflow-serving.json
b/components/camel-ai/camel-tensorflow-serving/src/generated/resources/META-INF/org/apache/camel/component/tensorflow/serving/tensorflow-serving.json
index d7692d3f6acd..43fbf4183aac 100644
---
a/components/camel-ai/camel-tensorflow-serving/src/generated/resources/META-INF/org/apache/camel/component/tensorflow/serving/tensorflow-serving.json
+++
b/components/camel-ai/camel-tensorflow-serving/src/generated/resources/META-INF/org/apache/camel/component/tensorflow/serving/tensorflow-serving.json
@@ -43,7 +43,7 @@
"CamelTensorFlowServingSignatureName": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A named signature to evaluate. If
unspecified, the default signature will be used.", "constantName":
"org.apache.camel.component.tensorflow.serving.TensorFlowServingConstants#SIGNATURE_NAME"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "any" ] }, { "option": "api", "value":
"model-metadata", "carriers": [ "any" ] } ], "description": "The protobuf
request of the api, or its Builder: a PredictRequest for predict, a
ClassificationRequest or RegressionRequest, or the Input of one, for classify
and regress. Another body, such as text, JSON or bytes, is not read: an empty
request is sent and the server rejects [...]
+ "producerIn": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "null", "pojo" ] }, { "option": "api",
"value": "model-metadata", "carriers": [ "null", "pojo" ] } ], "description":
"The protobuf request of the api, or its Builder: a PredictRequest for predict,
a ClassificationRequest or RegressionRequest, or the Input of one, for classify
and regress. Another body that is not empty, such as text, JSON, bytes or a
Map, is not parsed and [...]
"producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": "api",
"value": "model-status", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.GetModelStatus$GetModelStatusResponse" }, { "option":
"api", "value": "model-metadata", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.GetModelMetadata$GetModelMetadataResponse" }, { "option":
"api", "value": "classify", "carriers": [ "pojo" ], "javaType":
"tensorflow.serving.Classification$ClassificationResponse" }, { "op [...]
},
"properties": {
diff --git
a/components/camel-ai/camel-tensorflow-serving/src/main/docs/tensorflow-serving-component.adoc
b/components/camel-ai/camel-tensorflow-serving/src/main/docs/tensorflow-serving-component.adoc
index 0f3d5b3b7e32..6b88daf4333b 100644
---
a/components/camel-ai/camel-tensorflow-serving/src/main/docs/tensorflow-serving-component.adoc
+++
b/components/camel-ai/camel-tensorflow-serving/src/main/docs/tensorflow-serving-component.adoc
@@ -102,6 +102,12 @@ tensorflow-serving:<api>[?options]
| `PredictResponse` footnote:[`tensorflow.serving.Predict.PredictResponse`]
|===
+The input body is the protobuf request of the API or its `Builder`, and for
`classify` and `regress` also an `Input`
+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-status` and `model-metadata` need.
+
== Examples
=== Model status API
@@ -210,6 +216,9 @@ YAML::
Currently, TensorFlow Serving only supports `signature_def` as the metadata
field.
See:
https://github.com/tensorflow/serving/blob/2.18.0/tensorflow_serving/apis/get_model_metadata.proto#L26-L28[get_model_metadata.proto]
+The component asks for `signature_def`, unless the `GetModelMetadataRequest`
in the body names the metadata fields
+itself.
+
.Fetch model metadata
[tabs]
diff --git
a/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingEndpoint.java
b/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingEndpoint.java
index bbf106112137..204f13cec27a 100644
---
a/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingEndpoint.java
+++
b/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingEndpoint.java
@@ -42,16 +42,23 @@ import tensorflow.serving.PredictionServiceGrpc;
category = { Category.AI }, headersClass =
TensorFlowServingConstants.class)
@BodyTypes(producerIn = @BodyType(value = Carrier.POJO,
byOption = {
- @BodyType.When(option = "api", value
= "model-status", carriers = Carrier.ANY),
- @BodyType.When(option = "api", value
= "model-metadata", carriers = Carrier.ANY) },
+ @BodyType.When(option = "api", value
= "model-status",
+ carriers = {
Carrier.NULL, Carrier.POJO }),
+ @BodyType.When(option = "api", value
= "model-metadata",
+ carriers = {
Carrier.NULL, Carrier.POJO }) },
description = "The protobuf request of the
api, or its Builder: a PredictRequest for"
- + " predict, a
ClassificationRequest or RegressionRequest, or the Input of"
- + " one, for classify and
regress. Another body, such as text, JSON or"
- + " bytes, is not read: an
empty request is sent and the server rejects it."
- + " model-status and
model-metadata need no body, and read a"
- + " GetModelStatusRequest or
GetModelMetadataRequest when it is one. The"
- + " model comes from the
CamelTensorFlowServingModelName header, or else"
- + " the modelName option"),
+ + " predict, a
ClassificationRequest or RegressionRequest, or the Input"
+ + " of one, for classify and
regress. 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-status and"
+ + " model-metadata need; they
read a GetModelStatusRequest or"
+ + " GetModelMetadataRequest
when the body is one, and model-metadata"
+ + " asks for signature_def
unless the request names its metadata fields."
+ + " The model comes from the
CamelTensorFlowServingModelName header, or"
+ + " else the modelName
option"),
producerOut = @BodyType(value = Carrier.POJO,
byOption = {
@BodyType.When(option = "api",
value = "model-status", carriers = Carrier.POJO,
diff --git
a/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducer.java
b/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducer.java
index 398b3ef3281d..15e143280338 100644
---
a/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducer.java
+++
b/components/camel-ai/camel-tensorflow-serving/src/main/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducer.java
@@ -21,6 +21,7 @@ import java.util.Optional;
import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.Int64Value;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Message;
import org.apache.camel.support.DefaultProducer;
import tensorflow.serving.Classification;
@@ -34,6 +35,12 @@ import tensorflow.serving.RegressionOuterClass;
public class TensorFlowServingProducer 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 ModelServiceGrpc.ModelServiceBlockingStub modelService;
private final PredictionServiceGrpc.PredictionServiceBlockingStub
predictionService;
@@ -92,9 +99,35 @@ public class TensorFlowServingProducer extends
DefaultProducer {
return builder;
}
- private GetModelStatus.GetModelStatusResponse modelStatus(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();
- GetModelStatus.GetModelStatusRequest request =
message.getBody(GetModelStatus.GetModelStatusRequest.class);
+ 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 GetModelStatus.GetModelStatusResponse modelStatus(Exchange
exchange) throws InvalidPayloadException {
+ GetModelStatus.GetModelStatusRequest request = requestOf(exchange,
GetModelStatus.GetModelStatusRequest.class,
+ "a GetModelStatusRequest or its Builder" + OR_NO_BODY);
GetModelStatus.GetModelStatusRequest.Builder builder =
GetModelStatus.GetModelStatusRequest.newBuilder()
.setModelSpec(modelSpec(exchange));
if (request != null) {
@@ -103,21 +136,26 @@ public class TensorFlowServingProducer extends
DefaultProducer {
return modelService.getModelStatus(builder.build());
}
- private GetModelMetadata.GetModelMetadataResponse modelMetadata(Exchange
exchange) {
- Message message = exchange.getMessage();
- GetModelMetadata.GetModelMetadataRequest request =
message.getBody(GetModelMetadata.GetModelMetadataRequest.class);
+ private GetModelMetadata.GetModelMetadataResponse modelMetadata(Exchange
exchange) throws InvalidPayloadException {
+ GetModelMetadata.GetModelMetadataRequest request = requestOf(exchange,
+ GetModelMetadata.GetModelMetadataRequest.class,
+ "a GetModelMetadataRequest or its Builder" + OR_NO_BODY);
GetModelMetadata.GetModelMetadataRequest.Builder builder =
GetModelMetadata.GetModelMetadataRequest.newBuilder()
- .setModelSpec(modelSpec(exchange))
- .addMetadataField("signature_def");
+ .setModelSpec(modelSpec(exchange));
if (request != null) {
builder.mergeFrom(request);
}
+ // the metadata fields of the request, or else signature_def, the only
field TensorFlow Serving supports
+ // (merging appends a repeated field, so adding it before the merge
would ask for it twice)
+ if (builder.getMetadataFieldCount() == 0) {
+ builder.addMetadataField("signature_def");
+ }
return predictionService.getModelMetadata(builder.build());
}
- private Classification.ClassificationResponse classify(Exchange exchange) {
- Message message = exchange.getMessage();
- Classification.ClassificationRequest request =
message.getBody(Classification.ClassificationRequest.class);
+ private Classification.ClassificationResponse classify(Exchange exchange)
throws InvalidPayloadException {
+ Classification.ClassificationRequest request = requestOf(exchange,
Classification.ClassificationRequest.class,
+ "a ClassificationRequest, an Input, or the Builder of one" +
BUILD_THE_REQUEST);
Classification.ClassificationRequest.Builder builder =
Classification.ClassificationRequest.newBuilder()
.setModelSpec(modelSpec(exchange));
if (request != null) {
@@ -126,9 +164,10 @@ public class TensorFlowServingProducer extends
DefaultProducer {
return predictionService.classify(builder.build());
}
- private RegressionOuterClass.RegressionResponse regress(Exchange exchange)
{
- Message message = exchange.getMessage();
- RegressionOuterClass.RegressionRequest request =
message.getBody(RegressionOuterClass.RegressionRequest.class);
+ private RegressionOuterClass.RegressionResponse regress(Exchange exchange)
throws InvalidPayloadException {
+ RegressionOuterClass.RegressionRequest request = requestOf(
+ exchange, RegressionOuterClass.RegressionRequest.class,
+ "a RegressionRequest, an Input, or the Builder of one" +
BUILD_THE_REQUEST);
RegressionOuterClass.RegressionRequest.Builder builder =
RegressionOuterClass.RegressionRequest.newBuilder()
.setModelSpec(modelSpec(exchange));
if (request != null) {
@@ -137,9 +176,9 @@ public class TensorFlowServingProducer extends
DefaultProducer {
return predictionService.regress(builder.build());
}
- private Predict.PredictResponse predict(Exchange exchange) {
- Message message = exchange.getMessage();
- Predict.PredictRequest request =
message.getBody(Predict.PredictRequest.class);
+ private Predict.PredictResponse predict(Exchange exchange) throws
InvalidPayloadException {
+ Predict.PredictRequest request = requestOf(exchange,
Predict.PredictRequest.class,
+ "a PredictRequest or its Builder" + BUILD_THE_REQUEST);
Predict.PredictRequest.Builder builder =
Predict.PredictRequest.newBuilder()
.setModelSpec(modelSpec(exchange));
if (request != null) {
diff --git
a/components/camel-ai/camel-tensorflow-serving/src/test/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducerBodyTest.java
b/components/camel-ai/camel-tensorflow-serving/src/test/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducerBodyTest.java
new file mode 100644
index 000000000000..a986a9ca257a
--- /dev/null
+++
b/components/camel-ai/camel-tensorflow-serving/src/test/java/org/apache/camel/component/tensorflow/serving/TensorFlowServingProducerBodyTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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.tensorflow.serving;
+
+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 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 org.tensorflow.example.Example;
+import org.tensorflow.example.Feature;
+import org.tensorflow.example.Features;
+import org.tensorflow.example.FloatList;
+import org.tensorflow.framework.DataType;
+import org.tensorflow.framework.TensorProto;
+import tensorflow.serving.Classification;
+import tensorflow.serving.GetModelMetadata;
+import tensorflow.serving.GetModelStatus;
+import tensorflow.serving.InputOuterClass;
+import tensorflow.serving.Model;
+import tensorflow.serving.ModelServiceGrpc;
+import tensorflow.serving.Predict;
+import tensorflow.serving.PredictionServiceGrpc;
+import tensorflow.serving.RegressionOuterClass;
+
+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 TensorFlowServingProducerBodyTest extends CamelTestSupport {
+
+ private static final String MODEL = "half_plus_two";
+ private static final String JSON = "{\"inputs\": {\"x\": [1.0, 2.0,
5.0]}}";
+
+ 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 RecordingModelService())
+ .addService(new RecordingPredictionService())
+ .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("tensorflow-serving",
TensorFlowServingComponent.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", Map.of("x", List.of(1.0,
2.0, 5.0)));
+ return Stream.of(
+ Arguments.of("predict", Predict.PredictRequest.class, JSON),
+ Arguments.of("predict", Predict.PredictRequest.class, bytes),
+ Arguments.of("predict", Predict.PredictRequest.class, map),
+ Arguments.of("classify",
Classification.ClassificationRequest.class, JSON),
+ // the request of another api
+ Arguments.of("classify",
Classification.ClassificationRequest.class, predictRequest()),
+ Arguments.of("regress",
RegressionOuterClass.RegressionRequest.class, map),
+ Arguments.of("model-status",
GetModelStatus.GetModelStatusRequest.class, MODEL),
+ Arguments.of("model-metadata",
GetModelMetadata.GetModelMetadataRequest.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 predictSendsTheRequestOfTheBody() {
+ Predict.PredictRequest request = predictRequest();
+
+ Exchange exchange = send("predict", request);
+
+ assertNull(exchange.getException());
+ Predict.PredictRequest sent = sent(Predict.PredictRequest.class);
+ assertEquals(MODEL, sent.getModelSpec().getName());
+ assertEquals(request.getInputsMap(), sent.getInputsMap());
+ assertInstanceOf(Predict.PredictResponse.class,
exchange.getMessage().getBody());
+ }
+
+ @Test
+ void predictSendsTheRequestOfABuilderBody() {
+ Predict.PredictRequest.Builder builder = predictRequest().toBuilder();
+
+ Exchange exchange = send("predict", builder);
+
+ assertNull(exchange.getException());
+ assertEquals(builder.getInputsMap(),
sent(Predict.PredictRequest.class).getInputsMap());
+ }
+
+ @Test
+ void predictWithoutBodySendsTheModelOnly() {
+ Exchange exchange = send("predict", null);
+
+ assertNull(exchange.getException());
+ Predict.PredictRequest sent = sent(Predict.PredictRequest.class);
+ assertEquals(MODEL, sent.getModelSpec().getName());
+ assertEquals(0, sent.getInputsCount());
+ }
+
+ @Test
+ void classifySendsTheInputOfTheBody() {
+ InputOuterClass.Input input = input();
+
+ Exchange exchange = send("classify", input);
+
+ assertNull(exchange.getException());
+ Classification.ClassificationRequest sent =
sent(Classification.ClassificationRequest.class);
+ assertEquals(MODEL, sent.getModelSpec().getName());
+ assertEquals(input, sent.getInput());
+ }
+
+ @Test
+ void regressSendsTheInputOfTheBody() {
+ InputOuterClass.Input input = input();
+
+ Exchange exchange = send("regress", input);
+
+ assertNull(exchange.getException());
+ assertEquals(input,
sent(RegressionOuterClass.RegressionRequest.class).getInput());
+ }
+
+ @Test
+ void modelStatusWithoutBodyAsksForTheModelOfTheOptions() {
+ Exchange exchange = send("model-status", null);
+
+ assertNull(exchange.getException());
+ assertEquals(MODEL,
sent(GetModelStatus.GetModelStatusRequest.class).getModelSpec().getName());
+ assertInstanceOf(GetModelStatus.GetModelStatusResponse.class,
exchange.getMessage().getBody());
+ }
+
+ static Stream<Object> emptyBodies() {
+ return Stream.of("", " \n", new byte[0]);
+ }
+
+ @ParameterizedTest
+ @MethodSource("emptyBodies")
+ void modelStatusWithAnEmptyBodyAsksForTheModelOfTheOptions(Object body) {
+ Exchange exchange = send("model-status", body);
+
+ assertNull(exchange.getException());
+ assertEquals(MODEL,
sent(GetModelStatus.GetModelStatusRequest.class).getModelSpec().getName());
+ }
+
+ @Test
+ void modelMetadataWithoutBodyAsksForSignatureDef() {
+ Exchange exchange = send("model-metadata", null);
+
+ assertNull(exchange.getException());
+ GetModelMetadata.GetModelMetadataRequest sent =
sent(GetModelMetadata.GetModelMetadataRequest.class);
+ assertEquals(MODEL, sent.getModelSpec().getName());
+ assertEquals(List.of("signature_def"), sent.getMetadataFieldList());
+ }
+
+ @Test
+ void modelMetadataAsksOnceForSignatureDefWhenTheRequestNamesIt() {
+ GetModelMetadata.GetModelMetadataRequest request =
GetModelMetadata.GetModelMetadataRequest.newBuilder()
+ .addMetadataField("signature_def")
+ .build();
+
+ Exchange exchange = send("model-metadata", request);
+
+ assertNull(exchange.getException());
+ GetModelMetadata.GetModelMetadataRequest sent =
sent(GetModelMetadata.GetModelMetadataRequest.class);
+ assertEquals(MODEL, sent.getModelSpec().getName());
+ assertEquals(List.of("signature_def"), sent.getMetadataFieldList());
+ }
+
+ @Test
+ void modelMetadataAsksForSignatureDefWhenTheRequestNamesNoField() {
+ GetModelMetadata.GetModelMetadataRequest request =
GetModelMetadata.GetModelMetadataRequest.newBuilder()
+ .setModelSpec(Model.ModelSpec.newBuilder().setName("other"))
+ .build();
+
+ Exchange exchange = send("model-metadata", request);
+
+ assertNull(exchange.getException());
+ GetModelMetadata.GetModelMetadataRequest sent =
sent(GetModelMetadata.GetModelMetadataRequest.class);
+ assertEquals("other", sent.getModelSpec().getName());
+ assertEquals(List.of("signature_def"), sent.getMetadataFieldList());
+ }
+
+ private Exchange send(String api, Object body) {
+ return template.send("tensorflow-serving:" + 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 Predict.PredictRequest predictRequest() {
+ return Predict.PredictRequest.newBuilder()
+ .putInputs("x", TensorProto.newBuilder()
+ .setDtype(DataType.DT_FLOAT)
+ .addFloatVal(1.0f)
+ .addFloatVal(2.0f)
+ .addFloatVal(5.0f)
+ .build())
+ .build();
+ }
+
+ private static InputOuterClass.Input input() {
+ return InputOuterClass.Input.newBuilder()
+ .setExampleList(InputOuterClass.ExampleList.newBuilder()
+ .addExamples(Example.newBuilder()
+ .setFeatures(Features.newBuilder()
+ .putFeature("x", Feature.newBuilder()
+
.setFloatList(FloatList.newBuilder().addValue(1.0f))
+ .build()))))
+ .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 RecordingModelService extends
ModelServiceGrpc.ModelServiceImplBase {
+
+ @Override
+ public void getModelStatus(
+ GetModelStatus.GetModelStatusRequest request,
+ StreamObserver<GetModelStatus.GetModelStatusResponse>
observer) {
+ reply(request,
GetModelStatus.GetModelStatusResponse.getDefaultInstance(), observer);
+ }
+ }
+
+ private static final class RecordingPredictionService extends
PredictionServiceGrpc.PredictionServiceImplBase {
+
+ @Override
+ public void classify(
+ Classification.ClassificationRequest request,
+ StreamObserver<Classification.ClassificationResponse>
observer) {
+ Classification.ClassificationResponse response =
Classification.ClassificationResponse.newBuilder()
+ .setModelSpec(request.getModelSpec())
+ .build();
+ reply(request, response, observer);
+ }
+
+ @Override
+ public void regress(
+ RegressionOuterClass.RegressionRequest request,
+ StreamObserver<RegressionOuterClass.RegressionResponse>
observer) {
+ RegressionOuterClass.RegressionResponse response =
RegressionOuterClass.RegressionResponse.newBuilder()
+ .setModelSpec(request.getModelSpec())
+ .build();
+ reply(request, response, observer);
+ }
+
+ @Override
+ public void predict(Predict.PredictRequest request,
StreamObserver<Predict.PredictResponse> observer) {
+ reply(request,
Predict.PredictResponse.newBuilder().setModelSpec(request.getModelSpec()).build(),
observer);
+ }
+
+ @Override
+ public void getModelMetadata(
+ GetModelMetadata.GetModelMetadataRequest request,
+ StreamObserver<GetModelMetadata.GetModelMetadataResponse>
observer) {
+ reply(request,
+
GetModelMetadata.GetModelMetadataResponse.newBuilder().setModelSpec(request.getModelSpec()).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 092914b318d6..7350bf33ef9b 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
@@ -4359,6 +4359,22 @@ To keep the previous behaviour, filter the exchanges
with a bad status out right
`filter(simple("${body.statusCode.bad} == false"))`. This also drops the other
bad statuses, such as the refusal of a
node, and the current values delivered after an outage still arrive.
+=== camel-tensorflow-serving - a body that is not the request of the api fails
+
+The `tensorflow-serving` producer converted the body 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
+`predict`, `classify` and `regress` failed on the server with an error about
the missing inputs, and `model-status`
+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-status` or `model-metadata`.
+
+No body, a blank `String` or an empty `byte[]` still sends a request built
from the options and headers, and a body
+that is the request or its `Builder`, or an `Input` for `classify` and
`regress`, is sent as before.
+
+`model-metadata` asked for `signature_def` in addition to the metadata fields
of a `GetModelMetadataRequest` body, so
+a request that named `signature_def` asked for it twice. It now asks for
`signature_def` only when the request names
+no metadata field.
+
=== camel-milo - an awaited write, call or read that fails now fails the
exchange
An awaited `milo-client` write, method call or read that fails at the service
or transport level, for example a