HttpURLConnectionImpl.java revision 7aeaaefc891f6221f4b2cce536b1c1e816e09794
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.squareup.okhttp.internal.huc; 19 20import com.squareup.okhttp.Connection; 21import com.squareup.okhttp.Handshake; 22import com.squareup.okhttp.Headers; 23import com.squareup.okhttp.OkHttpClient; 24import com.squareup.okhttp.Protocol; 25import com.squareup.okhttp.Request; 26import com.squareup.okhttp.RequestBody; 27import com.squareup.okhttp.Response; 28import com.squareup.okhttp.Route; 29import com.squareup.okhttp.internal.Internal; 30import com.squareup.okhttp.internal.Platform; 31import com.squareup.okhttp.internal.http.RouteException; 32import com.squareup.okhttp.internal.Util; 33import com.squareup.okhttp.internal.http.HttpDate; 34import com.squareup.okhttp.internal.http.HttpEngine; 35import com.squareup.okhttp.internal.http.HttpMethod; 36import com.squareup.okhttp.internal.http.OkHeaders; 37import com.squareup.okhttp.internal.http.RequestException; 38import com.squareup.okhttp.internal.http.RetryableSink; 39import com.squareup.okhttp.internal.http.StatusLine; 40import java.io.FileNotFoundException; 41import java.io.IOException; 42import java.io.InputStream; 43import java.io.OutputStream; 44import java.net.HttpRetryException; 45import java.net.HttpURLConnection; 46import java.net.InetSocketAddress; 47import java.net.ProtocolException; 48import java.net.Proxy; 49import java.net.SocketPermission; 50import java.net.URL; 51import java.security.Permission; 52import java.util.ArrayList; 53import java.util.Arrays; 54import java.util.Collections; 55import java.util.Date; 56import java.util.LinkedHashSet; 57import java.util.List; 58import java.util.Map; 59import java.util.Set; 60import java.util.concurrent.TimeUnit; 61import okio.BufferedSink; 62import okio.Sink; 63 64/** 65 * This implementation uses HttpEngine to send requests and receive responses. 66 * This class may use multiple HttpEngines to follow redirects, authentication 67 * retries, etc. to retrieve the final response body. 68 * 69 * <h3>What does 'connected' mean?</h3> 70 * This class inherits a {@code connected} field from the superclass. That field 71 * is <strong>not</strong> used to indicate not whether this URLConnection is 72 * currently connected. Instead, it indicates whether a connection has ever been 73 * attempted. Once a connection has been attempted, certain properties (request 74 * header fields, request method, etc.) are immutable. 75 */ 76public class HttpURLConnectionImpl extends HttpURLConnection { 77 private static final Set<String> METHODS = new LinkedHashSet<>( 78 Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH")); 79 private static final RequestBody EMPTY_REQUEST_BODY = RequestBody.create(null, new byte[0]); 80 81 final OkHttpClient client; 82 83 private Headers.Builder requestHeaders = new Headers.Builder(); 84 85 /** Like the superclass field of the same name, but a long and available on all platforms. */ 86 private long fixedContentLength = -1; 87 private int followUpCount; 88 protected IOException httpEngineFailure; 89 protected HttpEngine httpEngine; 90 /** Lazily created (with synthetic headers) on first call to getHeaders(). */ 91 private Headers responseHeaders; 92 93 /** 94 * The most recently attempted route. This will be null if we haven't sent a 95 * request yet, or if the response comes from a cache. 96 */ 97 private Route route; 98 99 /** 100 * The most recently received TLS handshake. This will be null if we haven't 101 * connected yet, or if the most recent connection was HTTP (and not HTTPS). 102 */ 103 Handshake handshake; 104 105 public HttpURLConnectionImpl(URL url, OkHttpClient client) { 106 super(url); 107 this.client = client; 108 } 109 110 @Override public final void connect() throws IOException { 111 initHttpEngine(); 112 boolean success; 113 do { 114 success = execute(false); 115 } while (!success); 116 } 117 118 @Override public final void disconnect() { 119 // Calling disconnect() before a connection exists should have no effect. 120 if (httpEngine == null) return; 121 122 httpEngine.disconnect(); 123 124 // This doesn't close the stream because doing so would require all stream 125 // access to be synchronized. It's expected that the thread using the 126 // connection will close its streams directly. If it doesn't, the worst 127 // case is that the GzipSource's Inflater won't be released until it's 128 // finalized. (This logs a warning on Android.) 129 } 130 131 /** 132 * Returns an input stream from the server in the case of error such as the 133 * requested file (txt, htm, html) is not found on the remote server. 134 */ 135 @Override public final InputStream getErrorStream() { 136 try { 137 HttpEngine response = getResponse(); 138 if (HttpEngine.hasBody(response.getResponse()) 139 && response.getResponse().code() >= HTTP_BAD_REQUEST) { 140 return response.getResponse().body().byteStream(); 141 } 142 return null; 143 } catch (IOException e) { 144 return null; 145 } 146 } 147 148 private Headers getHeaders() throws IOException { 149 if (responseHeaders == null) { 150 Response response = getResponse().getResponse(); 151 Headers headers = response.headers(); 152 153 responseHeaders = headers.newBuilder() 154 .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response)) 155 .build(); 156 } 157 return responseHeaders; 158 } 159 160 private static String responseSourceHeader(Response response) { 161 if (response.networkResponse() == null) { 162 if (response.cacheResponse() == null) { 163 return "NONE"; 164 } 165 return "CACHE " + response.code(); 166 } 167 if (response.cacheResponse() == null) { 168 return "NETWORK " + response.code(); 169 } 170 return "CONDITIONAL_CACHE " + response.networkResponse().code(); 171 } 172 173 /** 174 * Returns the value of the field at {@code position}. Returns null if there 175 * are fewer than {@code position} headers. 176 */ 177 @Override public final String getHeaderField(int position) { 178 try { 179 return getHeaders().value(position); 180 } catch (IOException e) { 181 return null; 182 } 183 } 184 185 /** 186 * Returns the value of the field corresponding to the {@code fieldName}, or 187 * null if there is no such field. If the field has multiple values, the 188 * last value is returned. 189 */ 190 @Override public final String getHeaderField(String fieldName) { 191 try { 192 return fieldName == null 193 ? StatusLine.get(getResponse().getResponse()).toString() 194 : getHeaders().get(fieldName); 195 } catch (IOException e) { 196 return null; 197 } 198 } 199 200 @Override public final String getHeaderFieldKey(int position) { 201 try { 202 return getHeaders().name(position); 203 } catch (IOException e) { 204 return null; 205 } 206 } 207 208 @Override public final Map<String, List<String>> getHeaderFields() { 209 try { 210 return OkHeaders.toMultimap(getHeaders(), 211 StatusLine.get(getResponse().getResponse()).toString()); 212 } catch (IOException e) { 213 return Collections.emptyMap(); 214 } 215 } 216 217 @Override public final Map<String, List<String>> getRequestProperties() { 218 if (connected) { 219 throw new IllegalStateException( 220 "Cannot access request header fields after connection is set"); 221 } 222 223 return OkHeaders.toMultimap(requestHeaders.build(), null); 224 } 225 226 @Override public final InputStream getInputStream() throws IOException { 227 if (!doInput) { 228 throw new ProtocolException("This protocol does not support input"); 229 } 230 231 HttpEngine response = getResponse(); 232 233 // if the requested file does not exist, throw an exception formerly the 234 // Error page from the server was returned if the requested file was 235 // text/html this has changed to return FileNotFoundException for all 236 // file types 237 if (getResponseCode() >= HTTP_BAD_REQUEST) { 238 throw new FileNotFoundException(url.toString()); 239 } 240 241 return response.getResponse().body().byteStream(); 242 } 243 244 @Override public final OutputStream getOutputStream() throws IOException { 245 connect(); 246 247 BufferedSink sink = httpEngine.getBufferedRequestBody(); 248 if (sink == null) { 249 throw new ProtocolException("method does not support a request body: " + method); 250 } else if (httpEngine.hasResponse()) { 251 throw new ProtocolException("cannot write request body after response has been read"); 252 } 253 254 return sink.outputStream(); 255 } 256 257 @Override public final Permission getPermission() throws IOException { 258 String hostName = getURL().getHost(); 259 int hostPort = Util.getEffectivePort(getURL()); 260 if (usingProxy()) { 261 InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address(); 262 hostName = proxyAddress.getHostName(); 263 hostPort = proxyAddress.getPort(); 264 } 265 return new SocketPermission(hostName + ":" + hostPort, "connect, resolve"); 266 } 267 268 @Override public final String getRequestProperty(String field) { 269 if (field == null) return null; 270 return requestHeaders.get(field); 271 } 272 273 @Override public void setConnectTimeout(int timeoutMillis) { 274 client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 275 } 276 277 @Override 278 public void setInstanceFollowRedirects(boolean followRedirects) { 279 client.setFollowRedirects(followRedirects); 280 } 281 282 @Override public int getConnectTimeout() { 283 return client.getConnectTimeout(); 284 } 285 286 @Override public void setReadTimeout(int timeoutMillis) { 287 client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); 288 } 289 290 @Override public int getReadTimeout() { 291 return client.getReadTimeout(); 292 } 293 294 private void initHttpEngine() throws IOException { 295 if (httpEngineFailure != null) { 296 throw httpEngineFailure; 297 } else if (httpEngine != null) { 298 return; 299 } 300 301 connected = true; 302 try { 303 if (doOutput) { 304 if (method.equals("GET")) { 305 // they are requesting a stream to write to. This implies a POST method 306 method = "POST"; 307 } else if (!HttpMethod.permitsRequestBody(method)) { 308 throw new ProtocolException(method + " does not support writing"); 309 } 310 } 311 // If the user set content length to zero, we know there will not be a request body. 312 httpEngine = newHttpEngine(method, null, null, null); 313 } catch (IOException e) { 314 httpEngineFailure = e; 315 throw e; 316 } 317 } 318 319 private HttpEngine newHttpEngine(String method, Connection connection, 320 RetryableSink requestBody, Response priorResponse) { 321 // OkHttp's Call API requires a placeholder body; the real body will be streamed separately. 322 RequestBody placeholderBody = HttpMethod.requiresRequestBody(method) 323 ? EMPTY_REQUEST_BODY 324 : null; 325 Request.Builder builder = new Request.Builder() 326 .url(getURL()) 327 .method(method, placeholderBody); 328 Headers headers = requestHeaders.build(); 329 for (int i = 0, size = headers.size(); i < size; i++) { 330 builder.addHeader(headers.name(i), headers.value(i)); 331 } 332 333 boolean bufferRequestBody = false; 334 if (HttpMethod.permitsRequestBody(method)) { 335 // Specify how the request body is terminated. 336 if (fixedContentLength != -1) { 337 builder.header("Content-Length", Long.toString(fixedContentLength)); 338 } else if (chunkLength > 0) { 339 builder.header("Transfer-Encoding", "chunked"); 340 } else { 341 bufferRequestBody = true; 342 } 343 344 // Add a content type for the request body, if one isn't already present. 345 if (headers.get("Content-Type") == null) { 346 builder.header("Content-Type", "application/x-www-form-urlencoded"); 347 } 348 } 349 350 if (headers.get("User-Agent") == null) { 351 builder.header("User-Agent", defaultUserAgent()); 352 } 353 354 Request request = builder.build(); 355 356 // If we're currently not using caches, make sure the engine's client doesn't have one. 357 OkHttpClient engineClient = client; 358 if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) { 359 engineClient = client.clone().setCache(null); 360 } 361 362 return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null, 363 requestBody, priorResponse); 364 } 365 366 private String defaultUserAgent() { 367 String agent = System.getProperty("http.agent"); 368 return agent != null ? agent : ("Java" + System.getProperty("java.version")); 369 } 370 371 /** 372 * Aggressively tries to get the final HTTP response, potentially making 373 * many HTTP requests in the process in order to cope with redirects and 374 * authentication. 375 */ 376 private HttpEngine getResponse() throws IOException { 377 initHttpEngine(); 378 379 if (httpEngine.hasResponse()) { 380 return httpEngine; 381 } 382 383 while (true) { 384 if (!execute(true)) { 385 continue; 386 } 387 388 Response response = httpEngine.getResponse(); 389 Request followUp = httpEngine.followUpRequest(); 390 391 if (followUp == null) { 392 httpEngine.releaseConnection(); 393 return httpEngine; 394 } 395 396 if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) { 397 throw new ProtocolException("Too many follow-up requests: " + followUpCount); 398 } 399 400 // The first request was insufficient. Prepare for another... 401 url = followUp.url(); 402 requestHeaders = followUp.headers().newBuilder(); 403 404 // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect 405 // should keep the same method, Chrome, Firefox and the RI all issue GETs 406 // when following any redirect. 407 Sink requestBody = httpEngine.getRequestBody(); 408 if (!followUp.method().equals(method)) { 409 requestBody = null; 410 } 411 412 if (requestBody != null && !(requestBody instanceof RetryableSink)) { 413 throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode); 414 } 415 416 if (!httpEngine.sameConnection(followUp.url())) { 417 httpEngine.releaseConnection(); 418 } 419 420 Connection connection = httpEngine.close(); 421 httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody, 422 response); 423 } 424 } 425 426 /** 427 * Sends a request and optionally reads a response. Returns true if the 428 * request was successfully executed, and false if the request can be 429 * retried. Throws an exception if the request failed permanently. 430 */ 431 private boolean execute(boolean readResponse) throws IOException { 432 try { 433 httpEngine.sendRequest(); 434 route = httpEngine.getRoute(); 435 handshake = httpEngine.getConnection() != null 436 ? httpEngine.getConnection().getHandshake() 437 : null; 438 if (readResponse) { 439 httpEngine.readResponse(); 440 } 441 442 return true; 443 } catch (RequestException e) { 444 // An attempt to interpret a request failed. 445 IOException toThrow = e.getCause(); 446 httpEngineFailure = toThrow; 447 throw toThrow; 448 } catch (RouteException e) { 449 // The attempt to connect via a route failed. The request will not have been sent. 450 HttpEngine retryEngine = httpEngine.recover(e); 451 if (retryEngine != null) { 452 httpEngine = retryEngine; 453 return false; 454 } 455 456 // Give up; recovery is not possible. 457 IOException toThrow = e.getLastConnectException(); 458 httpEngineFailure = toThrow; 459 throw toThrow; 460 } catch (IOException e) { 461 // An attempt to communicate with a server failed. The request may have been sent. 462 HttpEngine retryEngine = httpEngine.recover(e); 463 if (retryEngine != null) { 464 httpEngine = retryEngine; 465 return false; 466 } 467 468 // Give up; recovery is not possible. 469 httpEngineFailure = e; 470 throw e; 471 } 472 } 473 474 /** 475 * Returns true if either: 476 * <ul> 477 * <li>A specific proxy was explicitly configured for this connection. 478 * <li>The response has already been retrieved, and a proxy was {@link 479 * java.net.ProxySelector selected} in order to get it. 480 * </ul> 481 * 482 * <p><strong>Warning:</strong> This method may return false before attempting 483 * to connect and true afterwards. 484 */ 485 @Override public final boolean usingProxy() { 486 Proxy proxy = route != null 487 ? route.getProxy() 488 : client.getProxy(); 489 return proxy != null && proxy.type() != Proxy.Type.DIRECT; 490 } 491 492 @Override public String getResponseMessage() throws IOException { 493 return getResponse().getResponse().message(); 494 } 495 496 @Override public final int getResponseCode() throws IOException { 497 return getResponse().getResponse().code(); 498 } 499 500 @Override public final void setRequestProperty(String field, String newValue) { 501 if (connected) { 502 throw new IllegalStateException("Cannot set request property after connection is made"); 503 } 504 if (field == null) { 505 throw new NullPointerException("field == null"); 506 } 507 if (newValue == null) { 508 // Silently ignore null header values for backwards compatibility with older 509 // android versions as well as with other URLConnection implementations. 510 // 511 // Some implementations send a malformed HTTP header when faced with 512 // such requests, we respect the spec and ignore the header. 513 Platform.get().logW("Ignoring header " + field + " because its value was null."); 514 return; 515 } 516 517 // TODO: Deprecate use of X-Android-Transports header? 518 if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { 519 setProtocols(newValue, false /* append */); 520 } else { 521 requestHeaders.set(field, newValue); 522 } 523 } 524 525 @Override public void setIfModifiedSince(long newValue) { 526 super.setIfModifiedSince(newValue); 527 if (ifModifiedSince != 0) { 528 requestHeaders.set("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince))); 529 } else { 530 requestHeaders.removeAll("If-Modified-Since"); 531 } 532 } 533 534 @Override public final void addRequestProperty(String field, String value) { 535 if (connected) { 536 throw new IllegalStateException("Cannot add request property after connection is made"); 537 } 538 if (field == null) { 539 throw new NullPointerException("field == null"); 540 } 541 if (value == null) { 542 // Silently ignore null header values for backwards compatibility with older 543 // android versions as well as with other URLConnection implementations. 544 // 545 // Some implementations send a malformed HTTP header when faced with 546 // such requests, we respect the spec and ignore the header. 547 Platform.get().logW("Ignoring header " + field + " because its value was null."); 548 return; 549 } 550 551 // TODO: Deprecate use of X-Android-Transports header? 552 if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) { 553 setProtocols(value, true /* append */); 554 } else { 555 requestHeaders.add(field, value); 556 } 557 } 558 559 /* 560 * Splits and validates a comma-separated string of protocols. 561 * When append == false, we require that the transport list contains "http/1.1". 562 * Throws {@link IllegalStateException} when one of the protocols isn't 563 * defined in {@link Protocol OkHttp's protocol enumeration}. 564 */ 565 private void setProtocols(String protocolsString, boolean append) { 566 List<Protocol> protocolsList = new ArrayList<>(); 567 if (append) { 568 protocolsList.addAll(client.getProtocols()); 569 } 570 for (String protocol : protocolsString.split(",", -1)) { 571 try { 572 protocolsList.add(Protocol.get(protocol)); 573 } catch (IOException e) { 574 throw new IllegalStateException(e); 575 } 576 } 577 client.setProtocols(protocolsList); 578 } 579 580 @Override public void setRequestMethod(String method) throws ProtocolException { 581 if (!METHODS.contains(method)) { 582 throw new ProtocolException("Expected one of " + METHODS + " but was " + method); 583 } 584 this.method = method; 585 } 586 587 @Override public void setFixedLengthStreamingMode(int contentLength) { 588 setFixedLengthStreamingMode((long) contentLength); 589 } 590 591 @Override public void setFixedLengthStreamingMode(long contentLength) { 592 if (super.connected) throw new IllegalStateException("Already connected"); 593 if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode"); 594 if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0"); 595 this.fixedContentLength = contentLength; 596 super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE); 597 } 598} 599