This is an automated email from the ASF dual-hosted git repository.

ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new c3837b37948 CXF-7232: Report a failure of the retransmits of an 
asynchronous call… (#3706)
c3837b37948 is described below

commit c3837b37948ccaa6e7be338d87d3fdc8cb2da3d6
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Thu Nov 12 09:59:12 2026 -0500

    CXF-7232: Report a failure of the retransmits of an asynchronous call… 
(#3706)
    
    * CXF-7232: Report a failure of the retransmits of an asynchronous call as 
before
    
    Since the retransmits of an asynchronous call are processed on the thread 
that
    handles the response, a failure of them, e.g. a refused connection with
    MaxRetransmits set, reached the fault observer as the raw exception. When 
they
    were processed by the caller's close(), it was mapped and reported as the 
fault
    of sending the message ("Could not send Message."). Report it the same way 
again.
    
    DispatchImpl.mapException() threw the WebServiceException of such a fault
    instead of returning it, so an asynchronous Dispatch call that got it never
    completed.
    
    * Rename handleRetransmitsAndResponseAsync to 
handleRetransmitsAndResponseOnWorkqueue
    
    It is only called by the runnable of handleResponseOnWorkqueue.
---
 .../java/org/apache/cxf/jaxws/DispatchImpl.java    |  2 +-
 .../org/apache/cxf/transport/http/HTTPConduit.java | 69 +++++++++++++++++++---
 .../cxf/systest/hc5/http/AsyncRetransmitTest.java  | 36 +++++++++++
 .../cxf/systest/http/AsyncRetransmitTest.java      | 36 +++++++++++
 4 files changed, 133 insertions(+), 10 deletions(-)

diff --git 
a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/DispatchImpl.java 
b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/DispatchImpl.java
index 702a3411dc7..3b4d9b2681c 100644
--- a/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/DispatchImpl.java
+++ b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/DispatchImpl.java
@@ -264,7 +264,7 @@ public class DispatchImpl<T> implements Dispatch<T>, 
BindingProvider, Closeable
     }
     private RuntimeException mapException(Exception ex) {
         if (ex instanceof Fault && ex.getCause() instanceof IOException) {
-            throw new WebServiceException(ex.getMessage(), ex.getCause());
+            return new WebServiceException(ex.getMessage(), ex.getCause());
         }
 
         if (getBinding() instanceof HTTPBinding) {
diff --git 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HTTPConduit.java
 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HTTPConduit.java
index 4bf882924fd..f3bfe4293b8 100644
--- 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HTTPConduit.java
+++ 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HTTPConduit.java
@@ -30,6 +30,7 @@ import java.lang.ref.WeakReference;
 import java.net.HttpRetryException;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
@@ -38,6 +39,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
@@ -47,6 +49,7 @@ import java.util.logging.Logger;
 import javax.xml.namespace.QName;
 
 import org.apache.cxf.Bus;
+import org.apache.cxf.common.i18n.BundleUtils;
 import org.apache.cxf.common.injection.NoJSR250Annotations;
 import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.common.util.PropertyUtils;
@@ -62,6 +65,8 @@ import org.apache.cxf.helpers.CastUtils;
 import org.apache.cxf.helpers.HttpHeaderHelper;
 import org.apache.cxf.helpers.IOUtils;
 import org.apache.cxf.helpers.LoadingByteArrayOutputStream;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.interceptor.MessageSenderInterceptor;
 import org.apache.cxf.io.AbstractThresholdOutputStream;
 import org.apache.cxf.io.CacheAndWriteOutputStream;
 import org.apache.cxf.io.CachedOutputStream;
@@ -223,6 +228,11 @@ public abstract class HTTPConduit
      */
     private static final String KEY_AUTH_URLS = "AuthURLs";
 
+    /**
+     * The messages of the faults of the out chain for a failure of the conduit
+     */
+    private static final ResourceBundle SENDER_BUNDLE = 
BundleUtils.getBundle(MessageSenderInterceptor.class);
+
     /**
      *  This field holds a reference to the CXF bus associated this conduit.
      */
@@ -1235,7 +1245,7 @@ public abstract class HTTPConduit
             Runnable runnable = new Runnable() {
                 public void run() {
                     try {
-                        handleRetransmitsAndResponse();
+                        handleRetransmitsAndResponseOnWorkqueue();
                     } catch (Throwable e) {
                         
((PhaseInterceptorChain)outMessage.getInterceptorChain()).abort();
                         outMessage.setContent(Exception.class, e);
@@ -1428,14 +1438,7 @@ public abstract class HTTPConduit
             } catch (HttpRetryException e) {
                 handleHttpRetryException(e);
             } catch (IOException e) {
-                String origMessage = e.getMessage();
-                if (origMessage != null && 
origMessage.contains(url.toString())) {
-                    throw e;
-                }
-                throw mapException(e.getClass().getSimpleName()
-                                   + " invoking " + url + ": "
-                                   + getExceptionMessage(e), e,
-                                   IOException.class);
+                throw mapException(e);
             } catch (RuntimeException e) {
                 throw mapException(e.getClass().getSimpleName()
                                    + " invoking " + url + ": "
@@ -1444,6 +1447,17 @@ public abstract class HTTPConduit
             }
         }
 
+        private IOException mapException(IOException e) {
+            String origMessage = e.getMessage();
+            if (origMessage != null && origMessage.contains(url.toString())) {
+                return e;
+            }
+            return mapException(e.getClass().getSimpleName()
+                                + " invoking " + url + ": "
+                                + getExceptionMessage(e), e,
+                                IOException.class);
+        }
+
         protected String getExceptionMessage(Throwable t) {
             return t.getMessage();
         }
@@ -1645,6 +1659,43 @@ public abstract class HTTPConduit
             handleResponseInternal();
         }
 
+        /**
+         * Processes the retransmits and the response on the workqueue, see
+         * {@link #handleResponseOnWorkqueue(boolean, boolean)}. A failure of 
the retransmits is
+         * reported as it was when they were processed by the caller: as 
close() maps it, and the
+         * out chain then reports it.
+         */
+        private void handleRetransmitsAndResponseOnWorkqueue() throws 
IOException {
+            try {
+                handleRetransmits();
+            } catch (HttpRetryException e) {
+                try {
+                    handleHttpRetryException(e);
+                } catch (IOException ex) {
+                    throw sendFault(ex);
+                }
+            } catch (IOException e) {
+                throw sendFault(mapException(e));
+            } catch (RuntimeException e) {
+                throw mapException(e.getClass().getSimpleName()
+                                   + " invoking " + url + ": "
+                                   + getExceptionMessage(e), e,
+                                   RuntimeException.class);
+            } finally {
+                releaseCachedStream();
+            }
+            handleResponseInternal();
+        }
+
+        /**
+         * The fault of the out chain for a failure of the conduit, as 
reported by
+         * {@link MessageSenderInterceptor.MessageSenderEndingInterceptor}
+         */
+        private Fault sendFault(IOException e) {
+            return new Fault(new org.apache.cxf.common.i18n.Message(e 
instanceof SocketTimeoutException
+                ? "COULD_NOT_RECEIVE" : "COULD_NOT_SEND", SENDER_BUNDLE), e);
+        }
+
         private void releaseCachedStream() throws IOException {
             if (cachingForRetransmission && cachedStream != null) {
                 cachedStream.close();
diff --git 
a/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/AsyncRetransmitTest.java
 
b/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/AsyncRetransmitTest.java
index 19a279ec5a5..14467be7640 100644
--- 
a/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/AsyncRetransmitTest.java
+++ 
b/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/AsyncRetransmitTest.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringReader;
+import java.net.ConnectException;
 import java.net.InetSocketAddress;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
@@ -50,6 +51,7 @@ import com.sun.net.httpserver.HttpServer;
 import jakarta.xml.ws.Dispatch;
 import jakarta.xml.ws.Response;
 import jakarta.xml.ws.Service;
+import jakarta.xml.ws.WebServiceException;
 import jakarta.xml.ws.soap.SOAPBinding;
 import org.apache.cxf.Bus;
 import org.apache.cxf.BusFactory;
@@ -89,6 +91,8 @@ import static org.junit.Assert.fail;
 @RunWith(Parameterized.class)
 public class AsyncRetransmitTest {
     public static final String PORT = 
TestUtil.getPortNumber(AsyncRetransmitTest.class);
+    // nothing listens on it
+    public static final String REFUSED_PORT = 
TestUtil.getPortNumber(AsyncRetransmitTest.class, 2);
     private static final QName SERVICE = new QName("urn:cxf7232", 
"EchoService");
     private static final QName PORT_NAME = new QName("urn:cxf7232", 
"EchoPort");
     private static final String REQUEST = "<echo 
xmlns=\"urn:cxf7232\">hello</echo>";
@@ -287,6 +291,38 @@ public class AsyncRetransmitTest {
         }
     }
 
+    @Test
+    public void testMaxRetransmitsConnectionRefused() throws Exception {
+        HTTPClientPolicy policy = new HTTPClientPolicy();
+        policy.setMaxRetransmits(1);
+        Dispatch<Source> dispatch = createDispatch("/echo", policy);
+        String address = "http://localhost:"; + REFUSED_PORT + "/echo";
+        dispatch.getRequestContext().put(Message.ENDPOINT_ADDRESS, address);
+        Throwable failure;
+        try {
+            Response<Source> response = dispatch.invokeAsync(new 
StreamSource(new StringReader(REQUEST)));
+            try {
+                response.get(30, TimeUnit.SECONDS);
+                fail("A refused connection must fail the asynchronous 
response");
+                return;
+            } catch (ExecutionException ex) {
+                failure = ex.getCause();
+            }
+        } catch (WebServiceException ex) {
+            // the connection may already be refused while the caller thread 
sends the request
+            failure = ex;
+        } finally {
+            destroy(dispatch);
+        }
+        // reported as when the retransmits were processed by the caller thread
+        assertEquals("Could not send Message.", failure.getMessage());
+        Throwable cause = failure.getCause();
+        while (cause != null && !(cause instanceof ConnectException)) {
+            cause = cause.getCause();
+        }
+        assertNotNull("Expected the refused connection: " + failure, cause);
+    }
+
     private void invokeAsync(String path, HTTPClientPolicy policy) throws 
Exception {
         Dispatch<Source> dispatch = createDispatch(path, policy);
         try {
diff --git 
a/systests/transports/src/test/java/org/apache/cxf/systest/http/AsyncRetransmitTest.java
 
b/systests/transports/src/test/java/org/apache/cxf/systest/http/AsyncRetransmitTest.java
index 8a2d00bce60..f1eaca5a48e 100644
--- 
a/systests/transports/src/test/java/org/apache/cxf/systest/http/AsyncRetransmitTest.java
+++ 
b/systests/transports/src/test/java/org/apache/cxf/systest/http/AsyncRetransmitTest.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringReader;
+import java.net.ConnectException;
 import java.net.InetSocketAddress;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
@@ -50,6 +51,7 @@ import com.sun.net.httpserver.HttpServer;
 import jakarta.xml.ws.Dispatch;
 import jakarta.xml.ws.Response;
 import jakarta.xml.ws.Service;
+import jakarta.xml.ws.WebServiceException;
 import jakarta.xml.ws.soap.SOAPBinding;
 import org.apache.cxf.Bus;
 import org.apache.cxf.BusFactory;
@@ -89,6 +91,8 @@ import static org.junit.Assert.fail;
 @RunWith(Parameterized.class)
 public class AsyncRetransmitTest {
     public static final String PORT = 
TestUtil.getPortNumber(AsyncRetransmitTest.class);
+    // nothing listens on it
+    public static final String REFUSED_PORT = 
TestUtil.getPortNumber(AsyncRetransmitTest.class, 2);
     private static final QName SERVICE = new QName("urn:cxf7232", 
"EchoService");
     private static final QName PORT_NAME = new QName("urn:cxf7232", 
"EchoPort");
     private static final String REQUEST = "<echo 
xmlns=\"urn:cxf7232\">hello</echo>";
@@ -287,6 +291,38 @@ public class AsyncRetransmitTest {
         }
     }
 
+    @Test
+    public void testMaxRetransmitsConnectionRefused() throws Exception {
+        HTTPClientPolicy policy = new HTTPClientPolicy();
+        policy.setMaxRetransmits(1);
+        Dispatch<Source> dispatch = createDispatch("/echo", policy);
+        String address = "http://localhost:"; + REFUSED_PORT + "/echo";
+        dispatch.getRequestContext().put(Message.ENDPOINT_ADDRESS, address);
+        Throwable failure;
+        try {
+            Response<Source> response = dispatch.invokeAsync(new 
StreamSource(new StringReader(REQUEST)));
+            try {
+                response.get(30, TimeUnit.SECONDS);
+                fail("A refused connection must fail the asynchronous 
response");
+                return;
+            } catch (ExecutionException ex) {
+                failure = ex.getCause();
+            }
+        } catch (WebServiceException ex) {
+            // the connection may already be refused while the caller thread 
sends the request
+            failure = ex;
+        } finally {
+            destroy(dispatch);
+        }
+        // reported as when the retransmits were processed by the caller thread
+        assertEquals("Could not send Message.", failure.getMessage());
+        Throwable cause = failure.getCause();
+        while (cause != null && !(cause instanceof ConnectException)) {
+            cause = cause.getCause();
+        }
+        assertNotNull("Expected the refused connection: " + failure, cause);
+    }
+
     private void invokeAsync(String path, HTTPClientPolicy policy) throws 
Exception {
         Dispatch<Source> dispatch = createDispatch(path, policy);
         try {

Reply via email to