Home | History | Annotate | Download | only in http
      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 
     18 package com.squareup.okhttp.internal.http;
     19 
     20 import com.squareup.okhttp.Address;
     21 import com.squareup.okhttp.Connection;
     22 import com.squareup.okhttp.OkHttpClient;
     23 import com.squareup.okhttp.OkResponseCache;
     24 import com.squareup.okhttp.ResponseSource;
     25 import com.squareup.okhttp.TunnelRequest;
     26 import com.squareup.okhttp.internal.Dns;
     27 import com.squareup.okhttp.internal.Platform;
     28 import com.squareup.okhttp.internal.Util;
     29 import java.io.ByteArrayInputStream;
     30 import java.io.IOException;
     31 import java.io.InputStream;
     32 import java.io.OutputStream;
     33 import java.net.CacheRequest;
     34 import java.net.CacheResponse;
     35 import java.net.CookieHandler;
     36 import java.net.HttpURLConnection;
     37 import java.net.Proxy;
     38 import java.net.URI;
     39 import java.net.URISyntaxException;
     40 import java.net.URL;
     41 import java.net.UnknownHostException;
     42 import java.util.Collections;
     43 import java.util.Date;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.zip.GZIPInputStream;
     48 import javax.net.ssl.HostnameVerifier;
     49 import javax.net.ssl.SSLSocketFactory;
     50 
     51 import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
     52 import static com.squareup.okhttp.internal.Util.getDefaultPort;
     53 import static com.squareup.okhttp.internal.Util.getEffectivePort;
     54 
     55 /**
     56  * Handles a single HTTP request/response pair. Each HTTP engine follows this
     57  * lifecycle:
     58  * <ol>
     59  * <li>It is created.
     60  * <li>The HTTP request message is sent with sendRequest(). Once the request
     61  * is sent it is an error to modify the request headers. After
     62  * sendRequest() has been called the request body can be written to if
     63  * it exists.
     64  * <li>The HTTP response message is read with readResponse(). After the
     65  * response has been read the response headers and body can be read.
     66  * All responses have a response body input stream, though in some
     67  * instances this stream is empty.
     68  * </ol>
     69  *
     70  * <p>The request and response may be served by the HTTP response cache, by the
     71  * network, or by both in the event of a conditional GET.
     72  *
     73  * <p>This class may hold a socket connection that needs to be released or
     74  * recycled. By default, this socket connection is held when the last byte of
     75  * the response is consumed. To release the connection when it is no longer
     76  * required, use {@link #automaticallyReleaseConnectionToPool()}.
     77  */
     78 public class HttpEngine {
     79   private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() {
     80     @Override public Map<String, List<String>> getHeaders() throws IOException {
     81       Map<String, List<String>> result = new HashMap<String, List<String>>();
     82       result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout"));
     83       return result;
     84     }
     85     @Override public InputStream getBody() throws IOException {
     86       return new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
     87     }
     88   };
     89   public static final int HTTP_CONTINUE = 100;
     90 
     91   protected final Policy policy;
     92   protected final OkHttpClient client;
     93 
     94   protected final String method;
     95 
     96   private ResponseSource responseSource;
     97 
     98   protected Connection connection;
     99   protected RouteSelector routeSelector;
    100   private OutputStream requestBodyOut;
    101 
    102   private Transport transport;
    103 
    104   private InputStream responseTransferIn;
    105   private InputStream responseBodyIn;
    106 
    107   private CacheResponse cacheResponse;
    108   private CacheRequest cacheRequest;
    109 
    110   /** The time when the request headers were written, or -1 if they haven't been written yet. */
    111   long sentRequestMillis = -1;
    112 
    113   /** Whether the connection has been established */
    114   boolean connected;
    115 
    116   /**
    117    * True if this client added an "Accept-Encoding: gzip" header field and is
    118    * therefore responsible for also decompressing the transfer stream.
    119    */
    120   private boolean transparentGzip;
    121 
    122   final URI uri;
    123 
    124   final RequestHeaders requestHeaders;
    125 
    126   /** Null until a response is received from the network or the cache. */
    127   ResponseHeaders responseHeaders;
    128 
    129   // The cache response currently being validated on a conditional get. Null
    130   // if the cached response doesn't exist or doesn't need validation. If the
    131   // conditional get succeeds, these will be used for the response headers and
    132   // body. If it fails, these be closed and set to null.
    133   private ResponseHeaders cachedResponseHeaders;
    134   private InputStream cachedResponseBody;
    135 
    136   /**
    137    * True if the socket connection should be released to the connection pool
    138    * when the response has been fully read.
    139    */
    140   private boolean automaticallyReleaseConnectionToPool;
    141 
    142   /** True if the socket connection is no longer needed by this engine. */
    143   private boolean connectionReleased;
    144 
    145   /**
    146    * @param requestHeaders the client's supplied request headers. This class
    147    *     creates a private copy that it can mutate.
    148    * @param connection the connection used for an intermediate response
    149    *     immediately prior to this request/response pair, such as a same-host
    150    *     redirect. This engine assumes ownership of the connection and must
    151    *     release it when it is unneeded.
    152    */
    153   public HttpEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
    154       Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
    155     this.client = client;
    156     this.policy = policy;
    157     this.method = method;
    158     this.connection = connection;
    159     this.requestBodyOut = requestBodyOut;
    160 
    161     try {
    162       uri = Platform.get().toUriLenient(policy.getURL());
    163     } catch (URISyntaxException e) {
    164       throw new IOException(e.getMessage());
    165     }
    166 
    167     this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
    168   }
    169 
    170   public URI getUri() {
    171     return uri;
    172   }
    173 
    174   /**
    175    * Figures out what the response source will be, and opens a socket to that
    176    * source if necessary. Prepares the request headers and gets ready to start
    177    * writing the request body if it exists.
    178    */
    179   public final void sendRequest() throws IOException {
    180     if (responseSource != null) {
    181       return;
    182     }
    183 
    184     prepareRawRequestHeaders();
    185     initResponseSource();
    186     OkResponseCache responseCache = client.getOkResponseCache();
    187     if (responseCache != null) {
    188       responseCache.trackResponse(responseSource);
    189     }
    190 
    191     // The raw response source may require the network, but the request
    192     // headers may forbid network use. In that case, dispose of the network
    193     // response and use a GATEWAY_TIMEOUT response instead, as specified
    194     // by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
    195     if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
    196       if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    197         Util.closeQuietly(cachedResponseBody);
    198       }
    199       this.responseSource = ResponseSource.CACHE;
    200       this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
    201       RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
    202       setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
    203     }
    204 
    205     if (responseSource.requiresConnection()) {
    206       sendSocketRequest();
    207     } else if (connection != null) {
    208       client.getConnectionPool().recycle(connection);
    209       connection = null;
    210     }
    211   }
    212 
    213   /**
    214    * Initialize the source for this response. It may be corrected later if the
    215    * request headers forbids network use.
    216    */
    217   private void initResponseSource() throws IOException {
    218     responseSource = ResponseSource.NETWORK;
    219     if (!policy.getUseCaches()) return;
    220 
    221     OkResponseCache responseCache = client.getOkResponseCache();
    222     if (responseCache == null) return;
    223 
    224     CacheResponse candidate = responseCache.get(
    225         uri, method, requestHeaders.getHeaders().toMultimap(false));
    226     if (candidate == null) return;
    227 
    228     Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
    229     cachedResponseBody = candidate.getBody();
    230     if (!acceptCacheResponseType(candidate)
    231         || responseHeadersMap == null
    232         || cachedResponseBody == null) {
    233       Util.closeQuietly(cachedResponseBody);
    234       return;
    235     }
    236 
    237     RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true);
    238     cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
    239     long now = System.currentTimeMillis();
    240     this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
    241     if (responseSource == ResponseSource.CACHE) {
    242       this.cacheResponse = candidate;
    243       setResponse(cachedResponseHeaders, cachedResponseBody);
    244     } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    245       this.cacheResponse = candidate;
    246     } else if (responseSource == ResponseSource.NETWORK) {
    247       Util.closeQuietly(cachedResponseBody);
    248     } else {
    249       throw new AssertionError();
    250     }
    251   }
    252 
    253   private void sendSocketRequest() throws IOException {
    254     if (connection == null) {
    255       connect();
    256     }
    257 
    258     if (transport != null) {
    259       throw new IllegalStateException();
    260     }
    261 
    262     transport = (Transport) connection.newTransport(this);
    263 
    264     if (hasRequestBody() && requestBodyOut == null) {
    265       // Create a request body if we don't have one already. We'll already
    266       // have one if we're retrying a failed POST.
    267       requestBodyOut = transport.createRequestBody();
    268     }
    269   }
    270 
    271   /** Connect to the origin server either directly or via a proxy. */
    272   protected final void connect() throws IOException {
    273     if (connection != null) {
    274       return;
    275     }
    276     if (routeSelector == null) {
    277       String uriHost = uri.getHost();
    278       if (uriHost == null) {
    279         throw new UnknownHostException(uri.toString());
    280       }
    281       SSLSocketFactory sslSocketFactory = null;
    282       HostnameVerifier hostnameVerifier = null;
    283       if (uri.getScheme().equalsIgnoreCase("https")) {
    284         sslSocketFactory = client.getSslSocketFactory();
    285         hostnameVerifier = client.getHostnameVerifier();
    286       }
    287       Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
    288           hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports());
    289       routeSelector = new RouteSelector(address, uri, client.getProxySelector(),
    290           client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase());
    291     }
    292     connection = routeSelector.next(method);
    293     if (!connection.isConnected()) {
    294       connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
    295       client.getConnectionPool().maybeShare(connection);
    296       client.getRoutesDatabase().connected(connection.getRoute());
    297     } else {
    298       connection.updateReadTimeout(client.getReadTimeout());
    299     }
    300     connected(connection);
    301     if (connection.getRoute().getProxy() != client.getProxy()) {
    302       // Update the request line if the proxy changed; it may need a host name.
    303       requestHeaders.getHeaders().setRequestLine(getRequestLine());
    304     }
    305   }
    306 
    307   /**
    308    * Called after a socket connection has been created or retrieved from the
    309    * pool. Subclasses use this hook to get a reference to the TLS data.
    310    */
    311   protected void connected(Connection connection) {
    312     policy.setSelectedProxy(connection.getRoute().getProxy());
    313     connected = true;
    314   }
    315 
    316   /**
    317    * Called immediately before the transport transmits HTTP request headers.
    318    * This is used to observe the sent time should the request be cached.
    319    */
    320   public void writingRequestHeaders() {
    321     if (sentRequestMillis != -1) {
    322       throw new IllegalStateException();
    323     }
    324     sentRequestMillis = System.currentTimeMillis();
    325   }
    326 
    327   /**
    328    * @param body the response body, or null if it doesn't exist or isn't
    329    * available.
    330    */
    331   private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
    332     if (this.responseBodyIn != null) {
    333       throw new IllegalStateException();
    334     }
    335     this.responseHeaders = headers;
    336     if (body != null) {
    337       initContentStream(body);
    338     }
    339   }
    340 
    341   boolean hasRequestBody() {
    342     return method.equals("POST") || method.equals("PUT");
    343   }
    344 
    345   /** Returns the request body or null if this request doesn't have a body. */
    346   public final OutputStream getRequestBody() {
    347     if (responseSource == null) {
    348       throw new IllegalStateException();
    349     }
    350     return requestBodyOut;
    351   }
    352 
    353   public final boolean hasResponse() {
    354     return responseHeaders != null;
    355   }
    356 
    357   public final RequestHeaders getRequestHeaders() {
    358     return requestHeaders;
    359   }
    360 
    361   public final ResponseHeaders getResponseHeaders() {
    362     if (responseHeaders == null) {
    363       throw new IllegalStateException();
    364     }
    365     return responseHeaders;
    366   }
    367 
    368   public final int getResponseCode() {
    369     if (responseHeaders == null) {
    370       throw new IllegalStateException();
    371     }
    372     return responseHeaders.getHeaders().getResponseCode();
    373   }
    374 
    375   public final InputStream getResponseBody() {
    376     if (responseHeaders == null) {
    377       throw new IllegalStateException();
    378     }
    379     return responseBodyIn;
    380   }
    381 
    382   public final CacheResponse getCacheResponse() {
    383     return cacheResponse;
    384   }
    385 
    386   public final Connection getConnection() {
    387     return connection;
    388   }
    389 
    390   /**
    391    * Returns true if {@code cacheResponse} is of the right type. This
    392    * condition is necessary but not sufficient for the cached response to
    393    * be used.
    394    */
    395   protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
    396     return true;
    397   }
    398 
    399   private void maybeCache() throws IOException {
    400     // Are we caching at all?
    401     if (!policy.getUseCaches()) return;
    402     OkResponseCache responseCache = client.getOkResponseCache();
    403     if (responseCache == null) return;
    404 
    405     HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
    406 
    407     // Should we cache this response for this request?
    408     if (!responseHeaders.isCacheable(requestHeaders)) {
    409       responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
    410       return;
    411     }
    412 
    413     // Offer this request to the cache.
    414     cacheRequest = responseCache.put(uri, connectionToCache);
    415   }
    416 
    417   /**
    418    * Cause the socket connection to be released to the connection pool when
    419    * it is no longer needed. If it is already unneeded, it will be pooled
    420    * immediately. Otherwise the connection is held so that redirects can be
    421    * handled by the same connection.
    422    */
    423   public final void automaticallyReleaseConnectionToPool() {
    424     automaticallyReleaseConnectionToPool = true;
    425     if (connection != null && connectionReleased) {
    426       client.getConnectionPool().recycle(connection);
    427       connection = null;
    428     }
    429   }
    430 
    431   /**
    432    * Releases this engine so that its resources may be either reused or
    433    * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
    434    * the connection will be used to follow a redirect.
    435    */
    436   public final void release(boolean streamCancelled) {
    437     // If the response body comes from the cache, close it.
    438     if (responseBodyIn == cachedResponseBody) {
    439       Util.closeQuietly(responseBodyIn);
    440     }
    441 
    442     if (!connectionReleased && connection != null) {
    443       connectionReleased = true;
    444 
    445       if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
    446           responseTransferIn)) {
    447         Util.closeQuietly(connection);
    448         connection = null;
    449       } else if (automaticallyReleaseConnectionToPool) {
    450         client.getConnectionPool().recycle(connection);
    451         connection = null;
    452       }
    453     }
    454   }
    455 
    456   private void initContentStream(InputStream transferStream) throws IOException {
    457     responseTransferIn = transferStream;
    458     if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
    459       // If the response was transparently gzipped, remove the gzip header field
    460       // so clients don't double decompress. http://b/3009828
    461       //
    462       // Also remove the Content-Length in this case because it contains the
    463       // length 528 of the gzipped response. This isn't terribly useful and is
    464       // dangerous because 529 clients can query the content length, but not
    465       // the content encoding.
    466       responseHeaders.stripContentEncoding();
    467       responseHeaders.stripContentLength();
    468       responseBodyIn = new GZIPInputStream(transferStream);
    469     } else {
    470       responseBodyIn = transferStream;
    471     }
    472   }
    473 
    474   /**
    475    * Returns true if the response must have a (possibly 0-length) body.
    476    * See RFC 2616 section 4.3.
    477    */
    478   public final boolean hasResponseBody() {
    479     int responseCode = responseHeaders.getHeaders().getResponseCode();
    480 
    481     // HEAD requests never yield a body regardless of the response headers.
    482     if (method.equals("HEAD")) {
    483       return false;
    484     }
    485 
    486     if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
    487         && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
    488         && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
    489       return true;
    490     }
    491 
    492     // If the Content-Length or Transfer-Encoding headers disagree with the
    493     // response code, the response is malformed. For best compatibility, we
    494     // honor the headers.
    495     if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
    496       return true;
    497     }
    498 
    499     return false;
    500   }
    501 
    502   /**
    503    * Populates requestHeaders with defaults and cookies.
    504    *
    505    * <p>This client doesn't specify a default {@code Accept} header because it
    506    * doesn't know what content types the application is interested in.
    507    */
    508   private void prepareRawRequestHeaders() throws IOException {
    509     requestHeaders.getHeaders().setRequestLine(getRequestLine());
    510 
    511     if (requestHeaders.getUserAgent() == null) {
    512       requestHeaders.setUserAgent(getDefaultUserAgent());
    513     }
    514 
    515     if (requestHeaders.getHost() == null) {
    516       requestHeaders.setHost(getOriginAddress(policy.getURL()));
    517     }
    518 
    519     if ((connection == null || connection.getHttpMinorVersion() != 0)
    520         && requestHeaders.getConnection() == null) {
    521       requestHeaders.setConnection("Keep-Alive");
    522     }
    523 
    524     if (requestHeaders.getAcceptEncoding() == null) {
    525       transparentGzip = true;
    526       requestHeaders.setAcceptEncoding("gzip");
    527     }
    528 
    529     if (hasRequestBody() && requestHeaders.getContentType() == null) {
    530       requestHeaders.setContentType("application/x-www-form-urlencoded");
    531     }
    532 
    533     long ifModifiedSince = policy.getIfModifiedSince();
    534     if (ifModifiedSince != 0) {
    535       requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
    536     }
    537 
    538     CookieHandler cookieHandler = client.getCookieHandler();
    539     if (cookieHandler != null) {
    540       requestHeaders.addCookies(
    541           cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
    542     }
    543   }
    544 
    545   /**
    546    * Returns the request status line, like "GET / HTTP/1.1". This is exposed
    547    * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
    548    * it needs to be set even if the transport is SPDY.
    549    */
    550   String getRequestLine() {
    551     String protocol =
    552         (connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
    553     return method + " " + requestString() + " " + protocol;
    554   }
    555 
    556   private String requestString() {
    557     URL url = policy.getURL();
    558     if (includeAuthorityInRequestLine()) {
    559       return url.toString();
    560     } else {
    561       return requestPath(url);
    562     }
    563   }
    564 
    565   /**
    566    * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
    567    * empty, even if the request URL is. Includes the query component if it
    568    * exists.
    569    */
    570   public static String requestPath(URL url) {
    571     String fileOnly = url.getFile();
    572     if (fileOnly == null) {
    573       return "/";
    574     } else if (!fileOnly.startsWith("/")) {
    575       return "/" + fileOnly;
    576     } else {
    577       return fileOnly;
    578     }
    579   }
    580 
    581   /**
    582    * Returns true if the request line should contain the full URL with host
    583    * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
    584    * (like "GET /foo HTTP/1.1").
    585    *
    586    * <p>This is non-final because for HTTPS it's never necessary to supply the
    587    * full URL, even if a proxy is in use.
    588    */
    589   protected boolean includeAuthorityInRequestLine() {
    590     return connection == null
    591         ? policy.usingProxy() // A proxy was requested.
    592         : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
    593   }
    594 
    595   public static String getDefaultUserAgent() {
    596     String agent = System.getProperty("http.agent");
    597     return agent != null ? agent : ("Java" + System.getProperty("java.version"));
    598   }
    599 
    600   public static String getOriginAddress(URL url) {
    601     int port = url.getPort();
    602     String result = url.getHost();
    603     if (port > 0 && port != getDefaultPort(url.getProtocol())) {
    604       result = result + ":" + port;
    605     }
    606     return result;
    607   }
    608 
    609   /**
    610    * Flushes the remaining request header and body, parses the HTTP response
    611    * headers and starts reading the HTTP response body if it exists.
    612    */
    613   public final void readResponse() throws IOException {
    614     if (hasResponse()) {
    615       responseHeaders.setResponseSource(responseSource);
    616       return;
    617     }
    618 
    619     if (responseSource == null) {
    620       throw new IllegalStateException("readResponse() without sendRequest()");
    621     }
    622 
    623     if (!responseSource.requiresConnection()) {
    624       return;
    625     }
    626 
    627     if (sentRequestMillis == -1) {
    628       if (requestBodyOut instanceof RetryableOutputStream) {
    629         int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
    630         requestHeaders.setContentLength(contentLength);
    631       }
    632       transport.writeRequestHeaders();
    633     }
    634 
    635     if (requestBodyOut != null) {
    636       requestBodyOut.close();
    637       if (requestBodyOut instanceof RetryableOutputStream) {
    638         transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
    639       }
    640     }
    641 
    642     transport.flushRequest();
    643 
    644     responseHeaders = transport.readResponseHeaders();
    645     responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
    646     responseHeaders.setResponseSource(responseSource);
    647 
    648     if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    649       if (cachedResponseHeaders.validate(responseHeaders)) {
    650         release(false);
    651         ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
    652         setResponse(combinedHeaders, cachedResponseBody);
    653         OkResponseCache responseCache = client.getOkResponseCache();
    654         responseCache.trackConditionalCacheHit();
    655         responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
    656         return;
    657       } else {
    658         Util.closeQuietly(cachedResponseBody);
    659       }
    660     }
    661 
    662     if (hasResponseBody()) {
    663       maybeCache(); // reentrant. this calls into user code which may call back into this!
    664     }
    665 
    666     initContentStream(transport.getTransferStream(cacheRequest));
    667   }
    668 
    669   protected TunnelRequest getTunnelConfig() {
    670     return null;
    671   }
    672 
    673   public void receiveHeaders(RawHeaders headers) throws IOException {
    674     CookieHandler cookieHandler = client.getCookieHandler();
    675     if (cookieHandler != null) {
    676       cookieHandler.put(uri, headers.toMultimap(true));
    677     }
    678   }
    679 }
    680