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 a72119122ec1 CAMEL-26266: camel-huaweicloud-functiongraph - keep the 
whole result when it has no body field, and take a JSON object event as bytes, 
a stream or a file (#28965)
a72119122ec1 is described below

commit a72119122ec14fed4580c81586b5f0f8ae7652d9
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 22:43:36 2026 +0100

    CAMEL-26266: camel-huaweicloud-functiongraph - keep the whole result when 
it has no body field, and take a JSON object event as bytes, a stream or a file 
(#28965)
    
    * CAMEL-26266: camel-huaweicloud-functiongraph - keep the whole result when 
it has no body field, and take a JSON object event as bytes, a stream or a file
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../catalog/components/hwcloud-functiongraph.json  |   4 +-
 .../docs/hwcloud-functiongraph-component.adoc      |  15 ++
 .../org/apache/camel/hwcloud-functiongraph.json    |   4 +-
 .../main/docs/hwcloud-functiongraph-component.adoc |  15 ++
 .../org/apache/camel/FunctionGraphEndpoint.java    |  20 +-
 .../org/apache/camel/FunctionGraphProducer.java    |  72 +++++--
 .../java/org/apache/camel/FunctionGraphUtils.java  |  38 +++-
 .../org/apache/camel/InvokeFunctionBodyTest.java   | 234 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   9 +
 9 files changed, 378 insertions(+), 33 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hwcloud-functiongraph.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hwcloud-functiongraph.json
index d4329e6c5174..5afdf192cc73 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hwcloud-functiongraph.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hwcloud-functiongraph.json
@@ -28,8 +28,8 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "map", "text" ], "description": "The event 
of the function, as a Map or a JSON object as text. Bytes, a stream, a file, a 
JSON array, a POJO or no body fail: convert the body to a String or a Map 
first" },
-    "producerOut": { "carriers": [ "text", "null" ], "description": "The body 
field of the result of the function as text: a string as it is, an object or an 
array as JSON. No body when the result has no body field. With the 
CamelHwCloudFgXCffLogType exchange property set, the log of the call is in the 
CamelHwCloudFgXCffLogs exchange property" }
+    "producerIn": { "carriers": [ "map", "text", "bytes", "stream", "file" ], 
"description": "The event of the function, which FunctionGraph takes as a JSON 
object: a Map, or a JSON object as text, bytes, a stream or a file. A JSON 
array or another JSON value fails: wrap it in a JSON object. A POJO fails 
unless its text is JSON: marshal it to JSON first. No body fails" },
+    "producerOut": { "carriers": [ "text", "null" ], "description": "The 
result of the function as text. When the result is a JSON object with a 
top-level body field, such as the result of a function behind an API Gateway 
trigger, the body field: a string as it is, an object or an array as JSON, and 
no body when the field is null. Otherwise the whole result as it is. No body 
when the function gives no result. With the CamelHwCloudFgXCffLogType exchange 
property set, the log of the call i [...]
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "producer", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Operation to be performed" 
},
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-functiongraph-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-functiongraph-component.adoc
index 24711b118adf..243daa75a167 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-functiongraph-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-functiongraph-component.adoc
@@ -76,6 +76,21 @@ If the operation, function name, or function package are 
set, they will override
 
 - invokeFunction - to invoke a serverless function
 
+=== Message body
+
+The body of the message is the event of the function, which FunctionGraph 
takes as a JSON object. It can be a `Map`,
+or a JSON object as a `String`, bytes, a stream or a file. A JSON array or 
another JSON value that is not an object
+fails with an `IllegalArgumentException`: wrap it in a JSON object, such as 
`{"items": [1, 2, 3]}`. Marshal a POJO to
+JSON first.
+
+After the call, the body of the message is the result of the function as text:
+
+* When the result is a JSON object with a top-level `body` field, such as the 
result of a function behind an API
+Gateway trigger (`{"statusCode": 200, "headers": {...}, "body": "..."}`), the 
body is that field: a string as it is,
+an object or an array as JSON. The body is null when the field is null.
+* Otherwise, the body is the whole result as it is, such as a JSON object 
without a `body` field, a JSON array or
+plain text.
+
 === Using ServiceKey Configuration Bean
 
 Access key and secret keys are required to authenticate against cloud 
FunctionGraph service. You can avoid having them being exposed
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/generated/resources/META-INF/org/apache/camel/hwcloud-functiongraph.json
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/generated/resources/META-INF/org/apache/camel/hwcloud-functiongraph.json
index d4329e6c5174..5afdf192cc73 100644
--- 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/generated/resources/META-INF/org/apache/camel/hwcloud-functiongraph.json
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/generated/resources/META-INF/org/apache/camel/hwcloud-functiongraph.json
@@ -28,8 +28,8 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "map", "text" ], "description": "The event 
of the function, as a Map or a JSON object as text. Bytes, a stream, a file, a 
JSON array, a POJO or no body fail: convert the body to a String or a Map 
first" },
-    "producerOut": { "carriers": [ "text", "null" ], "description": "The body 
field of the result of the function as text: a string as it is, an object or an 
array as JSON. No body when the result has no body field. With the 
CamelHwCloudFgXCffLogType exchange property set, the log of the call is in the 
CamelHwCloudFgXCffLogs exchange property" }
+    "producerIn": { "carriers": [ "map", "text", "bytes", "stream", "file" ], 
"description": "The event of the function, which FunctionGraph takes as a JSON 
object: a Map, or a JSON object as text, bytes, a stream or a file. A JSON 
array or another JSON value fails: wrap it in a JSON object. A POJO fails 
unless its text is JSON: marshal it to JSON first. No body fails" },
+    "producerOut": { "carriers": [ "text", "null" ], "description": "The 
result of the function as text. When the result is a JSON object with a 
top-level body field, such as the result of a function behind an API Gateway 
trigger, the body field: a string as it is, an object or an array as JSON, and 
no body when the field is null. Otherwise the whole result as it is. No body 
when the function gives no result. With the CamelHwCloudFgXCffLogType exchange 
property set, the log of the call i [...]
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "producer", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Operation to be performed" 
},
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/docs/hwcloud-functiongraph-component.adoc
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/docs/hwcloud-functiongraph-component.adoc
index 24711b118adf..243daa75a167 100644
--- 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/docs/hwcloud-functiongraph-component.adoc
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/docs/hwcloud-functiongraph-component.adoc
@@ -76,6 +76,21 @@ If the operation, function name, or function package are 
set, they will override
 
 - invokeFunction - to invoke a serverless function
 
+=== Message body
+
+The body of the message is the event of the function, which FunctionGraph 
takes as a JSON object. It can be a `Map`,
+or a JSON object as a `String`, bytes, a stream or a file. A JSON array or 
another JSON value that is not an object
+fails with an `IllegalArgumentException`: wrap it in a JSON object, such as 
`{"items": [1, 2, 3]}`. Marshal a POJO to
+JSON first.
+
+After the call, the body of the message is the result of the function as text:
+
+* When the result is a JSON object with a top-level `body` field, such as the 
result of a function behind an API
+Gateway trigger (`{"statusCode": 200, "headers": {...}, "body": "..."}`), the 
body is that field: a string as it is,
+an object or an array as JSON. The body is null when the field is null.
+* Otherwise, the body is the whole result as it is, such as a JSON object 
without a `body` field, a JSON array or
+plain text.
+
 === Using ServiceKey Configuration Bean
 
 Access key and secret keys are required to authenticate against cloud 
FunctionGraph service. You can avoid having them being exposed
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphEndpoint.java
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphEndpoint.java
index 2df0b0625ef1..89eb03cd4b44 100644
--- 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphEndpoint.java
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphEndpoint.java
@@ -38,15 +38,19 @@ import org.apache.camel.util.ObjectHelper;
 @UriEndpoint(firstVersion = "3.11.0", scheme = "hwcloud-functiongraph", title 
= "Huawei FunctionGraph",
              syntax = "hwcloud-functiongraph:operation",
              category = { Category.CLOUD, Category.SERVERLESS }, producerOnly 
= true)
-@BodyTypes(producerIn = @BodyType(value = { Carrier.MAP, Carrier.TEXT },
-                                  description = "The event of the function, as 
a Map or a JSON object as text. Bytes, a stream,"
-                                                + " a file, a JSON array, a 
POJO or no body fail: convert the body to a String or"
-                                                + " a Map first"),
+@BodyTypes(producerIn = @BodyType(value = { Carrier.MAP, Carrier.TEXT, 
Carrier.BYTES, Carrier.STREAM, Carrier.FILE },
+                                  description = "The event of the function, 
which FunctionGraph takes as a JSON object: a Map,"
+                                                + " or a JSON object as text, 
bytes, a stream or a file. A JSON array or another"
+                                                + " JSON value fails: wrap it 
in a JSON object. A POJO fails unless its text is"
+                                                + " JSON: marshal it to JSON 
first. No body fails"),
            producerOut = @BodyType(value = { Carrier.TEXT, Carrier.NULL },
-                                   description = "The body field of the result 
of the function as text: a string as it is, an"
-                                                 + " object or an array as 
JSON. No body when the result has no body field. With"
-                                                 + " the 
CamelHwCloudFgXCffLogType exchange property set, the log of the call is in"
-                                                 + " the 
CamelHwCloudFgXCffLogs exchange property"))
+                                   description = "The result of the function 
as text. When the result is a JSON object with a"
+                                                 + " top-level body field, 
such as the result of a function behind an API Gateway"
+                                                 + " trigger, the body field: 
a string as it is, an object or an array as JSON,"
+                                                 + " and no body when the 
field is null. Otherwise the whole result as it is. No"
+                                                 + " body when the function 
gives no result. With the CamelHwCloudFgXCffLogType"
+                                                 + " exchange property set, 
the log of the call is in the CamelHwCloudFgXCffLogs"
+                                                 + " exchange property"))
 public class FunctionGraphEndpoint extends DefaultEndpoint {
 
     @UriPath(description = "Operation to be performed", displayName = 
"Operation", label = "producer")
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphProducer.java
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphProducer.java
index 3d09bb1ef80e..90b46d51fefb 100644
--- 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphProducer.java
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphProducer.java
@@ -16,10 +16,11 @@
  */
 package org.apache.camel;
 
-import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.huaweicloud.sdk.functiongraph.v2.FunctionGraphClient;
 import com.huaweicloud.sdk.functiongraph.v2.model.InvokeFunctionRequest;
@@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory;
 
 public class FunctionGraphProducer extends DefaultProducer {
     private static final Logger LOG = 
LoggerFactory.getLogger(FunctionGraphProducer.class);
+    private static final ObjectMapper MAPPER = new ObjectMapper();
     private FunctionGraphEndpoint endpoint;
     private FunctionGraphClient functionGraphClient;
 
@@ -70,23 +72,11 @@ public class FunctionGraphProducer extends DefaultProducer {
      * @param exchange
      * @param clientConfigurations
      */
-    private void invokeFunction(Exchange exchange, ClientConfigurations 
clientConfigurations) {
+    private void invokeFunction(Exchange exchange, ClientConfigurations 
clientConfigurations)
+            throws InvalidPayloadException {
 
-        // convert exchange body to Map object
-        Object body = exchange.getMessage().getBody();
-        Map request;
-        if (body instanceof Map) {
-            request = exchange.getMessage().getBody(Map.class);
-        } else if (body instanceof String) {
-            String strBody = exchange.getMessage().getBody(String.class);
-            try {
-                request = new ObjectMapper().readValue(strBody, HashMap.class);
-            } catch (JsonProcessingException e) {
-                throw new IllegalArgumentException("Request body must be a 
JSON or a HashMap");
-            }
-        } else {
-            throw new IllegalArgumentException("Exchange body is mandatory and 
should be a valid Map or JSON string");
-        }
+        // the event of the function
+        Map<String, Object> request = toFunctionEvent(exchange.getMessage());
 
         // checking for function name and function package
         if (ObjectHelper.isEmpty(clientConfigurations.getFunctionName())) {
@@ -104,9 +94,8 @@ public class FunctionGraphProducer extends DefaultProducer {
         }
 
         InvokeFunctionResponse response = 
functionGraphClient.invokeFunction(invokeFunctionRequest);
-        String responseBody
-                = 
FunctionGraphUtils.extractJsonFieldAsString(response.getResult(), 
FunctionGraphConstants.RESPONSE_BODY);
-        exchange.getMessage().setBody(responseBody);
+        // the body field of the result, or the whole result when it has no 
body field
+        
exchange.getMessage().setBody(FunctionGraphUtils.extractResultBody(response.getResult()));
         if (ObjectHelper.isNotEmpty(clientConfigurations.getXCffLogType())) {
             exchange.setProperty(FunctionGraphProperties.XCFFLOGS, 
response.getLog());
         }
@@ -114,6 +103,49 @@ public class FunctionGraphProducer extends DefaultProducer 
{
         LOG.debug("Invoke Function results: {}", response);
     }
 
+    /**
+     * The event of the function, which FunctionGraph takes as a JSON object 
(the InvokeFunctionRequest of the SDK takes
+     * a Map): a Map as it is, or another body converted to text first, so 
that a JSON object as a String, bytes, a
+     * stream or a file works. Another JSON value, such as an array, fails.
+     *
+     * @param  message                 the message
+     * @return                         the event of the function
+     * @throws InvalidPayloadException when the body cannot be converted to 
text
+     */
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> toFunctionEvent(Message message) throws 
InvalidPayloadException {
+        Object body = message.getBody();
+        if (body == null) {
+            throw new IllegalArgumentException(
+                    "Exchange body is mandatory and should be a Map or a JSON 
object (as a String, bytes, a stream or a file)");
+        }
+        if (body instanceof Map) {
+            return message.getBody(Map.class);
+        }
+
+        String json = message.getMandatoryBody(String.class);
+        JsonNode event;
+        try {
+            event = MAPPER.readTree(json);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Exchange body should be a Map or a JSON object, but the 
body (" + body.getClass().getName()
+                                               + ") is not valid JSON",
+                    e);
+        }
+        if (event == null || event.isMissingNode()) {
+            throw new IllegalArgumentException("Exchange body should be a Map 
or a JSON object, but the body is empty");
+        }
+        if (!event.isObject()) {
+            throw new IllegalArgumentException(
+                    "Exchange body should be a Map or a JSON object, as 
FunctionGraph takes a JSON object as the event of the"
+                                               + " function, but the body is a 
JSON "
+                                               + 
event.getNodeType().name().toLowerCase(Locale.ROOT)
+                                               + ": wrap it in a JSON object");
+        }
+        return MAPPER.convertValue(event, Map.class);
+    }
+
     /**
      * Update dynamic client configurations. Some endpoint parameters 
(operation, function name, package, and
      * XCFFLogType) can also be passed via exchange properties, so they can be 
updated between each transaction. Since
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphUtils.java
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphUtils.java
index b895a6b92e1f..8a13720934ac 100644
--- 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphUtils.java
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/main/java/org/apache/camel/FunctionGraphUtils.java
@@ -19,6 +19,9 @@ package org.apache.camel;
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import org.apache.camel.constants.FunctionGraphConstants;
 import org.apache.camel.models.ClientConfigurations;
 
 public final class FunctionGraphUtils {
@@ -39,7 +42,40 @@ public final class FunctionGraphUtils {
         if (root == null) {
             return null;
         }
-        JsonElement field = root.get(fieldName);
+        return fieldAsString(root.get(fieldName));
+    }
+
+    /**
+     * Gets the body of the message from the result of a function. When the 
result is a JSON object with a top-level
+     * body field, such as the result of a function behind an API Gateway 
trigger, this is that field: a string as it
+     * is, an object or an array as JSON, and null when the field is null. 
Otherwise this is the whole result as it is,
+     * such as a JSON object without a body field, a JSON array or plain text, 
and null when there is no result.
+     *
+     * @param  result the result of the function
+     * @return        the body of the message
+     */
+    public static String extractResultBody(String result) {
+        if (result == null) {
+            return null;
+        }
+        JsonElement root;
+        try {
+            root = JsonParser.parseString(result);
+        } catch (JsonParseException e) {
+            // not JSON, such as plain text: keep the whole result
+            return result;
+        }
+        if (root.isJsonNull()) {
+            // an empty result or a JSON null
+            return null;
+        }
+        if (root.isJsonObject() && 
root.getAsJsonObject().has(FunctionGraphConstants.RESPONSE_BODY)) {
+            return 
fieldAsString(root.getAsJsonObject().get(FunctionGraphConstants.RESPONSE_BODY));
+        }
+        return result;
+    }
+
+    private static String fieldAsString(JsonElement field) {
         if (field == null || field.isJsonNull()) {
             return null;
         }
diff --git 
a/components/camel-huawei/camel-huaweicloud-functiongraph/src/test/java/org/apache/camel/InvokeFunctionBodyTest.java
 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/test/java/org/apache/camel/InvokeFunctionBodyTest.java
new file mode 100644
index 000000000000..73546705a8ec
--- /dev/null
+++ 
b/components/camel-huawei/camel-huaweicloud-functiongraph/src/test/java/org/apache/camel/InvokeFunctionBodyTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.huaweicloud.sdk.functiongraph.v2.FunctionGraphClient;
+import com.huaweicloud.sdk.functiongraph.v2.model.InvokeFunctionRequest;
+import com.huaweicloud.sdk.functiongraph.v2.model.InvokeFunctionResponse;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.FileConsumer;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The event the producer sends to the function and the body it gives from the 
result, with a client that records the
+ * request.
+ */
+class InvokeFunctionBodyTest extends CamelTestSupport {
+
+    private static final String EVENT
+            = 
"{\"department\":\"sales\",\"product\":\"monitors\",\"price\":20.13,\"quantity\":20,\"note\":\"5
 € off\"}";
+    private static final Map<String, Object> EVENT_MAP = Map.of(
+            "department", "sales", "product", "monitors", "price", 20.13, 
"quantity", 20, "note", "5 € off");
+
+    @TempDir
+    Path tempDir;
+
+    TestConfiguration testConfiguration = new TestConfiguration();
+
+    @BindToRegistry("functionGraphClient")
+    RecordingClient client = new RecordingClient();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:invoke")
+                        .to("hwcloud-functiongraph:invokeFunction?" +
+                            "accessKey=" + 
testConfiguration.getProperty("accessKey") +
+                            "&secretKey=" + 
testConfiguration.getProperty("secretKey") +
+                            "&projectId=" + 
testConfiguration.getProperty("projectId") +
+                            "&region=" + 
testConfiguration.getProperty("region") +
+                            "&functionName=" + 
testConfiguration.getProperty("functionName") +
+                            "&functionGraphClient=#functionGraphClient");
+            }
+        };
+    }
+
+    @Test
+    void stringEventIsSentAsJsonObject() {
+        template.requestBody("direct:invoke", EVENT);
+
+        assertEquals(EVENT_MAP, client.request.getBody());
+    }
+
+    @Test
+    void bytesEventIsSentAsJsonObject() {
+        template.requestBody("direct:invoke", 
EVENT.getBytes(StandardCharsets.UTF_8));
+
+        assertEquals(EVENT_MAP, client.request.getBody());
+    }
+
+    @Test
+    void streamEventIsSentAsJsonObject() {
+        template.requestBody("direct:invoke", new 
ByteArrayInputStream(EVENT.getBytes(StandardCharsets.UTF_8)));
+
+        assertEquals(EVENT_MAP, client.request.getBody());
+    }
+
+    @Test
+    void fileEventIsSentAsJsonObject() throws Exception {
+        File file = Files.writeString(tempDir.resolve("event.json"), EVENT, 
StandardCharsets.UTF_8).toFile();
+
+        template.requestBody("direct:invoke", file);
+
+        assertEquals(EVENT_MAP, client.request.getBody());
+    }
+
+    @Test
+    void genericFileEventIsSentAsJsonObject() throws Exception {
+        File file = Files.writeString(tempDir.resolve("event.json"), EVENT, 
StandardCharsets.UTF_8).toFile();
+
+        template.requestBody("direct:invoke", 
FileConsumer.asGenericFile(tempDir.toString(), file, "UTF-8", false));
+
+        assertEquals(EVENT_MAP, client.request.getBody());
+    }
+
+    @Test
+    void mapEventIsSentAsItIs() {
+        Map<String, Object> event = new HashMap<>(EVENT_MAP);
+
+        template.requestBody("direct:invoke", event);
+
+        assertSame(event, client.request.getBody());
+    }
+
+    @Test
+    void jsonArrayEventFails() {
+        IllegalArgumentException e = 
assertInvokeFails("[{\"id\":1},{\"id\":2}]");
+
+        assertTrue(e.getMessage().contains("the body is a JSON array: wrap it 
in a JSON object"), e.getMessage());
+    }
+
+    @Test
+    void jsonNumberEventAsBytesFails() {
+        IllegalArgumentException e = 
assertInvokeFails("42".getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(e.getMessage().contains("the body is a JSON number: wrap it 
in a JSON object"), e.getMessage());
+    }
+
+    @Test
+    void invalidJsonEventFails() {
+        IllegalArgumentException e = assertInvokeFails("not 
json".getBytes(StandardCharsets.UTF_8));
+
+        assertTrue(e.getMessage().contains("the body ([B) is not valid JSON"), 
e.getMessage());
+    }
+
+    @Test
+    void pojoEventFails() {
+        IllegalArgumentException e = assertInvokeFails(new Order("monitors", 
20));
+
+        assertTrue(e.getMessage().contains("the body (" + 
Order.class.getName() + ") is not valid JSON"), e.getMessage());
+    }
+
+    @Test
+    void noEventFails() {
+        IllegalArgumentException e = assertInvokeFails(null);
+
+        assertTrue(e.getMessage().contains("Exchange body is mandatory"), 
e.getMessage());
+    }
+
+    @Test
+    void resultWithoutBodyFieldIsTheBody() {
+        client.result = "{\"orderId\":1621950031517,\"status\":\"order 
submitted successfully\"}";
+
+        assertEquals(client.result, template.requestBody("direct:invoke", 
EVENT, String.class));
+    }
+
+    @Test
+    void jsonArrayResultIsTheBody() {
+        client.result = "[{\"id\":1},{\"id\":2}]";
+
+        assertEquals(client.result, template.requestBody("direct:invoke", 
EVENT, String.class));
+    }
+
+    @Test
+    void plainTextResultIsTheBody() {
+        client.result = "Hello, World!";
+
+        assertEquals(client.result, template.requestBody("direct:invoke", 
EVENT, String.class));
+    }
+
+    @Test
+    void bodyFieldOfResultIsTheBody() {
+        client.result = 
"{\"statusCode\":200,\"isBase64Encoded\":false,\"body\":\"{\\\"key\\\": 
\\\"value\\\"}\","
+                        + 
"\"headers\":{\"Content-Type\":\"application/json\"}}";
+
+        assertEquals("{\"key\": \"value\"}", 
template.requestBody("direct:invoke", EVENT, String.class));
+    }
+
+    @Test
+    void nullBodyFieldOfResultIsNoBody() {
+        client.result = "{\"statusCode\":204,\"body\":null}";
+
+        assertNull(template.requestBody("direct:invoke", EVENT));
+    }
+
+    @Test
+    void noResultIsNoBody() {
+        client.result = null;
+
+        assertNull(template.requestBody("direct:invoke", EVENT));
+    }
+
+    private IllegalArgumentException assertInvokeFails(Object body) {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.requestBody("direct:invoke", body));
+        assertNull(client.request, "the function must not be invoked");
+        return assertInstanceOf(IllegalArgumentException.class, e.getCause());
+    }
+
+    /**
+     * Records the request and gives the result that the test sets.
+     */
+    static final class RecordingClient extends FunctionGraphClient {
+
+        InvokeFunctionRequest request;
+        String result = "{\"statusCode\":200,\"body\":\"done\"}";
+
+        RecordingClient() {
+            super(null);
+        }
+
+        @Override
+        public InvokeFunctionResponse invokeFunction(InvokeFunctionRequest 
request) {
+            this.request = request;
+            return new 
InvokeFunctionResponse().withRequestId("1939bbbb-4009-4685-bcc0-2ff0381fa911").withStatus(200)
+                    .withResult(result);
+        }
+    }
+
+    record Order(String product, int quantity) {
+    }
+}
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 728612d4f988..914d44173c9a 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
@@ -3098,6 +3098,15 @@ failed (for example a 404 for a deleted account, or a 
server error). Such a comm
 for it. Its `CamelGitHubCommitAuthor` and `CamelGitHubCommitCommitter` headers 
then have the name that git recorded in
 the commit instead of the name of the GitHub profile, and are not set when the 
commit has no name either.
 
+=== camel-huaweicloud-functiongraph - the whole result of a function without a 
body field
+
+The `invokeFunction` operation of `hwcloud-functiongraph` now gives the whole 
result of the function as the message
+body when the result is not a JSON object with a top-level `body` field. 
Before, the body was null when the result was
+a JSON object without a `body` field, and the call failed with a Gson 
`JsonSyntaxException` when the result was a JSON
+array, another JSON value or plain text. A result with a `body` field, such as 
the result of a function behind an API
+Gateway trigger, still gives that field, and no body when the field is null. A 
route that tested for a null body after
+the call must now test the result instead.
+
 === camel-slack - consumer and a suspended route
 
 When a route is suspended (`RouteController.suspendRoute`, also from a route 
policy that suspends the route such as

Reply via email to