Skip to content

Commit a62c7f3

Browse files
committed
Reuse HttpClient throughout a Connection session
Enables http/2 connection reuse #2257
1 parent 6aa1b71 commit a62c7f3

2 files changed

Lines changed: 55 additions & 30 deletions

File tree

src/main/java/org/jsoup/helper/HttpConnection.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public class HttpConnection implements Connection {
7070
static final String DefaultUploadType = "application/octet-stream";
7171
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
7272

73+
private HttpConnection.Request req;
74+
private Connection.@Nullable Response res;
75+
@Nullable Object client; // The HttpClient for this Connection, if via the HttpClientExecutor
76+
@Nullable RequestAuthenticator lastAuth; // The previous Authenticator used by this Connection, if via the HttpClientExecutor
77+
7378
/**
7479
Create a new Connection, with the request URL specified.
7580
@param url the URL to fetch from
@@ -97,6 +102,7 @@ public static Connection connect(URL url) {
97102
*/
98103
public HttpConnection() {
99104
req = new Request();
105+
req.connection = this;
100106
}
101107

102108
/**
@@ -112,9 +118,6 @@ static String encodeMimeName(String val) {
112118
return val.replace("\"", "%22");
113119
}
114120

115-
private HttpConnection.Request req;
116-
private Connection.@Nullable Response res;
117-
118121
@Override
119122
public Connection newRequest() {
120123
// copy the prototype request for the different settings, cookie manager, etc
@@ -593,6 +596,7 @@ public static class Request extends HttpConnection.Base<Connection.Request> impl
593596
// make sure that we can send Sec-Fetch-Site headers etc.
594597
}
595598

599+
HttpConnection connection;
596600
private @Nullable Proxy proxy;
597601
private int timeoutMilliseconds;
598602
private int maxBodySizeBytes;
@@ -627,6 +631,7 @@ public static class Request extends HttpConnection.Base<Connection.Request> impl
627631

628632
Request(Request copy) {
629633
super(copy);
634+
connection = copy.connection;
630635
proxy = copy.proxy;
631636
postDataCharset = copy.postDataCharset;
632637
timeoutMilliseconds = copy.timeoutMilliseconds;

src/main/java11/org/jsoup/helper/HttpClientExecutor.java

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import java.net.http.HttpResponse;
1818
import java.time.Duration;
1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.List;
21-
import java.util.Map;
2222

2323
import static org.jsoup.helper.HttpConnection.Response;
2424
import static org.jsoup.helper.HttpConnection.Response.writePost;
@@ -28,42 +28,57 @@
2828
property {@code jsoup.useHttpClient} to {@code true}.
2929
*/
3030
class HttpClientExecutor extends RequestExecutor {
31+
// HttpClient expects proxy settings per client; we do per request, so held as a thread local. Can't do same for
32+
// auth because that callback is on a worker thread, so can only do auth per Connection. So we create a new client
33+
// if the authenticator is different between requests
34+
static ThreadLocal<Proxy> perRequestProxy = new ThreadLocal<>();
35+
3136
@Nullable
3237
HttpResponse<InputStream> hRes;
3338

3439
public HttpClientExecutor(HttpConnection.Request request, HttpConnection.@Nullable Response previousResponse) {
3540
super(request, previousResponse);
3641
}
3742

43+
/**
44+
Retrieve the HttpClient from the Connection, or create a new one. Allows for connection pooling of requests in the
45+
same Connection (session).
46+
*/
47+
HttpClient client() {
48+
// we try to reuse the same Client across requests in a given Connection; but if the request auth has changed, we need to create a new client
49+
RequestAuthenticator prevAuth = req.connection.lastAuth;
50+
req.connection.lastAuth = req.authenticator;
51+
if (req.connection.client != null && prevAuth == req.authenticator) { // might both be null
52+
return (HttpClient) req.connection.client;
53+
}
54+
55+
HttpClient.Builder builder = HttpClient.newBuilder();
56+
builder.followRedirects(HttpClient.Redirect.NEVER); // customized redirects
57+
builder.proxy(new ProxyWrap()); // thread local impl for per request; called on executing thread
58+
if (req.authenticator != null) builder.authenticator(new AuthenticationHandler(req.authenticator));
59+
60+
HttpClient client = builder.build();
61+
req.connection.client = client;
62+
return client;
63+
}
64+
3865
@Override
3966
HttpConnection.Response execute() throws IOException {
4067
try {
41-
HttpClient.Builder builder = HttpClient.newBuilder();
42-
Proxy proxy = req.proxy();
43-
if (proxy != null) builder.proxy(new ProxyWrap(proxy));
44-
builder.followRedirects(HttpClient.Redirect.NEVER); // customized redirects
45-
//builder.connectTimeout(Duration.ofMillis(req.timeout()/2)); // jsoup timeout is total connect + all reads
46-
// todo - how to handle socketfactory? HttpClient wants SSLContext...
47-
if (req.authenticator != null) {
48-
AuthenticationHandler.AuthShim handler = new RequestAuthHandler();
49-
handler.enable(req.authenticator, builder);
50-
}
51-
HttpClient client = builder.build();
52-
5368
HttpRequest.Builder reqBuilder =
5469
HttpRequest.newBuilder(req.url.toURI()).method(req.method.name(), requestBody(req));
5570
if (req.timeout() > 0) reqBuilder.timeout(
5671
Duration.ofMillis(req.timeout())); // infinite if unset (UrlConnection / jsoup uses 0 for same)
5772
CookieUtil.applyCookiesToRequest(req, reqBuilder::header);
5873

5974
// headers:
60-
for (Map.Entry<String, List<String>> header : req.multiHeaders().entrySet()) {
61-
for (String value : header.getValue()) {
62-
reqBuilder.header(header.getKey(), value);
63-
}
64-
}
75+
req.multiHeaders().forEach((key, values) -> {
76+
values.forEach(value -> reqBuilder.header(key, value));
77+
});
6578

79+
if (req.proxy() != null) perRequestProxy.set(req.proxy()); // set up per request proxy
6680
HttpRequest hReq = reqBuilder.build();
81+
HttpClient client = client();
6782
hRes = client.send(hReq, HttpResponse.BodyHandlers.ofInputStream());
6883
HttpHeaders headers = hRes.headers();
6984

@@ -84,9 +99,13 @@ HttpConnection.Response execute() throws IOException {
8499
throw e;
85100
} catch (InterruptedException e) {
86101
safeClose();
102+
Thread.currentThread().interrupt();
87103
throw new IOException(e);
88104
} catch (URISyntaxException e) {
89105
throw new IllegalArgumentException("Malformed URL: " + req.url, e);
106+
} finally {
107+
// detach per request proxy
108+
perRequestProxy.remove();
90109
}
91110
}
92111

@@ -99,8 +118,12 @@ InputStream responseBody() throws IOException {
99118
@Override
100119
void safeClose() {
101120
if (hRes != null) {
102-
// no real closer
103-
// todo - review
121+
InputStream body = hRes.body();
122+
if (body != null) {
123+
try {
124+
body.close();
125+
} catch (IOException ignored) {}
126+
}
104127
hRes = null;
105128
}
106129
}
@@ -116,16 +139,13 @@ static HttpRequest.BodyPublisher requestBody(final HttpConnection.Request req) t
116139
}
117140

118141
static class ProxyWrap extends ProxySelector {
119-
final List<Proxy> proxies;
120-
121-
public ProxyWrap(Proxy proxy) {
122-
this.proxies = new ArrayList<>(1);
123-
proxies.add(proxy);
124-
}
142+
// empty list for no proxy:
143+
static final List<Proxy> NoProxy = new ArrayList<>(0);
125144

126145
@Override
127146
public List<Proxy> select(URI uri) {
128-
return proxies;
147+
Proxy proxy = perRequestProxy.get();
148+
return proxy != null ? Collections.singletonList(proxy) : NoProxy;
129149
}
130150

131151
@Override

0 commit comments

Comments
 (0)