This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 50352373fe50 camel-ibm MED fixes backport to camel-4.22.x
(CAMEL-24488, 24490, 24491, 24493) (#25782)
50352373fe50 is described below
commit 50352373fe50a7b28952d4c18b081e0173c2386a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 27 11:19:36 2026 +0200
camel-ibm MED fixes backport to camel-4.22.x (CAMEL-24488, 24490, 24491,
24493) (#25782)
* CAMEL-24488: camel-ibm-watson-speech-to-text - close the audio
FileInputStream opened by the producer (#25737)
WatsonSpeechToTextProducer.recognize() opened a FileInputStream for the
audio
input (from the CamelIbmWatsonSttAudioFile header or a File body) but never
closed it, leaking a file descriptor on every File-based invocation and on
any
exception thrown during recognition. Track an ownStream flag (set only for
the
two producer-opened FileInputStream cases) and close the stream via
IOHelper.close in a finally block around the recognition. A stream supplied
as
the exchange body is owned by the caller and is left untouched.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
* CAMEL-24490: camel-ibm-watsonx-ai - apply the configured timeout to the
watsonx.ai service clients (#25739)
WatsonxAiServiceFactory built every watsonx.ai service (chat, text
generation,
embedding, rerank, tokenization, detection, text extraction/classification,
time series, foundation model, deployment, tool) without applying the
timeout
@UriParam, so the documented "Request timeout in milliseconds" option was
silently ignored and every request used the SDK default. Apply the
configured
timeout to each builder via
applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t))).
The SDK then governs how the timeout applies to both unary and streaming
requests, so the streaming CompletableFuture is no longer left unbounded
when a
timeout is configured.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
* CAMEL-24493: camel-ibm-cos - honor the multiPartUpload, partSize,
storageClass and deleteAfterWrite producer options (#25750)
IBMCOSProducer.putObject() built a plain PutObjectRequest and ignored the
four
producer options declared on IBMCOSConfiguration and advertised in the docs.
Apply them, mirroring camel-aws2-s3: set storageClass on the request,
upload via
the SDK TransferManager (minimum part size / multipart threshold =
partSize) when
multiPartUpload is enabled, and delete the local File payload after a
successful
upload when deleteAfterWrite is set.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
* CAMEL-24491: camel-ibm-cos - make consumer in-progress deduplication
effective (#25740)
* CAMEL-24491: camel-ibm-cos - make consumer in-progress deduplication
effective
The IBM COS consumer's in-progress deduplication never worked:
createExchanges()
only checked getInProgressRepository().contains(key) without ever adding the
key, the MemoryIdempotentRepository was started only after doStart()'s early
returns (so never in the normal consuming case), and
processCommit/processRollback
never removed the key. As a result overlapping polls could re-deliver an
object
still being processed when deleteAfterRead/moveAfterRead were off.
Mirror the camel-aws2-s3 consumer this module was copied from: use
getInProgressRepository().add(key) as the atomic guard (skip when it returns
false), remove the key in processCommit (finally) and processRollback via a
null-safe helper, and start the in-progress repository up front in doStart()
before any early return.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-24491: release the in-progress key when the object fetch or
exchange creation fails
Addresses review feedback: if getObject/createExchange throws after add(key)
has claimed the in-progress key, no Synchronization is attached to remove
it, so
the object would be left permanently unconsumable. Release the key (and
skip the
object for this poll) so a transient failure does not silently block the
object.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../camel/component/ibm/cos/IBMCOSConsumer.java | 27 +++++++++--
.../camel/component/ibm/cos/IBMCOSEndpoint.java | 5 +-
.../camel/component/ibm/cos/IBMCOSProducer.java | 51 ++++++++++++++++++--
.../ibm/watson/stt/WatsonSpeechToTextProducer.java | 56 +++++++++++++---------
.../ai/service/WatsonxAiServiceFactory.java | 13 +++++
5 files changed, 119 insertions(+), 33 deletions(-)
diff --git
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSConsumer.java
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSConsumer.java
index 233723ac43f7..66de7917f054 100644
---
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSConsumer.java
+++
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSConsumer.java
@@ -160,15 +160,23 @@ public class IBMCOSConsumer extends
ScheduledBatchPollingConsumer {
}
if (getEndpoint().getInProgressRepository() != null
- &&
getEndpoint().getInProgressRepository().contains(s3ObjectSummary.getKey())) {
+ &&
!getEndpoint().getInProgressRepository().add(s3ObjectSummary.getKey())) {
LOG.trace("Object {} is already in progress",
s3ObjectSummary.getKey());
continue;
}
- S3Object s3Object = getCosClient().getObject(
- new GetObjectRequest(s3ObjectSummary.getBucketName(),
s3ObjectSummary.getKey()));
- Exchange exchange = createExchange(s3Object,
s3ObjectSummary.getKey());
- exchanges.add(exchange);
+ try {
+ S3Object s3Object = getCosClient().getObject(
+ new GetObjectRequest(s3ObjectSummary.getBucketName(),
s3ObjectSummary.getKey()));
+ Exchange exchange = createExchange(s3Object,
s3ObjectSummary.getKey());
+ exchanges.add(exchange);
+ } catch (Exception e) {
+ // Fetching the object or creating the exchange failed after
we claimed the in-progress key;
+ // release it so the object is not left permanently
unconsumable, and skip it for this poll.
+ LOG.warn("Error fetching object {} from bucket {}: {}.
Skipping it for this poll.",
+ s3ObjectSummary.getKey(),
s3ObjectSummary.getBucketName(), e.getMessage());
+ removeInProgress(s3ObjectSummary.getKey());
+ }
}
return exchanges;
@@ -246,11 +254,20 @@ public class IBMCOSConsumer extends
ScheduledBatchPollingConsumer {
}
} catch (Exception e) {
LOG.warn("Error during post processing of object {} from bucket
{}: {}", key, bucketName, e.getMessage());
+ } finally {
+ removeInProgress(key);
}
}
protected void processRollback(String key) {
LOG.trace("Processing failed for object with key {}", key);
+ removeInProgress(key);
+ }
+
+ private void removeInProgress(String key) {
+ if (getEndpoint().getInProgressRepository() != null) {
+ getEndpoint().getInProgressRepository().remove(key);
+ }
}
private void copyObject(String bucketName, String key) {
diff --git
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSEndpoint.java
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSEndpoint.java
index 63d9d5b5478b..dcbafcfd3dc9 100644
---
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSEndpoint.java
+++
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSEndpoint.java
@@ -115,6 +115,9 @@ public class IBMCOSEndpoint extends ScheduledPollEndpoint
implements EndpointSer
protected void doStart() throws Exception {
super.doStart();
+ // Start the in-progress repository up front so consumer deduplication
works even when doStart returns early
+ ServiceHelper.startService(inProgressRepository);
+
cosClient = configuration.getCosClient() != null
? configuration.getCosClient() : createCosClient();
@@ -146,8 +149,6 @@ public class IBMCOSEndpoint extends ScheduledPollEndpoint
implements EndpointSer
cosClient.createBucket(bucketName);
LOG.trace("Bucket created");
}
-
- ServiceHelper.startService(inProgressRepository);
}
@Override
diff --git
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSProducer.java
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSProducer.java
index e8bed18528fa..c4e105af2e00 100644
---
a/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSProducer.java
+++
b/components/camel-ibm/camel-ibm-cos/src/main/java/org/apache/camel/component/ibm/cos/IBMCOSProducer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.ibm.cos;
+import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@@ -33,10 +34,14 @@ import
com.ibm.cloud.objectstorage.services.s3.model.ObjectMetadata;
import com.ibm.cloud.objectstorage.services.s3.model.PutObjectRequest;
import com.ibm.cloud.objectstorage.services.s3.model.PutObjectResult;
import com.ibm.cloud.objectstorage.services.s3.model.S3Object;
+import com.ibm.cloud.objectstorage.services.s3.transfer.TransferManager;
+import com.ibm.cloud.objectstorage.services.s3.transfer.TransferManagerBuilder;
+import com.ibm.cloud.objectstorage.services.s3.transfer.model.UploadResult;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.WrappedFile;
import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -154,12 +159,52 @@ public class IBMCOSProducer extends DefaultProducer {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName,
key, inputStream, metadata);
+ String storageClass = getConfiguration().getStorageClass();
+ if (ObjectHelper.isNotEmpty(storageClass)) {
+ putObjectRequest.withStorageClass(storageClass);
+ }
+
LOG.trace("Putting object [{}] into bucket [{}]...", key, bucketName);
- PutObjectResult putObjectResult =
cosClient.putObject(putObjectRequest);
+
+ String eTag;
+ String versionId;
+ if (getConfiguration().isMultiPartUpload()) {
+ // Upload via the TransferManager so large payloads are sent as a
multipart upload with the configured part size
+ TransferManager transferManager = TransferManagerBuilder.standard()
+ .withS3Client(cosClient)
+
.withMinimumUploadPartSize(getConfiguration().getPartSize())
+
.withMultipartUploadThreshold(getConfiguration().getPartSize())
+ .build();
+ try {
+ UploadResult uploadResult =
transferManager.upload(putObjectRequest).waitForUploadResult();
+ eTag = uploadResult.getETag();
+ versionId = uploadResult.getVersionId();
+ } finally {
+ // shut down the TransferManager's own thread pool but keep
the shared COS client open
+ transferManager.shutdownNow(false);
+ }
+ } else {
+ PutObjectResult putObjectResult =
cosClient.putObject(putObjectRequest);
+ eTag = putObjectResult.getETag();
+ versionId = putObjectResult.getVersionId();
+ }
Message message = getMessageForResponse(exchange);
- message.setHeader(IBMCOSConstants.E_TAG, putObjectResult.getETag());
- message.setHeader(IBMCOSConstants.VERSION_ID,
putObjectResult.getVersionId());
+ message.setHeader(IBMCOSConstants.E_TAG, eTag);
+ message.setHeader(IBMCOSConstants.VERSION_ID, versionId);
+
+ if (getConfiguration().isDeleteAfterWrite()) {
+ File filePayload = null;
+ if (body instanceof File file) {
+ filePayload = file;
+ } else if (body instanceof WrappedFile<?> wrapped &&
wrapped.getFile() instanceof File file) {
+ filePayload = file;
+ }
+ if (filePayload != null) {
+ LOG.trace("Deleting file payload [{}] after write",
filePayload);
+ FileUtil.deleteFile(filePayload);
+ }
+ }
}
private void getObject(AmazonS3 cosClient, Exchange exchange) {
diff --git
a/components/camel-ibm/camel-ibm-watson-speech-to-text/src/main/java/org/apache/camel/component/ibm/watson/stt/WatsonSpeechToTextProducer.java
b/components/camel-ibm/camel-ibm-watson-speech-to-text/src/main/java/org/apache/camel/component/ibm/watson/stt/WatsonSpeechToTextProducer.java
index c732fecfbe7f..eaed1831268d 100644
---
a/components/camel-ibm/camel-ibm-watson-speech-to-text/src/main/java/org/apache/camel/component/ibm/watson/stt/WatsonSpeechToTextProducer.java
+++
b/components/camel-ibm/camel-ibm-watson-speech-to-text/src/main/java/org/apache/camel/component/ibm/watson/stt/WatsonSpeechToTextProducer.java
@@ -35,6 +35,7 @@ import
com.ibm.watson.speech_to_text.v1.model.SpeechRecognitionResults;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -100,15 +101,18 @@ public class WatsonSpeechToTextProducer extends
DefaultProducer {
// Get audio input from header or body
File audioFile =
exchange.getIn().getHeader(WatsonSpeechToTextConstants.AUDIO_FILE, File.class);
InputStream audioStream = null;
+ boolean ownStream = false;
if (audioFile != null) {
audioStream = new FileInputStream(audioFile);
+ ownStream = true;
} else {
audioStream = exchange.getIn().getBody(InputStream.class);
if (audioStream == null) {
File bodyFile = exchange.getIn().getBody(File.class);
if (bodyFile != null) {
audioStream = new FileInputStream(bodyFile);
+ ownStream = true;
}
}
}
@@ -130,32 +134,38 @@ public class WatsonSpeechToTextProducer extends
DefaultProducer {
LOG.trace("Recognizing audio with STT: model={}, contentType={}",
model, contentType);
- RecognizeOptions options = new RecognizeOptions.Builder()
- .audio(audioStream)
- .model(model)
- .contentType(contentType)
- .timestamps(timestamps)
- .wordConfidence(wordConfidence)
- .speakerLabels(speakerLabels)
- .build();
-
- SpeechRecognitionResults results =
stt.recognize(options).execute().getResult();
-
- // Extract transcript text
- StringBuilder transcript = new StringBuilder();
- if (results.getResults() != null && !results.getResults().isEmpty()) {
- for (SpeechRecognitionResult result : results.getResults()) {
- if (result.getAlternatives() != null &&
!result.getAlternatives().isEmpty()) {
-
transcript.append(result.getAlternatives().get(0).getTranscript());
+ try {
+ RecognizeOptions options = new RecognizeOptions.Builder()
+ .audio(audioStream)
+ .model(model)
+ .contentType(contentType)
+ .timestamps(timestamps)
+ .wordConfidence(wordConfidence)
+ .speakerLabels(speakerLabels)
+ .build();
+
+ SpeechRecognitionResults results =
stt.recognize(options).execute().getResult();
+
+ // Extract transcript text
+ StringBuilder transcript = new StringBuilder();
+ if (results.getResults() != null &&
!results.getResults().isEmpty()) {
+ for (SpeechRecognitionResult result : results.getResults()) {
+ if (result.getAlternatives() != null &&
!result.getAlternatives().isEmpty()) {
+
transcript.append(result.getAlternatives().get(0).getTranscript());
+ }
}
}
- }
- Message message = getMessageForResponse(exchange);
- message.setBody(results);
- message.setHeader(WatsonSpeechToTextConstants.TRANSCRIPT,
transcript.toString());
- message.setHeader(WatsonSpeechToTextConstants.MODEL, model);
- message.setHeader(WatsonSpeechToTextConstants.CONTENT_TYPE,
contentType);
+ Message message = getMessageForResponse(exchange);
+ message.setBody(results);
+ message.setHeader(WatsonSpeechToTextConstants.TRANSCRIPT,
transcript.toString());
+ message.setHeader(WatsonSpeechToTextConstants.MODEL, model);
+ message.setHeader(WatsonSpeechToTextConstants.CONTENT_TYPE,
contentType);
+ } finally {
+ if (ownStream) {
+ IOHelper.close(audioStream);
+ }
+ }
}
private void listModels(Exchange exchange) {
diff --git
a/components/camel-ibm/camel-ibm-watsonx-ai/src/main/java/org/apache/camel/component/ibm/watsonx/ai/service/WatsonxAiServiceFactory.java
b/components/camel-ibm/camel-ibm-watsonx-ai/src/main/java/org/apache/camel/component/ibm/watsonx/ai/service/WatsonxAiServiceFactory.java
index 70797eed2f68..74ecfe172507 100644
---
a/components/camel-ibm/camel-ibm-watsonx-ai/src/main/java/org/apache/camel/component/ibm/watsonx/ai/service/WatsonxAiServiceFactory.java
+++
b/components/camel-ibm/camel-ibm-watsonx-ai/src/main/java/org/apache/camel/component/ibm/watsonx/ai/service/WatsonxAiServiceFactory.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.ibm.watsonx.ai.service;
+import java.time.Duration;
import java.util.function.Consumer;
import com.ibm.watsonx.ai.chat.ChatService;
@@ -51,6 +52,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -66,6 +68,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -81,6 +84,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -96,6 +100,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -111,6 +116,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -126,6 +132,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
// DetectionService doesn't have modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -150,6 +157,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
// TextExtractionService doesn't have modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -171,6 +179,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
// TextClassificationService doesn't have modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -186,6 +195,7 @@ public final class WatsonxAiServiceFactory {
applyIfNotNull(config.getSpaceId(), builder::spaceId);
applyIfNotNull(config.getModelId(), builder::modelId);
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -198,6 +208,7 @@ public final class WatsonxAiServiceFactory {
// FoundationModelService doesn't have projectId, spaceId, modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -211,6 +222,7 @@ public final class WatsonxAiServiceFactory {
// DeploymentService doesn't have projectId, spaceId, modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));
@@ -231,6 +243,7 @@ public final class WatsonxAiServiceFactory {
// ToolService doesn't have projectId, spaceId, modelId
applyIfNotNull(config.getVerifySsl(), builder::verifySsl);
+ applyIfNotNull(config.getTimeout(), t ->
builder.timeout(Duration.ofMillis(t)));
applyIfTrue(config.getLogRequests(), () -> builder.logRequests(true));
applyIfTrue(config.getLogResponses(), () ->
builder.logResponses(true));