diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java index 2a99b9bfe..5f3e8d99f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java +++ b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java @@ -23,7 +23,6 @@ import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; -import com.squareup.okhttp.internal.http.OkResponseCache; import com.squareup.okhttp.internal.http.RawHeaders; import com.squareup.okhttp.internal.http.ResponseHeaders; import java.io.BufferedWriter; diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index 921b61aaa..a86123d7e 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -16,10 +16,10 @@ package com.squareup.okhttp; import com.squareup.okhttp.internal.Util; +import com.squareup.okhttp.internal.http.Dispatcher; import com.squareup.okhttp.internal.http.HttpAuthenticator; import com.squareup.okhttp.internal.http.HttpURLConnectionImpl; import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl; -import com.squareup.okhttp.internal.http.OkResponseCache; import com.squareup.okhttp.internal.http.OkResponseCacheAdapter; import com.squareup.okhttp.internal.tls.OkHostnameVerifier; import java.net.CookieHandler; @@ -32,10 +32,7 @@ import java.net.URLConnection; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -46,9 +43,10 @@ public final class OkHttpClient implements URLStreamHandlerFactory { private static final List DEFAULT_TRANSPORTS = Util.immutableList(Arrays.asList("spdy/3", "http/1.1")); + private final RouteDatabase routeDatabase; + private final Dispatcher dispatcher; private Proxy proxy; private List transports; - private final Set failedRoutes; private ProxySelector proxySelector; private CookieHandler cookieHandler; private ResponseCache responseCache; @@ -59,14 +57,15 @@ public final class OkHttpClient implements URLStreamHandlerFactory { private boolean followProtocolRedirects = true; private int connectTimeout; private int readTimeout; - private Dispatcher dispatcher = new Dispatcher(); public OkHttpClient() { - this.failedRoutes = Collections.synchronizedSet(new LinkedHashSet()); + routeDatabase = new RouteDatabase(); + dispatcher = new Dispatcher(); } private OkHttpClient(OkHttpClient copyFrom) { - this.failedRoutes = copyFrom.failedRoutes; // Avoid allocating an unnecessary LinkedHashSet. + routeDatabase = copyFrom.routeDatabase; + dispatcher = copyFrom.dispatcher; } /** @@ -109,7 +108,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory { if (millis > Integer.MAX_VALUE) { throw new IllegalArgumentException("Timeout too large."); } - connectTimeout = (int) millis; + readTimeout = (int) millis; } /** Default read timeout (in milliseconds). */ @@ -181,7 +180,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory { return responseCache; } - private OkResponseCache okResponseCache() { + public OkResponseCache getOkResponseCache() { if (responseCache instanceof HttpResponseCache) { return ((HttpResponseCache) responseCache).okResponseCache; } else if (responseCache != null) { @@ -269,6 +268,10 @@ public final class OkHttpClient implements URLStreamHandlerFactory { return followProtocolRedirects; } + public RouteDatabase getRoutesDatabase() { + return routeDatabase; + } + /** * Configure the transports used by this client to communicate with remote * servers. By default this client will prefer the most efficient transport @@ -304,6 +307,9 @@ public final class OkHttpClient implements URLStreamHandlerFactory { if (transports.contains(null)) { throw new IllegalArgumentException("transports must not contain null"); } + if (transports.contains("")) { + throw new IllegalArgumentException("transports contains an empty string"); + } this.transports = transports; return this; } @@ -331,15 +337,7 @@ public final class OkHttpClient implements URLStreamHandlerFactory { } public HttpURLConnection open(URL url) { - String protocol = url.getProtocol(); - OkHttpClient copy = copyWithDefaults(); - if (protocol.equals("http")) { - return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); - } else if (protocol.equals("https")) { - return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); - } else { - throw new IllegalArgumentException("Unexpected protocol: " + protocol); - } + return open(url, proxy); } HttpURLConnection open(URL url, Proxy proxy) { @@ -347,18 +345,9 @@ public final class OkHttpClient implements URLStreamHandlerFactory { OkHttpClient copy = copyWithDefaults(); copy.proxy = proxy; - HttpURLConnection connection; - if (protocol.equals("http")) { - connection = new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); - } else if (protocol.equals("https")) { - connection = new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes); - } else { - throw new IllegalArgumentException("Unexpected protocol: " + protocol); - } - - connection.setReadTimeout(readTimeout); - connection.setConnectTimeout(connectTimeout); - return connection; + if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy); + if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy); + throw new IllegalArgumentException("Unexpected protocol: " + protocol); } /** diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java similarity index 87% rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java rename to okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java index 97b1ea547..ffe6f54b1 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCache.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.squareup.okhttp.internal.http; +package com.squareup.okhttp; -import com.squareup.okhttp.ResponseSource; import java.io.IOException; import java.net.CacheRequest; import java.net.CacheResponse; @@ -29,9 +28,8 @@ import java.util.Map; * An extended response cache API. Unlike {@link java.net.ResponseCache}, this * interface supports conditional caching and statistics. * - *

Along with the rest of the {@code internal} package, this is not a public - * API. Applications wishing to supply their own caches must use the more - * limited {@link java.net.ResponseCache} interface. + *

Warning: Experimental OkHttp 2.0 API

+ * This class is in beta. APIs are subject to change! */ public interface OkResponseCache { CacheResponse get(URI uri, String requestMethod, Map> requestHeaders) diff --git a/okhttp/src/main/java/com/squareup/okhttp/Route.java b/okhttp/src/main/java/com/squareup/okhttp/Route.java index 6968c6046..4b8786d22 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Route.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Route.java @@ -59,13 +59,13 @@ public class Route { return inetSocketAddress; } - /** Returns true if this route uses modern tls. */ + /** Returns true if this route uses modern TLS. */ public boolean isModernTls() { return modernTls; } - /** Returns a copy of this route with flipped tls mode. */ - public Route flipTlsMode() { + /** Returns a copy of this route with flipped TLS mode. */ + Route flipTlsMode() { return new Route(address, proxy, inetSocketAddress, !modernTls); } diff --git a/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java b/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java new file mode 100644 index 000000000..9cbeaa73f --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 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 java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.net.ssl.SSLHandshakeException; + +/** + * A blacklist of failed routes to avoid when creating a new connection to a + * target address. This is used so that OkHttp can learn from its mistakes: if + * there was a failure attempting to connect to a specific IP address, proxy + * server or TLS mode, that failure is remembered and alternate routes are + * preferred. + */ +public final class RouteDatabase { + private final Set failedRoutes = new LinkedHashSet(); + + /** Records a failure connecting to {@code failedRoute}. */ + public synchronized void failed(Route failedRoute, IOException failure) { + failedRoutes.add(failedRoute); + + if (!(failure instanceof SSLHandshakeException)) { + // If the problem was not related to SSL then it will also fail with + // a different TLS mode therefore we can be proactive about it. + failedRoutes.add(failedRoute.flipTlsMode()); + } + } + + /** Records success connecting to {@code failedRoute}. */ + public synchronized void connected(Route route) { + failedRoutes.remove(route); + } + + /** Returns true if {@code route} has failed recently and should be avoided. */ + public synchronized boolean shouldPostpone(Route route) { + return failedRoutes.contains(route); + } + + public synchronized int failedRoutesCount() { + return failedRoutes.size(); + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java similarity index 51% rename from okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java index e9499a56c..d5c5006ee 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Dispatcher.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.squareup.okhttp; +package com.squareup.okhttp.internal.http; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -26,7 +28,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -class Dispatcher { +public final class Dispatcher { // TODO: thread pool size should be configurable; possibly configurable per host. private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( 8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); @@ -34,7 +36,7 @@ class Dispatcher { public synchronized void enqueue( HttpURLConnection connection, Request request, Response.Receiver responseReceiver) { - Job job = new Job(connection, request, responseReceiver); + Job job = new Job(this, connection, request, responseReceiver); List jobsForTag = enqueuedJobs.get(request.tag()); if (jobsForTag == null) { jobsForTag = new ArrayList(2); @@ -52,75 +54,11 @@ class Dispatcher { } } - private synchronized void finished(Job job) { + synchronized void finished(Job job) { List jobs = enqueuedJobs.get(job.request.tag()); if (jobs != null) jobs.remove(job); } - public class Job implements Runnable { - private final HttpURLConnection connection; - private final Request request; - private final Response.Receiver responseReceiver; - - public Job(HttpURLConnection connection, Request request, Response.Receiver responseReceiver) { - this.connection = connection; - this.request = request; - this.responseReceiver = responseReceiver; - } - - @Override public void run() { - try { - sendRequest(); - Response response = readResponse(); - responseReceiver.onResponse(response); - } catch (IOException e) { - responseReceiver.onFailure(new Failure.Builder() - .request(request) - .exception(e) - .build()); - } finally { - connection.disconnect(); - finished(this); - } - } - - private HttpURLConnection sendRequest() - throws IOException { - for (int i = 0; i < request.headerCount(); i++) { - connection.addRequestProperty(request.headerName(i), request.headerValue(i)); - } - Request.Body body = request.body(); - if (body != null) { - connection.setDoOutput(true); - long contentLength = body.contentLength(); - if (contentLength == -1 || contentLength > Integer.MAX_VALUE) { - connection.setChunkedStreamingMode(0); - } else { - // Don't call setFixedLengthStreamingMode(long); that's only available on Java 1.7+. - connection.setFixedLengthStreamingMode((int) contentLength); - } - body.writeTo(connection.getOutputStream()); - } - return connection; - } - - private Response readResponse() throws IOException { - int responseCode = connection.getResponseCode(); - Response.Builder responseBuilder = new Response.Builder(request, responseCode); - - for (int i = 0; true; i++) { - String name = connection.getHeaderFieldKey(i); - if (name == null) break; - String value = connection.getHeaderField(i); - responseBuilder.addHeader(name, value); - } - - responseBuilder.body(new RealResponseBody(connection, connection.getInputStream())); - // TODO: set redirectedBy - return responseBuilder.build(); - } - } - static class RealResponseBody extends Response.Body { private final HttpURLConnection connection; private final InputStream in; diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 47cba5957..e2eca75cb 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -19,6 +19,8 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.OkResponseCache; import com.squareup.okhttp.ResponseSource; import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.internal.Dns; @@ -87,6 +89,7 @@ public class HttpEngine { public static final int HTTP_CONTINUE = 100; protected final HttpURLConnectionImpl policy; + protected final OkHttpClient client; protected final String method; @@ -147,6 +150,7 @@ public class HttpEngine { public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, Connection connection, RetryableOutputStream requestBodyOut) throws IOException { this.policy = policy; + this.client = policy.client; this.method = method; this.connection = connection; this.requestBodyOut = requestBodyOut; @@ -176,8 +180,9 @@ public class HttpEngine { prepareRawRequestHeaders(); initResponseSource(); - if (policy.responseCache != null) { - policy.responseCache.trackResponse(responseSource); + OkResponseCache responseCache = client.getOkResponseCache(); + if (responseCache != null) { + responseCache.trackResponse(responseSource); } // The raw response source may require the network, but the request @@ -197,7 +202,7 @@ public class HttpEngine { if (responseSource.requiresConnection()) { sendSocketRequest(); } else if (connection != null) { - policy.connectionPool.recycle(connection); + client.getConnectionPool().recycle(connection); connection = null; } } @@ -208,15 +213,14 @@ public class HttpEngine { */ private void initResponseSource() throws IOException { responseSource = ResponseSource.NETWORK; - if (!policy.getUseCaches() || policy.responseCache == null) { - return; - } + if (!policy.getUseCaches()) return; - CacheResponse candidate = - policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false)); - if (candidate == null) { - return; - } + OkResponseCache responseCache = client.getOkResponseCache(); + if (responseCache == null) return; + + CacheResponse candidate = responseCache.get( + uri, method, requestHeaders.getHeaders().toMultimap(false)); + if (candidate == null) return; Map> responseHeadersMap = candidate.getHeaders(); cachedResponseBody = candidate.getBody(); @@ -274,22 +278,22 @@ public class HttpEngine { SSLSocketFactory sslSocketFactory = null; HostnameVerifier hostnameVerifier = null; if (uri.getScheme().equalsIgnoreCase("https")) { - sslSocketFactory = policy.sslSocketFactory; - hostnameVerifier = policy.hostnameVerifier; + sslSocketFactory = client.getSslSocketFactory(); + hostnameVerifier = client.getHostnameVerifier(); } Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory, - hostnameVerifier, policy.authenticator, policy.requestedProxy, policy.getTransports()); - routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, - Dns.DEFAULT, policy.getFailedRoutes()); + hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports()); + routeSelector = new RouteSelector(address, uri, client.getProxySelector(), + client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase()); } connection = routeSelector.next(); if (!connection.isConnected()) { - connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig()); - policy.connectionPool.maybeShare(connection); - policy.getFailedRoutes().remove(connection.getRoute()); + connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig()); + client.getConnectionPool().maybeShare(connection); + client.getRoutesDatabase().connected(connection.getRoute()); } connected(connection); - if (connection.getRoute().getProxy() != policy.requestedProxy) { + if (connection.getRoute().getProxy() != client.getProxy()) { // Update the request line if the proxy changed; it may need a host name. requestHeaders.getHeaders().setRequestLine(getRequestLine()); } @@ -387,20 +391,20 @@ public class HttpEngine { private void maybeCache() throws IOException { // Are we caching at all? - if (!policy.getUseCaches() || policy.responseCache == null) { - return; - } + if (!policy.getUseCaches()) return; + OkResponseCache responseCache = client.getOkResponseCache(); + if (responseCache == null) return; HttpURLConnection connectionToCache = policy.getHttpConnectionToCache(); // Should we cache this response for this request? if (!responseHeaders.isCacheable(requestHeaders)) { - policy.responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri); + responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri); return; } // Offer this request to the cache. - cacheRequest = policy.responseCache.put(uri, connectionToCache); + cacheRequest = responseCache.put(uri, connectionToCache); } /** @@ -412,7 +416,7 @@ public class HttpEngine { public final void automaticallyReleaseConnectionToPool() { automaticallyReleaseConnectionToPool = true; if (connection != null && connectionReleased) { - policy.connectionPool.recycle(connection); + client.getConnectionPool().recycle(connection); connection = null; } } @@ -436,7 +440,7 @@ public class HttpEngine { Util.closeQuietly(connection); connection = null; } else if (automaticallyReleaseConnectionToPool) { - policy.connectionPool.recycle(connection); + client.getConnectionPool().recycle(connection); connection = null; } } @@ -524,7 +528,7 @@ public class HttpEngine { requestHeaders.setIfModifiedSince(new Date(ifModifiedSince)); } - CookieHandler cookieHandler = policy.cookieHandler; + CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { requestHeaders.addCookies( cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false))); @@ -639,8 +643,9 @@ public class HttpEngine { release(false); ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); setResponse(combinedHeaders, cachedResponseBody); - policy.responseCache.trackConditionalCacheHit(); - policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache()); + OkResponseCache responseCache = client.getOkResponseCache(); + responseCache.trackConditionalCacheHit(); + responseCache.update(cacheResponse, policy.getHttpConnectionToCache()); return; } else { Util.closeQuietly(cachedResponseBody); @@ -659,7 +664,7 @@ public class HttpEngine { } public void receiveHeaders(RawHeaders headers) throws IOException { - CookieHandler cookieHandler = policy.cookieHandler; + CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { cookieHandler.put(uri, headers.toMultimap(true)); } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index b4cf9d68a..f2315ce91 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -18,10 +18,7 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; -import com.squareup.okhttp.ConnectionPool; -import com.squareup.okhttp.OkAuthenticator; import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Route; import com.squareup.okhttp.internal.AbstractOutputStream; import com.squareup.okhttp.internal.FaultRecoveringOutputStream; import com.squareup.okhttp.internal.Platform; @@ -30,13 +27,11 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.CookieHandler; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.Proxy; -import java.net.ProxySelector; import java.net.SocketPermission; import java.net.URL; import java.security.Permission; @@ -44,10 +39,8 @@ import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; -import javax.net.ssl.HostnameVerifier; +import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; import static com.squareup.okhttp.internal.Util.getEffectivePort; @@ -83,52 +76,17 @@ public class HttpURLConnectionImpl extends HttpURLConnection { */ private static final int MAX_REPLAY_BUFFER_LENGTH = 8192; - private final boolean followProtocolRedirects; - - /** The proxy requested by the client, or null for a proxy to be selected automatically. */ - final Proxy requestedProxy; - - final ProxySelector proxySelector; - final CookieHandler cookieHandler; - final OkResponseCache responseCache; - final ConnectionPool connectionPool; - /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */ - SSLSocketFactory sslSocketFactory; - HostnameVerifier hostnameVerifier; - private List transports; - OkAuthenticator authenticator; - final Set failedRoutes; + final OkHttpClient client; private final RawHeaders rawRequestHeaders = new RawHeaders(); - private int redirectionCount; private FaultRecoveringOutputStream faultRecoveringRequestBody; - protected IOException httpEngineFailure; protected HttpEngine httpEngine; - public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache, - Set failedRoutes) { + public HttpURLConnectionImpl(URL url, OkHttpClient client) { super(url); - this.followProtocolRedirects = client.getFollowProtocolRedirects(); - this.failedRoutes = failedRoutes; - this.requestedProxy = client.getProxy(); - this.proxySelector = client.getProxySelector(); - this.cookieHandler = client.getCookieHandler(); - this.connectionPool = client.getConnectionPool(); - this.sslSocketFactory = client.getSslSocketFactory(); - this.hostnameVerifier = client.getHostnameVerifier(); - this.transports = client.getTransports(); - this.authenticator = client.getAuthenticator(); - this.responseCache = responseCache; - } - - Set getFailedRoutes() { - return failedRoutes; - } - - List getTransports() { - return transports; + this.client = client; } @Override public final void connect() throws IOException { @@ -274,7 +232,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { String hostName = getURL().getHost(); int hostPort = Util.getEffectivePort(getURL()); if (usingProxy()) { - InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address(); + InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address(); hostName = proxyAddress.getHostName(); hostPort = proxyAddress.getPort(); } @@ -288,6 +246,22 @@ public class HttpURLConnectionImpl extends HttpURLConnection { return rawRequestHeaders.get(field); } + @Override public void setConnectTimeout(int timeoutMillis) { + client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override public int getConnectTimeout() { + return client.getConnectTimeout(); + } + + @Override public void setReadTimeout(int timeoutMillis) { + client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + + @Override public int getReadTimeout() { + return client.getReadTimeout(); + } + private void initHttpEngine() throws IOException { if (httpEngineFailure != null) { throw httpEngineFailure; @@ -468,7 +442,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { private Retry processResponseHeaders() throws IOException { Proxy selectedProxy = httpEngine.connection != null ? httpEngine.connection.getRoute().getProxy() - : requestedProxy; + : client.getProxy(); final int responseCode = getResponseCode(); switch (responseCode) { case HTTP_PROXY_AUTH: @@ -477,7 +451,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { } // fall-through case HTTP_UNAUTHORIZED: - boolean credentialsFound = HttpAuthenticator.processAuthHeader(authenticator, + boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(), getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url); return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; @@ -508,7 +482,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection { return Retry.NONE; // Don't follow redirects to unsupported protocols. } boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol()); - if (!sameProtocol && !followProtocolRedirects) { + if (!sameProtocol && !client.getFollowProtocolRedirects()) { return Retry.NONE; // This client doesn't follow redirects across protocols. } boolean sameHost = previousUrl.getHost().equals(url.getHost()); @@ -535,7 +509,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection { } @Override public final boolean usingProxy() { - return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT); + Proxy proxy = client.getProxy(); + return proxy != null && proxy.type() != Proxy.Type.DIRECT; } @Override public String getResponseMessage() throws IOException { @@ -599,37 +574,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection { * When append == false, we require that the transport list contains "http/1.1". */ private void setTransports(String transportsString, boolean append) { - String[] transports = transportsString.split(",", -1); - ArrayList transportsList = new ArrayList(); - if (!append) { - // If we're not appending to the list, we need to make sure - // the list contains "http/1.1". We do this in a separate loop - // to avoid modifying any state before we validate the input. - boolean containsHttp = false; - for (String transport : transports) { - if ("http/1.1".equals(transport)) { - containsHttp = true; - break; - } - } - - if (!containsHttp) { - throw new IllegalArgumentException("Transport list doesn't contain http/1.1"); - } - } else { - transportsList.addAll(this.transports); + List transportsList = new ArrayList(); + if (append) { + transportsList.addAll(client.getTransports()); } - - for (String transport : transports) { - if (transport.length() == 0) { - throw new IllegalArgumentException("Transport list contains an empty transport"); - } - - if (!transportsList.contains(transport)) { - transportsList.add(transport); - } + for (String transport : transportsString.split(",", -1)) { + transportsList.add(transport); } - - this.transports = Util.immutableList(transportsList); + client.setTransports(transportsList); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java index 249fb98da..1b870c955 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java @@ -18,7 +18,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Route; import com.squareup.okhttp.TunnelRequest; import java.io.IOException; import java.io.InputStream; @@ -33,7 +32,6 @@ import java.security.Principal; import java.security.cert.Certificate; import java.util.List; import java.util.Map; -import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; @@ -47,10 +45,9 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */ private final HttpUrlConnectionDelegate delegate; - public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache, - Set failedRoutes) { + public HttpsURLConnectionImpl(URL url, OkHttpClient client) { super(url); - delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes); + delegate = new HttpUrlConnectionDelegate(url, client); } @Override public String getCipherSuite() { @@ -386,25 +383,24 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection { } @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { - delegate.hostnameVerifier = hostnameVerifier; + delegate.client.setHostnameVerifier(hostnameVerifier); } @Override public HostnameVerifier getHostnameVerifier() { - return delegate.hostnameVerifier; + return delegate.client.getHostnameVerifier(); } @Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { - delegate.sslSocketFactory = sslSocketFactory; + delegate.client.setSslSocketFactory(sslSocketFactory); } @Override public SSLSocketFactory getSSLSocketFactory() { - return delegate.sslSocketFactory; + return delegate.client.getSslSocketFactory(); } private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { - private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache, - Set failedRoutes) { - super(url, client, responseCache, failedRoutes); + private HttpUrlConnectionDelegate(URL url, OkHttpClient client) { + super(url, client); } @Override protected HttpURLConnection getHttpConnectionToCache() { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Job.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Job.java new file mode 100644 index 000000000..33c58e4e5 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Job.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013 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.internal.http; + +import com.squareup.okhttp.Failure; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import java.io.IOException; +import java.net.HttpURLConnection; + +public final class Job implements Runnable { + final HttpURLConnection connection; + final Request request; + final Response.Receiver responseReceiver; + final Dispatcher dispatcher; + + public Job(Dispatcher dispatcher, HttpURLConnection connection, Request request, + Response.Receiver responseReceiver) { + this.dispatcher = dispatcher; + this.connection = connection; + this.request = request; + this.responseReceiver = responseReceiver; + } + + @Override public void run() { + try { + sendRequest(); + Response response = readResponse(); + responseReceiver.onResponse(response); + } catch (IOException e) { + responseReceiver.onFailure(new Failure.Builder() + .request(request) + .exception(e) + .build()); + } finally { + connection.disconnect(); + dispatcher.finished(this); + } + } + + private HttpURLConnection sendRequest() throws IOException { + for (int i = 0; i < request.headerCount(); i++) { + connection.addRequestProperty(request.headerName(i), request.headerValue(i)); + } + Request.Body body = request.body(); + if (body != null) { + connection.setDoOutput(true); + long contentLength = body.contentLength(); + if (contentLength == -1 || contentLength > Integer.MAX_VALUE) { + connection.setChunkedStreamingMode(0); + } else { + // Don't call setFixedLengthStreamingMode(long); that's only available on Java 1.7+. + connection.setFixedLengthStreamingMode((int) contentLength); + } + body.writeTo(connection.getOutputStream()); + } + return connection; + } + + private Response readResponse() throws IOException { + int responseCode = connection.getResponseCode(); + Response.Builder responseBuilder = new Response.Builder(request, responseCode); + + for (int i = 0; true; i++) { + String name = connection.getHeaderFieldKey(i); + if (name == null) break; + String value = connection.getHeaderField(i); + responseBuilder.addHeader(name, value); + } + + responseBuilder.body(new Dispatcher.RealResponseBody(connection, connection.getInputStream())); + // TODO: set redirectedBy + return responseBuilder.build(); + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java index fc8ffca26..5335c2bce 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java @@ -15,6 +15,7 @@ */ package com.squareup.okhttp.internal.http; +import com.squareup.okhttp.OkResponseCache; import com.squareup.okhttp.ResponseSource; import java.io.IOException; import java.net.CacheRequest; diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java index ce0a71d84..bab9df238 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java @@ -19,6 +19,7 @@ import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.Route; +import com.squareup.okhttp.RouteDatabase; import com.squareup.okhttp.internal.Dns; import java.io.IOException; import java.net.InetAddress; @@ -32,8 +33,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; -import java.util.Set; -import javax.net.ssl.SSLHandshakeException; import static com.squareup.okhttp.internal.Util.getEffectivePort; @@ -55,7 +54,7 @@ public final class RouteSelector { private final ProxySelector proxySelector; private final ConnectionPool pool; private final Dns dns; - private final Set failedRoutes; + private final RouteDatabase routeDatabase; /* The most recently attempted route. */ private Proxy lastProxy; @@ -78,13 +77,13 @@ public final class RouteSelector { private final List postponedRoutes; public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool, - Dns dns, Set failedRoutes) { + Dns dns, RouteDatabase routeDatabase) { this.address = address; this.uri = uri; this.proxySelector = proxySelector; this.pool = pool; this.dns = dns; - this.failedRoutes = failedRoutes; + this.routeDatabase = routeDatabase; this.postponedRoutes = new LinkedList(); resetNextProxy(uri, address.getProxy()); @@ -128,7 +127,7 @@ public final class RouteSelector { boolean modernTls = nextTlsMode() == TLS_MODE_MODERN; Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls); - if (failedRoutes.contains(route)) { + if (routeDatabase.shouldPostpone(route)) { postponedRoutes.add(route); // We will only recurse in order to skip previously failed routes. They will be // tried last. @@ -149,12 +148,7 @@ public final class RouteSelector { proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure); } - failedRoutes.add(failedRoute); - if (!(failure instanceof SSLHandshakeException)) { - // If the problem was not related to SSL then it will also fail with - // a different Tls mode therefore we can be proactive about it. - failedRoutes.add(failedRoute.flipTlsMode()); - } + routeDatabase.failed(failedRoute, failure); } /** Resets {@link #nextProxy} to the first option. */ diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java index 73709b581..daa4e8010 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java @@ -55,7 +55,7 @@ public final class SpdyTransport implements Transport { boolean hasResponseBody = true; stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody, hasResponseBody); - stream.setReadTimeout(httpEngine.policy.getReadTimeout()); + stream.setReadTimeout(httpEngine.client.getReadTimeout()); } @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java index 687a39753..1cdcb1dfe 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java @@ -19,7 +19,7 @@ import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.OkAuthenticator; -import com.squareup.okhttp.Route; +import com.squareup.okhttp.RouteDatabase; import com.squareup.okhttp.internal.Dns; import com.squareup.okhttp.internal.SslContextBuilder; import java.io.IOException; @@ -32,11 +32,8 @@ import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.NoSuchElementException; -import java.util.Set; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; @@ -87,7 +84,7 @@ public final class RouteSelectorTest { @Test public void singleRoute() throws Exception { Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); assertTrue(routeSelector.hasNext()); dns.inetAddresses = makeFakeAddresses(255, 1); @@ -105,15 +102,14 @@ public final class RouteSelectorTest { @Test public void singleRouteReturnsFailedRoute() throws Exception { Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); assertTrue(routeSelector.hasNext()); dns.inetAddresses = makeFakeAddresses(255, 1); Connection connection = routeSelector.next(); - Set failedRoutes = new LinkedHashSet(); - failedRoutes.add(connection.getRoute()); - routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + RouteDatabase routeDatabase = new RouteDatabase(); + routeDatabase.failed(connection.getRoute(), new IOException()); + routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase); assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); assertFalse(routeSelector.hasNext()); try { @@ -126,7 +122,7 @@ public final class RouteSelectorTest { @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception { Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); assertTrue(routeSelector.hasNext()); dns.inetAddresses = makeFakeAddresses(255, 2); @@ -144,7 +140,7 @@ public final class RouteSelectorTest { Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); assertTrue(routeSelector.hasNext()); dns.inetAddresses = makeFakeAddresses(255, 2); @@ -161,7 +157,7 @@ public final class RouteSelectorTest { proxySelector.proxies = null; RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); proxySelector.assertRequests(uri); assertTrue(routeSelector.hasNext()); @@ -175,7 +171,7 @@ public final class RouteSelectorTest { @Test public void proxySelectorReturnsNoProxies() throws Exception { Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); assertTrue(routeSelector.hasNext()); dns.inetAddresses = makeFakeAddresses(255, 2); @@ -193,7 +189,7 @@ public final class RouteSelectorTest { proxySelector.proxies.add(proxyA); proxySelector.proxies.add(proxyB); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); proxySelector.assertRequests(uri); // First try the IP addresses of the first proxy, in sequence. @@ -226,7 +222,7 @@ public final class RouteSelectorTest { proxySelector.proxies.add(NO_PROXY); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); proxySelector.assertRequests(uri); // Only the origin server will be attempted. @@ -245,7 +241,7 @@ public final class RouteSelectorTest { proxySelector.proxies.add(proxyB); proxySelector.proxies.add(proxyA); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); proxySelector.assertRequests(uri); assertTrue(routeSelector.hasNext()); @@ -280,27 +276,27 @@ public final class RouteSelectorTest { @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, Proxy.NO_PROXY, transports); - Set failedRoutes = new LinkedHashSet(); + RouteDatabase routeDatabase = new RouteDatabase(); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - failedRoutes); + routeDatabase); dns.inetAddresses = makeFakeAddresses(255, 1); Connection connection = routeSelector.next(); routeSelector.connectFailed(connection, new IOException("Non SSL exception")); - assertTrue(failedRoutes.size() == 2); + assertTrue(routeDatabase.failedRoutesCount() == 2); } @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, Proxy.NO_PROXY, transports); - Set failedRoutes = new LinkedHashSet(); + RouteDatabase routeDatabase = new RouteDatabase(); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - failedRoutes); + routeDatabase); dns.inetAddresses = makeFakeAddresses(255, 1); Connection connection = routeSelector.next(); routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception")); - assertTrue(failedRoutes.size() == 1); + assertTrue(routeDatabase.failedRoutesCount() == 1); } @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception { @@ -309,7 +305,7 @@ public final class RouteSelectorTest { proxySelector.proxies.add(proxyA); proxySelector.proxies.add(proxyB); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - Collections.EMPTY_SET); + new RouteDatabase()); // Proxy A dns.inetAddresses = makeFakeAddresses(255, 2); @@ -346,9 +342,9 @@ public final class RouteSelectorTest { Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator, Proxy.NO_PROXY, transports); - Set failedRoutes = new LinkedHashSet(1); + RouteDatabase routeDatabase = new RouteDatabase(); RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, - failedRoutes); + routeDatabase); dns.inetAddresses = makeFakeAddresses(255, 1); // Extract the regular sequence of routes from selector. @@ -360,9 +356,9 @@ public final class RouteSelectorTest { // Check that we do indeed have more than one route. assertTrue(regularRoutes.size() > 1); // Add first regular route as failed. - failedRoutes.add(regularRoutes.get(0).getRoute()); + routeDatabase.failed(regularRoutes.get(0).getRoute(), new SSLHandshakeException("none")); // Reset selector - routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, failedRoutes); + routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase); List routesWithFailedRoute = new ArrayList(); while (routeSelector.hasNext()) {