Skip to content

Commit a9a3a7e

Browse files
authored
fix: send CONNECT first when recovering a HTTPS request (#2077)
# Issue description AHC has retry mechanism enabled with up to 5 attempts by default. But the initial CONNECT is omitted when recovering the HTTPS requests with IO exceptions. This MR fixes this issue and guarantees the proper workflow in retries. It's related to #2071 and fixes a different failing case. # How the issue is fixed * For any new connections, make sure there is an initial CONNECT for WebSocket/HTTPS request. * For the condition check that a CONNECT has been sent, make sure the connection the current future attaches is reusable/active. # Unit test IOException has various reasons but in the unit test, we emulate it by closing the connection after receiving the CONNECT request. The internal recovery process will retry another 4 times, and through an IOException eventually. Signed-off-by: Jason Joo <[email protected]>
1 parent 8189c92 commit a9a3a7e

File tree

2 files changed

+50
-9
lines changed

2 files changed

+50
-9
lines changed

client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java

+22-7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public NettyRequestSender(AsyncHttpClientConfig config, ChannelManager channelMa
9797
requestFactory = new NettyRequestFactory(config);
9898
}
9999

100+
// needConnect returns true if the request is secure/websocket and a HTTP proxy is set
101+
private boolean needConnect(final Request request, final ProxyServer proxyServer) {
102+
return proxyServer != null
103+
&& proxyServer.getProxyType().isHttp()
104+
&& (request.getUri().isSecured() || request.getUri().isWebSocket());
105+
}
106+
100107
public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHandler<T> asyncHandler, NettyResponseFuture<T> future) {
101108
if (isClosed()) {
102109
throw new IllegalStateException("Closed");
@@ -106,9 +113,7 @@ public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHan
106113
ProxyServer proxyServer = getProxyServer(config, request);
107114

108115
// WebSockets use connect tunneling to work with proxies
109-
if (proxyServer != null && proxyServer.getProxyType().isHttp() &&
110-
(request.getUri().isSecured() || request.getUri().isWebSocket()) &&
111-
!isConnectAlreadyDone(request, future)) {
116+
if (needConnect(request, proxyServer) && !isConnectAlreadyDone(request, future)) {
112117
// Proxy with HTTPS or WebSocket: CONNECT for sure
113118
if (future != null && future.isConnectAllowed()) {
114119
// Perform CONNECT
@@ -125,6 +130,8 @@ public <T> ListenableFuture<T> sendRequest(final Request request, final AsyncHan
125130

126131
private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture<?> future) {
127132
return future != null
133+
// If the channel can't be reused or closed, a CONNECT is still required
134+
&& future.isReuseChannel() && Channels.isChannelActive(future.channel())
128135
&& future.getNettyRequest() != null
129136
&& future.getNettyRequest().getHttpRequest().method() == HttpMethod.CONNECT
130137
&& !request.getMethod().equals(CONNECT);
@@ -137,11 +144,19 @@ private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture
137144
*/
138145
private <T> ListenableFuture<T> sendRequestWithCertainForceConnect(Request request, AsyncHandler<T> asyncHandler, NettyResponseFuture<T> future,
139146
ProxyServer proxyServer, boolean performConnectRequest) {
140-
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, proxyServer, performConnectRequest);
141147
Channel channel = getOpenChannel(future, request, proxyServer, asyncHandler);
142-
return Channels.isChannelActive(channel)
143-
? sendRequestWithOpenChannel(newFuture, asyncHandler, channel)
144-
: sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
148+
if (Channels.isChannelActive(channel)) {
149+
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
150+
proxyServer, performConnectRequest);
151+
return sendRequestWithOpenChannel(newFuture, asyncHandler, channel);
152+
} else {
153+
// A new channel is not expected when performConnectRequest is false. We need to
154+
// revisit the condition of sending
155+
// the CONNECT request to the new channel.
156+
NettyResponseFuture<T> newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
157+
proxyServer, needConnect(request, proxyServer));
158+
return sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
159+
}
145160
}
146161

147162
/**

client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java

+28-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.asynchttpclient.proxy;
1414

1515
import io.github.artsok.RepeatedIfExceptionsTest;
16+
import io.netty.handler.codec.http.DefaultHttpHeaders;
1617
import jakarta.servlet.ServletException;
1718
import jakarta.servlet.http.HttpServletRequest;
1819
import jakarta.servlet.http.HttpServletResponse;
@@ -43,8 +44,10 @@
4344
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
4445
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
4546
import static org.junit.jupiter.api.Assertions.assertEquals;
47+
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
4648

4749
import java.io.IOException;
50+
import java.util.concurrent.ExecutionException;
4851

4952
/**
5053
* Proxy usage tests.
@@ -156,7 +159,7 @@ public void testPooledConnectionsWithProxy() throws Exception {
156159
public void testFailedConnectWithProxy() throws Exception {
157160
try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
158161
Builder proxyServer = proxyServer("localhost", port1);
159-
proxyServer.setCustomHeaders(r -> r.getHeaders().add(ProxyHandler.HEADER_FORBIDDEN, "1"));
162+
proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "1"));
160163
RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
161164

162165
Response response1 = asyncHttpClient.executeRequest(rb.build()).get();
@@ -170,16 +173,39 @@ public void testFailedConnectWithProxy() throws Exception {
170173
}
171174
}
172175

176+
@RepeatedIfExceptionsTest(repeats = 5)
177+
public void testClosedConnectionWithProxy() throws Exception {
178+
try (AsyncHttpClient asyncHttpClient = asyncHttpClient(
179+
config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
180+
Builder proxyServer = proxyServer("localhost", port1);
181+
proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "2"));
182+
RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
183+
184+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
185+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
186+
assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
187+
}
188+
}
189+
173190
public static class ProxyHandler extends ConnectHandler {
174191
final static String HEADER_FORBIDDEN = "X-REJECT-REQUEST";
175192

176193
@Override
177194
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
178195
if (HttpConstants.Methods.CONNECT.equalsIgnoreCase(request.getMethod())) {
179-
if (request.getHeader(HEADER_FORBIDDEN) != null) {
196+
String headerValue = request.getHeader(HEADER_FORBIDDEN);
197+
if (headerValue == null) {
198+
headerValue = "";
199+
}
200+
switch (headerValue) {
201+
case "1":
180202
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
181203
r.setHandled(true);
182204
return;
205+
case "2":
206+
r.getHttpChannel().getConnection().close();
207+
r.setHandled(true);
208+
return;
183209
}
184210
}
185211
super.handle(s, r, request, response);

0 commit comments

Comments
 (0)