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);
+ }
+}