/* * 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 com.squareup.okhttp.internal.http; import com.squareup.okhttp.Address; import com.squareup.okhttp.CertificatePinner; import com.squareup.okhttp.Connection; import com.squareup.okhttp.Headers; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import com.squareup.okhttp.Route; import com.squareup.okhttp.internal.Internal; import com.squareup.okhttp.internal.InternalCache; import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.Version; import java.io.IOException; import java.net.CookieHandler; import java.net.ProtocolException; import java.net.Proxy; import java.util.Date; import java.util.List; import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; 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 com.squareup.okhttp.internal.Util.closeQuietly; import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE; import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT; import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT; 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; /** * Handles a single HTTP request/response pair. Each HTTP engine follows this * lifecycle: *
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.getConnectionPool(), 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(RouteException)}. * @throws IOException if there was a problem while making a request. Sometimes recoverable. See * {@link #recover(IOException)}. * */ 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 (networkRequest != null) { httpStream = connect(); httpStream.setHttpEngine(this); // 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.) if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) { 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); } } } else { if (cacheResponse != null) { // We have a valid cached response. Promote it to the user response immediately. this.userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); } else { // We're forbidden from using the network, and the cache is insufficient. this.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) .build(); } userResponse = unzip(userResponse); } } private HttpStream connect() throws RouteException, RequestException, IOException { boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET"); return streamAllocation.newStream(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(), client.getRetryOnConnectionFailure(), 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(); } /** * Attempt to recover from failure to connect via a route. Returns a new HTTP engine * that should be used for the retry if there are other routes to try, or null if * there are no more routes to try. */ public HttpEngine recover(RouteException e) { if (!streamAllocation.recover(e)) { return null; } if (!client.getRetryOnConnectionFailure()) { return null; } 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); } /** * 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, Sink requestBodyOut) { if (!streamAllocation.recover(e, requestBodyOut)) { return null; } if (!client.getRetryOnConnectionFailure()) { return null; } 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) { return recover(e, requestBodyOut); } 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(stripBody(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.connectionFailed(); } 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", Util.hostHeader(request.httpUrl(), false));
}
if (request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
if (request.header("Accept-Encoding") == null) {
transparentGzip = true;
result.header("Accept-Encoding", "gzip");
}
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
// Capture the request headers added so far so that they can be offered to the CookieHandler.
// This is mostly to stay close to the RI; it is unlikely any of the headers above would
// affect cookie choice besides "Host".
Map