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 b33aa1c7d12 [CXF-9250] Stop HttpClientHTTPConduit from endlessly 
resending failed bodyless requests (#3514)
b33aa1c7d12 is described below

commit b33aa1c7d12a22f7b50616a732ceebf13e7a844a
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Mon Sep 28 16:25:28 2026 -0400

    [CXF-9250] Stop HttpClientHTTPConduit from endlessly resending failed 
bodyless requests (#3514)
    
    For a request without a body (e.g. a GET fetching a remote WSDL), close()
    invoked from the async send's exceptionally() callback re-enters
    handleHeadersTrustCaching() -> setProtocolHeaders() and resends the
    request, since `written` is never set and no exception has been recorded
    on the message. The renewed failure triggers another callback, another
    close(), and another resend, indefinitely.
    
    The shared WrappedOutputStream#close() already guards against exactly
    this by checking outMessage.getContent(Exception.class) before calling
    handleHeadersTrustCaching() again; HttpClientHTTPConduit just never
    populated it on an async failure. Record the failure there from the
    exceptionally() callback so that existing, already-shared guard skips
    the resend, instead of adding new conduit-specific state.
    
    Add HttpClientHTTPConduitResendLoopTest, reproducing the reporter's
    suggested scenario (a server that accepts and immediately drops every
    connection), which observed thousands of connection attempts within
    seconds on the unpatched code.
---
 .../cxf/transport/http/HttpClientHTTPConduit.java  |   7 +
 .../http/HttpClientHTTPConduitResendLoopTest.java  | 141 +++++++++++++++++++++
 2 files changed, 148 insertions(+)

diff --git 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java
 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java
index a4840ec06ef..a3960acd60f 100644
--- 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java
+++ 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java
@@ -1064,6 +1064,13 @@ public class HttpClientHTTPConduit extends 
URLConnectionHTTPConduit {
                 future = cl.sendAsync(request, handler);
             }
             future.exceptionally(ex -> {
+                // Record the failure on the message so the close() invoked 
below
+                // does not treat this as an un-sent request and resend it: the
+                // shared WrappedOutputStream#close() already skips
+                // handleHeadersTrustCaching() once outMessage carries an 
Exception
+                // (CXF-9250).
+                outMessage.setContent(Exception.class,
+                        ex instanceof Exception ? (Exception) ex : new 
IOException(ex));
                 if (pout != null) {
                     synchronized (pout) {
                         pout.notifyAll();
diff --git 
a/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientHTTPConduitResendLoopTest.java
 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientHTTPConduitResendLoopTest.java
new file mode 100644
index 00000000000..a0459eb90c3
--- /dev/null
+++ 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientHTTPConduitResendLoopTest.java
@@ -0,0 +1,141 @@
+/**
+ * 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.cxf.transport.http;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.bus.extension.ExtensionManagerBus;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.service.model.EndpointInfo;
+import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
+
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * CXF-9250: a failed exchange for a request without a body (e.g. a GET 
request,
+ * such as fetching a remote WSDL, whose connection attempt or TLS handshake
+ * never succeeds) must be reported once, not endlessly re-sent in the 
background
+ * from the CompletableFuture's exceptionally() callback of 
HttpClientHTTPConduit.
+ *
+ * This reproduces the scenario described by the issue reporter: a raw server
+ * socket that accepts every connection and immediately drops it, so the
+ * HttpClient never completes an exchange.
+ */
+public class HttpClientHTTPConduitResendLoopTest {
+
+    private ServerSocket serverSocket;
+    private volatile boolean stopped;
+    private final AtomicInteger connectionAttempts = new AtomicInteger();
+
+    @After
+    public void tearDown() throws IOException {
+        stopped = true;
+        if (serverSocket != null) {
+            serverSocket.close();
+        }
+    }
+
+    private int startFailingServer() throws IOException {
+        serverSocket = new ServerSocket(0, 50, 
InetAddress.getByName("localhost"));
+        Thread t = new Thread(() -> {
+            while (!stopped) {
+                try {
+                    Socket s = serverSocket.accept();
+                    connectionAttempts.incrementAndGet();
+                    // Simulate a connection / TLS handshake failure: accept 
and
+                    // immediately drop the connection without ever completing
+                    // the HTTP exchange.
+                    s.close();
+                } catch (IOException e) {
+                    // expected once the server socket is closed in tearDown()
+                    break;
+                }
+            }
+        });
+        t.setDaemon(true);
+        t.setName("CXF-9250-failing-server");
+        t.start();
+        return serverSocket.getLocalPort();
+    }
+
+    private Message getNewMessage() {
+        Message message = new MessageImpl();
+        Map<String, List<String>> headers = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        List<String> contentTypes = new ArrayList<>();
+        contentTypes.add("text/xml");
+        headers.put("content-type", contentTypes);
+        message.put(Message.PROTOCOL_HEADERS, headers);
+        return message;
+    }
+
+    @Test
+    public void testFailedGetIsNotResentEndlessly() throws Exception {
+        int port = startFailingServer();
+
+        Bus bus = new ExtensionManagerBus();
+        EndpointInfo ei = new EndpointInfo();
+        ei.setAddress("http://localhost:"; + port + "/");
+
+        HttpClientHTTPConduit conduit = new HttpClientHTTPConduit(bus, ei, 
null);
+        HTTPClientPolicy policy = new HTTPClientPolicy();
+        policy.setConnectionTimeout(2000);
+        policy.setReceiveTimeout(2000);
+        conduit.setClient(policy);
+        conduit.finalizeConfig();
+
+        Message message = getNewMessage();
+        message.put(Message.HTTP_REQUEST_METHOD, "GET");
+
+        conduit.prepare(message);
+        OutputStream os = message.getContent(OutputStream.class);
+
+        try {
+            os.close();
+            fail("Expected an IOException to be reported since the server 
drops every connection");
+        } catch (IOException expected) {
+            // expected: the failed exchange must be reported to the caller
+        }
+
+        // Give a resend loop, if any, a chance to run for a while before 
asserting on it.
+        // Without the fix, hundreds of connection attempts would happen well 
within this window.
+        Thread.sleep(3000);
+
+        int attempts = connectionAttempts.get();
+        assertTrue("Expected only a small, bounded number of connection 
attempts, but the server "
+                   + "observed " + attempts + " - the failed request appears 
to have been "
+                   + "resent in a loop from the async failure callback 
(CXF-9250)",
+                   attempts <= 5);
+    }
+}

Reply via email to