This is an automated email from the ASF dual-hosted git repository.
ffang pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/3.6.x-fixes by this push:
new 37751c4f1e0 [CXF-9250] Stop HttpClientHTTPConduit from endlessly
resending failed bodyless requests (#3514)
37751c4f1e0 is described below
commit 37751c4f1e0406df3aeaa480d1c3e7bec8a36292
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);
+ }
+}