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 c55c94104a07 CAMEL-26525: camel-azure-functions - invokeFunction uses
the Content-Type header of the message (#29449)
c55c94104a07 is described below
commit c55c94104a07042d8d7c8f0cea326a4be09ae428
Author: Urmila Unni <[email protected]>
AuthorDate: Mon Nov 9 17:45:59 2026 +0530
CAMEL-26525: camel-azure-functions - invokeFunction uses the Content-Type
header of the message (#29449)
* CAMEL-26525: camel-azure-functions - invokeFunction uses the Content-…
* CAMEL-26525: test that GET does not send the Content-Type header of t…
* CAMEL-26525: describe the new Content-Type order in the component docs
Co-authored-by: Claude <[email protected]>
---
.../camel/catalog/components/azure-functions.json | 2 +-
.../catalog/docs/azure-functions-component.adoc | 6 ++-
.../component/azure/functions/azure-functions.json | 2 +-
.../src/main/docs/azure-functions-component.adoc | 6 ++-
.../azure/functions/FunctionsEndpoint.java | 5 ++-
.../azure/functions/FunctionsProducer.java | 13 ++++++
.../functions/FunctionsProducerInvokeTest.java | 50 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 10 +++++
8 files changed, 86 insertions(+), 8 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-functions.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-functions.json
index e7b5662081d7..cb5aa1bf72ac 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-functions.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-functions.json
@@ -68,7 +68,7 @@
"CamelAzureFunctionsStorageConnectionString": { "index": 17, "kind":
"header", "displayName": "", "group": "producer", "label": "producer",
"required": false, "javaType": "String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Storage account connection string for the function app", "constantName":
"org.apache.camel.component.azure.functions.FunctionsConstants#STORAGE_CONNECTION_STRING"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "value", "bytes", "stream", "file",
"null" ], "byOption": [ { "option": "operation", "value": "listFunctionApps",
"carriers": [ "any" ] }, { "option": "operation", "value": "getFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "createFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "deleteFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "startFunctionApp",
"carriers": [ "any" [...]
+ "producerIn": { "carriers": [ "text", "value", "bytes", "stream", "file",
"null" ], "byOption": [ { "option": "operation", "value": "listFunctionApps",
"carriers": [ "any" ] }, { "option": "operation", "value": "getFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "createFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "deleteFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "startFunctionApp",
"carriers": [ "any" [...]
"producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "listFunctionApps", "carriers":
[ "list" ], "javaType": "java.util.List<java.util.Map<String, Object>>" }, {
"option": "operation", "value": "getFunctionApp", "carriers": [ "map" ],
"javaType": "java.util.Map<String, Object>" }, { "option": "operation",
"value": "createFunctionApp", "carriers": [ "map" ], "javaType":
"java.util.Map<String, Object>" }, { "option": " [...]
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-functions-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-functions-component.adoc
index bb3c5c1178a6..15c03b8e1c8e 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-functions-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-functions-component.adoc
@@ -211,8 +211,10 @@ The `invokeFunction` operation sends the message body to
the function: text, a n
marshalled: marshal it to JSON first, for example with `marshal().json()`, as
a body that does not convert to bytes
fails the exchange. `GET` and `DELETE` send no body.
-A request with a body has the `Content-Type: application/json` header, unless
the `CamelAzureFunctionsHttpHeaders`
-header, a `Map<String, String>` of HTTP headers to send, sets a `Content-Type`.
+A request with a body has the `Content-Type` that the
`CamelAzureFunctionsHttpHeaders` header, a `Map<String, String>`
+of HTTP headers to send, sets. When that map sets none, the `Content-Type`
header of the message is used, and when the
+message has none either, `Content-Type: application/json`. Text is always sent
as UTF-8: the charset parameter of a
+`Content-Type` from the message, for example `text/plain; charset=ISO-8859-1`,
is not used for the encoding.
The response body of the function becomes the message body, as text, with the
HTTP status in the
`CamelAzureFunctionsStatusCode` header and the HTTP headers of the response in
the `CamelAzureFunctionsResponseHeaders`
diff --git
a/components/camel-azure/camel-azure-functions/src/generated/resources/META-INF/org/apache/camel/component/azure/functions/azure-functions.json
b/components/camel-azure/camel-azure-functions/src/generated/resources/META-INF/org/apache/camel/component/azure/functions/azure-functions.json
index e7b5662081d7..cb5aa1bf72ac 100644
---
a/components/camel-azure/camel-azure-functions/src/generated/resources/META-INF/org/apache/camel/component/azure/functions/azure-functions.json
+++
b/components/camel-azure/camel-azure-functions/src/generated/resources/META-INF/org/apache/camel/component/azure/functions/azure-functions.json
@@ -68,7 +68,7 @@
"CamelAzureFunctionsStorageConnectionString": { "index": 17, "kind":
"header", "displayName": "", "group": "producer", "label": "producer",
"required": false, "javaType": "String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Storage account connection string for the function app", "constantName":
"org.apache.camel.component.azure.functions.FunctionsConstants#STORAGE_CONNECTION_STRING"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "value", "bytes", "stream", "file",
"null" ], "byOption": [ { "option": "operation", "value": "listFunctionApps",
"carriers": [ "any" ] }, { "option": "operation", "value": "getFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "createFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "deleteFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "startFunctionApp",
"carriers": [ "any" [...]
+ "producerIn": { "carriers": [ "text", "value", "bytes", "stream", "file",
"null" ], "byOption": [ { "option": "operation", "value": "listFunctionApps",
"carriers": [ "any" ] }, { "option": "operation", "value": "getFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "createFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "deleteFunctionApp",
"carriers": [ "any" ] }, { "option": "operation", "value": "startFunctionApp",
"carriers": [ "any" [...]
"producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "listFunctionApps", "carriers":
[ "list" ], "javaType": "java.util.List<java.util.Map<String, Object>>" }, {
"option": "operation", "value": "getFunctionApp", "carriers": [ "map" ],
"javaType": "java.util.Map<String, Object>" }, { "option": "operation",
"value": "createFunctionApp", "carriers": [ "map" ], "javaType":
"java.util.Map<String, Object>" }, { "option": " [...]
},
"properties": {
diff --git
a/components/camel-azure/camel-azure-functions/src/main/docs/azure-functions-component.adoc
b/components/camel-azure/camel-azure-functions/src/main/docs/azure-functions-component.adoc
index bb3c5c1178a6..15c03b8e1c8e 100644
---
a/components/camel-azure/camel-azure-functions/src/main/docs/azure-functions-component.adoc
+++
b/components/camel-azure/camel-azure-functions/src/main/docs/azure-functions-component.adoc
@@ -211,8 +211,10 @@ The `invokeFunction` operation sends the message body to
the function: text, a n
marshalled: marshal it to JSON first, for example with `marshal().json()`, as
a body that does not convert to bytes
fails the exchange. `GET` and `DELETE` send no body.
-A request with a body has the `Content-Type: application/json` header, unless
the `CamelAzureFunctionsHttpHeaders`
-header, a `Map<String, String>` of HTTP headers to send, sets a `Content-Type`.
+A request with a body has the `Content-Type` that the
`CamelAzureFunctionsHttpHeaders` header, a `Map<String, String>`
+of HTTP headers to send, sets. When that map sets none, the `Content-Type`
header of the message is used, and when the
+message has none either, `Content-Type: application/json`. Text is always sent
as UTF-8: the charset parameter of a
+`Content-Type` from the message, for example `text/plain; charset=ISO-8859-1`,
is not used for the encoding.
The response body of the function becomes the message body, as text, with the
HTTP status in the
`CamelAzureFunctionsStatusCode` header and the HTTP headers of the response in
the `CamelAzureFunctionsResponseHeaders`
diff --git
a/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsEndpoint.java
b/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsEndpoint.java
index 61eaeae45ec9..c24444a6a3ea 100644
---
a/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsEndpoint.java
+++
b/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsEndpoint.java
@@ -77,8 +77,9 @@ import org.apache.camel.util.ObjectHelper;
+ " is sent as text (UTF-8),
and bytes, a stream or a file as their content. A Map"
+ " or a POJO is not"
+ " marshalled: marshal it to
JSON first, as a body that does not convert to bytes"
- + " fails. The Content-Type is
application/json unless the"
- + "
CamelAzureFunctionsHttpHeaders header sets one. GET and DELETE send no body."
+ + " fails. The Content-Type is
the one the CamelAzureFunctionsHttpHeaders header"
+ + " sets, else the
Content-Type header of the message, else application/json."
+ + " GET and DELETE send no
body."
+ " The management operations
do not use the body: they take their input from"
+ " headers, such as the app
settings and the tags"),
producerOut = @BodyType(value = Carrier.TEXT, javaType =
"java.lang.String",
diff --git
a/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsProducer.java
b/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsProducer.java
index 4d7fd20f246e..50447076b979 100644
---
a/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsProducer.java
+++
b/components/camel-azure/camel-azure-functions/src/main/java/org/apache/camel/component/azure/functions/FunctionsProducer.java
@@ -169,6 +169,14 @@ public class FunctionsProducer extends DefaultProducer {
body = getRequestBody(exchange);
}
+ // the Content-Type header of the message, when the headers map does
not set one
+ String contentType =
exchange.getMessage().getHeader(Exchange.CONTENT_TYPE, String.class);
+ if (body != null && contentType != null && !hasContentType(headers)) {
+ // copy the map, as it can be the map of the
CamelAzureFunctionsHttpHeaders header
+ headers = headers != null ? new HashMap<>(headers) : new
HashMap<>();
+ headers.put(Exchange.CONTENT_TYPE, contentType);
+ }
+
LOG.debug("Invoking function: {} with method: {}", url, httpMethod);
FunctionInvocationResponse response = invocationClient.invoke(url,
httpMethod, body, headers);
@@ -184,6 +192,11 @@ public class FunctionsProducer extends DefaultProducer {
message.setBody(response.getBody());
}
+ private static boolean hasContentType(Map<String, String> headers) {
+ // HTTP header names are case-insensitive
+ return headers != null &&
headers.keySet().stream().anyMatch(Exchange.CONTENT_TYPE::equalsIgnoreCase);
+ }
+
/**
* Text, a number or a boolean is sent as its text (as UTF-8), and bytes,
a stream, a file or any other body as its
* content, converted to bytes with the type converter: a body that cannot
be converted fails, instead of being sent
diff --git
a/components/camel-azure/camel-azure-functions/src/test/java/org/apache/camel/component/azure/functions/FunctionsProducerInvokeTest.java
b/components/camel-azure/camel-azure-functions/src/test/java/org/apache/camel/component/azure/functions/FunctionsProducerInvokeTest.java
index 05ef21dbb0c7..ff03f1030fa6 100644
---
a/components/camel-azure/camel-azure-functions/src/test/java/org/apache/camel/component/azure/functions/FunctionsProducerInvokeTest.java
+++
b/components/camel-azure/camel-azure-functions/src/test/java/org/apache/camel/component/azure/functions/FunctionsProducerInvokeTest.java
@@ -190,6 +190,20 @@ class FunctionsProducerInvokeTest extends CamelTestSupport
{
assertNull(request.contentTypes());
}
+ @Test
+ void getSendsNoContentTypeOfMessage() throws Exception {
+ // GET sends no body, so the Content-Type header of the message is not
sent either
+ Exchange result = template.send(functionsEndpoint("&httpMethod=GET"),
e -> {
+ e.getIn().setBody("Hello");
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain");
+ });
+
+ assertNull(result.getException());
+ RecordedRequest request = nextRequest();
+ assertEquals("GET", request.method());
+ assertNull(request.contentTypes());
+ }
+
@Test
void defaultContentTypeWithoutHttpHeaders() throws Exception {
Exchange result = template.send(functionsEndpoint(""), e ->
e.getIn().setBody("{\"name\":\"Camel\"}"));
@@ -221,6 +235,42 @@ class FunctionsProducerInvokeTest extends CamelTestSupport
{
assertEquals(List.of("text/plain"), nextRequest().contentTypes());
}
+ @Test
+ void contentTypeOfMessageIsUsedWithoutHttpHeaders() throws Exception {
+ Exchange result = template.send(functionsEndpoint(""), e -> {
+ e.getIn().setBody("Hello");
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain");
+ });
+
+ assertNull(result.getException());
+ assertEquals(List.of("text/plain"), nextRequest().contentTypes());
+ }
+
+ @Test
+ void contentTypeOfMessageIsUsedWithHttpHeadersWithoutContentType() throws
Exception {
+ // Map.of is immutable: the producer must copy the map to add the
Content-Type
+ Exchange result = template.send(functionsEndpoint(""), e -> {
+ e.getIn().setBody("Hello");
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain");
+ e.getIn().setHeader(FunctionsConstants.HTTP_HEADERS,
Map.of("X-Custom", "value"));
+ });
+
+ assertNull(result.getException());
+ assertEquals(List.of("text/plain"), nextRequest().contentTypes());
+ }
+
+ @Test
+ void contentTypeOfHttpHeadersWinsOverContentTypeOfMessage() throws
Exception {
+ Exchange result = template.send(functionsEndpoint(""), e -> {
+ e.getIn().setBody("Hello");
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "text/csv");
+ e.getIn().setHeader(FunctionsConstants.HTTP_HEADERS,
Map.of("content-type", "text/plain"));
+ });
+
+ assertNull(result.getException());
+ assertEquals(List.of("text/plain"), nextRequest().contentTypes());
+ }
+
@Test
void serverErrorFailsTheExchange() throws Exception {
responseStatus = 500;
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 7d1a8214d90f..3f82626e4ff3 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
@@ -7515,3 +7515,13 @@ The `logWarnOnBadRequest` option of a `netty-http`
consumer is documented as ena
left at `false`, so a request that Netty could not decode got its HTTP 400
returned without a log line. The option now
defaults to `true`, as documented, so such a request logs `Netty request
decoder failure due: <reason> returning HTTP
Status 400 to client` at `WARN` level. Set `logWarnOnBadRequest=false` on the
consumer to keep the previous silence.
+
+=== camel-azure-functions - invokeFunction uses the Content-Type header of the
message
+
+When the `CamelAzureFunctionsHttpHeaders` header does not set a
`Content-Type`, `invokeFunction` now sends the
+`Content-Type` header of the message, if there is one, before falling back to
`application/json`. Before, the
+request was always sent as `application/json` in that case.
+
+A route whose message still carries a `Content-Type` header from an earlier
step, for example from an HTTP
+consumer, now sends that Content-Type. Remove the header, or set the
Content-Type in
+`CamelAzureFunctionsHttpHeaders`, to keep the old behavior.