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 b77a58944178 CAMEL-25360: camel-cloudevents - write the data of
application-cloudevents+json as its datacontenttype says (#27587)
b77a58944178 is described below
commit b77a5894417813bf0bf98af97af110873b103535
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 10 01:29:08 2026 +0530
CAMEL-25360: camel-cloudevents - write the data of
application-cloudevents+json as its datacontenttype says (#27587)
The application-cloudevents+json data type nested any JSON object or array
body
whatever the datacontenttype, wrote JSON scalars as strings under a JSON
content type, and decoded byte[] bodies as text. The data is now written as
the
CloudEvents JSON format describes, using the event's datacontenttype: JSON
values are nested for a JSON media type, bytes go to data_base64, and a null
body gives an event without data. Upgrade guide note added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../CloudEventJsonDataTypeTransformer.java | 122 +++++++++++++-
.../CloudEventJsonDataTypeTransformerTest.java | 177 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 15 ++
3 files changed, 306 insertions(+), 8 deletions(-)
diff --git
a/components/camel-cloudevents/src/main/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformer.java
b/components/camel-cloudevents/src/main/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformer.java
index cc7be5efbc93..e3eec532d1db 100644
---
a/components/camel-cloudevents/src/main/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformer.java
+++
b/components/camel-cloudevents/src/main/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformer.java
@@ -17,8 +17,14 @@
package org.apache.camel.component.cloudevents.transformer;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import org.apache.camel.Exchange;
@@ -28,12 +34,25 @@ import org.apache.camel.cloudevents.CloudEvents;
import org.apache.camel.spi.DataType;
import org.apache.camel.spi.DataTypeTransformer;
import org.apache.camel.spi.Transformer;
+import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.MessageHelper;
/**
* Data type represents a default Camel CloudEvent V1 Json format binding. The
data type reads Camel specific CloudEvent
* headers and transforms these to a Json object representing the CloudEvents
Json format specification. Sets default
* values for CloudEvent attributes such as the Http content type header,
event source, event type.
+ * <p/>
+ * The body is written as the data of the event according to its
datacontenttype (CloudEvents Json format, section 3.1):
+ * <ul>
+ * <li>declared Json content type (subtype {@code json} or suffix {@code
+json}): a body that is a Json value, scalars
+ * included, is nested as that value; any other body is a Json string</li>
+ * <li>no declared content type ({@code application/json} is assumed): only a
Json object or array is nested, so that
+ * text such as a long number or {@code null} stays a string</li>
+ * <li>any other content type: the body is a Json string</li>
+ * <li>a {@code byte[]} body that is not valid text in the charset of the
exchange: {@code data_base64}, with the
+ * datacontenttype {@code application/octet-stream} unless one was
declared</li>
+ * <li>a null body: no data</li>
+ * </ul>
*/
@DataTypeTransformer(name = "application-cloudevents+json",
description = "Adds default CloudEvent (JSon binding)
headers to the Camel message (such as content-type, event source, event type
etc.)")
@@ -55,6 +74,9 @@ public class CloudEventJsonDataTypeTransformer extends
Transformer {
final Map<String, Object> headers = message.getHeaders();
String dataContentType =
headers.getOrDefault(CloudEvent.CAMEL_CLOUD_EVENT_CONTENT_TYPE,
APPLICATION_JSON).toString();
+ // whether the content type of the data was declared, rather than
assumed to be application/json
+ boolean contentTypeDeclared =
headers.get(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE) != null
+ || headers.get(CloudEvent.CAMEL_CLOUD_EVENT_CONTENT_TYPE) !=
null;
if (!APPLICATION_CLOUDEVENTS_JSON.equals(dataContentType)) {
Map<String, Object> cloudEventAttributes = new HashMap<>();
CloudEvent cloudEvent = CloudEvents.v1_0;
@@ -76,28 +98,52 @@ public class CloudEventJsonDataTypeTransformer extends
Transformer {
cloudEventAttributes.putIfAbsent(cloudEvent.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_TIME).json(),
cloudEvent.getEventTime(message.getExchange()));
- String body = MessageHelper.extractBodyAsString(message);
- cloudEventAttributes.putIfAbsent("data", body);
- cloudEventAttributes.putIfAbsent(
-
cloudEvent.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json(),
dataContentType);
+ String dataContentTypeKey =
cloudEvent.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json();
+ cloudEventAttributes.putIfAbsent(dataContentTypeKey,
dataContentType);
+ // the data is written as the datacontenttype of the event says
+ Object eventDataContentType =
cloudEventAttributes.get(dataContentTypeKey);
+ boolean jsonData = isJsonContentType(eventDataContentType != null
? eventDataContentType.toString() : null);
+ if (message.getBody() instanceof byte[] bytes) {
+ String text = decodeText(bytes,
ExchangeHelper.getCharset(message.getExchange()));
+ if (text != null) {
+ cloudEventAttributes.putIfAbsent("data", text);
+ } else {
+ // decoding these bytes as text would replace the invalid
ones, so the data would be lost
+ cloudEventAttributes.putIfAbsent("data_base64",
Base64.getEncoder().encodeToString(bytes));
+ // a declared datacontenttype is kept: the route states
the format of these bytes, which may be valid
+ // in a charset other than the one of the exchange, so
only an assumed application/json is replaced
+ if (!contentTypeDeclared) {
+ // these bytes are not Json, and the datacontenttype
must reflect the format of the data
+ cloudEventAttributes.put(dataContentTypeKey,
CloudEvent.APPLICATION_OCTET_STREAM_MIME_TYPE);
+ }
+ }
+ } else {
+ String data = MessageHelper.extractBodyAsString(message);
+ // the data is optional, so a null body has none rather than
the text "null"
+ if (data != null) {
+ cloudEventAttributes.putIfAbsent("data", data);
+ }
+ }
headers.put(Exchange.CONTENT_TYPE, APPLICATION_CLOUDEVENTS_JSON);
- message.setBody(createCouldEventJsonObject(cloudEventAttributes));
+ message.setBody(createCloudEventJsonObject(cloudEventAttributes,
jsonData, contentTypeDeclared));
cloudEvent.attributes().stream().map(CloudEvent.Attribute::id).forEach(headers::remove);
}
}
- private String createCouldEventJsonObject(Map<String, Object>
cloudEventAttributes) {
+ private String createCloudEventJsonObject(
+ Map<String, Object> cloudEventAttributes, boolean jsonData,
boolean contentTypeDeclared) {
StringBuilder builder = new StringBuilder("{");
cloudEventAttributes.forEach((key, value) -> {
builder.append(" ");
appendJsonString(builder, key);
builder.append(":");
- if ("data".equals(key) && value instanceof String data &&
isJson(data)) {
- // set Json data as nested object in the data field
+ if (jsonData && "data".equals(key) && value instanceof String data
+ && (contentTypeDeclared ? isJsonValue(data) :
isJson(data))) {
+ // set Json data as nested value in the data field, a scalar
only if Json was declared
builder.append(data);
} else {
appendJsonString(builder, String.valueOf(value));
@@ -140,6 +186,66 @@ public class CloudEventJsonDataTypeTransformer extends
Transformer {
builder.append('"');
}
+ /**
+ * Whether the content type declares Json data: the subtype of its media
type, without parameters, is {@code json}
+ * or ends with {@code +json} (CloudEvents Json format, section 3.1). An
absent content type is Json, as the
+ * transformer declares {@code application/json} then.
+ */
+ static boolean isJsonContentType(String contentType) {
+ if (contentType == null) {
+ return true;
+ }
+ String subtype = mediaSubtype(contentType);
+ return subtype != null && (subtype.equals("json") ||
subtype.endsWith("+json"));
+ }
+
+ /**
+ * The bytes decoded as text in the given charset, or {@code null} if they
are not valid text in it.
+ */
+ static String decodeText(byte[] data, Charset charset) {
+ try {
+ return charset.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .decode(ByteBuffer.wrap(data))
+ .toString();
+ } catch (CharacterCodingException e) {
+ return null;
+ }
+ }
+
+ private static String mediaType(String contentType) {
+ int semicolon = contentType.indexOf(';');
+ return (semicolon < 0 ? contentType : contentType.substring(0,
semicolon)).trim().toLowerCase(Locale.ROOT);
+ }
+
+ private static String mediaSubtype(String contentType) {
+ String mediaType = mediaType(contentType);
+ int slash = mediaType.indexOf('/');
+ return slash < 0 ? null : mediaType.substring(slash + 1);
+ }
+
+ /**
+ * Whether the data is a Json value: an object, an array, a string, a
number, true, false or null.
+ */
+ static boolean isJsonValue(String data) {
+ if (data == null) {
+ return false;
+ }
+ final int length = data.length();
+ int i = skipWhitespace(data, 0, length);
+ if (i == length) {
+ return false;
+ }
+ char ch = data.charAt(i);
+ if (ch == '{' || ch == '[') {
+ return isJson(data);
+ }
+ i = skipScalar(data, i, length);
+ // nothing but whitespace may follow the value
+ return i >= 0 && skipWhitespace(data, i, length) == length;
+ }
+
/**
* Whether the data is a Json object or array, which is then set as nested
Json value. Text that only starts like
* Json (such as a log line "[INFO] ...") is set as a Json string.
diff --git
a/components/camel-cloudevents/src/test/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformerTest.java
b/components/camel-cloudevents/src/test/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformerTest.java
index f9ba5294e240..ad8fe6a458e5 100644
---
a/components/camel-cloudevents/src/test/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformerTest.java
+++
b/components/camel-cloudevents/src/test/java/org/apache/camel/component/cloudevents/transformer/CloudEventJsonDataTypeTransformerTest.java
@@ -18,7 +18,10 @@
package org.apache.camel.component.cloudevents.transformer;
import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collection;
import org.apache.camel.Exchange;
import org.apache.camel.cloudevents.CloudEvent;
@@ -33,8 +36,10 @@ import org.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -214,6 +219,178 @@ class CloudEventJsonDataTypeTransformerTest {
assertEquals(text, event.getString("data"));
}
+ private JsonObject transformToEvent(String contentType, Object body)
throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ if (contentType != null) {
+
exchange.getMessage().setHeader(CloudEvent.CAMEL_CLOUD_EVENT_CONTENT_TYPE,
contentType);
+ }
+ exchange.getMessage().setBody(body);
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ return (JsonObject)
Jsoner.deserialize(exchange.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void shouldNotNestJsonDataOfANonJsonContentType() throws Exception {
+ // CloudEvents JSON format 3.1: only data whose datacontenttype is
JSON is a JSON value, other text is a string
+ assertEquals("[1,2]", transformToEvent("text/plain",
"[1,2]").get("data"));
+ assertEquals("{\"a\": 1}", transformToEvent("application/xml",
"{\"a\": 1}").get("data"));
+ }
+
+ @Test
+ void shouldUseTheDataContentTypeOfTheEvent() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ // the datacontenttype attribute of the event wins over the
Content-Type of the message
+
exchange.getMessage().setHeader(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE,
"text/plain");
+
exchange.getMessage().setHeader(CloudEvent.CAMEL_CLOUD_EVENT_CONTENT_TYPE,
"application/json");
+ exchange.getMessage().setBody("[1,2]");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ JsonObject event = (JsonObject)
Jsoner.deserialize(exchange.getMessage().getBody(String.class));
+ assertEquals("text/plain", event.getString(
+
CloudEvents.v1_0.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json()));
+ assertEquals("[1,2]", event.get("data"));
+ }
+
+ @Test
+ void shouldNestJsonScalarsOfAJsonContentType() throws Exception {
+ assertEquals(new BigDecimal("42"),
transformToEvent("application/json", "42").get("data"));
+ assertEquals(new BigDecimal("-1.5E+3"),
transformToEvent("application/json", " -1.5e3 ").get("data"));
+ assertEquals(Boolean.TRUE, transformToEvent("application/json",
"true").get("data"));
+ assertEquals("text", transformToEvent("application/json",
"\"text\"").get("data"));
+ JsonObject event = transformToEvent("application/json", "null");
+ assertTrue(event.containsKey("data"));
+ assertNull(event.get("data"));
+ assertEquals(Boolean.FALSE,
transformToEvent("application/vnd.example+json; charset=UTF-8",
"false").get("data"));
+ }
+
+ @Test
+ void shouldNestJsonScalarsOfAJsonDataContentType() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+
exchange.getMessage().setHeader(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE,
"application/json");
+ exchange.getMessage().setBody("42");
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ JsonObject event = (JsonObject)
Jsoner.deserialize(exchange.getMessage().getBody(String.class));
+ assertEquals(new BigDecimal("42"), event.get("data"));
+ }
+
+ @Test
+ void shouldNestOnlyJsonObjectsAndArraysWithoutADeclaredContentType()
throws Exception {
+ // the transformer only assumes application/json, so scalars stay
strings as before (CAMEL-22339)
+ for (String text : new String[] { "42", "1234567890123456789",
"-1.5e3", "true", "null", "\"text\"" }) {
+ JsonObject event = transformToEvent(null, text);
+ assertEquals(text, event.get("data"), text);
+ assertEquals("application/json", event.getString(
+
CloudEvents.v1_0.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json()),
text);
+ }
+ assertEquals("42", transformToEvent(null,
"42".getBytes(StandardCharsets.UTF_8)).get("data"));
+ JsonObject data = (JsonObject) transformToEvent(null, "{\"a\":
1}").get("data");
+ assertEquals(1, data.getInteger("a"));
+ assertEquals(2, ((Collection<?>) transformToEvent(null, "[1,
2]").get("data")).size());
+ }
+
+ @Test
+ void shouldWriteNoDataForANullBody() throws Exception {
+ for (String contentType : new String[] { "application/json",
"text/plain", null }) {
+ JsonObject event = transformToEvent(contentType, null);
+
+ assertFalse(event.containsKey("data"), contentType);
+ assertFalse(event.containsKey("data_base64"), contentType);
+ }
+ }
+
+ @Test
+ void shouldNestJsonOfAnyJsonContentType() throws Exception {
+ JsonObject data = (JsonObject)
transformToEvent("application/vnd.example+json; charset=UTF-8", "{\"a\": 1}")
+ .get("data");
+ assertEquals(1, data.getInteger("a"));
+ data = (JsonObject) transformToEvent("Application/JSON", "{\"a\":
1}").get("data");
+ assertEquals(1, data.getInteger("a"));
+ }
+
+ @Test
+ void shouldKeepTextOfAJsonContentTypeThatIsNotJsonAsString() throws
Exception {
+ for (String text : new String[] {
+ "", " ", "Test", "42 apples", "4 2", "trueish", "nul", "01",
"+1", "\"open", "\"a\\x\"", "\"a\" \"b\"",
+ "\"raw\u0001control\"" }) {
+ assertEquals(text, transformToEvent("application/json",
text).get("data"), text);
+ }
+ assertEquals("42 apples", transformToEvent(null, "42
apples").get("data"));
+ }
+
+ @Test
+ void shouldWriteBinaryDataAsBase64() throws Exception {
+ byte[] bytes = { 0, 1, (byte) 0x80, (byte) 0xff, '"', '\\' };
+
+ JsonObject event = transformToEvent("application/octet-stream", bytes);
+
+ assertEquals(Base64.getEncoder().encodeToString(bytes),
event.get("data_base64"));
+ assertFalse(event.containsKey("data"));
+ assertEquals("application/octet-stream", event.getString(
+
CloudEvents.v1_0.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json()));
+ }
+
+ @Test
+ void shouldWriteBytesThatAreNotValidTextAsBase64WhateverTheContentType()
throws Exception {
+ byte[] bytes = { 'a', (byte) 0xc3, '(' };
+
+ for (String contentType : new String[] { "text/plain",
"application/json", null }) {
+ JsonObject event = transformToEvent(contentType, bytes);
+
+ assertEquals(Base64.getEncoder().encodeToString(bytes),
event.get("data_base64"), contentType);
+ assertFalse(event.containsKey("data"), contentType);
+ // a declared content type is kept, the default application/json
would not reflect these bytes
+ assertEquals(contentType != null ? contentType :
CloudEvent.APPLICATION_OCTET_STREAM_MIME_TYPE,
+ event.getString(
+
CloudEvents.v1_0.mandatoryAttribute(CloudEvent.CAMEL_CLOUD_EVENT_DATA_CONTENT_TYPE).json()),
+ contentType);
+ }
+ }
+
+ @Test
+ void shouldWriteBytesThatAreValidTextAsStringWhenTheContentTypeIsBinary()
throws Exception {
+ // valid UTF-8 is written without loss as a Json string, as before: a
reader gets the same bytes back
+ byte[] bytes = { 0, 1, '"', '\\', 'a' };
+ Object data = transformToEvent("application/octet-stream",
bytes).get("data");
+ assertArrayEquals(bytes, assertInstanceOf(String.class,
data).getBytes(StandardCharsets.UTF_8));
+ assertEquals("Grüße", transformToEvent("application/octet-stream",
"Grüße".getBytes(StandardCharsets.UTF_8))
+ .get("data"));
+ // a Json body of a content type that is not Json is a string, not
nested
+ JsonObject event = transformToEvent("application/octet-stream",
"{\"a\": 1}".getBytes(StandardCharsets.UTF_8));
+ assertEquals("{\"a\": 1}", event.get("data"));
+ assertFalse(event.containsKey("data_base64"));
+ }
+
+ @Test
+ void shouldDecodeBytesInTheCharsetOfTheExchange() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.setProperty(Exchange.CHARSET_NAME, "ISO-8859-1");
+
exchange.getMessage().setHeader(CloudEvent.CAMEL_CLOUD_EVENT_CONTENT_TYPE,
"application/octet-stream");
+ exchange.getMessage().setBody(new byte[] { 'c', 'a', 'f', (byte) 0xe9
});
+
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ JsonObject event = (JsonObject)
Jsoner.deserialize(exchange.getMessage().getBody(String.class));
+ assertEquals("caf\u00e9", event.get("data"));
+ }
+
+ @Test
+ void shouldWriteBytesOfTextOrJsonContentTypesAsData() throws Exception {
+ byte[] text = "Grüße".getBytes(StandardCharsets.UTF_8);
+ assertEquals("Grüße", transformToEvent("text/plain",
text).get("data"));
+ assertEquals("<a>Grüße</a>", transformToEvent("application/xml",
"<a>Grüße</a>".getBytes(StandardCharsets.UTF_8))
+ .get("data"));
+ JsonObject data = (JsonObject) transformToEvent("application/json",
"{\"a\": 1}".getBytes(StandardCharsets.UTF_8))
+ .get("data");
+ assertEquals(1, data.getInteger("a"));
+ // without a datacontenttype the data is Json, so bytes are read as
text as before
+ assertEquals("Test", transformToEvent(null,
"Test".getBytes(StandardCharsets.UTF_8)).get("data"));
+ }
+
@Test
void shouldRecognizeJsonObjectsAndArrays() {
assertTrue(CloudEventJsonDataTypeTransformer.isJson("{}"));
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index fa318f661406..003caaa164bb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -645,6 +645,21 @@ sent to the partition with the same id in the target Event
Hub. To keep doing so
Event Hubs that have the same partitions, remove the
`CamelAzureEventHubsReceivedPartitionId` exchange property before
the producer.
+=== camel-cloudevents - data of the application-cloudevents+json data type
(Breaking change)
+
+The `application-cloudevents+json` data type now writes the body according to
the `datacontenttype` of the event
+(CloudEvents JSON format, section 3.1): with a declared JSON content type (a
`Content-Type` or
+`CamelCloudEventDataContentType` header with a `json` subtype or `+json`
suffix) a JSON scalar body is now nested too
+(`"data":42` instead of `"data":"42"`), while without a declared content type
only a JSON object or array is nested, as
+before; with any other content type a JSON object or array body is now written
as a string; a `byte[]` body that is not
+valid text in the exchange charset is written as `data_base64`, with the
`datacontenttype` `application/octet-stream`
+when no content type was declared; and a null body gives an event without
`data` instead of `"data":"null"`.
+Only a `byte[]` body is kept as bytes: a body of any other type, such as an
`InputStream`, is still read as text in
+the exchange charset, as before, so bytes that are not valid text are still
replaced.
+Component CloudEvent data types that declare `application/octet-stream` or
`text/plain` (such as `aws2-s3`,
+`aws-cloudtrail` or `google-pubsub`) therefore no longer nest a JSON body: set
the `CamelCloudEventDataContentType`
+header to `application/json` before `application-cloudevents+json` to keep it
nested.
+
=== camel-consul - readTimeout and writeTimeout
The `readTimeout` and `writeTimeout` options (also on the Consul cluster
service) set the connect timeout of the