All Downloads are FREE. Search and download functionalities are using the official Maven repository.

okhttp3.internal.http.HttpEngine Maven / Gradle / Ivy

/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package okhttp3.internal.http;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.SocketTimeoutException;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
import okhttp3.Address;
import okhttp3.CertificatePinner;
import okhttp3.Connection;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.Route;
import okhttp3.internal.Internal;
import okhttp3.internal.InternalCache;
import okhttp3.internal.Version;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.GzipSource;
import okio.Okio;
import okio.Sink;
import okio.Source;
import okio.Timeout;

import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static okhttp3.internal.Util.closeQuietly;
import static okhttp3.internal.Util.discard;
import static okhttp3.internal.Util.hostHeader;
import static okhttp3.internal.http.StatusLine.HTTP_CONTINUE;
import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT;
import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT;

/**
 * Handles a single HTTP request/response pair. Each HTTP engine follows this lifecycle: 
  1. It * is created.
  2. The HTTP request message is sent with sendRequest(). Once the request is sent it * is an error to modify the request headers. After sendRequest() has been called the request body * can be written to if it exists.
  3. The HTTP response message is read with readResponse(). After * the response has been read the response headers and body can be read. All responses have a * response body input stream, though in some instances this stream is empty.
* *

The request and response may be served by the HTTP response cache, by the network, or by both * in the event of a conditional GET. */ public final class HttpEngine { /** * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox, * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. */ public static final int MAX_FOLLOW_UPS = 20; private static final ResponseBody EMPTY_BODY = new ResponseBody() { @Override public MediaType contentType() { return null; } @Override public long contentLength() { return 0; } @Override public BufferedSource source() { return new Buffer(); } }; final OkHttpClient client; public final StreamAllocation streamAllocation; private final Response priorResponse; private HttpStream httpStream; /** The time when the request headers were written, or -1 if they haven't been written yet. */ long sentRequestMillis = -1; /** * True if this client added an "Accept-Encoding: gzip" header field and is therefore responsible * for also decompressing the transfer stream. */ private boolean transparentGzip; /** * True if the request body must be completely buffered before transmission; false if it can be * streamed. Buffering has two advantages: we don't need the content-length in advance and we can * retransmit if necessary. The upside of streaming is that we can save memory. */ public final boolean bufferRequestBody; /** * The original application-provided request. Never modified by OkHttp. When follow-up requests * are necessary, they are derived from this request. */ private final Request userRequest; /** * The request to send on the network, or null for no network request. This is derived from the * user request, and customized to support OkHttp features like compression and caching. */ private Request networkRequest; /** * The cached response, or null if the cache doesn't exist or cannot be used for this request. * Conditional caching means this may be non-null even when the network request is non-null. Never * modified by OkHttp. */ private Response cacheResponse; /** * The user-visible response. This is derived from either the network response, cache response, or * both. It is customized to support OkHttp features like compression and caching. */ private Response userResponse; private Sink requestBodyOut; private BufferedSink bufferedRequestBody; private final boolean callerWritesRequestBody; private final boolean forWebSocket; /** The cache request currently being populated from a network response. */ private CacheRequest storeRequest; private CacheStrategy cacheStrategy; /** * @param request the HTTP request without a body. The body must be written via the engine's * request body stream. * @param callerWritesRequestBody true for the {@code HttpURLConnection}-style interaction model * where control flow is returned to the calling application to write the request body before the * response body is readable. */ public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody, boolean callerWritesRequestBody, boolean forWebSocket, StreamAllocation streamAllocation, RetryableSink requestBodyOut, Response priorResponse) { this.client = client; this.userRequest = request; this.bufferRequestBody = bufferRequestBody; this.callerWritesRequestBody = callerWritesRequestBody; this.forWebSocket = forWebSocket; this.streamAllocation = streamAllocation != null ? streamAllocation : new StreamAllocation(client.connectionPool(), createAddress(client, request)); this.requestBodyOut = requestBodyOut; this.priorResponse = priorResponse; } /** * Figures out what the response source will be, and opens a socket to that source if necessary. * Prepares the request headers and gets ready to start writing the request body if it exists. * * @throws RequestException if there was a problem with request setup. Unrecoverable. * @throws RouteException if the was a problem during connection via a specific route. Sometimes * recoverable. See {@link #recover}. * @throws IOException if there was a problem while making a request. Sometimes recoverable. See * {@link #recover(IOException, boolean)}. */ public void sendRequest() throws RequestException, RouteException, IOException { if (cacheStrategy != null) return; // Already sent. if (httpStream != null) throw new IllegalStateException(); Request request = networkRequest(userRequest); InternalCache responseCache = Internal.instance.internalCache(client); Response cacheCandidate = responseCache != null ? responseCache.get(request) : null; long now = System.currentTimeMillis(); cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get(); networkRequest = cacheStrategy.networkRequest; cacheResponse = cacheStrategy.cacheResponse; if (responseCache != null) { responseCache.trackResponse(cacheStrategy); } if (cacheCandidate != null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } // If we're forbidden from using the network and the cache is insufficient, fail. if (networkRequest == null && cacheResponse == null) { userResponse = new Response.Builder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(EMPTY_BODY) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); return; } // If we don't need the network, we're done. if (networkRequest == null) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); userResponse = unzip(userResponse); return; } // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean success = false; try { httpStream = connect(); httpStream.setHttpEngine(this); if (writeRequestHeadersEagerly()) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. httpStream.writeRequestHeaders(networkRequest); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request headers until the // entire body is ready; otherwise we can't set the Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { httpStream.writeRequestHeaders(networkRequest); requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength); } } success = true; } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (!success && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } } /** * If the caller's control flow writes the request body, we need to create that stream * immediately. And that means we need to immediately write the request headers, so we can * start streaming the request body. (We may already have a request body if we're retrying a * failed POST.) */ private boolean writeRequestHeadersEagerly() { return callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null; } private HttpStream connect() throws RouteException, RequestException, IOException { boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET"); return streamAllocation.newStream(client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis(), client.retryOnConnectionFailure(), doExtensiveHealthChecks); } private static Response stripBody(Response response) { return response != null && response.body() != null ? response.newBuilder().body(null).build() : response; } /** * Called immediately before the transport transmits HTTP request headers. This is used to observe * the sent time should the request be cached. */ public void writingRequestHeaders() { if (sentRequestMillis != -1) throw new IllegalStateException(); sentRequestMillis = System.currentTimeMillis(); } boolean permitsRequestBody(Request request) { return HttpMethod.permitsRequestBody(request.method()); } /** Returns the request body or null if this request doesn't have a body. */ public Sink getRequestBody() { if (cacheStrategy == null) throw new IllegalStateException(); return requestBodyOut; } public BufferedSink getBufferedRequestBody() { BufferedSink result = bufferedRequestBody; if (result != null) return result; Sink requestBody = getRequestBody(); return requestBody != null ? (bufferedRequestBody = Okio.buffer(requestBody)) : null; } public boolean hasResponse() { return userResponse != null; } public Request getRequest() { return userRequest; } /** Returns the engine's response. */ // TODO: the returned body will always be null. public Response getResponse() { if (userResponse == null) throw new IllegalStateException(); return userResponse; } public Connection getConnection() { return streamAllocation.connection(); } /** * Report and attempt to recover from a failure to communicate with a server. Returns a new HTTP * engine that should be used for the retry if {@code e} is recoverable, or null if the failure is * permanent. Requests with a body can only be recovered if the body is buffered. */ public HttpEngine recover(IOException e, boolean routeException, Sink requestBodyOut) { streamAllocation.streamFailed(e); if (!client.retryOnConnectionFailure()) { return null; // The application layer has forbidden retries. } if (requestBodyOut != null && !(requestBodyOut instanceof RetryableSink)) { return null; // The body on this request cannot be retried. } if (!isRecoverable(e, routeException)) { return null; // This exception is fatal. } if (!streamAllocation.hasMoreRoutes()) { return null; // No more routes to attempt. } StreamAllocation streamAllocation = close(); // For failure recovery, use the same route selector with a new connection. return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody, forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse); } public HttpEngine recover(IOException e, boolean routeException) { return recover(e, routeException, requestBodyOut); } private boolean isRecoverable(IOException e, boolean routeException) { // If there was a protocol problem, don't recover. if (e instanceof ProtocolException) { return false; } // If there was an interruption don't recover, but if there was a timeout connecting to a route // we should try the next route (if there is one). if (e instanceof InterruptedIOException) { return e instanceof SocketTimeoutException && routeException; } // Look for known client-side or negotiation errors that are unlikely to be fixed by trying // again with a different route. if (e instanceof SSLHandshakeException) { // If the problem was a CertificateException from the X509TrustManager, // do not retry. if (e.getCause() instanceof CertificateException) { return false; } } if (e instanceof SSLPeerUnverifiedException) { // e.g. a certificate pinning error. return false; } // An example of one we might want to retry with a different route is a problem connecting to a // proxy and would manifest as a standard IOException. Unless it is one we know we should not // retry, we return true and try a new route. return true; } private void maybeCache() throws IOException { InternalCache responseCache = Internal.instance.internalCache(client); if (responseCache == null) return; // Should we cache this response for this request? if (!CacheStrategy.isCacheable(userResponse, networkRequest)) { if (HttpMethod.invalidatesCache(networkRequest.method())) { try { responseCache.remove(networkRequest); } catch (IOException ignored) { // The cache cannot be written. } } return; } // Offer this request to the cache. storeRequest = responseCache.put(userResponse); } /** * Configure the socket connection to be either pooled or closed when it is either exhausted or * closed. If it is unneeded when this is called, it will be released immediately. */ public void releaseStreamAllocation() throws IOException { streamAllocation.release(); } /** * Immediately closes the socket connection if it's currently held by this engine. Use this to * interrupt an in-flight request from any thread. It's the caller's responsibility to close the * request body and response body streams; otherwise resources may be leaked. * *

This method is safe to be called concurrently, but provides limited guarantees. If a * transport layer connection has been established (such as a HTTP/2 stream) that is terminated. * Otherwise if a socket connection is being established, that is terminated. */ public void cancel() { streamAllocation.cancel(); } /** * Release any resources held by this engine. Returns the stream allocation held by this engine, * which itself must be used or released. */ public StreamAllocation close() { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. closeQuietly(bufferedRequestBody); } else if (requestBodyOut != null) { closeQuietly(requestBodyOut); } if (userResponse != null) { closeQuietly(userResponse.body()); } else { // If this engine never achieved a response body, its stream allocation is dead. streamAllocation.streamFailed(null); } return streamAllocation; } /** * Returns a new response that does gzip decompression on {@code response}, if transparent gzip * was both offered by OkHttp and used by the origin server. * *

In addition to decompression, this will also strip the corresponding headers. We strip the * Content-Encoding header to prevent the application from attempting to double decompress. We * strip the Content-Length header because it is the length of the compressed content, but the * application is only interested in the length of the uncompressed content. * *

This method should only be used for non-empty response bodies. Response codes like "304 Not * Modified" can include "Content-Encoding: gzip" without a response body and we will crash if we * attempt to decompress the zero-byte source. */ private Response unzip(final Response response) throws IOException { if (!transparentGzip || !"gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) { return response; } if (response.body() == null) { return response; } GzipSource responseBody = new GzipSource(response.body().source()); Headers strippedHeaders = response.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); return response.newBuilder() .headers(strippedHeaders) .body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody))) .build(); } /** * Returns true if the response must have a (possibly 0-length) body. See RFC 2616 section 4.3. */ public static boolean hasBody(Response response) { // HEAD requests never yield a body regardless of the response headers. if (response.request().method().equals("HEAD")) { return false; } int responseCode = response.code(); if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode != HTTP_NO_CONTENT && responseCode != HTTP_NOT_MODIFIED) { return true; } // If the Content-Length or Transfer-Encoding headers disagree with the // response code, the response is malformed. For best compatibility, we // honor the headers. if (OkHeaders.contentLength(response) != -1 || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) { return true; } return false; } /** * Populates request with defaults and cookies. * *

This client doesn't specify a default {@code Accept} header because it doesn't know what * content types the application is interested in. */ private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder(); if (request.header("Host") == null) { result.header("Host", hostHeader(request.url(), false)); } if (request.header("Connection") == null) { result.header("Connection", "Keep-Alive"); } if (request.header("Accept-Encoding") == null) { transparentGzip = true; result.header("Accept-Encoding", "gzip"); } List cookies = client.cookieJar().loadForRequest(request.url()); if (!cookies.isEmpty()) { result.header("Cookie", cookieHeader(cookies)); } if (request.header("User-Agent") == null) { result.header("User-Agent", Version.userAgent()); } return result.build(); } /** Returns a 'Cookie' HTTP request header with all cookies, like {@code a=b; c=d}. */ private String cookieHeader(List cookies) { StringBuilder cookieHeader = new StringBuilder(); for (int i = 0, size = cookies.size(); i < size; i++) { if (i > 0) { cookieHeader.append("; "); } Cookie cookie = cookies.get(i); cookieHeader.append(cookie.name()).append('=').append(cookie.value()); } return cookieHeader.toString(); } /** * Flushes the remaining request header and body, parses the HTTP response headers and starts * reading the HTTP response body if it exists. */ public void readResponse() throws IOException { if (userResponse != null) { return; // Already ready. } if (networkRequest == null && cacheResponse == null) { throw new IllegalStateException("call sendRequest() first!"); } if (networkRequest == null) { return; // No network response to read. } Response networkResponse; if (forWebSocket) { httpStream.writeRequestHeaders(networkRequest); networkResponse = readNetworkResponse(); } else if (!callerWritesRequestBody) { networkResponse = new NetworkInterceptorChain(0, networkRequest, streamAllocation.connection()).proceed(networkRequest); } else { // Emit the request body's buffer so that everything is in requestBodyOut. if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) { bufferedRequestBody.emit(); } // Emit the request headers if we haven't yet. We might have just learned the Content-Length. if (sentRequestMillis == -1) { if (OkHeaders.contentLength(networkRequest) == -1 && requestBodyOut instanceof RetryableSink) { long contentLength = ((RetryableSink) requestBodyOut).contentLength(); networkRequest = networkRequest.newBuilder() .header("Content-Length", Long.toString(contentLength)) .build(); } httpStream.writeRequestHeaders(networkRequest); } // Write the request body to the socket. if (requestBodyOut != null) { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. bufferedRequestBody.close(); } else { requestBodyOut.close(); } if (requestBodyOut instanceof RetryableSink) { httpStream.writeRequestBody((RetryableSink) requestBodyOut); } } networkResponse = readNetworkResponse(); } receiveHeaders(networkResponse.headers()); // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (validate(cacheResponse, networkResponse)) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .headers(combine(cacheResponse.headers(), networkResponse.headers())) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); releaseStreamAllocation(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). InternalCache responseCache = Internal.instance.internalCache(client); responseCache.trackConditionalCacheHit(); responseCache.update(cacheResponse, userResponse); userResponse = unzip(userResponse); return; } else { closeQuietly(cacheResponse.body()); } } userResponse = networkResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (hasBody(userResponse)) { maybeCache(); userResponse = unzip(cacheWritingResponse(storeRequest, userResponse)); } } class NetworkInterceptorChain implements Interceptor.Chain { private final int index; private final Request request; private final Connection connection; private int calls; NetworkInterceptorChain(int index, Request request, Connection connection) { this.index = index; this.request = request; this.connection = connection; } @Override public Connection connection() { return connection; } @Override public Request request() { return request; } @Override public Response proceed(Request request) throws IOException { calls++; if (index > 0) { Interceptor caller = client.networkInterceptors().get(index - 1); Address address = connection().route().address(); // Confirm that the interceptor uses the connection we've already prepared. if (!request.url().host().equals(address.url().host()) || request.url().port() != address.url().port()) { throw new IllegalStateException("network interceptor " + caller + " must retain the same host and port"); } // Confirm that this is the interceptor's first call to chain.proceed(). if (calls > 1) { throw new IllegalStateException("network interceptor " + caller + " must call proceed() exactly once"); } } if (index < client.networkInterceptors().size()) { // There's another interceptor in the chain. Call that. NetworkInterceptorChain chain = new NetworkInterceptorChain(index + 1, request, connection); Interceptor interceptor = client.networkInterceptors().get(index); Response interceptedResponse = interceptor.intercept(chain); // Confirm that the interceptor made the required call to chain.proceed(). if (chain.calls != 1) { throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once"); } if (interceptedResponse == null) { throw new NullPointerException("network interceptor " + interceptor + " returned null"); } return interceptedResponse; } httpStream.writeRequestHeaders(request); //Update the networkRequest with the possibly updated interceptor request. networkRequest = request; if (permitsRequestBody(request) && request.body() != null) { Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength()); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); } Response response = readNetworkResponse(); int code = response.code(); if ((code == 204 || code == 205) && response.body().contentLength() > 0) { throw new ProtocolException( "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength()); } return response; } } private Response readNetworkResponse() throws IOException { httpStream.finishRequest(); Response networkResponse = httpStream.readResponseHeaders() .request(networkRequest) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); if (!forWebSocket || networkResponse.code() != 101) { networkResponse = networkResponse.newBuilder() .body(httpStream.openResponseBody(networkResponse)) .build(); } if ("close".equalsIgnoreCase(networkResponse.request().header("Connection")) || "close".equalsIgnoreCase(networkResponse.header("Connection"))) { streamAllocation.noNewStreams(); } return networkResponse; } /** * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we * may never exhaust the source stream and therefore not complete the cached response. */ private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response) throws IOException { // Some apps return a null body; for compatibility we treat that like a null cache request. if (cacheRequest == null) return response; Sink cacheBodyUnbuffered = cacheRequest.body(); if (cacheBodyUnbuffered == null) return response; final BufferedSource source = response.body().source(); final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered); Source cacheWritingSource = new Source() { boolean cacheRequestClosed; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead; try { bytesRead = source.read(sink, byteCount); } catch (IOException e) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheRequest.abort(); // Failed to write a complete cache response. } throw e; } if (bytesRead == -1) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheBody.close(); // The cache response is complete! } return -1; } sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead); cacheBody.emitCompleteSegments(); return bytesRead; } @Override public Timeout timeout() { return source.timeout(); } @Override public void close() throws IOException { if (!cacheRequestClosed && !discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) { cacheRequestClosed = true; cacheRequest.abort(); } source.close(); } }; return response.newBuilder() .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource))) .build(); } /** * Returns true if {@code cached} should be used; false if {@code network} response should be * used. */ private static boolean validate(Response cached, Response network) { if (network.code() == HTTP_NOT_MODIFIED) { return true; } // The HTTP spec says that if the network's response is older than our // cached response, we may return the cache's response. Like Chrome (but // unlike Firefox), this client prefers to return the newer response. Date lastModified = cached.headers().getDate("Last-Modified"); if (lastModified != null) { Date networkLastModified = network.headers().getDate("Last-Modified"); if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) { return true; } } return false; } /** * Combines cached headers with a network headers as defined by RFC 2616, 13.5.3. */ private static Headers combine(Headers cachedHeaders, Headers networkHeaders) throws IOException { Headers.Builder result = new Headers.Builder(); for (int i = 0, size = cachedHeaders.size(); i < size; i++) { String fieldName = cachedHeaders.name(i); String value = cachedHeaders.value(i); if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) { continue; // Drop 100-level freshness warnings. } if (!OkHeaders.isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) { Internal.instance.addLenient(result, fieldName, value); } } for (int i = 0, size = networkHeaders.size(); i < size; i++) { String fieldName = networkHeaders.name(i); if ("Content-Length".equalsIgnoreCase(fieldName)) { continue; // Ignore content-length headers of validating responses. } if (OkHeaders.isEndToEnd(fieldName)) { Internal.instance.addLenient(result, fieldName, networkHeaders.value(i)); } } return result.build(); } public void receiveHeaders(Headers headers) throws IOException { if (client.cookieJar() == CookieJar.NO_COOKIES) return; List cookies = Cookie.parseAll(userRequest.url(), headers); if (cookies.isEmpty()) return; client.cookieJar().saveFromResponse(userRequest.url(), cookies); } /** * Figures out the HTTP request to make in response to receiving this engine's response. This will * either add authentication headers, follow redirects or handle a client request timeout. If a * follow-up is either unnecessary or not applicable, this returns null. */ public Request followUpRequest() throws IOException { if (userResponse == null) throw new IllegalStateException(); Connection connection = streamAllocation.connection(); Route route = connection != null ? connection.route() : null; int responseCode = userResponse.code(); final String method = userRequest.method(); switch (responseCode) { case HTTP_PROXY_AUTH: Proxy selectedProxy = route != null ? route.proxy() : client.proxy(); if (selectedProxy.type() != Proxy.Type.HTTP) { throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); } return client.proxyAuthenticator().authenticate(route, userResponse); case HTTP_UNAUTHORIZED: return client.authenticator().authenticate(route, userResponse); case HTTP_PERM_REDIRECT: case HTTP_TEMP_REDIRECT: // "If the 307 or 308 status code is received in response to a request other than GET // or HEAD, the user agent MUST NOT automatically redirect the request" if (!method.equals("GET") && !method.equals("HEAD")) { return null; } // fall-through case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: // Does the client allow redirects? if (!client.followRedirects()) return null; String location = userResponse.header("Location"); if (location == null) return null; HttpUrl url = userRequest.url().resolve(location); // Don't follow redirects to unsupported protocols. if (url == null) return null; // If configured, don't follow redirects between SSL and non-SSL. boolean sameScheme = url.scheme().equals(userRequest.url().scheme()); if (!sameScheme && !client.followSslRedirects()) return null; // Redirects don't include a request body. Request.Builder requestBuilder = userRequest.newBuilder(); if (HttpMethod.permitsRequestBody(method)) { if (HttpMethod.redirectsToGet(method)) { requestBuilder.method("GET", null); } else { requestBuilder.method(method, null); } requestBuilder.removeHeader("Transfer-Encoding"); requestBuilder.removeHeader("Content-Length"); requestBuilder.removeHeader("Content-Type"); } // When redirecting across hosts, drop all authentication headers. This // is potentially annoying to the application layer since they have no // way to retain them. if (!sameConnection(url)) { requestBuilder.removeHeader("Authorization"); } return requestBuilder.url(url).build(); case HTTP_CLIENT_TIMEOUT: // 408's are rare in practice, but some servers like HAProxy use this response code. The // spec says that we may repeat the request without modifications. Modern browsers also // repeat the request (even non-idempotent ones.) boolean retryableBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; if (callerWritesRequestBody && !retryableBody) { return null; } return userRequest; default: return null; } } /** * Returns true if an HTTP request for {@code followUp} can reuse the connection used by this * engine. */ public boolean sameConnection(HttpUrl followUp) { HttpUrl url = userRequest.url(); return url.host().equals(followUp.host()) && url.port() == followUp.port() && url.scheme().equals(followUp.scheme()); } private static Address createAddress(OkHttpClient client, Request request) { SSLSocketFactory sslSocketFactory = null; HostnameVerifier hostnameVerifier = null; CertificatePinner certificatePinner = null; if (request.isHttps()) { sslSocketFactory = client.sslSocketFactory(); hostnameVerifier = client.hostnameVerifier(); certificatePinner = client.certificatePinner(); } return new Address(request.url().host(), request.url().port(), client.dns(), client.socketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(), client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy