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