1717import java .net .http .HttpResponse ;
1818import java .time .Duration ;
1919import java .util .ArrayList ;
20+ import java .util .Collections ;
2021import java .util .List ;
21- import java .util .Map ;
2222
2323import static org .jsoup .helper .HttpConnection .Response ;
2424import static org .jsoup .helper .HttpConnection .Response .writePost ;
2828 property {@code jsoup.useHttpClient} to {@code true}.
2929 */
3030class 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