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

ffang pushed a commit to branch 4.0.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.0.x-fixes by this push:
     new e844be2eb36 [CXF-9250] Stop HttpClientHTTPConduit from endlessly 
resending failed bodyless requests (#3514)
e844be2eb36 is described below

commit e844be2eb36bd7cbc84415a6fea35b48c0b0e879
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.
    
    (cherry picked from commit b33aa1c7d12a22f7b50616a732ceebf13e7a844a)
    (cherry picked from commit b861fd4a262807b4c606ec547e7e18d82531d75d)
---
 .../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 5efbef4c1e6..1b0df0ba582 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
@@ -1060,6 +1060,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