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