| /* |
| * Copyright (C) 2014 Square, Inc. |
| * |
| * Licensed 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; |
| |
| import com.squareup.okhttp.internal.NamedRunnable; |
| import com.squareup.okhttp.internal.http.HttpEngine; |
| import com.squareup.okhttp.internal.http.RequestException; |
| import com.squareup.okhttp.internal.http.RouteException; |
| import com.squareup.okhttp.internal.http.StreamAllocation; |
| import java.io.IOException; |
| import java.net.ProtocolException; |
| import java.util.logging.Level; |
| |
| import static com.squareup.okhttp.internal.Internal.logger; |
| import static com.squareup.okhttp.internal.http.HttpEngine.MAX_FOLLOW_UPS; |
| |
| /** |
| * A call is a request that has been prepared for execution. A call can be |
| * canceled. As this object represents a single request/response pair (stream), |
| * it cannot be executed twice. |
| */ |
| public class Call { |
| private final OkHttpClient client; |
| |
| // Guarded by this. |
| private boolean executed; |
| volatile boolean canceled; |
| |
| /** The application's original request unadulterated by redirects or auth headers. */ |
| Request originalRequest; |
| HttpEngine engine; |
| |
| protected Call(OkHttpClient client, Request originalRequest) { |
| // Copy the client. Otherwise changes (socket factory, redirect policy, |
| // etc.) may incorrectly be reflected in the request when it is executed. |
| this.client = client.copyWithDefaults(); |
| this.originalRequest = originalRequest; |
| } |
| |
| /** |
| * Invokes the request immediately, and blocks until the response can be |
| * processed or is in error. |
| * |
| * <p>The caller may read the response body with the response's |
| * {@link Response#body} method. To facilitate connection recycling, callers |
| * should always {@link ResponseBody#close() close the response body}. |
| * |
| * <p>Note that transport-layer success (receiving a HTTP response code, |
| * headers and body) does not necessarily indicate application-layer success: |
| * {@code response} may still indicate an unhappy HTTP response code like 404 |
| * or 500. |
| * |
| * @throws IOException if the request could not be executed due to |
| * cancellation, a connectivity problem or timeout. Because networks can |
| * fail during an exchange, it is possible that the remote server |
| * accepted the request before the failure. |
| * |
| * @throws IllegalStateException when the call has already been executed. |
| */ |
| public Response execute() throws IOException { |
| synchronized (this) { |
| if (executed) throw new IllegalStateException("Already Executed"); |
| executed = true; |
| } |
| try { |
| client.getDispatcher().executed(this); |
| Response result = getResponseWithInterceptorChain(false); |
| if (result == null) throw new IOException("Canceled"); |
| return result; |
| } finally { |
| client.getDispatcher().finished(this); |
| } |
| } |
| |
| Object tag() { |
| return originalRequest.tag(); |
| } |
| |
| /** |
| * Schedules the request to be executed at some point in the future. |
| * |
| * <p>The {@link OkHttpClient#getDispatcher dispatcher} defines when the |
| * request will run: usually immediately unless there are several other |
| * requests currently being executed. |
| * |
| * <p>This client will later call back {@code responseCallback} with either |
| * an HTTP response or a failure exception. If you {@link #cancel} a request |
| * before it completes the callback will not be invoked. |
| * |
| * @throws IllegalStateException when the call has already been executed. |
| */ |
| public void enqueue(Callback responseCallback) { |
| enqueue(responseCallback, false); |
| } |
| |
| void enqueue(Callback responseCallback, boolean forWebSocket) { |
| synchronized (this) { |
| if (executed) throw new IllegalStateException("Already Executed"); |
| executed = true; |
| } |
| client.getDispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket)); |
| } |
| |
| /** |
| * Cancels the request, if possible. Requests that are already complete |
| * cannot be canceled. |
| */ |
| public void cancel() { |
| canceled = true; |
| if (engine != null) engine.cancel(); |
| } |
| |
| /** |
| * Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain |
| * #enqueue(Callback) enqueued}. It is an error to execute a call more than once. |
| */ |
| public synchronized boolean isExecuted() { |
| return executed; |
| } |
| |
| public boolean isCanceled() { |
| return canceled; |
| } |
| |
| final class AsyncCall extends NamedRunnable { |
| private final Callback responseCallback; |
| private final boolean forWebSocket; |
| |
| private AsyncCall(Callback responseCallback, boolean forWebSocket) { |
| super("OkHttp %s", originalRequest.urlString()); |
| this.responseCallback = responseCallback; |
| this.forWebSocket = forWebSocket; |
| } |
| |
| String host() { |
| return originalRequest.httpUrl().host(); |
| } |
| |
| Request request() { |
| return originalRequest; |
| } |
| |
| Object tag() { |
| return originalRequest.tag(); |
| } |
| |
| void cancel() { |
| Call.this.cancel(); |
| } |
| |
| Call get() { |
| return Call.this; |
| } |
| |
| @Override protected void execute() { |
| boolean signalledCallback = false; |
| try { |
| Response response = getResponseWithInterceptorChain(forWebSocket); |
| if (canceled) { |
| signalledCallback = true; |
| responseCallback.onFailure(originalRequest, new IOException("Canceled")); |
| } else { |
| signalledCallback = true; |
| responseCallback.onResponse(response); |
| } |
| } catch (IOException e) { |
| if (signalledCallback) { |
| // Do not signal the callback twice! |
| logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e); |
| } else { |
| Request request = engine == null ? originalRequest : engine.getRequest(); |
| responseCallback.onFailure(request, e); |
| } |
| } finally { |
| client.getDispatcher().finished(this); |
| } |
| } |
| } |
| |
| /** |
| * Returns a string that describes this call. Doesn't include a full URL as that might contain |
| * sensitive information. |
| */ |
| private String toLoggableString() { |
| String string = canceled ? "canceled call" : "call"; |
| HttpUrl redactedUrl = originalRequest.httpUrl().resolve("/..."); |
| return string + " to " + redactedUrl; |
| } |
| |
| private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { |
| Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket); |
| return chain.proceed(originalRequest); |
| } |
| |
| class ApplicationInterceptorChain implements Interceptor.Chain { |
| private final int index; |
| private final Request request; |
| private final boolean forWebSocket; |
| |
| ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) { |
| this.index = index; |
| this.request = request; |
| this.forWebSocket = forWebSocket; |
| } |
| |
| @Override public Connection connection() { |
| return null; |
| } |
| |
| @Override public Request request() { |
| return request; |
| } |
| |
| @Override public Response proceed(Request request) throws IOException { |
| // If there's another interceptor in the chain, call that. |
| if (index < client.interceptors().size()) { |
| Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket); |
| Interceptor interceptor = client.interceptors().get(index); |
| Response interceptedResponse = interceptor.intercept(chain); |
| |
| if (interceptedResponse == null) { |
| throw new NullPointerException("application interceptor " + interceptor |
| + " returned null"); |
| } |
| |
| return interceptedResponse; |
| } |
| |
| // No more interceptors. Do HTTP. |
| return getResponse(request, forWebSocket); |
| } |
| } |
| |
| /** |
| * Performs the request and returns the response. May return null if this |
| * call was canceled. |
| */ |
| Response getResponse(Request request, boolean forWebSocket) throws IOException { |
| // Copy body metadata to the appropriate request headers. |
| RequestBody body = request.body(); |
| if (body != null) { |
| Request.Builder requestBuilder = request.newBuilder(); |
| |
| MediaType contentType = body.contentType(); |
| if (contentType != null) { |
| requestBuilder.header("Content-Type", contentType.toString()); |
| } |
| |
| long contentLength = body.contentLength(); |
| if (contentLength != -1) { |
| requestBuilder.header("Content-Length", Long.toString(contentLength)); |
| requestBuilder.removeHeader("Transfer-Encoding"); |
| } else { |
| requestBuilder.header("Transfer-Encoding", "chunked"); |
| requestBuilder.removeHeader("Content-Length"); |
| } |
| |
| request = requestBuilder.build(); |
| } |
| |
| // Create the initial HTTP engine. Retries and redirects need new engine for each attempt. |
| engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null); |
| |
| int followUpCount = 0; |
| while (true) { |
| if (canceled) { |
| engine.releaseStreamAllocation(); |
| throw new IOException("Canceled"); |
| } |
| |
| boolean releaseConnection = true; |
| try { |
| engine.sendRequest(); |
| engine.readResponse(); |
| releaseConnection = false; |
| } catch (RequestException e) { |
| // The attempt to interpret the request failed. Give up. |
| throw e.getCause(); |
| } catch (RouteException e) { |
| // The attempt to connect via a route failed. The request will not have been sent. |
| HttpEngine retryEngine = engine.recover(e); |
| if (retryEngine != null) { |
| releaseConnection = false; |
| engine = retryEngine; |
| continue; |
| } |
| // Give up; recovery is not possible. |
| throw e.getLastConnectException(); |
| } catch (IOException e) { |
| // An attempt to communicate with a server failed. The request may have been sent. |
| HttpEngine retryEngine = engine.recover(e, null); |
| if (retryEngine != null) { |
| releaseConnection = false; |
| engine = retryEngine; |
| continue; |
| } |
| |
| // Give up; recovery is not possible. |
| throw e; |
| } finally { |
| // We're throwing an unchecked exception. Release any resources. |
| if (releaseConnection) { |
| StreamAllocation streamAllocation = engine.close(); |
| streamAllocation.release(); |
| } |
| } |
| |
| Response response = engine.getResponse(); |
| Request followUp = engine.followUpRequest(); |
| |
| if (followUp == null) { |
| if (!forWebSocket) { |
| engine.releaseStreamAllocation(); |
| } |
| return response; |
| } |
| |
| StreamAllocation streamAllocation = engine.close(); |
| |
| if (++followUpCount > MAX_FOLLOW_UPS) { |
| streamAllocation.release(); |
| throw new ProtocolException("Too many follow-up requests: " + followUpCount); |
| } |
| |
| if (!engine.sameConnection(followUp.httpUrl())) { |
| streamAllocation.release(); |
| streamAllocation = null; |
| } |
| |
| request = followUp; |
| engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null, |
| response); |
| } |
| } |
| } |