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

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


The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
     new 7b27e9677f6 CXF-9226: AsyncHTTPConduit throws ISE instead of 
HTTPException(407) o… (#3255)
7b27e9677f6 is described below

commit 7b27e9677f624be8352858077c5c88cc432c44f6
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Mon Jul 6 09:37:22 2026 -0400

    CXF-9226: AsyncHTTPConduit throws ISE instead of HTTPException(407) o… 
(#3255)
    
    * CXF-9226: AsyncHTTPConduit throws ISE instead of HTTPException(407) on 
proxy auth failure
    
    When the HC5 async conduit receives a 407 Proxy Authentication Required
    response with wrong credentials and MaxRetransmits>0 or AutoRedirect=true
    (as in the JIRA), the request entity is marked repeatable so HC5 retries
    after the first 407. A concurrent write-failure on the Connection: close
    socket races with the retry's execute() call, causing
    InternalHttpAsyncExecRuntime.ensureValid() to find a null endpointRef and
    throw IllegalStateException. Without MaxRetransmits/AutoRedirect the entity
    is not repeatable and HC5 intercepts the 407 internally, calling
    asyncExecCallback.completed() without signaling CXF's future and leaving
    getHttpResponse() blocked indefinitely on wait().
    
    Fix:
    - Promote HttpClientContext ctx to a field on AsyncWrappedOutputStream so
      it is accessible after HC5 calls back.
    - Replace wait() with wait(receiveTimeout) to bound the wait.
    - In both the timeout path and the RuntimeException (ISE) path, check
      ctx.getResponse() for a 407 status and throw HTTPException(407) instead
      of re-throwing the raw exception or blocking forever.
    
    Test:
    - ProxyAuthIllegalStateTest: reproduces the ISE via a raw ServerSocket
      proxy returning 407+Connection:close, with MaxRetransmits=5 and
      AutoRedirect=true matching the exact JIRA configuration.
    
    * Always configure AuthCache if proxy authorization policy is configured
    
    * Use exact proxy host and BasicAuthCache implementation
    
    ---------
    
    Co-authored-by: Andriy Redko <[email protected]>
    (cherry picked from commit 4d64e044badb296b0de5696e29475c8b738ca624)
---
 .../http/asyncclient/hc5/AsyncHTTPConduit.java     |   7 +
 .../hc5/http/auth/ProxyAuthIllegalStateTest.java   | 197 +++++++++++++++++++++
 2 files changed, 204 insertions(+)

diff --git 
a/rt/transports/http-hc5/src/main/java/org/apache/cxf/transport/http/asyncclient/hc5/AsyncHTTPConduit.java
 
b/rt/transports/http-hc5/src/main/java/org/apache/cxf/transport/http/asyncclient/hc5/AsyncHTTPConduit.java
index 92a68a2956d..2d280edf8ba 100644
--- 
a/rt/transports/http-hc5/src/main/java/org/apache/cxf/transport/http/asyncclient/hc5/AsyncHTTPConduit.java
+++ 
b/rt/transports/http-hc5/src/main/java/org/apache/cxf/transport/http/asyncclient/hc5/AsyncHTTPConduit.java
@@ -78,7 +78,9 @@ import org.apache.hc.client5.http.auth.Credentials;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
 import org.apache.hc.core5.concurrent.BasicFuture;
@@ -553,6 +555,11 @@ public class AsyncHTTPConduit extends 
HttpClientHTTPConduit {
             };
 
             ctx.setCredentialsProvider(credsProvider);
+            if (proxyAuthorizationPolicy != null && 
proxyAuthorizationPolicy.getUserName() != null) {
+                final BasicAuthCache cache = new BasicAuthCache();
+                cache.put(entity.getConfig().getProxy(), new BasicScheme());
+                ctx.setAuthCache(cache);
+            }
 
             TlsStrategy tlsStrategy = null;
             if ("https".equals(url.getScheme())) {
diff --git 
a/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/auth/ProxyAuthIllegalStateTest.java
 
b/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/auth/ProxyAuthIllegalStateTest.java
new file mode 100644
index 00000000000..a42104ba96b
--- /dev/null
+++ 
b/systests/transport-hc5/src/test/java/org/apache/cxf/systest/hc5/http/auth/ProxyAuthIllegalStateTest.java
@@ -0,0 +1,197 @@
+/**
+ * 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.systest.hc5.http.auth;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+import javax.xml.namespace.QName;
+
+import jakarta.xml.ws.BindingProvider;
+import jakarta.xml.ws.WebServiceException;
+import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
+import org.apache.cxf.frontend.ClientProxy;
+import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase;
+import org.apache.cxf.transport.http.HTTPConduit;
+import org.apache.cxf.transport.http.HTTPException;
+import org.apache.cxf.transport.http.asyncclient.hc5.AsyncHTTPConduit;
+import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
+import org.apache.hello_world.Greeter;
+import org.apache.hello_world.services.SOAPService;
+
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Reproducer for CXF-9226 (connection-close + retransmit variant): async
+ * HC5 conduit throws IllegalStateException instead of HTTPException(407)
+ * when the proxy returns 407, closes the TCP connection (Connection: close),
+ * and the client has MaxRetransmits/AutoRedirect configured (as in the JIRA).
+ *
+ * <p>With MaxRetransmits &gt; 0 or AutoRedirect=true, CXF caches the request
+ * body so HC5 marks the entity as repeatable. HC5 then retries after the
+ * first 407, reconnects to the proxy, and sends a second request with the
+ * (wrong) Basic credentials. While HC5 is reconnecting, a concurrent
+ * write-failure on the closed socket calls discardEndpoint() which sets
+ * endpointRef to null. When the retry then calls execute() →
+ * ensureValid(), it finds endpointRef==null and throws
+ * IllegalStateException("Endpoint not acquired / already released").
+ *
+ * <p>Without the CXF-9226 fix that ISE surfaces to the caller as an opaque
+ * IOException; with the fix CXF checks the 407 stored in HttpClientContext
+ * and throws HTTPException(407) instead.
+ */
+public class ProxyAuthIllegalStateTest
+        extends AbstractBusClientServerTestBase {
+
+    static final String PROXY_PORT =
+        allocatePort(ProxyAuthIllegalStateTest.class);
+
+    private static final QName SERVICE_NAME =
+        new QName("http://apache.org/hello_world";, "SOAPService");
+    private static final QName PORT_NAME =
+        new QName("http://apache.org/hello_world";, "Mortimer");
+
+    private static ServerSocket proxySocket;
+
+    @BeforeClass
+    public static void setup() throws Exception {
+        proxySocket = new ServerSocket(Integer.parseInt(PROXY_PORT));
+        final ServerSocket socket = proxySocket;
+        Thread t = new Thread(() -> {
+            while (!socket.isClosed()) {
+                try {
+                    drainAndReject(socket.accept());
+                } catch (IOException e) {
+                    // socket closed; exit loop
+                }
+            }
+        });
+        t.setDaemon(true);
+        t.start();
+        createStaticBus();
+    }
+
+    private static void drainAndReject(Socket client) throws IOException {
+        try (Socket s = client) {
+            InputStream in = s.getInputStream();
+            // Slide a 4-byte window; stop at \r\n\r\n (end of headers)
+            int[] w = new int[]{-1, -1, -1, -1};
+            int b;
+            while ((b = in.read()) != -1) {
+                System.arraycopy(w, 1, w, 0, 3);
+                w[3] = b;
+                if (w[0] == '\r' && w[1] == '\n'
+                        && w[2] == '\r' && w[3] == '\n') {
+                    break;
+                }
+            }
+            OutputStream out = s.getOutputStream();
+            out.write(("HTTP/1.1 407 Proxy Authentication Required\r\n"
+                + "Proxy-Authenticate: Basic realm=\"test\"\r\n"
+                + "Content-Length: 0\r\n"
+                + "Connection: close\r\n"
+                + "\r\n").getBytes(StandardCharsets.US_ASCII));
+            out.flush();
+        }
+    }
+
+    @AfterClass
+    public static void teardown() {
+        if (proxySocket != null) {
+            try {
+                proxySocket.close();
+            } catch (IOException e) {
+                // ignore
+            }
+            proxySocket = null;
+        }
+    }
+
+    /**
+     * CXF-9226: wrong proxy credentials must produce HTTPException(407),
+     * not IllegalStateException, when the proxy closes the connection and
+     * MaxRetransmits/AutoRedirect are configured (exact JIRA reproduction).
+     */
+    @Test(timeout = 30_000)
+    public void testProxyAuthFailsWithHTTPExceptionNotIllegalState()
+            throws Exception {
+        Assume.assumeFalse("Skipped in forceURLConnection mode",
+            Boolean.getBoolean(
+                "org.apache.cxf.transport.http.forceURLConnection"));
+        URL wsdl = getClass().getResource("../greeting.wsdl");
+        assertNotNull("WSDL not found", wsdl);
+
+        SOAPService service = new SOAPService(wsdl, SERVICE_NAME);
+        Greeter greeter = service.getPort(PORT_NAME, Greeter.class);
+        assertNotNull("Port is null", greeter);
+
+        BindingProvider bp = (BindingProvider) greeter;
+        bp.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
+            "http://localhost:1/greeter";);
+        bp.getRequestContext().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE);
+
+        HTTPConduit conduit = (HTTPConduit)
+            ClientProxy.getClient(greeter).getConduit();
+        HTTPClientPolicy policy = new HTTPClientPolicy();
+        policy.setConnectionTimeout(10_000L);
+        policy.setReceiveTimeout(10_000L);
+        policy.setProxyServer("localhost");
+        policy.setProxyServerPort(Integer.parseInt(PROXY_PORT));
+        // Match the exact JIRA configuration: MaxRetransmits and AutoRedirect
+        // force CXF to cache the request body, which makes the entity
+        // repeatable. HC5 then retries after the 407, and the concurrent
+        // write-failure on the Connection: close socket can set 
endpointRef=null
+        // before the retry's execute() call, triggering IllegalStateException.
+        policy.setMaxRetransmits(5);
+        policy.setAutoRedirect(true);
+        conduit.setClient(policy);
+
+        ProxyAuthorizationPolicy proxyAuth = new ProxyAuthorizationPolicy();
+        proxyAuth.setUserName("unknown-user");
+        proxyAuth.setPassword("wrong-password");
+        proxyAuth.setAuthorizationType("Basic");
+        conduit.setProxyAuthorization(proxyAuth);
+
+        try {
+            greeter.sayHi();
+            fail("Expected exception for invalid proxy credentials");
+        } catch (WebServiceException e) {
+            Throwable cause = e.getCause();
+            assertNotNull("Expected a cause on the WebServiceException", 
cause);
+            if (!(cause instanceof HTTPException)) {
+                fail("Expected HTTPException(407) but got "
+                    + cause.getClass().getName() + ": " + cause.getMessage());
+            }
+            assertEquals("Expected 407 Proxy Authentication Required",
+                407, ((HTTPException) cause).getResponseCode());
+        }
+    }
+}

Reply via email to