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 f29a90e6f159 CAMEL-26696: camel-cxf: async producer may never complete
the exchange when the connection fails (#29450)
f29a90e6f159 is described below
commit f29a90e6f15905b8a5a3e0d810dd17d9758c9ddc
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Nov 10 14:47:40 2026 +0100
CAMEL-26696: camel-cxf: async producer may never complete the exchange when
the connection fails (#29450)
The camel-cxf async producer always signals the Camel async callback
exactly once, even when the cookie store or the response binding throws, so the
calling thread no longer hangs in DefaultAsyncProcessorAwaitManager. Binding
failures are kept as suppressed exceptions on the root cause, a done-guard
prevents double completion, and the ConduitSelector branch leaves the exchange
clean when failover already succeeded.
CXF-9272 (the hang inside CXF 4.2.4) still needs a CXF release; CAMEL-26696
stays open until then. testInvokingAWrongServerAsync is disabled pending
CXF-9272.
Hermes Agent (Claude Sonnet 4.6) on behalf of Guillaume Nodet
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../component/cxf/jaxws/CxfClientCallback.java | 135 +++++--
.../component/cxf/jaxws/CxfClientCallbackTest.java | 396 +++++++++++++++++++++
.../camel/component/cxf/jaxws/CxfProducerTest.java | 28 +-
3 files changed, 523 insertions(+), 36 deletions(-)
diff --git
a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfClientCallback.java
b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfClientCallback.java
index ad655051c868..724d3d297107 100644
---
a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfClientCallback.java
+++
b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfClientCallback.java
@@ -16,9 +16,9 @@
*/
package org.apache.camel.component.cxf.jaxws;
-import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
@@ -38,6 +38,9 @@ public class CxfClientCallback extends ClientCallback {
private final org.apache.cxf.message.Exchange cxfExchange;
private final BindingOperationInfo boi;
private final CxfEndpoint endpoint;
+ // Guard against done(false) being called more than once: CXF may call
handleException
+ // after handleResponse throws (e.g. ClientImpl.onMessage catches and
re-dispatches).
+ private final AtomicBoolean doneFired = new AtomicBoolean();
public CxfClientCallback(AsyncCallback callback,
Exchange camelExchange,
@@ -56,23 +59,40 @@ public class CxfClientCallback extends ClientCallback {
try {
super.handleResponse(ctx, res);
} finally {
- // add cookies to the cookie store
- if (endpoint.getCookieHandler() != null &&
cxfExchange.getInMessage() != null) {
- try {
- Map<String, List<String>> cxfHeaders
- = CastUtils.cast((Map<?, ?>)
cxfExchange.getInMessage().get(CxfConstants.PROTOCOL_HEADERS));
- endpoint.getCookieHandler().storeCookies(camelExchange,
endpoint.getRequestUri(camelExchange), cxfHeaders);
- } catch (IOException e) {
- LOG.error("Cannot store cookies", e);
- }
- }
- // bind the CXF response to Camel exchange and
- // call camel callback
// for one way messages callback is already called in
// process method of org.apache.camel.component.cxf.CxfProducer
if (!boi.getOperationInfo().isOneWay()) {
-
endpoint.getCxfBinding().populateExchangeFromCxfResponse(camelExchange,
cxfExchange, ctx);
- camelAsyncCallback.done(false);
+ // always signal the Camel callback, even if cookie store or
binding population throws,
+ // to prevent the calling thread from hanging in
DefaultAsyncProcessorAwaitManager
+ try {
+ // add cookies to the cookie store; catch Throwable so an
unexpected runtime failure
+ // does not prevent populateExchangeFromCxfResponse from
running or done() from firing
+ if (endpoint.getCookieHandler() != null &&
cxfExchange.getInMessage() != null) {
+ try {
+ Map<String, List<String>> cxfHeaders
+ = CastUtils.cast((Map<?, ?>)
cxfExchange.getInMessage().get(CxfConstants.PROTOCOL_HEADERS));
+
endpoint.getCookieHandler().storeCookies(camelExchange,
endpoint.getRequestUri(camelExchange),
+ cxfHeaders);
+ } catch (Throwable e) {
+ LOG.error("Cannot store cookies", e);
+ camelExchange.setException(e);
+ }
+ }
+ // bind the CXF response to Camel exchange
+ try {
+
endpoint.getCxfBinding().populateExchangeFromCxfResponse(camelExchange,
cxfExchange, ctx);
+ } catch (Throwable t) {
+ // preserve any exception set by the cookie store;
record binding error as suppressed
+ Exception cur = camelExchange.getException();
+ if (cur != null && cur != t) {
+ cur.addSuppressed(t);
+ } else if (cur == null) {
+ camelExchange.setException(t);
+ }
+ }
+ } finally {
+ signalDone();
+ }
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} calling handleResponse",
Thread.currentThread().getName());
@@ -82,42 +102,76 @@ public class CxfClientCallback extends ClientCallback {
@Override
public void handleException(Map<String, Object> ctx, Throwable ex) {
+ // If done(false) was already signalled (e.g. handleResponse ran first
and then CXF re-dispatched
+ // to handleException after it threw), skip all exchange mutations:
the exchange is already done.
+ if (doneFired.get()) {
+ return;
+ }
try {
super.handleException(ctx, ex);
// need to call the conduitSelector complete method to enable the
fail over feature
ConduitSelector conduitSelector =
cxfExchange.get(ConduitSelector.class);
if (conduitSelector != null) {
conduitSelector.complete(cxfExchange);
+ // For FailoverTargetSelector, conduitSelector.complete() runs
the retry itself.
+ // If failover succeeds, neither message will carry an
exception — do not set one,
+ // as the exchange should remain clean (failover already
succeeded).
+ // Only propagate an exception when a message explicitly
carries one.
+ Throwable resolved = null;
if (cxfExchange.getOutMessage() != null) {
- ex =
cxfExchange.getOutMessage().getContent(Exception.class);
+ resolved =
cxfExchange.getOutMessage().getContent(Exception.class);
}
- if (ex == null && cxfExchange.getInMessage() != null) {
- ex =
cxfExchange.getInMessage().getContent(Exception.class);
+ if (resolved == null && cxfExchange.getInMessage() != null) {
+ resolved =
cxfExchange.getInMessage().getContent(Exception.class);
}
- if (ex != null) {
- camelExchange.setException(ex);
+ if (resolved != null) {
+ camelExchange.setException(resolved);
}
} else {
camelExchange.setException(ex);
}
} finally {
- // add cookies to the cookie store
- if (endpoint.getCookieHandler() != null &&
cxfExchange.getInMessage() != null) {
- try {
- Map<String, List<String>> cxfHeaders
- = CastUtils.cast((Map<?, ?>)
cxfExchange.getInMessage().get(CxfConstants.PROTOCOL_HEADERS));
- endpoint.getCookieHandler().storeCookies(camelExchange,
endpoint.getRequestUri(camelExchange), cxfHeaders);
- } catch (IOException e) {
- LOG.error("Cannot store cookies", e);
- }
- }
- // copy the context information and
- // call camel callback
// for one way messages callback is already called in
// process method of org.apache.camel.component.cxf.CxfProducer
if (!boi.getOperationInfo().isOneWay()) {
-
endpoint.getCxfBinding().populateExchangeFromCxfResponse(camelExchange,
cxfExchange, ctx);
- camelAsyncCallback.done(false);
+ // always signal the Camel callback, even if cookie store or
binding population throws,
+ // to prevent the calling thread from hanging in
DefaultAsyncProcessorAwaitManager
+ try {
+ // add cookies to the cookie store; catch Throwable so an
unexpected runtime failure
+ // does not prevent populateExchangeFromCxfResponse from
running or done() from firing;
+ // use suppressed to preserve the root transport/fault
exception already on the exchange
+ if (endpoint.getCookieHandler() != null &&
cxfExchange.getInMessage() != null) {
+ try {
+ Map<String, List<String>> cxfHeaders
+ = CastUtils.cast((Map<?, ?>)
cxfExchange.getInMessage().get(CxfConstants.PROTOCOL_HEADERS));
+
endpoint.getCookieHandler().storeCookies(camelExchange,
endpoint.getRequestUri(camelExchange),
+ cxfHeaders);
+ } catch (Throwable e) {
+ LOG.error("Cannot store cookies", e);
+ Throwable root = camelExchange.getException();
+ if (root != null && root != e) {
+ root.addSuppressed(e);
+ } else if (root == null) {
+ camelExchange.setException(e);
+ }
+ }
+ }
+ // copy the context information
+ try {
+
endpoint.getCxfBinding().populateExchangeFromCxfResponse(camelExchange,
cxfExchange, ctx);
+ } catch (Throwable t) {
+ // preserve the root transport/fault exception; record
the binding error as suppressed;
+ // guard against self-suppression (cur == t) which
throws IllegalArgumentException
+ Exception cur = camelExchange.getException();
+ if (cur != null && cur != t) {
+ cur.addSuppressed(t);
+ } else if (cur == null) {
+ camelExchange.setException(t);
+ }
+ }
+ } finally {
+ signalDone();
+ }
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} calling handleException",
Thread.currentThread().getName());
@@ -125,4 +179,17 @@ public class CxfClientCallback extends ClientCallback {
}
}
+ /**
+ * Signals the Camel async callback exactly once. Guards against CXF
calling both {@code handleResponse} and
+ * {@code handleException} on the same callback instance (e.g. when {@code
handleResponse} throws and
+ * {@code ClientImpl.onMessage} re-dispatches).
+ */
+ private void signalDone() {
+ if (doneFired.compareAndSet(false, true)) {
+ camelAsyncCallback.done(false);
+ } else {
+ LOG.warn("done(false) already fired for this callback — duplicate
call suppressed");
+ }
+ }
+
}
diff --git
a/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfClientCallbackTest.java
b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfClientCallbackTest.java
new file mode 100644
index 000000000000..20ec68636d4e
--- /dev/null
+++
b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfClientCallbackTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.cxf.jaxws;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.cxf.common.CxfBinding;
+import org.apache.camel.http.base.cookie.CookieHandler;
+import org.apache.cxf.endpoint.ConduitSelector;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.service.model.BindingOperationInfo;
+import org.apache.cxf.service.model.OperationInfo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link CxfClientCallback} verifying that {@code
camelAsyncCallback.done(false)} is always called
+ * exactly once, and that exception handling preserves the root cause.
+ *
+ * CAMEL-26696
+ */
+class CxfClientCallbackTest {
+
+ private Exchange camelExchange;
+ private org.apache.cxf.message.Exchange cxfExchange;
+ private BindingOperationInfo boi;
+ private CxfEndpoint endpoint;
+ private CxfBinding cxfBinding;
+
+ @BeforeEach
+ void setUp() {
+ camelExchange = mock(Exchange.class);
+ cxfExchange = mock(org.apache.cxf.message.Exchange.class);
+ boi = mock(BindingOperationInfo.class);
+ endpoint = mock(CxfEndpoint.class);
+ cxfBinding = mock(CxfBinding.class);
+
+ OperationInfo operationInfo = mock(OperationInfo.class);
+ when(boi.getOperationInfo()).thenReturn(operationInfo);
+ when(operationInfo.isOneWay()).thenReturn(false);
+
+ when(endpoint.getCxfBinding()).thenReturn(cxfBinding);
+ when(endpoint.getCookieHandler()).thenReturn(null); // no cookie
handler
+ }
+
+ // --- handleResponse tests ---
+
+ @Test
+ void handleResponse_normalPath_callsDoneExactlyOnce() {
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+ Map<String, Object> ctx = new HashMap<>();
+
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called exactly
once");
+ }
+
+ @Test
+ void handleResponse_bindingThrows_callsDoneAndSetsException() {
+ RuntimeException bindingError = new RuntimeException("binding
failure");
+
doThrow(bindingError).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ // Capture the exception set on the exchange
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ camelExchange = mock(Exchange.class);
+ doAnswer(inv -> {
+ captured.set((Throwable) inv.getArgument(0));
+ return null;
+ }).when(camelExchange).setException(any(Throwable.class));
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
binding throws");
+ assertSame(bindingError, captured.get());
+ }
+
+ @Test
+ void handleResponse_bindingThrowsError_callsDoneAndSetsException() {
+ OutOfMemoryError bindingError = new OutOfMemoryError("simulated OOM in
binding");
+
doThrow(bindingError).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ camelExchange = mock(Exchange.class);
+ doAnswer(inv -> {
+ captured.set((Throwable) inv.getArgument(0));
+ return null;
+ }).when(camelExchange).setException(any(Throwable.class));
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
binding throws an Error");
+ assertSame(bindingError, captured.get());
+ }
+
+ // --- handleException tests ---
+
+ @Test
+ void handleException_normalPath_callsDoneExactlyOnce() {
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called exactly
once");
+ verify(camelExchange).setException(transportEx);
+ }
+
+ @Test
+ void
handleException_withConduitSelector_usesResolvedExceptionFromOutMessage() {
+ // When a ConduitSelector is set and outMessage carries a richer
exception, that takes priority
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ RuntimeException resolvedEx = new RuntimeException("resolved from
outMessage");
+
+ ConduitSelector conduitSelector = mock(ConduitSelector.class);
+ Message outMessage = mock(Message.class);
+
when(cxfExchange.get(ConduitSelector.class)).thenReturn(conduitSelector);
+ when(cxfExchange.getOutMessage()).thenReturn(outMessage);
+ when(outMessage.getContent(Exception.class)).thenReturn(resolvedEx);
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called exactly
once");
+ verify(camelExchange).setException(resolvedEx);
+ }
+
+ @Test
+ void
handleException_withConduitSelector_noExceptionOnMessages_doesNotSetException()
{
+ // When ConduitSelector is set and neither message carries an
exception, the failover
+ // succeeded — do NOT fall back to the original transport exception,
leave the exchange
+ // clean (failover already succeeded).
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+
+ ConduitSelector conduitSelector = mock(ConduitSelector.class);
+ Message outMessage = mock(Message.class);
+
when(cxfExchange.get(ConduitSelector.class)).thenReturn(conduitSelector);
+ when(cxfExchange.getOutMessage()).thenReturn(outMessage);
+ when(outMessage.getContent(Exception.class)).thenReturn(null);
+ when(cxfExchange.getInMessage()).thenReturn(null);
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called exactly
once");
+ // failover succeeded: setException must NOT be called (exchange
should remain clean)
+ verify(camelExchange, never()).setException(any());
+ }
+
+ @Test
+ void handleException_bindingThrows_preservesRootCauseAndCallsDone() {
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ RuntimeException bindingEx = new RuntimeException("binding failure");
+
doThrow(bindingEx).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ // Simulate exchange already has the transport exception set
+ when(camelExchange.getException()).thenReturn(transportEx);
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
binding throws");
+ // binding error should be suppressed on the root cause, not replace it
+ assertEquals(1, transportEx.getSuppressed().length);
+ assertSame(bindingEx, transportEx.getSuppressed()[0]);
+ }
+
+ @Test
+ void handleException_bindingThrows_noRootCause_setsBindingException() {
+ RuntimeException bindingEx = new RuntimeException("binding failure");
+
doThrow(bindingEx).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ // No prior exception on the exchange
+ when(camelExchange.getException()).thenReturn(null);
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ doAnswer(inv -> {
+ captured.set((Throwable) inv.getArgument(0));
+ return null;
+ }).when(camelExchange).setException(any(Throwable.class));
+
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get());
+ // when there's no root cause, the binding exception should be set
+ assertSame(bindingEx, captured.get());
+ }
+
+ @Test
+ void handleException_bindingThrowsError_callsDoneAndPreservesRootCause() {
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ OutOfMemoryError bindingError = new OutOfMemoryError("simulated OOM in
binding");
+
doThrow(bindingError).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ when(camelExchange.getException()).thenReturn(transportEx);
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
binding throws an Error");
+ assertEquals(1, transportEx.getSuppressed().length);
+ assertSame(bindingError, transportEx.getSuppressed()[0]);
+ }
+
+ @Test
+ void doneFiredOnlyOnce_whenHandleExceptionCalledAfterHandleResponse() {
+ // Simulate the scenario where CXF calls handleException after
handleResponse
+ // (e.g. because ClientImpl.onMessage catches a thrown exception and
re-dispatches).
+ // The AtomicBoolean guard must prevent done(false) from firing twice.
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ Map<String, Object> ctx = new HashMap<>();
+
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(),
+ "done(false) must fire exactly once even if both
handleResponse and handleException are called");
+ }
+
+ // --- cookie handler tests ---
+
+ @Test
+ void handleResponse_cookieStoreThrows_callsDoneAndSetsException() throws
Exception {
+ // CookieHandler.storeCookies declares IOException but could throw
RuntimeException too;
+ // any Throwable from the cookie store must be recorded and must not
prevent done() from firing.
+ RuntimeException cookieError = new RuntimeException("cookie store
failure");
+ CookieHandler cookieHandler = mock(CookieHandler.class);
+ doThrow(cookieError).when(cookieHandler).storeCookies(any(),
any(URI.class), any());
+
+ Message inMessage = mock(Message.class);
+ when(inMessage.get(any())).thenReturn(Collections.emptyMap());
+ when(cxfExchange.getInMessage()).thenReturn(inMessage);
+ when(endpoint.getCookieHandler()).thenReturn(cookieHandler);
+
when(endpoint.getRequestUri(any())).thenReturn(URI.create("http://wrong-server/"));
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ camelExchange = mock(Exchange.class);
+ doAnswer(inv -> {
+ captured.set((Throwable) inv.getArgument(0));
+ return null;
+ }).when(camelExchange).setException(any(Throwable.class));
+
+ when(endpoint.getCxfBinding()).thenReturn(cxfBinding);
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
cookie store throws");
+ assertSame(cookieError, captured.get());
+ }
+
+ @Test
+ void handleException_cookieStoreThrows_suppressesOnRootCauseAndCallsDone()
throws Exception {
+ // Cookie store failure in the exception path: must be suppressed on
the root transport exception.
+ RuntimeException transportEx = new RuntimeException("connection
refused");
+ RuntimeException cookieError = new RuntimeException("cookie store
failure");
+ CookieHandler cookieHandler = mock(CookieHandler.class);
+ doThrow(cookieError).when(cookieHandler).storeCookies(any(),
any(URI.class), any());
+
+ Message inMessage = mock(Message.class);
+ when(inMessage.get(any())).thenReturn(Collections.emptyMap());
+ when(cxfExchange.getInMessage()).thenReturn(inMessage);
+ when(endpoint.getCookieHandler()).thenReturn(cookieHandler);
+
when(endpoint.getRequestUri(any())).thenReturn(URI.create("http://wrong-server/"));
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ when(camelExchange.getException()).thenReturn(transportEx);
+ when(endpoint.getCxfBinding()).thenReturn(cxfBinding);
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleException(ctx, transportEx);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
cookie store throws");
+ // cookie error must be suppressed on root transport exception, not
replace it
+ assertEquals(1, transportEx.getSuppressed().length);
+ assertSame(cookieError, transportEx.getSuppressed()[0]);
+ }
+
+ @Test
+ void
handleResponse_cookieAndBindingThrow_bindingErrorSuppressedOnCookieError()
throws Exception {
+ // Both cookie store and binding throw in the success path; the
binding error must be suppressed
+ // on the cookie error (not silently lost), and done(false) must still
fire.
+ RuntimeException cookieError = new RuntimeException("cookie store
failure");
+ RuntimeException bindingError = new RuntimeException("binding
failure");
+ CookieHandler cookieHandler = mock(CookieHandler.class);
+ doThrow(cookieError).when(cookieHandler).storeCookies(any(),
any(URI.class), any());
+
doThrow(bindingError).when(cxfBinding).populateExchangeFromCxfResponse(any(),
any(), any());
+
+ Message inMessage = mock(Message.class);
+ when(inMessage.get(any())).thenReturn(Collections.emptyMap());
+ when(cxfExchange.getInMessage()).thenReturn(inMessage);
+ when(endpoint.getCookieHandler()).thenReturn(cookieHandler);
+
when(endpoint.getRequestUri(any())).thenReturn(URI.create("http://wrong-server/"));
+
+ AtomicInteger doneCount = new AtomicInteger();
+ AsyncCallback callback = sync -> doneCount.incrementAndGet();
+
+ AtomicReference<Throwable> captured = new AtomicReference<>();
+ camelExchange = mock(Exchange.class);
+ doAnswer(inv -> {
+ captured.set((Throwable) inv.getArgument(0));
+ return null;
+ }).when(camelExchange).setException(any(Throwable.class));
+ when(camelExchange.getException()).thenAnswer(inv -> captured.get());
+
+ when(endpoint.getCxfBinding()).thenReturn(cxfBinding);
+
+ Map<String, Object> ctx = new HashMap<>();
+ CxfClientCallback cb = new CxfClientCallback(callback, camelExchange,
cxfExchange, boi, endpoint);
+ cb.handleResponse(ctx, new Object[0]);
+
+ assertEquals(1, doneCount.get(), "done(false) must be called even when
both cookie store and binding throw");
+ // cookie error is the primary exception; binding error must be
suppressed on it, not silently lost
+ assertSame(cookieError, captured.get());
+ assertEquals(1, cookieError.getSuppressed().length);
+ assertSame(bindingError, cookieError.getSuppressed()[0]);
+ }
+}
diff --git
a/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfProducerTest.java
b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfProducerTest.java
index e444b9e04017..3b83162c250c 100644
---
a/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfProducerTest.java
+++
b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfProducerTest.java
@@ -48,6 +48,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.Timeout.ThreadMode;
@@ -192,7 +193,7 @@ public class CxfProducerTest {
}
@Test
- public void testInvokingAWrongServer() throws Exception {
+ void testInvokingAWrongServer() throws Exception {
Exchange reply = sendSimpleMessage(getWrongEndpointUri());
assertNotNull(reply.getException(), "We should get the exception
here");
assertInstanceOf(IOException.class, reply.getException().getCause());
@@ -208,6 +209,22 @@ public class CxfProducerTest {
assertInstanceOf(IOException.class, reply.getException().getCause());
}
+ // CAMEL-26696: async producer must always complete the exchange when the
connection fails.
+ // The @Timeout(30s) on the class guards against the exchange hanging in
+ // DefaultAsyncProcessorAwaitManager if camelAsyncCallback.done() is never
called.
+ // For the callback exception-handling semantics (binding throws, Error
thrown, etc.)
+ // see CxfClientCallbackTest which tests CxfClientCallback directly via
Mockito.
+ // Disabled: this test passes with or without the fix (CXF goes through
handleException, not
+ // through populateExchangeFromCxfResponse, on a connection failure), and
CXF-9272 makes it
+ // hang occasionally under load. The CxfClientCallbackTest unit tests
cover the invariants.
+ @Disabled("CXF-9272: may hang under load; callback invariants covered by
CxfClientCallbackTest")
+ @Test
+ void testInvokingAWrongServerAsync() throws Exception {
+ Exchange reply = sendSimpleMessage(getWrongEndpointUriAsync());
+ assertNotNull(reply.getException(), "Async wrong-server call must
complete with an exception");
+ assertInstanceOf(IOException.class, reply.getException().getCause());
+ }
+
@Test
public void testInvokingJaxWsServerWithParams() throws Exception {
Exchange exchange = sendJaxWsMessage();
@@ -238,13 +255,20 @@ public class CxfProducerTest {
protected String getWrongEndpointUri() {
// CAMEL-26600: synchronous=true, as with the CXF async client the
callback sometimes never completes
// when the connection fails, leaving the exchange waiting forever.
- // The async error path is not covered anymore, see CAMEL-26696
+ // The async error path is not covered here; see CxfClientCallbackTest
and CAMEL-26696.
return "cxf://" + getWrongServerAddress()
+
"?serviceClass=org.apache.camel.component.cxf.jaxws.HelloService"
+ "&cxfConfigurer=#wrongServerConfigurer"
+ "&synchronous=true";
}
+ // CAMEL-26696: async (synchronous=false) wrong-server endpoint — callback
must always complete
+ protected String getWrongEndpointUriAsync() {
+ return "cxf://" + getWrongServerAddress()
+ +
"?serviceClass=org.apache.camel.component.cxf.jaxws.HelloService"
+ + "&cxfConfigurer=#wrongServerConfigurer";
+ }
+
protected Exchange sendSimpleMessage() {
return sendSimpleMessage(getSimpleEndpointUri());
}