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 52b62b9eaf7 CXF-9267: HttpClientHTTPConduit.HttpClientBodyPublisher 
hides ConnectionTimeout Exception (#3660)
52b62b9eaf7 is described below

commit 52b62b9eaf79ec86b6a5d3f96632f23d3af66b5b
Author: Valentino Porta <[email protected]>
AuthorDate: Wed Nov 4 20:28:57 2026 +0100

    CXF-9267: HttpClientHTTPConduit.HttpClientBodyPublisher hides 
ConnectionTimeout Exception (#3660)
    
    * Race condition CXF-9267 ...made pin available also if stream==null
    
    * Race condition CXF-9267 ...add some tests.
    testSubscribeAfterPublisherClose failed with the code before.
    
    * Add comments
    
    * Address issue of java.io.IOException: Pipe not connected in test where 
http method was without body (GET/DELETE)
    
    * Add one new http2 GET test with chunking enable to cover also this 
scenario
---
 .../cxf/transport/http/HttpClientHTTPConduit.java  |  18 +-
 .../http/HttpClientBodyPublisherTest.java          | 319 +++++++++++++++++++++
 .../apache/cxf/systest/http2_jetty/BookStore.java  |   7 +
 .../http2_jetty/JettyClientServerHttp2Test.java    |  32 +++
 4 files changed, 369 insertions(+), 7 deletions(-)

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 1e217005bdb..8b068f77a20 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
@@ -653,12 +653,12 @@ public class HttpClientHTTPConduit extends 
URLConnectionHTTPConduit {
     /**
      * The {@link BodyPublisher} that wraps around the output stream.
      */
-    private static final class HttpClientBodyPublisher implements 
CloseableBodyPublisher {
+    static final class HttpClientBodyPublisher implements 
CloseableBodyPublisher {
         private Supplier<InputStream> pin;
         private HttpClientWrappedOutputStream stream;
         private long contentLen;
 
-        private HttpClientBodyPublisher(HttpClientWrappedOutputStream s, 
Supplier<InputStream> pin) {
+        HttpClientBodyPublisher(HttpClientWrappedOutputStream s, 
Supplier<InputStream> pin) {
             this.stream = s;
             this.pin = pin;
         }
@@ -682,13 +682,17 @@ public class HttpClientHTTPConduit extends 
URLConnectionHTTPConduit {
                     if (stream != null) {
                         contentLen = stream.contentLen;
                     }
-                    BodyPublishers.ofInputStream(pin).subscribe(subscriber);
                     stream = null;
-                    pin = null;
-                    return;
                 }
             }
-            BodyPublishers.noBody().subscribe(subscriber);
+            // See https://issues.apache.org/jira/browse/CXF-9267
+            if (pin != null) {
+                BodyPublishers.ofInputStream(pin).subscribe(subscriber);
+                pin = null;
+            } else {
+                // Avoiding java.io.IOException: Pipe closed if pin already 
subscribed
+                BodyPublishers.noBody().subscribe(subscriber);
+            }
         }
 
         @Override
@@ -1018,7 +1022,7 @@ public class HttpClientHTTPConduit extends 
URLConnectionHTTPConduit {
             if (csPolicy.isAllowChunking() || contentLen >= 0) {
                 final PipedInputStream pin = new 
PipedInputStream(csPolicy.getChunkLength() <= 0
                         ? 4096 : csPolicy.getChunkLength());
-                this.publisher = new HttpClientBodyPublisher(this, () -> pin);
+                this.publisher = new HttpClientBodyPublisher(this, contentLen 
!= 0 ? () -> pin : null);
                 if (contentLen != 0) {
                     pout = new HttpClientPipedOutputStream(this, pin, 
csPolicy, publisher);
                 }
diff --git 
a/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientBodyPublisherTest.java
 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientBodyPublisherTest.java
new file mode 100644
index 00000000000..6a993927eee
--- /dev/null
+++ 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/http/HttpClientBodyPublisherTest.java
@@ -0,0 +1,319 @@
+/**
+ * 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.ByteArrayOutputStream;
+import java.io.PipedInputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Flow;
+import java.util.concurrent.TimeUnit;
+
+import 
org.apache.cxf.transport.http.HttpClientHTTPConduit.HttpClientBodyPublisher;
+import 
org.apache.cxf.transport.http.HttpClientHTTPConduit.HttpClientPipedOutputStream;
+import 
org.apache.cxf.transport.http.HttpClientHTTPConduit.HttpClientWrappedOutputStream;
+import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * See https://issues.apache.org/jira/browse/CXF-9267
+ */
+public class HttpClientBodyPublisherTest {
+
+    private static final int REQUEST_SIZE = 206;
+    private static final int CHUNK_THRESHOLD_SIZE = 4096;
+
+    private static final int AWAIT_TEST_TIMEOUT = 4;
+
+    /**
+     * The HTTP client subscribes and receives the complete request body.
+     */
+    @Test
+    public void testSubscribeBeforePublisherClose() throws Exception {
+        byte[] expected = body(REQUEST_SIZE);
+
+        TestPubPipeWrap pubStream = streams(expected.length);
+
+        pubStream.pipedOutputStream.write(expected);
+
+        /*
+         * We subscribe first
+         */
+        CountDownLatch countDownLatchBeforeSubscribe = new CountDownLatch(1);
+        CountDownLatch countDownLatchAfterSubscribe = new CountDownLatch(1);
+        // The HttpClient arrive before the close ... and subscribe...
+        // Using PipedInputStream as pin I use another thread to subscribe
+        CompletableFuture<byte[]> received = subscribeAsync(
+                pubStream.publisher,
+                countDownLatchBeforeSubscribe,
+                countDownLatchAfterSubscribe
+        );
+        // Wait to be subscribed
+        assertTrue(countDownLatchBeforeSubscribe.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        assertTrue(countDownLatchAfterSubscribe.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+
+        /*
+         * ...and then close
+         */
+
+        // The caller close the stream
+        pubStream.wrappedOutputStream.close();
+        // Close also the publisher
+        pubStream.pipedOutputStream.close();
+
+        // Assert the body is the same
+        assertArrayEquals(expected,  received.get(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        // ... and the connection marked completed
+        assertTrue(pubStream.wrappedOutputStream.connectionComplete);
+    }
+
+    /**
+     * The HTTP client subscribes twice .
+     */
+    @Test
+    public void testSubscribeTwiceBeforePublisherClose() throws Exception {
+        byte[] expected = body(REQUEST_SIZE);
+
+        TestPubPipeWrap pubStream = streams(expected.length);
+
+        pubStream.pipedOutputStream.write(expected);
+
+        /*
+         * We subscribe first ... 1°
+         */
+        CountDownLatch countDownLatchBeforeSubscribe1 = new CountDownLatch(1);
+        CountDownLatch countDownLatchAfterSubscribe1 = new CountDownLatch(1);
+        // The HttpClient arrive before the close ... and subscribe...
+        // Using PipedInputStream as pin I use another thread to subscribe
+        CompletableFuture<byte[]> received = subscribeAsync(
+                pubStream.publisher,
+                countDownLatchBeforeSubscribe1,
+                countDownLatchAfterSubscribe1
+        );
+        // Wait to be subscribed 1°
+        assertTrue(countDownLatchBeforeSubscribe1.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        assertTrue(countDownLatchAfterSubscribe1.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+
+        /*
+         * We subscribe first ... 2°
+         */
+        CountDownLatch countDownLatchBeforeSubscribe2 = new CountDownLatch(1);
+        CountDownLatch countDownLatchAfterSubscribe2 = new CountDownLatch(1);
+        // The HttpClient arrive before the close ... and subscribe...
+        // Using PipedInputStream as pin I use another thread to subscribe
+        CompletableFuture<byte[]> received2 = subscribeAsync(
+                pubStream.publisher,
+                countDownLatchBeforeSubscribe2,
+                countDownLatchAfterSubscribe2
+        );
+
+        //We assure also the 2° thread is in position in 
publisher.subscribe(...)
+        assertTrue(countDownLatchBeforeSubscribe2.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+
+        /*
+         * ...and then close
+         */
+
+        // The caller close the stream
+        pubStream.wrappedOutputStream.close();
+
+        // Close also the publisher
+        pubStream.pipedOutputStream.close();
+
+        // Wait to be subscribed 2° ...the lock on the pin is Publisher is 
release by now
+        assertTrue(countDownLatchAfterSubscribe2.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+
+        // Assert the body is the same
+        assertArrayEquals(expected,  received.get(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        assertArrayEquals(new byte[0],  received2.get(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        // ... and the connection marked completed
+        assertTrue(pubStream.wrappedOutputStream.connectionComplete);
+    }
+
+    /**
+     * The publisher is closed before the HTTP client subscribes.
+     * The complete request body must still be available.
+     */
+    @Test
+    public void testSubscribeAfterPublisherClose() throws Exception {
+        byte[] expected = body(REQUEST_SIZE);
+
+        TestPubPipeWrap pubStream = streams(expected.length);
+
+        pubStream.pipedOutputStream.write(expected);
+
+        /*
+         * We close before the publisher
+         */
+
+        // The caller close the stream
+        pubStream.wrappedOutputStream.close();
+        // Close also the publisher
+        pubStream.pipedOutputStream.close();
+
+
+        /*
+         * ...and then subscribe
+         */
+
+        CountDownLatch countDownLatchBeforeSubscribe = new CountDownLatch(1);
+        CountDownLatch countDownLatchAfterSubscribe = new CountDownLatch(1);
+        // The HttpClient arrive before the close ... and subscribe...
+        // Using PipedInputStream as pin I use another thread to subscribe
+        CompletableFuture<byte[]> received = subscribeAsync(
+                pubStream.publisher,
+                countDownLatchBeforeSubscribe,
+                countDownLatchAfterSubscribe
+        );
+        // Wait to be subscribed
+        assertTrue(countDownLatchBeforeSubscribe.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        assertTrue(countDownLatchAfterSubscribe.await(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+
+        // Assert the body is the same
+        assertArrayEquals(expected,  received.get(AWAIT_TEST_TIMEOUT, 
TimeUnit.SECONDS));
+        // ... and the connection not marked completed (the stream is null in 
the publisher)
+        assertFalse(pubStream.wrappedOutputStream.connectionComplete);
+    }
+
+    /**
+     * The content-length must remain available even after publisher.close()
+     */
+    @Test
+    public void testContentLengthAfterClose() throws Exception {
+        TestPubPipeWrap streams = streams(REQUEST_SIZE);
+
+        assertEquals(REQUEST_SIZE, streams.publisher.contentLength());
+
+        streams.publisher.close();
+
+        assertEquals(REQUEST_SIZE, streams.publisher.contentLength());
+    }
+
+    private static TestPubPipeWrap streams(int contentLen) throws Exception {
+        PipedInputStream pin = new PipedInputStream(CHUNK_THRESHOLD_SIZE);
+
+        HttpClientWrappedOutputStream wrappedOutputStream =
+                mock(HttpClientWrappedOutputStream.class);
+
+        wrappedOutputStream.future = new CompletableFuture<>();
+        wrappedOutputStream.contentLen = contentLen;
+
+        HTTPClientPolicy policy = new HTTPClientPolicy();
+        policy.setConnectionTimeout(180);
+
+        HttpClientBodyPublisher publisher =
+                new HttpClientBodyPublisher(wrappedOutputStream, () -> pin);
+
+        HttpClientPipedOutputStream pipedOutputStream =
+                new HttpClientPipedOutputStream(wrappedOutputStream, pin, 
policy, publisher);
+
+        wrappedOutputStream.pout = pipedOutputStream;
+
+        return new TestPubPipeWrap(publisher, pipedOutputStream, 
wrappedOutputStream);
+    }
+
+    private static CompletableFuture<byte[]> subscribeAsync(
+            HttpClientBodyPublisher publisher,
+            CountDownLatch countDownLatchBeforeSubscribe,
+            CountDownLatch countDownLatchAfterSubscribe
+    ) {
+
+        CompletableFuture<byte[]> received = new CompletableFuture<>();
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+
+        executor.submit(() -> {
+            try {
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+                Flow.Subscriber<ByteBuffer> subscriber = new 
Flow.Subscriber<ByteBuffer>() {
+                    @Override
+                    public void onSubscribe(Flow.Subscription subscription) {
+                        countDownLatchAfterSubscribe.countDown();
+                        subscription.request(Long.MAX_VALUE);
+                    }
+
+                    @Override
+                    public void onNext(ByteBuffer buffer) {
+                        byte[] bytes = new byte[buffer.remaining()];
+                        buffer.get(bytes);
+                        baos.writeBytes(bytes);
+                    }
+
+                    @Override
+                    public void onError(Throwable error) {
+                        received.completeExceptionally(error);
+                        executor.shutdown();
+                    }
+
+                    @Override
+                    public void onComplete() {
+                        received.complete(baos.toByteArray());
+                        executor.shutdown();
+                    }
+                };
+
+                countDownLatchBeforeSubscribe.countDown();
+                publisher.subscribe(subscriber);
+
+            } catch (Throwable e) {
+                received.completeExceptionally(e);
+                executor.shutdown();
+            }
+        });
+
+        return received;
+    }
+
+    private static byte[] body(int size) {
+        byte[] request = "<soap:Envelope 
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\";>"
+                .getBytes(StandardCharsets.UTF_8);
+        byte[] body = Arrays.copyOf(request, size);
+        Arrays.fill(body, Math.min(request.length, size), size, (byte)' ');
+        return body;
+    }
+
+    private static class TestPubPipeWrap {
+        final HttpClientBodyPublisher publisher;
+        final HttpClientPipedOutputStream pipedOutputStream;
+        final HttpClientWrappedOutputStream wrappedOutputStream;
+
+        TestPubPipeWrap(
+                HttpClientBodyPublisher publisher,
+                HttpClientPipedOutputStream pipedOutputStream,
+                HttpClientWrappedOutputStream wrappedOutputStream
+        ) {
+            this.publisher = publisher;
+            this.pipedOutputStream = pipedOutputStream;
+            this.wrappedOutputStream = wrappedOutputStream;
+        }
+    }
+}
diff --git 
a/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/BookStore.java
 
b/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/BookStore.java
index 52fcd88ad02..9ce552cdb8a 100644
--- 
a/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/BookStore.java
+++ 
b/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/BookStore.java
@@ -50,6 +50,13 @@ public class BookStore {
         return "CXF in Action".getBytes();
     }
 
+    @GET
+    @Path("/pluto")
+    @Produces("text/plain")
+    public byte[] getPluto(@Context HttpServletRequest request) {
+        return (request.getProtocol() + " CXF in Action - 
Pluto").getBytes(StandardCharsets.UTF_8);
+    }
+
     @POST
     @Path("/echo")
     @Consumes("text/plain")
diff --git 
a/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/JettyClientServerHttp2Test.java
 
b/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/JettyClientServerHttp2Test.java
index e041a50d273..13645927fbd 100644
--- 
a/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/JettyClientServerHttp2Test.java
+++ 
b/systests/transports/src/test/java/org/apache/cxf/systest/http2_jetty/JettyClientServerHttp2Test.java
@@ -49,6 +49,38 @@ public class JettyClientServerHttp2Test extends 
AbstractJettyClientServerHttp2Te
         createStaticBus();
     }
 
+    @Test(timeout = 60000)
+    public void testGetWithChunkingHttp2() throws Exception {
+        // the URLConnection conduit does not support HTTP/2
+        
assumeFalse(Boolean.getBoolean("org.apache.cxf.transport.http.forceURLConnection"));
+        final WebClient wc = WebClient
+                .create(getAddress() + getContext() + "/web/bookstore/pluto")
+                .accept("text/plain");
+
+        final ClientConfiguration config = WebClient.getConfig(wc);
+        config.getRequestContext().put(HTTPConduit.FORCE_HTTP_VERSION, "2");
+        final HTTPConduit conduit = config.getHttpConduit();
+        conduit.getClient().setAllowChunking(true);
+        TLSClientParameters params = new TLSClientParameters();
+        
params.setTrustManagers(InsecureTrustManager.getNoOpX509TrustManagers());
+        params.setDisableCNCheck(true);
+        conduit.setTlsClientParameters(params);
+        conduit.setTrustDecider(new MessageTrustDecider() {
+            @Override
+            public void establishTrust(String conduitName, URLConnectionInfo 
connectionInfo, Message message)
+                    throws UntrustedURLConnectionIOException {
+                String peer = 
((HttpsURLConnectionInfo)connectionInfo).getPeerPrincipal().getName();
+                if (!peer.contains("OU=Bethal")) {
+                    throw new UntrustedURLConnectionIOException("Untrusted 
peer " + peer);
+                }
+            }
+        });
+
+        try (Response resp = wc.get()) {
+            assertEquals(200, resp.getStatus());
+            assertEquals("HTTP/2.0 CXF in Action - Pluto", 
resp.readEntity(String.class));
+        }
+    }
     
     /**
      * The trust decision is made after the request is sent and before its 
body is written. Without

Reply via email to