Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/mobile
Path: blob/master/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java
41171 views
1
/*
2
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
3
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4
*
5
* This code is free software; you can redistribute it and/or modify it
6
* under the terms of the GNU General Public License version 2 only, as
7
* published by the Free Software Foundation. Oracle designates this
8
* particular file as subject to the "Classpath" exception as provided
9
* by Oracle in the LICENSE file that accompanied this code.
10
*
11
* This code is distributed in the hope that it will be useful, but WITHOUT
12
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14
* version 2 for more details (a copy is included in the LICENSE file that
15
* accompanied this code).
16
*
17
* You should have received a copy of the GNU General Public License version
18
* 2 along with this work; if not, write to the Free Software Foundation,
19
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20
*
21
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22
* or visit www.oracle.com if you need additional information or have any
23
* questions.
24
*/
25
26
package jdk.internal.net.http;
27
28
import java.io.IOException;
29
import java.lang.ref.WeakReference;
30
import java.net.ConnectException;
31
import java.net.http.HttpConnectTimeoutException;
32
import java.time.Duration;
33
import java.util.Iterator;
34
import java.util.LinkedList;
35
import java.security.AccessControlContext;
36
import java.util.Objects;
37
import java.util.Optional;
38
import java.util.concurrent.CancellationException;
39
import java.util.concurrent.CompletableFuture;
40
import java.util.concurrent.CompletionStage;
41
import java.util.concurrent.CompletionException;
42
import java.util.concurrent.ExecutionException;
43
import java.util.concurrent.Executor;
44
import java.util.concurrent.Flow;
45
import java.util.concurrent.atomic.AtomicInteger;
46
import java.util.concurrent.atomic.AtomicLong;
47
import java.util.concurrent.atomic.AtomicReference;
48
import java.util.function.Function;
49
50
import java.net.http.HttpClient;
51
import java.net.http.HttpHeaders;
52
import java.net.http.HttpRequest;
53
import java.net.http.HttpResponse;
54
import java.net.http.HttpResponse.BodySubscriber;
55
import java.net.http.HttpResponse.PushPromiseHandler;
56
import java.net.http.HttpTimeoutException;
57
import jdk.internal.net.http.common.Cancelable;
58
import jdk.internal.net.http.common.Log;
59
import jdk.internal.net.http.common.Logger;
60
import jdk.internal.net.http.common.MinimalFuture;
61
import jdk.internal.net.http.common.ConnectionExpiredException;
62
import jdk.internal.net.http.common.Utils;
63
import static jdk.internal.net.http.common.MinimalFuture.completedFuture;
64
import static jdk.internal.net.http.common.MinimalFuture.failedFuture;
65
66
/**
67
* Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
68
* - manages filters
69
* - retries due to filters.
70
* - I/O errors and most other exceptions get returned directly to user
71
*
72
* Creates a new Exchange for each request/response interaction
73
*/
74
class MultiExchange<T> implements Cancelable {
75
76
static final Logger debug =
77
Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG);
78
79
private final HttpRequest userRequest; // the user request
80
private final HttpRequestImpl request; // a copy of the user request
81
private final ConnectTimeoutTracker connectTimeout; // null if no timeout
82
@SuppressWarnings("removal")
83
final AccessControlContext acc;
84
final HttpClientImpl client;
85
final HttpResponse.BodyHandler<T> responseHandler;
86
final HttpClientImpl.DelegatingExecutor executor;
87
final AtomicInteger attempts = new AtomicInteger();
88
HttpRequestImpl currentreq; // used for retries & redirect
89
HttpRequestImpl previousreq; // used for retries & redirect
90
Exchange<T> exchange; // the current exchange
91
Exchange<T> previous;
92
volatile Throwable retryCause;
93
volatile boolean expiredOnce;
94
volatile HttpResponse<T> response = null;
95
96
// Maximum number of times a request will be retried/redirected
97
// for any reason
98
static final int DEFAULT_MAX_ATTEMPTS = 5;
99
static final int max_attempts = Utils.getIntegerNetProperty(
100
"jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
101
);
102
103
private final LinkedList<HeaderFilter> filters;
104
ResponseTimerEvent responseTimerEvent;
105
volatile boolean cancelled;
106
AtomicReference<CancellationException> interrupted = new AtomicReference<>();
107
final PushGroup<T> pushGroup;
108
109
/**
110
* Filter fields. These are attached as required by filters
111
* and only used by the filter implementations. This could be
112
* generalised into Objects that are passed explicitly to the filters
113
* (one per MultiExchange object, and one per Exchange object possibly)
114
*/
115
volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
116
// RedirectHandler
117
volatile int numberOfRedirects = 0;
118
119
// This class is used to keep track of the connection timeout
120
// across retries, when a ConnectException causes a retry.
121
// In that case - we will retry the connect, but we don't
122
// want to double the timeout by starting a new timer with
123
// the full connectTimeout again.
124
// Instead we use the ConnectTimeoutTracker to return a new
125
// duration that takes into account the time spent in the
126
// first connect attempt.
127
// If however, the connection gets connected, but we later
128
// retry the whole operation, then we reset the timer before
129
// retrying (since the connection used for the second request
130
// will not necessarily be the same: it could be a new
131
// unconnected connection) - see getExceptionalCF().
132
private static final class ConnectTimeoutTracker {
133
final Duration max;
134
final AtomicLong startTime = new AtomicLong();
135
ConnectTimeoutTracker(Duration connectTimeout) {
136
this.max = Objects.requireNonNull(connectTimeout);
137
}
138
139
Duration getRemaining() {
140
long now = System.nanoTime();
141
long previous = startTime.compareAndExchange(0, now);
142
if (previous == 0 || max.isZero()) return max;
143
Duration remaining = max.minus(Duration.ofNanos(now - previous));
144
assert remaining.compareTo(max) <= 0;
145
return remaining.isNegative() ? Duration.ZERO : remaining;
146
}
147
148
void reset() { startTime.set(0); }
149
}
150
151
/**
152
* MultiExchange with one final response.
153
*/
154
MultiExchange(HttpRequest userRequest,
155
HttpRequestImpl requestImpl,
156
HttpClientImpl client,
157
HttpResponse.BodyHandler<T> responseHandler,
158
PushPromiseHandler<T> pushPromiseHandler,
159
@SuppressWarnings("removal") AccessControlContext acc) {
160
this.previous = null;
161
this.userRequest = userRequest;
162
this.request = requestImpl;
163
this.currentreq = request;
164
this.previousreq = null;
165
this.client = client;
166
this.filters = client.filterChain();
167
this.acc = acc;
168
this.executor = client.theExecutor();
169
this.responseHandler = responseHandler;
170
171
if (pushPromiseHandler != null) {
172
Executor executor = acc == null
173
? this.executor.delegate()
174
: new PrivilegedExecutor(this.executor.delegate(), acc);
175
this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
176
} else {
177
pushGroup = null;
178
}
179
this.connectTimeout = client.connectTimeout()
180
.map(ConnectTimeoutTracker::new).orElse(null);
181
this.exchange = new Exchange<>(request, this);
182
}
183
184
static final class CancelableRef implements Cancelable {
185
private final WeakReference<Cancelable> cancelableRef;
186
CancelableRef(Cancelable cancelable) {
187
cancelableRef = new WeakReference<>(cancelable);
188
}
189
@Override
190
public boolean cancel(boolean mayInterruptIfRunning) {
191
Cancelable cancelable = cancelableRef.get();
192
if (cancelable != null) {
193
return cancelable.cancel(mayInterruptIfRunning);
194
} else return false;
195
}
196
}
197
198
synchronized Exchange<T> getExchange() {
199
return exchange;
200
}
201
202
HttpClientImpl client() {
203
return client;
204
}
205
206
HttpClient.Version version() {
207
HttpClient.Version vers = request.version().orElse(client.version());
208
if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
209
vers = HttpClient.Version.HTTP_1_1;
210
return vers;
211
}
212
213
private synchronized void setExchange(Exchange<T> exchange) {
214
if (this.exchange != null && exchange != this.exchange) {
215
this.exchange.released();
216
if (cancelled) exchange.cancel();
217
}
218
this.exchange = exchange;
219
}
220
221
public Optional<Duration> remainingConnectTimeout() {
222
return Optional.ofNullable(connectTimeout)
223
.map(ConnectTimeoutTracker::getRemaining);
224
}
225
226
private void cancelTimer() {
227
if (responseTimerEvent != null) {
228
client.cancelTimer(responseTimerEvent);
229
}
230
}
231
232
private void requestFilters(HttpRequestImpl r) throws IOException {
233
Log.logTrace("Applying request filters");
234
for (HeaderFilter filter : filters) {
235
Log.logTrace("Applying {0}", filter);
236
filter.request(r, this);
237
}
238
Log.logTrace("All filters applied");
239
}
240
241
private HttpRequestImpl responseFilters(Response response) throws IOException
242
{
243
Log.logTrace("Applying response filters");
244
Iterator<HeaderFilter> reverseItr = filters.descendingIterator();
245
while (reverseItr.hasNext()) {
246
HeaderFilter filter = reverseItr.next();
247
Log.logTrace("Applying {0}", filter);
248
HttpRequestImpl newreq = filter.response(response);
249
if (newreq != null) {
250
Log.logTrace("New request: stopping filters");
251
return newreq;
252
}
253
}
254
Log.logTrace("All filters applied");
255
return null;
256
}
257
258
public void cancel(IOException cause) {
259
cancelled = true;
260
getExchange().cancel(cause);
261
}
262
263
/**
264
* Used to relay a call from {@link CompletableFuture#cancel(boolean)}
265
* to this multi exchange for the purpose of cancelling the
266
* HTTP exchange.
267
* @param mayInterruptIfRunning if true, and this exchange is not already
268
* cancelled, this method will attempt to interrupt and cancel the
269
* exchange. Otherwise, the exchange is allowed to proceed and this
270
* method does nothing.
271
* @return true if the exchange was cancelled, false otherwise.
272
*/
273
@Override
274
public boolean cancel(boolean mayInterruptIfRunning) {
275
boolean cancelled = this.cancelled;
276
if (!cancelled && mayInterruptIfRunning) {
277
if (interrupted.get() == null) {
278
interrupted.compareAndSet(null,
279
new CancellationException("Request cancelled"));
280
}
281
this.cancelled = true;
282
var exchange = getExchange();
283
if (exchange != null) {
284
exchange.cancel();
285
}
286
return true;
287
}
288
return false;
289
}
290
291
public CompletableFuture<HttpResponse<T>> responseAsync(Executor executor) {
292
CompletableFuture<Void> start = new MinimalFuture<>(new CancelableRef(this));
293
CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
294
start.completeAsync( () -> null, executor); // trigger execution
295
return cf;
296
}
297
298
// return true if the response is a type where a response body is never possible
299
// and therefore doesn't have to include header information which indicates no
300
// body is present. This is distinct from responses that also do not contain
301
// response bodies (possibly ever) but which are required to have content length
302
// info in the header (eg 205). Those cases do not have to be handled specially
303
304
private static boolean bodyNotPermitted(Response r) {
305
return r.statusCode == 204;
306
}
307
308
private boolean bodyIsPresent(Response r) {
309
HttpHeaders headers = r.headers();
310
if (headers.firstValueAsLong("Content-length").orElse(0L) != 0L)
311
return true;
312
if (headers.firstValue("Transfer-encoding").isPresent())
313
return true;
314
return false;
315
}
316
317
// Call the user's body handler to get an empty body object
318
319
private CompletableFuture<HttpResponse<T>> handleNoBody(Response r, Exchange<T> exch) {
320
BodySubscriber<T> bs = responseHandler.apply(new ResponseInfoImpl(r.statusCode(),
321
r.headers(), r.version()));
322
bs.onSubscribe(new NullSubscription());
323
bs.onComplete();
324
CompletionStage<T> cs = ResponseSubscribers.getBodyAsync(executor, bs);
325
MinimalFuture<HttpResponse<T>> result = new MinimalFuture<>();
326
cs.whenComplete((nullBody, exception) -> {
327
if (exception != null)
328
result.completeExceptionally(exception);
329
else {
330
this.response =
331
new HttpResponseImpl<>(r.request(), r, this.response, nullBody, exch);
332
result.complete(this.response);
333
}
334
});
335
// ensure that the connection is closed or returned to the pool.
336
return result.whenComplete(exch::nullBody);
337
}
338
339
private CompletableFuture<HttpResponse<T>>
340
responseAsync0(CompletableFuture<Void> start) {
341
return start.thenCompose( v -> responseAsyncImpl())
342
.thenCompose((Response r) -> {
343
Exchange<T> exch = getExchange();
344
if (bodyNotPermitted(r)) {
345
if (bodyIsPresent(r)) {
346
IOException ioe = new IOException(
347
"unexpected content length header with 204 response");
348
exch.cancel(ioe);
349
return MinimalFuture.failedFuture(ioe);
350
} else
351
return handleNoBody(r, exch);
352
}
353
return exch.readBodyAsync(responseHandler)
354
.thenApply((T body) -> {
355
this.response =
356
new HttpResponseImpl<>(r.request(), r, this.response, body, exch);
357
return this.response;
358
});
359
}).exceptionallyCompose(this::whenCancelled);
360
}
361
362
private CompletableFuture<HttpResponse<T>> whenCancelled(Throwable t) {
363
CancellationException x = interrupted.get();
364
if (x != null) {
365
// make sure to fail with CancellationException if cancel(true)
366
// was called.
367
t = x.initCause(Utils.getCancelCause(t));
368
if (debug.on()) {
369
debug.log("MultiExchange interrupted with: " + t.getCause());
370
}
371
}
372
return MinimalFuture.failedFuture(t);
373
}
374
375
static class NullSubscription implements Flow.Subscription {
376
@Override
377
public void request(long n) {
378
}
379
380
@Override
381
public void cancel() {
382
}
383
}
384
385
private CompletableFuture<Response> responseAsyncImpl() {
386
CompletableFuture<Response> cf;
387
if (attempts.incrementAndGet() > max_attempts) {
388
cf = failedFuture(new IOException("Too many retries", retryCause));
389
} else {
390
if (currentreq.timeout().isPresent()) {
391
responseTimerEvent = ResponseTimerEvent.of(this);
392
client.registerTimer(responseTimerEvent);
393
}
394
try {
395
// 1. apply request filters
396
// if currentreq == previousreq the filters have already
397
// been applied once. Applying them a second time might
398
// cause some headers values to be added twice: for
399
// instance, the same cookie might be added again.
400
if (currentreq != previousreq) {
401
requestFilters(currentreq);
402
}
403
} catch (IOException e) {
404
return failedFuture(e);
405
}
406
Exchange<T> exch = getExchange();
407
// 2. get response
408
cf = exch.responseAsync()
409
.thenCompose((Response response) -> {
410
HttpRequestImpl newrequest;
411
try {
412
// 3. apply response filters
413
newrequest = responseFilters(response);
414
} catch (IOException e) {
415
return failedFuture(e);
416
}
417
// 4. check filter result and repeat or continue
418
if (newrequest == null) {
419
if (attempts.get() > 1) {
420
Log.logError("Succeeded on attempt: " + attempts);
421
}
422
return completedFuture(response);
423
} else {
424
this.response =
425
new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
426
Exchange<T> oldExch = exch;
427
if (currentreq.isWebSocket()) {
428
// need to close the connection and open a new one.
429
exch.exchImpl.connection().close();
430
}
431
return exch.ignoreBody().handle((r,t) -> {
432
previousreq = currentreq;
433
currentreq = newrequest;
434
expiredOnce = false;
435
setExchange(new Exchange<>(currentreq, this, acc));
436
return responseAsyncImpl();
437
}).thenCompose(Function.identity());
438
} })
439
.handle((response, ex) -> {
440
// 5. handle errors and cancel any timer set
441
cancelTimer();
442
if (ex == null) {
443
assert response != null;
444
return completedFuture(response);
445
}
446
// all exceptions thrown are handled here
447
CompletableFuture<Response> errorCF = getExceptionalCF(ex);
448
if (errorCF == null) {
449
return responseAsyncImpl();
450
} else {
451
return errorCF;
452
} })
453
.thenCompose(Function.identity());
454
}
455
return cf;
456
}
457
458
private static boolean retryPostValue() {
459
String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
460
if (s == null)
461
return false;
462
return s.isEmpty() ? true : Boolean.parseBoolean(s);
463
}
464
465
private static boolean disableRetryConnect() {
466
String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect");
467
if (s == null)
468
return false;
469
return s.isEmpty() ? true : Boolean.parseBoolean(s);
470
}
471
472
/** True if ALL ( even non-idempotent ) requests can be automatic retried. */
473
private static final boolean RETRY_ALWAYS = retryPostValue();
474
/** True if ConnectException should cause a retry. Enabled by default */
475
static final boolean RETRY_CONNECT = !disableRetryConnect();
476
477
/** Returns true is given request has an idempotent method. */
478
private static boolean isIdempotentRequest(HttpRequest request) {
479
String method = request.method();
480
return switch (method) {
481
case "GET", "HEAD" -> true;
482
default -> false;
483
};
484
}
485
486
/** Returns true if the given request can be automatically retried. */
487
private static boolean canRetryRequest(HttpRequest request) {
488
if (RETRY_ALWAYS)
489
return true;
490
if (isIdempotentRequest(request))
491
return true;
492
return false;
493
}
494
495
// Returns true if cancel(true) was called.
496
// This is an important distinction in several scenarios:
497
// for instance, if cancel(true) was called 1. we don't want
498
// to retry, 2. we don't want to wrap the exception in
499
// a timeout exception.
500
boolean requestCancelled() {
501
return interrupted.get() != null;
502
}
503
504
private boolean retryOnFailure(Throwable t) {
505
if (requestCancelled()) return false;
506
return t instanceof ConnectionExpiredException
507
|| (RETRY_CONNECT && (t instanceof ConnectException));
508
}
509
510
private Throwable retryCause(Throwable t) {
511
Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t;
512
return cause == null ? t : cause;
513
}
514
515
/**
516
* Takes a Throwable and returns a suitable CompletableFuture that is
517
* completed exceptionally, or null.
518
*/
519
private CompletableFuture<Response> getExceptionalCF(Throwable t) {
520
if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
521
if (t.getCause() != null) {
522
t = t.getCause();
523
}
524
}
525
if (cancelled && !requestCancelled() && t instanceof IOException) {
526
if (!(t instanceof HttpTimeoutException)) {
527
t = toTimeoutException((IOException)t);
528
}
529
} else if (retryOnFailure(t)) {
530
Throwable cause = retryCause(t);
531
532
if (!(t instanceof ConnectException)) {
533
// we may need to start a new connection, and if so
534
// we want to start with a fresh connect timeout again.
535
if (connectTimeout != null) connectTimeout.reset();
536
if (!canRetryRequest(currentreq)) {
537
return failedFuture(cause); // fails with original cause
538
}
539
} // ConnectException: retry, but don't reset the connectTimeout.
540
541
// allow the retry mechanism to do its work
542
retryCause = cause;
543
if (!expiredOnce) {
544
if (debug.on())
545
debug.log(t.getClass().getSimpleName() + " (async): retrying...", t);
546
expiredOnce = true;
547
// The connection was abruptly closed.
548
// We return null to retry the same request a second time.
549
// The request filters have already been applied to the
550
// currentreq, so we set previousreq = currentreq to
551
// prevent them from being applied again.
552
previousreq = currentreq;
553
return null;
554
} else {
555
if (debug.on()) {
556
debug.log(t.getClass().getSimpleName()
557
+ " (async): already retried once.", t);
558
}
559
t = cause;
560
}
561
}
562
return failedFuture(t);
563
}
564
565
private HttpTimeoutException toTimeoutException(IOException ioe) {
566
HttpTimeoutException t = null;
567
568
// more specific, "request timed out", when connected
569
Exchange<?> exchange = getExchange();
570
if (exchange != null) {
571
ExchangeImpl<?> exchangeImpl = exchange.exchImpl;
572
if (exchangeImpl != null) {
573
if (exchangeImpl.connection().connected()) {
574
t = new HttpTimeoutException("request timed out");
575
t.initCause(ioe);
576
}
577
}
578
}
579
if (t == null) {
580
t = new HttpConnectTimeoutException("HTTP connect timed out");
581
t.initCause(new ConnectException("HTTP connect timed out"));
582
}
583
return t;
584
}
585
}
586
587