1 /* 2 * Copyright (c) 2015, 2018, 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.net.ConnectException; 30 import java.time.Duration; 31 import java.util.Iterator; 32 import java.util.LinkedList; 33 import java.security.AccessControlContext; 34 import java.util.concurrent.CompletableFuture; 35 import java.util.concurrent.CompletionException; 36 import java.util.concurrent.ExecutionException; 37 import java.util.concurrent.Executor; 38 import java.util.concurrent.atomic.AtomicInteger; 39 import java.util.function.Function; 40 41 import java.net.http.HttpClient; 42 import java.net.http.HttpRequest; 43 import java.net.http.HttpResponse; 44 import java.net.http.HttpResponse.PushPromiseHandler; 45 import java.net.http.HttpTimeoutException; 46 import jdk.internal.net.http.common.Log; 47 import jdk.internal.net.http.common.Logger; 48 import jdk.internal.net.http.common.MinimalFuture; 49 import jdk.internal.net.http.common.ConnectionExpiredException; 50 import jdk.internal.net.http.common.Utils; 51 import static jdk.internal.net.http.common.MinimalFuture.completedFuture; 52 import static jdk.internal.net.http.common.MinimalFuture.failedFuture; 53 54 /** 55 * Encapsulates multiple Exchanges belonging to one HttpRequestImpl. 56 * - manages filters 57 * - retries due to filters. 58 * - I/O errors and most other exceptions get returned directly to user 59 * 60 * Creates a new Exchange for each request/response interaction 61 */ 62 class MultiExchange<T> { 63 64 static final Logger debug = 65 Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG); 66 67 private final HttpRequest userRequest; // the user request 68 private final HttpRequestImpl request; // a copy of the user request 69 final AccessControlContext acc; 70 final HttpClientImpl client; 71 final HttpResponse.BodyHandler<T> responseHandler; 72 final HttpClientImpl.DelegatingExecutor executor; 73 final AtomicInteger attempts = new AtomicInteger(); 74 HttpRequestImpl currentreq; // used for retries & redirect 75 HttpRequestImpl previousreq; // used for retries & redirect 76 Exchange<T> exchange; // the current exchange 77 Exchange<T> previous; 78 volatile Throwable retryCause; 79 volatile boolean expiredOnce; 80 volatile HttpResponse<T> response = null; 81 82 // Maximum number of times a request will be retried/redirected 83 // for any reason 84 85 static final int DEFAULT_MAX_ATTEMPTS = 5; 86 static final int max_attempts = Utils.getIntegerNetProperty( 87 "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS 88 ); 89 90 private final LinkedList<HeaderFilter> filters; 91 TimedEvent timedEvent; 92 volatile boolean cancelled; 93 final PushGroup<T> pushGroup; 94 95 /** 96 * Filter fields. These are attached as required by filters 97 * and only used by the filter implementations. This could be 98 * generalised into Objects that are passed explicitly to the filters 99 * (one per MultiExchange object, and one per Exchange object possibly) 100 */ 101 volatile AuthenticationFilter.AuthInfo serverauth, proxyauth; 102 // RedirectHandler 103 volatile int numberOfRedirects = 0; 104 105 /** 106 * MultiExchange with one final response. 107 */ 108 MultiExchange(HttpRequest userRequest, 109 HttpRequestImpl requestImpl, 110 HttpClientImpl client, 111 HttpResponse.BodyHandler<T> responseHandler, 112 PushPromiseHandler<T> pushPromiseHandler, 113 AccessControlContext acc) { 114 this.previous = null; 115 this.userRequest = userRequest; 116 this.request = requestImpl; 117 this.currentreq = request; 118 this.previousreq = null; 119 this.client = client; 120 this.filters = client.filterChain(); 121 this.acc = acc; 122 this.executor = client.theExecutor(); 123 this.responseHandler = responseHandler; 124 125 if (pushPromiseHandler != null) { 126 Executor executor = acc == null 127 ? this.executor.delegate() 128 : new PrivilegedExecutor(this.executor.delegate(), acc); 129 this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor); 130 } else { 131 pushGroup = null; 132 } 133 134 this.exchange = new Exchange<>(request, this); 135 } 136 137 private synchronized Exchange<T> getExchange() { 138 return exchange; 139 } 140 141 HttpClientImpl client() { 142 return client; 143 } 144 145 HttpClient.Version version() { 146 HttpClient.Version vers = request.version().orElse(client.version()); 147 if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null) 148 vers = HttpClient.Version.HTTP_1_1; 149 return vers; 150 } 151 152 private synchronized void setExchange(Exchange<T> exchange) { 153 if (this.exchange != null && exchange != this.exchange) { 154 this.exchange.released(); 155 } 156 this.exchange = exchange; 157 } 158 159 private void cancelTimer() { 160 if (timedEvent != null) { 161 client.cancelTimer(timedEvent); 162 } 163 } 164 165 private void requestFilters(HttpRequestImpl r) throws IOException { 166 Log.logTrace("Applying request filters"); 167 for (HeaderFilter filter : filters) { 168 Log.logTrace("Applying {0}", filter); 169 filter.request(r, this); 170 } 171 Log.logTrace("All filters applied"); 172 } 173 174 private HttpRequestImpl responseFilters(Response response) throws IOException 175 { 176 Log.logTrace("Applying response filters"); 177 Iterator<HeaderFilter> reverseItr = filters.descendingIterator(); 178 while (reverseItr.hasNext()) { 179 HeaderFilter filter = reverseItr.next(); 180 Log.logTrace("Applying {0}", filter); 181 HttpRequestImpl newreq = filter.response(response); 182 if (newreq != null) { 183 Log.logTrace("New request: stopping filters"); 184 return newreq; 185 } 186 } 187 Log.logTrace("All filters applied"); 188 return null; 189 } 190 191 public void cancel(IOException cause) { 192 cancelled = true; 193 getExchange().cancel(cause); 194 } 195 196 public CompletableFuture<HttpResponse<T>> responseAsync(Executor executor) { 197 CompletableFuture<Void> start = new MinimalFuture<>(); 198 CompletableFuture<HttpResponse<T>> cf = responseAsync0(start); 199 start.completeAsync( () -> null, executor); // trigger execution 200 return cf; 201 } 202 203 private CompletableFuture<HttpResponse<T>> 204 responseAsync0(CompletableFuture<Void> start) { 205 return start.thenCompose( v -> responseAsyncImpl()) 206 .thenCompose((Response r) -> { 207 Exchange<T> exch = getExchange(); 208 return exch.readBodyAsync(responseHandler) 209 .thenApply((T body) -> { 210 this.response = 211 new HttpResponseImpl<>(r.request(), r, this.response, body, exch); 212 return this.response; 213 }); 214 }); 215 } 216 217 private CompletableFuture<Response> responseAsyncImpl() { 218 CompletableFuture<Response> cf; 219 if (attempts.incrementAndGet() > max_attempts) { 220 cf = failedFuture(new IOException("Too many retries", retryCause)); 221 } else { 222 if (currentreq.timeout().isPresent()) { 223 timedEvent = new TimedEvent(currentreq.timeout().get()); 224 client.registerTimer(timedEvent); 225 } 226 try { 227 // 1. apply request filters 228 // if currentreq == previousreq the filters have already 229 // been applied once. Applying them a second time might 230 // cause some headers values to be added twice: for 231 // instance, the same cookie might be added again. 232 if (currentreq != previousreq) { 233 requestFilters(currentreq); 234 } 235 } catch (IOException e) { 236 return failedFuture(e); 237 } 238 Exchange<T> exch = getExchange(); 239 // 2. get response 240 cf = exch.responseAsync() 241 .thenCompose((Response response) -> { 242 HttpRequestImpl newrequest; 243 try { 244 // 3. apply response filters 245 newrequest = responseFilters(response); 246 } catch (IOException e) { 247 return failedFuture(e); 248 } 249 // 4. check filter result and repeat or continue 250 if (newrequest == null) { 251 if (attempts.get() > 1) { 252 Log.logError("Succeeded on attempt: " + attempts); 253 } 254 return completedFuture(response); 255 } else { 256 this.response = 257 new HttpResponseImpl<>(currentreq, response, this.response, null, exch); 258 Exchange<T> oldExch = exch; 259 return exch.ignoreBody().handle((r,t) -> { 260 previousreq = currentreq; 261 currentreq = newrequest; 262 expiredOnce = false; 263 setExchange(new Exchange<>(currentreq, this, acc)); 264 return responseAsyncImpl(); 265 }).thenCompose(Function.identity()); 266 } }) 267 .handle((response, ex) -> { 268 // 5. handle errors and cancel any timer set 269 cancelTimer(); 270 if (ex == null) { 271 assert response != null; 272 return completedFuture(response); 273 } 274 // all exceptions thrown are handled here 275 CompletableFuture<Response> errorCF = getExceptionalCF(ex); 276 if (errorCF == null) { 277 return responseAsyncImpl(); 278 } else { 279 return errorCF; 280 } }) 281 .thenCompose(Function.identity()); 282 } 283 return cf; 284 } 285 286 private static boolean retryPostValue() { 287 String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry"); 288 if (s == null) 289 return false; 290 return s.isEmpty() ? true : Boolean.parseBoolean(s); 291 } 292 293 private static boolean retryConnect() { 294 String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect"); 295 if (s == null) 296 return false; 297 return s.isEmpty() ? true : Boolean.parseBoolean(s); 298 } 299 300 /** True if ALL ( even non-idempotent ) requests can be automatic retried. */ 301 private static final boolean RETRY_ALWAYS = retryPostValue(); 302 /** True if ConnectException should cause a retry. Enabled by default */ 303 private static final boolean RETRY_CONNECT = retryConnect(); 304 305 /** Returns true is given request has an idempotent method. */ 306 private static boolean isIdempotentRequest(HttpRequest request) { 307 String method = request.method(); 308 switch (method) { 309 case "GET" : 310 case "HEAD" : 311 return true; 312 default : 313 return false; 314 } 315 } 316 317 /** Returns true if the given request can be automatically retried. */ 318 private static boolean canRetryRequest(HttpRequest request) { 319 if (RETRY_ALWAYS) 320 return true; 321 if (isIdempotentRequest(request)) 322 return true; 323 return false; 324 } 325 326 private boolean retryOnFailure(Throwable t) { 327 return t instanceof ConnectionExpiredException 328 || (RETRY_CONNECT && (t instanceof ConnectException)); 329 } 330 331 private Throwable retryCause(Throwable t) { 332 Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t; 333 return cause == null ? t : cause; 334 } 335 336 /** 337 * Takes a Throwable and returns a suitable CompletableFuture that is 338 * completed exceptionally, or null. 339 */ 340 private CompletableFuture<Response> getExceptionalCF(Throwable t) { 341 if ((t instanceof CompletionException) || (t instanceof ExecutionException)) { 342 if (t.getCause() != null) { 343 t = t.getCause(); 344 } 345 } 346 if (cancelled && t instanceof IOException) { 347 t = new HttpTimeoutException("request timed out"); 348 } else if (retryOnFailure(t)) { 349 Throwable cause = retryCause(t); 350 351 if (!(t instanceof ConnectException)) { 352 if (!canRetryRequest(currentreq)) { 353 return failedFuture(cause); // fails with original cause 354 } 355 } 356 357 // allow the retry mechanism to do its work 358 retryCause = cause; 359 if (!expiredOnce) { 360 if (debug.on()) 361 debug.log(t.getClass().getSimpleName() + " (async): retrying...", t); 362 expiredOnce = true; 363 // The connection was abruptly closed. 364 // We return null to retry the same request a second time. 365 // The request filters have already been applied to the 366 // currentreq, so we set previousreq = currentreq to 367 // prevent them from being applied again. 368 previousreq = currentreq; 369 return null; 370 } else { 371 if (debug.on()) { 372 debug.log(t.getClass().getSimpleName() 373 + " (async): already retried once.", t); 374 } 375 t = cause; 376 } 377 } 378 return failedFuture(t); 379 } 380 381 class TimedEvent extends TimeoutEvent { 382 TimedEvent(Duration duration) { 383 super(duration); 384 } 385 @Override 386 public void handle() { 387 if (debug.on()) { 388 debug.log("Cancelling MultiExchange due to timeout for request %s", 389 request); 390 } 391 cancel(new HttpTimeoutException("request timed out")); 392 } 393 } 394 }