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 3809ed64d72d CAMEL-25546: camel-whatsapp - Complete the exchange when
the HTTP request fails (#27694)
3809ed64d72d is described below
commit 3809ed64d72dba18c1d60caf81df35ad1cc5aaa1
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Oct 12 23:56:54 2026 +0530
CAMEL-25546: camel-whatsapp - Complete the exchange when the HTTP request
fails (#27694)
The producer handled the result of HttpClient.sendAsync with thenAccept,
which
only runs for a response. A request that failed without one (connection
refused or reset, DNS failure, connect timeout) left the callback uncalled,
so
the exchange never completed: the caller blocked forever and graceful
shutdown
waited for its timeout.
The result is now handled with whenComplete, which calls the callback once
in
all cases. A failed request fails the exchange with a RuntimeCamelException
"Could not send message to <uri>" with the HTTP client exception as cause;
the
URI is sanitized (the access token is sent in the Authorization header, not
the
URI). The exception for an error status also names the sanitized URI, and
any
exception from the response handling is now set on the exchange instead of
being lost.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../service/WhatsAppServiceRestAPIAdapter.java | 27 +++--
.../whatsapp/WhatsAppProducerFailureTest.java | 132 +++++++++++++++++++++
2 files changed, 152 insertions(+), 7 deletions(-)
diff --git
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/service/WhatsAppServiceRestAPIAdapter.java
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/service/WhatsAppServiceRestAPIAdapter.java
index 895bb0bfb249..53ea9c2c2796 100644
---
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/service/WhatsAppServiceRestAPIAdapter.java
+++
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/service/WhatsAppServiceRestAPIAdapter.java
@@ -30,6 +30,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -51,6 +52,7 @@ import
org.apache.camel.component.whatsapp.model.TextMessageRequest;
import org.apache.camel.component.whatsapp.model.UploadMediaRequest;
import org.apache.camel.component.whatsapp.util.FileUploadStreamSupplier;
import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -197,8 +199,20 @@ public class WhatsAppServiceRestAPIAdapter implements
WhatsAppService {
CompletableFuture<HttpResponse<String>> asyncResponse
= httpClient.sendAsync(httpRequestBuilder.build(),
HttpResponse.BodyHandlers.ofString());
- asyncResponse.thenAccept(response -> {
+ // whenComplete (and not thenAccept), so that the exchange is also
completed when there is no response,
+ // for example when the connection failed or the connect timeout
expired. The request itself has no
+ // timeout, so a server that accepts the connection and never
answers is not covered here.
+ asyncResponse.whenComplete((response, failure) -> {
try {
+ if (failure != null) {
+ Throwable cause = failure instanceof
CompletionException && failure.getCause() != null
+ ? failure.getCause() : failure;
+ // the exception of the HTTP client often has no
message (a refused connection), so say
+ // where the request went
+ exchange.setException(new RuntimeCamelException(
+ "Could not send message to " +
URISupport.sanitizeUri(uri), cause));
+ return;
+ }
int statusCode = response.statusCode();
LOG.info("Response received with status {} and body {}",
statusCode, response.body());
final boolean success = statusCode >= 200 && statusCode <
300;
@@ -208,14 +222,13 @@ public class WhatsAppServiceRestAPIAdapter implements
WhatsAppService {
exchange.getMessage().setBody(result);
} else {
LOG.debug("Error response Headers {}",
response.headers());
- RuntimeException exception = new RuntimeCamelException(
- uri + " responded: " + statusCode + " and
body: " + response.body());
- exchange.setException(exception);
- throw exception;
+ exchange.setException(new RuntimeCamelException(
+ URISupport.sanitizeUri(uri) + " responded: " +
statusCode + " and body: "
+ +
response.body()));
}
- } catch (JsonProcessingException e) {
+ } catch (Exception e) {
+ // any failure while handling the response (not only a
JSON one) must fail the exchange
exchange.setException(e);
- throw RuntimeCamelException.wrapRuntimeCamelException(e);
} finally {
callback.done(false);
}
diff --git
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppProducerFailureTest.java
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppProducerFailureTest.java
new file mode 100644
index 000000000000..4198a831b137
--- /dev/null
+++
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppProducerFailureTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.whatsapp;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.whatsapp.model.MessageResponse;
+import org.apache.camel.component.whatsapp.model.TextMessage;
+import org.apache.camel.component.whatsapp.model.TextMessageRequest;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The producer must complete the exchange for every outcome of the HTTP
request: a response, an error response, and no
+ * response at all.
+ */
+class WhatsAppProducerFailureTest extends CamelTestSupport {
+
+ private HttpServer server;
+ private volatile int status = 200;
+ private volatile String responseBody = "{}";
+ // nothing listens on this port
+ private final int closedPort = AvailablePortFinder.getNextAvailable();
+
+ @Override
+ protected void setupResources() throws Exception {
+ server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", exchange -> {
+ byte[] body = responseBody.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type",
"application/json");
+ exchange.sendResponseHeaders(status, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+ server.start();
+ }
+
+ @Override
+ protected void cleanupResources() {
+ server.stop(0);
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ WhatsAppComponent component = new WhatsAppComponent();
+ component.setAuthorizationToken("token");
+ context.addComponent("whatsapp", component);
+ return context;
+ }
+
+ @Test
+ void failsTheExchangeWhenTheConnectionFails() throws Exception {
+ // an asynchronous send, so that an exchange that is never completed
fails the test instead of blocking it
+ Exchange exchange = template.asyncSend("direct:unreachable", e ->
e.getIn().setBody(textMessage()))
+ .get(30, TimeUnit.SECONDS);
+
+ // the exception of the HTTP client (a ConnectException without a
message) is the cause, and the message says
+ // where the request went
+
assertThat(exchange.getException()).isInstanceOf(RuntimeCamelException.class)
+ .hasMessageContaining("Could not send message to
http://localhost:" + closedPort)
+ .hasCauseInstanceOf(IOException.class);
+ }
+
+ @Test
+ void failsTheExchangeForAnErrorResponse() throws Exception {
+ status = 400;
+ responseBody = "{\"error\":{\"message\":\"Unsupported post
request\",\"code\":100}}";
+
+ Exchange exchange = template.asyncSend("direct:server", e ->
e.getIn().setBody(textMessage()))
+ .get(30, TimeUnit.SECONDS);
+
+
assertThat(exchange.getException()).isInstanceOf(RuntimeCamelException.class)
+ .hasMessageContaining("http://localhost:" +
server.getAddress().getPort())
+ .hasMessageContaining("responded:
400").hasMessageContaining("Unsupported post request");
+ }
+
+ @Test
+ void returnsTheResponse() throws Exception {
+ responseBody =
"{\"messaging_product\":\"whatsapp\",\"messages\":[{\"id\":\"wamid.1\"}]}";
+
+ Exchange exchange = template.asyncSend("direct:server", e ->
e.getIn().setBody(textMessage()))
+ .get(30, TimeUnit.SECONDS);
+
+ assertThat(exchange.getException()).isNull();
+
assertThat(exchange.getMessage().getBody()).isInstanceOf(MessageResponse.class);
+ }
+
+ private static TextMessageRequest textMessage() {
+ TextMessageRequest request = new TextMessageRequest();
+ request.setTo("15550001111");
+ request.setText(new TextMessage());
+ request.getText().setBody("Hello");
+ return request;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:unreachable").to("whatsapp:123?baseUri=http://localhost:" +
closedPort);
+
from("direct:server").to("whatsapp:123?baseUri=http://localhost:" +
server.getAddress().getPort());
+ }
+ };
+ }
+}