Fix regression in the HTTP request line am: af79cbf93e
am: ffe9365e03

Change-Id: Ie8eb743f9ca9afefc508992b9418e317a21ca6ba
diff --git a/.buildscript/deploy_snapshot.sh b/.buildscript/deploy_snapshot.sh
new file mode 100755
index 0000000..4e141ca
--- /dev/null
+++ b/.buildscript/deploy_snapshot.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
+#
+# Adapted from https://coderwall.com/p/9b_lfq and
+# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
+
+SLUG="square/okhttp"
+JDK="oraclejdk8"
+BRANCH="master"
+
+set -e
+
+if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
+  echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
+elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
+  echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
+elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+  echo "Skipping snapshot deployment: was pull request."
+elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
+  echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
+else
+  echo "Deploying snapshot..."
+  mvn clean source:jar javadoc:jar deploy --settings=".buildscript/settings.xml" -Dmaven.test.skip=true
+  echo "Snapshot deployed!"
+fi
diff --git a/.buildscript/settings.xml b/.buildscript/settings.xml
new file mode 100644
index 0000000..91f444b
--- /dev/null
+++ b/.buildscript/settings.xml
@@ -0,0 +1,9 @@
+<settings>
+  <servers>
+    <server>
+      <id>sonatype-nexus-snapshots</id>
+      <username>${env.CI_DEPLOY_USERNAME}</username>
+      <password>${env.CI_DEPLOY_PASSWORD}</password>
+    </server>
+  </servers>
+</settings>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..226a3f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+.classpath
+.project
+.settings
+eclipsebin
+
+bin
+gen
+build
+out
+lib
+
+target
+pom.xml.*
+release.properties
+
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+
+obj
+
+.DS_Store
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..d29f0b1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "okhttp-hpacktests/src/test/resources/hpack-test-case"]
+	path = okhttp-hpacktests/src/test/resources/hpack-test-case
+	url = git://github.com/http2jp/hpack-test-case.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed135a7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,26 @@
+language: java
+
+jdk:
+  - oraclejdk7
+  - oraclejdk8
+
+after_success:
+  - .buildscript/deploy_snapshot.sh
+
+env:
+  global:
+    - secure: "S0BTJVrF4fUCwhTdmoQY6LYr5r1wgXZ/p8lc5bIgUUsc1Ckalwt7s/GDwPuLJ4702sI5t56Eye2iEIMUjeFJKqebZRsX1C5oYsYFxGi3BGlepstYpmj0gLXuSWqCLniS9zmHXCxLhLkC6KxPVjhDlbq76XQx0o3K1J8oEIj/PCE="
+    - secure: "awV7yLXURjlPbTOladsNDZk74KYCNXoiZpAP0gQFfK4Sc0fc7+kg8z/yhdWXeTxjsIZ6m0dVDHTqnH8ytnydwXpBam8JdQJ+EAWA6R3Svq1BR1bzl/PcZUoz+Xn8lMXdU3yA1p4qtQlUhMxwsE3MOVe24HSDJPAu4XeWFj1j3qo="
+
+branches:
+  except:
+    - gh-pages
+
+notifications:
+  email: false
+
+sudo: false
+
+cache:
+  directories:
+    - $HOME/.m2
diff --git a/Android.mk b/Android.mk
index d98f068..f82a7b5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -19,6 +19,7 @@
 okhttp_common_src_files += $(call all-java-files-under,okhttp-urlconnection/src/main/java)
 okhttp_common_src_files += $(call all-java-files-under,okhttp-android-support/src/main/java)
 okhttp_common_src_files += $(call all-java-files-under,okio/okio/src/main/java)
+
 okhttp_system_src_files := $(filter-out %/Platform.java, $(okhttp_common_src_files))
 okhttp_system_src_files += $(call all-java-files-under, android/main/java)
 
@@ -33,12 +34,9 @@
 okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/main/java)
 okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/test/java)
 
-# Exclude tests Android currently has problems with:
-# 1) Parameterized (requires JUnit 4.11).
-# 2) New dependencies like gson.
+# Exclude test Android currently has problems with due to @Parameterized (requires JUnit 4.11).
 okhttp_test_src_excludes := \
-    okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java \
-    okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
+    okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java
 
 okhttp_test_src_files := \
     $(filter-out $(okhttp_test_src_excludes), $(okhttp_test_src_files))
@@ -48,7 +46,7 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_system_src_files)
 LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
+LOCAL_JAVA_LIBRARIES := core-oj core-libart
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
@@ -59,7 +57,7 @@
 LOCAL_MODULE := okhttp-nojarjar
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
+LOCAL_JAVA_LIBRARIES := core-oj core-libart
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00ad4c3..6ceddcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,136 @@
 Change Log
 ==========
 
+## Version 2.7.5
+
+_2016-02-25_
+
+ *  Fix: Change the certificate pinner to always build full chains. This
+    prevents a potential crash when using certificate pinning with the Google
+    Play Services security provider.
+
+
+## Version 2.7.4
+
+_2016-02-07_
+
+ *  Fix: Don't crash when finding the trust manager if the Play Services (GMS)
+    security provider is installed.
+ *  Fix: The previous release introduced a performance regression on Android,
+    caused by looking up CA certificates. This is now fixed.
+
+
+## Version 2.7.3
+
+_2016-02-06_
+
+ *  Fix: Permit the trusted CA root to be pinned by `CertificatePinner`.
+
+
+## Version 2.7.2
+
+_2016-01-07_
+
+ *  Fix: Don't eagerly release stream allocations on cache hits. We might still
+    need them to handle redirects.
+
+
+## Version 2.7.1
+
+_2016-01-01_
+
+ *  Fix: Don't do a health check on newly-created connections. This is
+    unnecessary work that could put the client in an inconsistent state if the
+    health check fails.
+
+
+## Version 2.7.0
+
+_2015-12-12_
+
+ *  **Rewritten connection management.** Previously OkHttp's connection pool
+    managed both idle and active connections for HTTP/2, but only idle
+    connections for HTTP/1.x. Wth this update the connection pool manages both
+    idle and active connections for everything. OkHttp now detects and warns on
+    connections that were allocated but never released, and will enforce HTTP/2
+    stream limits. This update also fixes `Call.cancel()` to not do I/O on the
+    calling thread.
+ *  Fix: Don't log gzipped data in the logging interceptor.
+ *  Fix: Don't resolve DNS addresses when connecting through a SOCKS proxy.
+ *  Fix: Drop the synthetic `OkHttp-Selected-Protocol` response header.
+ *  Fix: Support 204 and 205 'No Content' replies in the logging interceptor.
+ *  New: Add `Call.isExecuted()`.
+
+
+## Version 2.6.0
+
+_2015-11-22_
+
+ *  **New Logging Interceptor.** The `logging-interceptor` subproject offers
+    simple request and response logging. It may be configured to log headers and
+    bodies for debugging. It requires this Maven dependency:
+
+     ```xml
+     <dependency>
+       <groupId>com.squareup.okhttp</groupId>
+       <artifactId>logging-interceptor</artifactId>
+       <version>2.6.0</version>
+     </dependency>
+     ```
+
+    Configure basic logging like this:
+
+    ```java
+    HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
+    loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
+    client.networkInterceptors().add(loggingInterceptor);
+    ```
+
+    **Warning:** Avoid `Level.HEADERS` and `Level.BODY` in production because
+    they could leak passwords and other authentication credentials to insecure
+    logs.
+
+ *  **WebSocket API now uses `RequestBody` and `ResponseBody` for messages.**
+    This is a backwards-incompatible API change.
+
+ *  **The DNS service is now pluggable.** In some situations this may be useful
+    to manually prioritize specific IP addresses.
+
+ *  Fix: Don't throw when converting an `HttpUrl` to a `java.net.URI`.
+    Previously URLs with special characters like `|` and `[` would break when
+    subjected to URI’s overly-strict validation.
+ *  Fix: Don't re-encode `+` as `%20` in encoded URL query strings. OkHttp
+    prefers `%20` when doing its own encoding, but will retain `+` when that is
+    provided.
+ *  Fix: Enforce that callers call `WebSocket.close()` on IO errors. Error
+    handling in WebSockets is significantly improved.
+ *  Fix: Don't use SPDY/3 style header concatenation for HTTP/2 request headers.
+    This could have corrupted requests where multiple headers had the same name,
+    as in cookies.
+ *  Fix: Reject bad characters in the URL hostname. Previously characters like
+    `\0` would cause a late crash when building the request.
+ *  Fix: Allow interceptors to change the request method.
+ *  Fix: Don’t use the request's `User-Agent` or `Proxy-Authorization` when
+    connecting to an HTTPS server via an HTTP tunnel. The `Proxy-Authorization`
+    header was being leaked to the origin server.
+ *  Fix: Digits may be used in a URL scheme.
+ *  Fix: Improve connection timeout recovery.
+ *  Fix: Recover from `getsockname` crashes impacting Android releases prior to
+    4.2.2.
+ *  Fix: Drop partial support for HTTP/1.0. Previously OkHttp would send
+    `HTTP/1.0` on connections after seeing a response with `HTTP/1.0`. The fixed
+    behavior is consistent with Firefox and Chrome.
+ *  Fix: Allow a body in `OPTIONS` requests.
+ *  Fix: Don't percent-encode non-ASCII characters in URL fragments.
+ *  Fix: Handle null fragments.
+ *  Fix: Don’t crash on interceptors that throw `IOException` before a
+    connection is attempted.
+ *  New: Support [WebDAV][webdav] HTTP methods.
+ *  New: Buffer WebSocket frames for better performance.
+ *  New: Drop support for `TLS_DHE_DSS_WITH_AES_128_CBC_SHA`, our only remaining
+    DSS cipher suite. This is consistent with Firefox and Chrome which have also
+    dropped these cipher suite.
+
 ## Version 2.5.0
 
 _2015-08-25_
@@ -22,7 +152,7 @@
     where changing a URL from `http` to `https` would leave it on port 80.
 
  *  **Okio has been updated to 1.6.0.**
-     ```
+     ```xml
      <dependency>
        <groupId>com.squareup.okio</groupId>
        <artifactId>okio</artifactId>
@@ -81,7 +211,7 @@
     Both are permitted-by-spec, but `%20` requires fewer special cases.
 
  *  **Okio has been updated to 1.4.0.**
-     ```
+     ```xml
      <dependency>
        <groupId>com.squareup.okio</groupId>
        <artifactId>okio</artifactId>
@@ -93,7 +223,7 @@
     Passing null will now fail for request methods that require a body. Instead
     use an empty body such as this one:
 
-    ```
+    ```java
         RequestBody.create(null, new byte[0]);
     ```
 
@@ -102,7 +232,7 @@
    your app. You'll need to pin both the top-level domain and the `*.` domain
    for full coverage.
 
-    ```
+    ```java
      client.setCertificatePinner(new CertificatePinner.Builder()
          .add("publicobject.com",   "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
          .add("*.publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -159,7 +289,7 @@
 
  *  **Okio updated to 1.3.0.**
 
-    ```
+    ```xml
     <dependency>
       <groupId>com.squareup.okio</groupId>
       <artifactId>okio</artifactId>
@@ -264,14 +394,14 @@
 
     To disable TLS fallback:
 
-    ```
+    ```java
     client.setConnectionSpecs(Arrays.asList(
         ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
     ```
 
     To disable cleartext connections, permitting `https` URLs only:
 
-    ```
+    ```java
     client.setConnectionSpecs(Arrays.asList(
         ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS));
     ```
@@ -305,7 +435,7 @@
 
  *  **Okio updated to 1.0.1.**
 
-    ```
+    ```xml
     <dependency>
       <groupId>com.squareup.okio</groupId>
       <artifactId>okio</artifactId>
@@ -385,7 +515,7 @@
     agent.
  *  New: Guava-like API to create headers:
 
-    ```
+    ```java
     Headers headers = Headers.of(name1, value1, name2, value2, ...).
     ```
 
@@ -428,7 +558,7 @@
     add the `okhttp-urlconnection` module to your project and use the
     `OkUrlFactory` to create new instances of `HttpURLConnection`:
 
-    ```
+    ```java
     // OkHttp 1.x:
     HttpURLConnection connection = client.open(url);
 
@@ -683,4 +813,5 @@
 
 Initial release.
 
- [brick]: (https://noncombatant.org/2015/05/01/about-http-public-key-pinning/)
+ [brick]: https://noncombatant.org/2015/05/01/about-http-public-key-pinning/
+ [webdav]: https://tools.ietf.org/html/rfc4918
diff --git a/README.android b/README.android
index 0a1b91b..3ad84fc 100644
--- a/README.android
+++ b/README.android
@@ -7,16 +7,19 @@
 
 Addition of classes in android/ :
   - com.squareup.okhttp.internal.Platform - to replace the Platform class that
-    comes with okhttp. No use of reflection.
+    comes with okhttp. Avoids use of reflection where possible.
   - com.squareup.okhttp.Http(s)Handler - integration with Android's corelibs.
   - com.squareup.okhttp.ConfigAwareConnectionPool - support for a
     ConnectionPool that listens for network configuration changes.
-  - com.squareup.okhttp.internal.Version - a hard-crafted version of
+  - com.squareup.okhttp.internal.Version - a hand-crafted version of
     okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
     for Android.
 
 All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
   - Commenting of code that references APIs not present on Android.
+  - @Ignore or comment out tests for functionality not supported on Android.
+  - Work around JUnit 4.11 not being available on Android by adding a
+    assertNotEquals() method to affect tests.
 
 okio/ contains a snapshot of the Okio project. See okio/README.android for
 details.
diff --git a/README.md b/README.md
index 4fde155..f10aaeb 100644
--- a/README.md
+++ b/README.md
@@ -11,12 +11,12 @@
 <dependency>
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>okhttp</artifactId>
-  <version>2.5.0</version>
+  <version>2.6.0</version>
 </dependency>
 ```
 or Gradle:
 ```groovy
-compile 'com.squareup.okhttp:okhttp:2.5.0'
+compile 'com.squareup.okhttp:okhttp:2.6.0'
 ```
 
 Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
@@ -36,13 +36,13 @@
 <dependency>
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>mockwebserver</artifactId>
-  <version>2.5.0</version>
+  <version>2.6.0</version>
   <scope>test</scope>
 </dependency>
 ```
 or Gradle:
 ```groovy
-testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
+testCompile 'com.squareup.okhttp:mockwebserver:2.6.0'
 ```
 
 
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index 6b127b2..ca5048b 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -98,6 +98,12 @@
         // Use Android's preferred fallback approach and cipher suite selection.
         okHttpClient.setConnectionSpecs(SECURE_CONNECTION_SPECS);
 
+        // Android support certificate pinning via NetworkSecurityConfig so there is no need to
+        // also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections
+        // in Android should therefore always use the default certificate pinner, whose set of
+        // {@code hostNamesToPin} is empty.
+        okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);
+
         // OkHttp does not automatically honor the system-wide HostnameVerifier set with
         // HttpsURLConnection.setDefaultHostnameVerifier().
         okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 322c875..f4828f2 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,30 +16,62 @@
  */
 package com.squareup.okhttp.internal;
 
-import dalvik.system.SocketTagger;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.tls.RealTrustRootIndex;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
+
 import java.io.IOException;
-import java.io.OutputStream;
+import java.lang.reflect.Field;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
 
-import com.squareup.okhttp.Protocol;
-
+import dalvik.system.SocketTagger;
 import okio.Buffer;
 
 /**
- * Access to proprietary Android APIs. Doesn't use reflection.
+ * Access to proprietary Android APIs. Avoids use of reflection where possible.
  */
-public final class Platform {
-    private static final Platform PLATFORM = new Platform();
+// only tests should extend this class
+public class Platform {
+    private static final AtomicReference<Platform> INSTANCE_HOLDER
+            = new AtomicReference<>(new Platform());
+
+    // only for private use and in tests
+    protected Platform() {
+    }
 
     public static Platform get() {
-        return PLATFORM;
+        return INSTANCE_HOLDER.get();
+    }
+
+    /**
+     * Atomically replaces the Platform instance returned by future
+     * invocations of {@link #get()}, for use in tests.
+     * Invocations of this method should typically be followed by
+     * a try/finally block to reset the previous value:
+     *
+     * <pre>
+     * Platform p = getAndSetForTest(...);
+     * try {
+     *   ...
+     * } finally {
+     *   getAndSetForTest(p);
+     * }
+     * </pre>
+     *
+     * @return the previous value of {@link #get()}.
+     */
+    public static Platform getAndSetForTest(Platform platform) {
+        if (platform == null) {
+            throw new NullPointerException();
+        }
+        return INSTANCE_HOLDER.getAndSet(platform);
     }
 
     /** setUseSessionTickets(boolean) */
@@ -119,6 +151,56 @@
     }
 
     /**
+     * Stripped down/adapted from OkHttp's {@code Platform.Android.trustManager()}.
+     * OkHttp 2.7.5 uses this only for certificate pinning logic that we don't use
+     * on Android, so this method should never be called outside of OkHttp's tests.
+     * This method has been stripped down to the minimum for OkHttp's tests to pass.
+     */
+    public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+        Class sslParametersClass;
+        try {
+            sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters");
+        return readFieldOrNull(context, X509TrustManager.class, "x509TrustManager");
+    }
+
+    /**
+     * Stripped down from OkHttp's implementation to the minimum to get OkHttp's tests
+     * to pass. OkHttp 2.7.5 uses this for certificate pinning logic which is unused
+     * in Android. This method should never be called outside of OkHttp's tests.
+     */
+    public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+        return new RealTrustRootIndex(trustManager.getAcceptedIssuers());
+    }
+
+    // Helper method from OkHttp's Platform.java
+    private static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
+        for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
+            try {
+                Field field = c.getDeclaredField(fieldName);
+                field.setAccessible(true);
+                Object value = field.get(instance);
+                if (value == null || !fieldType.isInstance(value)) return null;
+                return fieldType.cast(value);
+            } catch (NoSuchFieldException ignored) {
+            } catch (IllegalAccessException e) {
+                throw new AssertionError();
+            }
+        }
+
+        // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate.
+        if (!fieldName.equals("delegate")) {
+            Object delegate = readFieldOrNull(instance, Object.class, "delegate");
+            if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName);
+        }
+
+        return null;
+    }
+
+    /**
      * Returns the concatenation of 8-bit, length prefixed protocol names.
      * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
      */
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
index 2e5dcdd..9735913 100644
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -24,19 +24,25 @@
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.List;
-
 import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocket;
-
-import okio.ByteString;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Tests for {@link Platform}.
@@ -77,6 +83,44 @@
     assertEquals(selectedProtocol, platform.getSelectedProtocol(openSslSocket));
   }
 
+  @Test public void rootTrustIndex_notNull_viaSocketFactory() throws Exception {
+    Platform platform = new Platform();
+    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+    sslContext.init(null, new TrustManager[] { TRUST_NO_ONE_TRUST_MANAGER }, new SecureRandom());
+    SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+    X509TrustManager trustManager = platform.trustManager(socketFactory);
+    assertNotNull(platform.trustRootIndex(trustManager));
+  }
+
+  @Test public void rootTrustIndex_notNull() throws Exception {
+    Platform platform = new Platform();
+    assertNotNull(platform.trustRootIndex(TRUST_NO_ONE_TRUST_MANAGER));
+  }
+
+  @Test public void trustManager() throws Exception {
+    Platform platform = new Platform();
+    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+    sslContext.init(null, new TrustManager[] { TRUST_NO_ONE_TRUST_MANAGER }, new SecureRandom());
+    SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+    X509TrustManager trustManager = platform.trustManager(socketFactory);
+    assertEquals(TRUST_NO_ONE_TRUST_MANAGER, trustManager);
+  }
+
+  private static final X509TrustManager TRUST_NO_ONE_TRUST_MANAGER = new X509TrustManager() {
+    @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+            throws CertificateException {
+      throw new CertificateException();
+    }
+
+    @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
+      throw new AssertionError();
+    }
+
+    @Override public X509Certificate[] getAcceptedIssuers() {
+      return new X509Certificate[0];
+    }
+  };
+
   private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
     private boolean useSessionTickets;
     private String hostname;
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index d0d2566..b938848 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>benchmarks</artifactId>
diff --git a/deploy_website.sh b/deploy_website.sh
index bbeedc2..c9b7f15 100755
--- a/deploy_website.sh
+++ b/deploy_website.sh
@@ -3,9 +3,6 @@
 set -ex
 
 REPO="git@github.com:square/okhttp.git"
-GROUP_ID="com.squareup.okhttp"
-ARTIFACT_ID="okhttp"
-
 DIR=temp-clone
 
 # Delete any existing temporary website clone
@@ -20,28 +17,12 @@
 # Checkout and track the gh-pages branch
 git checkout -t origin/gh-pages
 
-# Delete everything
-rm -rf *
+# Delete everything that isn't versioned (1.x, 2.x)
+ls | grep -E -v '^\d+\.x$' | xargs rm -rf
 
 # Copy website files from real repo
 cp -R ../website/* .
 
-# Download the latest javadoc to directories like 'javadoc' or 'javadoc-urlconnection'.
-for DOCUMENTED_ARTIFACT in okhttp okhttp-urlconnection okhttp-apache
-do
-  curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$DOCUMENTED_ARTIFACT&v=LATEST&c=javadoc" > javadoc.zip
-  JAVADOC_DIR="javadoc${DOCUMENTED_ARTIFACT//okhttp/}"
-  mkdir $JAVADOC_DIR
-  unzip javadoc.zip -d $JAVADOC_DIR
-  rm javadoc.zip
-done
-
-# Download the 1.6.0 javadoc to '1.x/javadoc'.
-curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$ARTIFACT_ID&v=1.6.0&c=javadoc" > javadoc.zip
-mkdir -p 1.x/javadoc
-unzip javadoc.zip -d 1.x/javadoc
-rm javadoc.zip
-
 # Stage all files in git and create a commit
 git add .
 git add -u
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
index 3fd6ca6..ba40bf5 100644
--- a/mockwebserver/README.md
+++ b/mockwebserver/README.md
@@ -42,7 +42,7 @@
   server.start();
 
   // Ask the server for its URL. You'll need this to make HTTP requests.
-  URL baseUrl = server.url("/v1/chat/");
+  HttpUrl baseUrl = server.url("/v1/chat/");
 
   // Exercise your application code, which should make those HTTP requests.
   // Responses are returned in the same order that they are enqueued.
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index 9ef5211..7681ad1 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>mockwebserver</artifactId>
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
new file mode 100644
index 0000000..2fff99c
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.UUID;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.X509Extensions;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
+
+/**
+ * A certificate and its private key. This can be used on the server side by HTTPS servers, or on
+ * the client side to verify those HTTPS servers. A held certificate can also be used to sign other
+ * held certificates, as done in practice by certificate authorities.
+ */
+public final class HeldCertificate {
+  public final X509Certificate certificate;
+  public final KeyPair keyPair;
+
+  public HeldCertificate(X509Certificate certificate, KeyPair keyPair) {
+    this.certificate = certificate;
+    this.keyPair = keyPair;
+  }
+
+  public static final class Builder {
+    static {
+      Security.addProvider(new BouncyCastleProvider());
+    }
+
+    private final long duration = 1000L * 60 * 60 * 24; // One day.
+    private String hostname;
+    private String serialNumber = "1";
+    private KeyPair keyPair;
+    private HeldCertificate issuedBy;
+    private int maxIntermediateCas;
+
+    public Builder serialNumber(String serialNumber) {
+      this.serialNumber = serialNumber;
+      return this;
+    }
+
+    /**
+     * Set this certificate's name. Typically this is the URL hostname for TLS certificates. This is
+     * the CN (common name) in the certificate. Will be a random string if no value is provided.
+     */
+    public Builder commonName(String hostname) {
+      this.hostname = hostname;
+      return this;
+    }
+
+    public Builder keyPair(KeyPair keyPair) {
+      this.keyPair = keyPair;
+      return this;
+    }
+
+    /**
+     * Set the certificate that signs this certificate. If unset, a self-signed certificate will be
+     * generated.
+     */
+    public Builder issuedBy(HeldCertificate signedBy) {
+      this.issuedBy = signedBy;
+      return this;
+    }
+
+    /**
+     * Set this certificate to be a certificate authority, with up to {@code maxIntermediateCas}
+     * intermediate certificate authorities beneath it.
+     */
+    public Builder ca(int maxIntermediateCas) {
+      this.maxIntermediateCas = maxIntermediateCas;
+      return this;
+    }
+
+    public HeldCertificate build() throws GeneralSecurityException {
+      // Subject, public & private keys for this certificate.
+      KeyPair heldKeyPair = keyPair != null
+          ? keyPair
+          : generateKeyPair();
+      X500Principal subject = hostname != null
+          ? new X500Principal("CN=" + hostname)
+          : new X500Principal("CN=" + UUID.randomUUID());
+
+      // Subject, public & private keys for this certificate's signer. It may be self signed!
+      KeyPair signedByKeyPair;
+      X500Principal signedByPrincipal;
+      if (issuedBy != null) {
+        signedByKeyPair = issuedBy.keyPair;
+        signedByPrincipal = issuedBy.certificate.getSubjectX500Principal();
+      } else {
+        signedByKeyPair = heldKeyPair;
+        signedByPrincipal = subject;
+      }
+
+      // Generate & sign the certificate.
+      long now = System.currentTimeMillis();
+      X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+      generator.setSerialNumber(new BigInteger(serialNumber));
+      generator.setIssuerDN(signedByPrincipal);
+      generator.setNotBefore(new Date(now));
+      generator.setNotAfter(new Date(now + duration));
+      generator.setSubjectDN(subject);
+      generator.setPublicKey(heldKeyPair.getPublic());
+      generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+      if (maxIntermediateCas > 0) {
+        generator.addExtension(X509Extensions.BasicConstraints, true,
+            new BasicConstraints(maxIntermediateCas));
+      }
+
+      X509Certificate certificate = generator.generateX509Certificate(
+          signedByKeyPair.getPrivate(), "BC");
+      return new HeldCertificate(certificate, heldKeyPair);
+    }
+
+    public KeyPair generateKeyPair() throws GeneralSecurityException {
+      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+      keyPairGenerator.initialize(1024, new SecureRandom());
+      return keyPairGenerator.generateKeyPair();
+    }
+  }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
index 546d660..fd1d020 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -18,24 +18,18 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.math.BigInteger;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.SecureRandom;
-import java.security.Security;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
-import java.util.Date;
+import java.util.ArrayList;
+import java.util.List;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManagerFactory;
-import javax.security.auth.x500.X500Principal;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.x509.X509V3CertificateGenerator;
 
 /**
  * Constructs an SSL context for testing. This uses Bouncy Castle to generate a
@@ -45,51 +39,70 @@
  * reuse SSL context instances where possible.
  */
 public final class SslContextBuilder {
-  static {
-    Security.addProvider(new BouncyCastleProvider());
-  }
-
-  private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
   private static SSLContext localhost; // Lazily initialized.
 
-  private final String hostName;
-  private long notBefore = System.currentTimeMillis();
-  private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
-
-  /**
-   * @param hostName the subject of the host. For TLS this should be the
-   * domain name that the client uses to identify the server.
-   */
-  public SslContextBuilder(String hostName) {
-    this.hostName = hostName;
-  }
-
   /** Returns a new SSL context for this host's current localhost address. */
   public static synchronized SSLContext localhost() {
-    if (localhost == null) {
-      try {
-        localhost = new SslContextBuilder(InetAddress.getByName("localhost").getHostName()).build();
-      } catch (GeneralSecurityException e) {
-        throw new RuntimeException(e);
-      } catch (UnknownHostException e) {
-        throw new RuntimeException(e);
-      }
+    if (localhost != null) return localhost;
+
+    try {
+      // Generate a self-signed cert for the server to serve and the client to trust.
+      HeldCertificate heldCertificate = new HeldCertificate.Builder()
+          .serialNumber("1")
+          .commonName(InetAddress.getByName("localhost").getHostName())
+          .build();
+
+      localhost = new SslContextBuilder()
+          .certificateChain(heldCertificate)
+          .addTrustedCertificate(heldCertificate.certificate)
+          .build();
+
+      return localhost;
+    } catch (GeneralSecurityException e) {
+      throw new RuntimeException(e);
+    } catch (UnknownHostException e) {
+      throw new RuntimeException(e);
     }
-    return localhost;
+  }
+
+  private HeldCertificate[] chain;
+  private List<X509Certificate> trustedCertificates = new ArrayList<>();
+
+  /**
+   * Configure the certificate chain to use when serving HTTPS responses. The first certificate
+   * in this chain is the server's certificate, further certificates are included in the handshake
+   * so the client can build a trusted path to a CA certificate.
+   */
+  public SslContextBuilder certificateChain(HeldCertificate... chain) {
+    this.chain = chain;
+    return this;
+  }
+
+  /**
+   * Add a certificate authority that this client trusts. Servers that provide certificate chains
+   * signed by these roots (or their intermediates) will be accepted.
+   */
+  public SslContextBuilder addTrustedCertificate(X509Certificate certificate) {
+    trustedCertificates.add(certificate);
+    return this;
   }
 
   public SSLContext build() throws GeneralSecurityException {
+    // Put the certificate in a key store.
     char[] password = "password".toCharArray();
-
-    // Generate public and private keys and use them to make a self-signed certificate.
-    KeyPair keyPair = generateKeyPair();
-    X509Certificate certificate = selfSignedCertificate(keyPair, "1");
-
-    // Put 'em in a key store.
     KeyStore keyStore = newEmptyKeyStore(password);
-    Certificate[] certificateChain = { certificate };
-    keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
-    keyStore.setCertificateEntry("cert", certificate);
+
+    if (chain != null) {
+      Certificate[] certificates = new Certificate[chain.length];
+      for (int i = 0; i < chain.length; i++) {
+        certificates[i] = chain[i].certificate;
+      }
+      keyStore.setKeyEntry("private", chain[0].keyPair.getPrivate(), password, certificates);
+    }
+
+    for (int i = 0; i < trustedCertificates.size(); i++) {
+      keyStore.setCertificateEntry("cert_" + i, trustedCertificates.get(i));
+    }
 
     // Wrap it up in an SSL context.
     KeyManagerFactory keyManagerFactory =
@@ -104,32 +117,6 @@
     return sslContext;
   }
 
-  public KeyPair generateKeyPair() throws GeneralSecurityException {
-    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
-    keyPairGenerator.initialize(1024, new SecureRandom());
-    return keyPairGenerator.generateKeyPair();
-  }
-
-  /**
-   * Generates a certificate for {@code hostName} containing {@code keyPair}'s
-   * public key, signed by {@code keyPair}'s private key.
-   */
-  @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
-  public X509Certificate selfSignedCertificate(KeyPair keyPair, String serialNumber)
-      throws GeneralSecurityException {
-    X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
-    X500Principal issuer = new X500Principal("CN=" + hostName);
-    X500Principal subject = new X500Principal("CN=" + hostName);
-    generator.setSerialNumber(new BigInteger(serialNumber));
-    generator.setIssuerDN(issuer);
-    generator.setNotBefore(new Date(notBefore));
-    generator.setNotAfter(new Date(notAfter));
-    generator.setSubjectDN(subject);
-    generator.setPublicKey(keyPair.getPublic());
-    generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
-    return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
-  }
-
   private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
     try {
       KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
index b95b64d..1574806 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
@@ -36,7 +36,7 @@
 import okio.Source;
 
 /** A basic SPDY/HTTP_2 server that serves the contents of a local directory. */
-public final class FramedServer implements IncomingStreamHandler {
+public final class FramedServer extends FramedConnection.Listener {
   static final Logger logger = Logger.getLogger(FramedServer.class.getName());
 
   private final List<Protocol> framedProtocols =
@@ -65,9 +65,10 @@
         if (protocol == null || !framedProtocols.contains(protocol)) {
           throw new ProtocolException("Protocol " + protocol + " unsupported");
         }
-        FramedConnection framedConnection = new FramedConnection.Builder(false, sslSocket)
+        FramedConnection framedConnection = new FramedConnection.Builder(false)
+            .socket(sslSocket)
             .protocol(protocol)
-            .handler(this)
+            .listener(this)
             .build();
         framedConnection.sendConnectionPreface();
       } catch (IOException e) {
@@ -89,7 +90,7 @@
     return sslSocket;
   }
 
-  @Override public void receive(final FramedStream stream) throws IOException {
+  @Override public void onStream(final FramedStream stream) throws IOException {
     try {
       List<Header> requestHeaders = stream.getRequestHeaders();
       String path = null;
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
index bc43bd4..db68595 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -17,6 +17,7 @@
 
 import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.framed.Settings;
 import com.squareup.okhttp.ws.WebSocketListener;
 import java.util.ArrayList;
 import java.util.List;
@@ -42,6 +43,7 @@
   private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;
 
   private List<PushPromise> promises = new ArrayList<>();
+  private Settings settings;
   private WebSocketListener webSocketListener;
 
   /** Creates a new mock response with an empty body. */
@@ -242,6 +244,20 @@
   }
 
   /**
+   * When {@linkplain MockWebServer#setProtocols(java.util.List) protocols}
+   * include {@linkplain com.squareup.okhttp.Protocol#HTTP_2 HTTP/2}, this
+   * pushes {@code settings} before writing the response.
+   */
+  public MockResponse withSettings(Settings settings) {
+    this.settings = settings;
+    return this;
+  }
+
+  public Settings getSettings() {
+    return settings;
+  }
+
+  /**
    * Attempts to perform a web socket upgrade on the connection. This will overwrite any previously
    * set status or body.
    */
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
index 0e746d3..2c76398 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -29,7 +29,7 @@
 import com.squareup.okhttp.internal.framed.FramedConnection;
 import com.squareup.okhttp.internal.framed.FramedStream;
 import com.squareup.okhttp.internal.framed.Header;
-import com.squareup.okhttp.internal.framed.IncomingStreamHandler;
+import com.squareup.okhttp.internal.framed.Settings;
 import com.squareup.okhttp.internal.http.HttpMethod;
 import com.squareup.okhttp.internal.ws.RealWebSocket;
 import com.squareup.okhttp.internal.ws.WebSocketProtocol;
@@ -82,6 +82,7 @@
 import org.junit.runners.model.Statement;
 
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_REQUEST_BODY;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -174,8 +175,10 @@
   }
 
   public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
-    if (executor != null) throw new IllegalStateException(
-        "setServerSocketFactory() must be called before start()");
+    if (executor != null) {
+      throw new IllegalStateException(
+          "setServerSocketFactory() must be called before start()");
+    }
     this.serverSocketFactory = serverSocketFactory;
   }
 
@@ -463,11 +466,12 @@
         }
 
         if (protocol != Protocol.HTTP_1_1) {
-          FramedSocketHandler framedSocketHandler = new FramedSocketHandler(socket, protocol);
-          FramedConnection framedConnection =
-              new FramedConnection.Builder(false, socket).protocol(protocol)
-                  .handler(framedSocketHandler)
-                  .build();
+          FramedSocketHandler framedSocketListener = new FramedSocketHandler(socket, protocol);
+          FramedConnection framedConnection = new FramedConnection.Builder(false)
+              .socket(socket)
+              .protocol(protocol)
+              .listener(framedSocketListener)
+              .build();
           openFramedConnections.add(framedConnection);
           openClientSockets.remove(socket);
           return;
@@ -672,7 +676,7 @@
     final RealWebSocket webSocket =
         new RealWebSocket(false /* is server */, source, sink, new SecureRandom(), replyExecutor,
             listener, request.getPath()) {
-          @Override protected void closeConnection() throws IOException {
+          @Override protected void close() throws IOException {
             connectionClose.countDown();
           }
         };
@@ -704,6 +708,7 @@
       throw new RuntimeException(e);
     }
 
+    replyExecutor.shutdown();
     Util.closeQuietly(sink);
     Util.closeQuietly(source);
   }
@@ -754,8 +759,9 @@
     long periodDelayMs = policy.getThrottlePeriod(TimeUnit.MILLISECONDS);
 
     long halfByteCount = byteCount / 2;
-    boolean disconnectHalfway =
-        !isRequest && policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
+    boolean disconnectHalfway = isRequest
+        ? policy.getSocketPolicy() == DISCONNECT_DURING_REQUEST_BODY
+        : policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
 
     while (!socket.isClosed()) {
       for (int b = 0; b < bytesPerPeriod; ) {
@@ -847,7 +853,7 @@
   }
 
   /** Processes HTTP requests layered over framed protocols. */
-  private class FramedSocketHandler implements IncomingStreamHandler {
+  private class FramedSocketHandler extends FramedConnection.Listener {
     private final Socket socket;
     private final Protocol protocol;
     private final AtomicInteger sequenceNumber = new AtomicInteger();
@@ -857,7 +863,7 @@
       this.protocol = protocol;
     }
 
-    @Override public void receive(FramedStream stream) throws IOException {
+    @Override public void onStream(FramedStream stream) throws IOException {
       RecordedRequest request = readRequest(stream);
       requestQueue.add(request);
       MockResponse response;
@@ -888,8 +894,14 @@
           path = value;
         } else if (name.equals(Header.VERSION)) {
           version = value;
-        } else {
+        } else if (protocol == Protocol.SPDY_3) {
+          for (String s : value.split("\u0000", -1)) {
+            httpHeaders.add(name.utf8(), s);
+          }
+        } else if (protocol == Protocol.HTTP_2) {
           httpHeaders.add(name.utf8(), value);
+        } else {
+          throw new IllegalStateException();
         }
       }
 
@@ -904,6 +916,11 @@
     }
 
     private void writeResponse(FramedStream stream, MockResponse response) throws IOException {
+      Settings settings = response.getSettings();
+      if (settings != null) {
+        stream.getConnection().setSettings(settings);
+      }
+
       if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
         return;
       }
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
index c9c206c..bb36cdf 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -18,12 +18,14 @@
 import java.net.HttpURLConnection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Logger;
 
 /**
  * Default dispatcher that processes a script of responses. Populate the script
  * by calling {@link #enqueueResponse(MockResponse)}.
  */
 public class QueueDispatcher extends Dispatcher {
+  private static final Logger logger = Logger.getLogger(QueueDispatcher.class.getName());
   protected final BlockingQueue<MockResponse> responseQueue = new LinkedBlockingQueue<>();
   private MockResponse failFastResponse;
 
@@ -31,7 +33,7 @@
     // To permit interactive/browser testing, ignore requests for favicons.
     final String requestLine = request.getRequestLine();
     if (requestLine != null && requestLine.equals("GET /favicon.ico HTTP/1.1")) {
-      System.out.println("served " + requestLine);
+      logger.info("served " + requestLine);
       return new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND);
     }
 
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
index 4583621..f71f33d 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -50,6 +50,9 @@
    */
   DISCONNECT_AFTER_REQUEST,
 
+  /** Close connection after reading half of the request body (if present). */
+  DISCONNECT_DURING_REQUEST_BODY,
+
   /** Close connection after writing half of the response body (if present). */
   DISCONNECT_DURING_RESPONSE_BODY,
 
@@ -69,7 +72,7 @@
   SHUTDOWN_OUTPUT_AT_END,
 
   /**
-   * Don't response to the request but keep the socket open. For testing
+   * Don't respond to the request but keep the socket open. For testing
    * read response header timeout issue.
    */
   NO_RESPONSE
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
index 0f757dd..1682dfe 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -20,6 +20,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.net.ConnectException;
 import java.net.HttpURLConnection;
 import java.net.ProtocolException;
@@ -253,7 +254,33 @@
     in.close();
   }
 
-  @Test public void disconnectHalfway() throws IOException {
+  @Test public void disconnectRequestHalfway() throws IOException {
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY));
+    // Limit the size of the request body that the server holds in memory to an arbitrary
+    // 3.5 MBytes so this test can pass on devices with little memory.
+    server.setBodyLimit(7 * 512 * 1024);
+
+    HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
+    connection.setRequestMethod("POST");
+    connection.setDoOutput(true);
+    connection.setFixedLengthStreamingMode(1024 * 1024 * 1024); // 1 GB
+    connection.connect();
+    OutputStream out = connection.getOutputStream();
+
+    byte[] data = new byte[1024 * 1024];
+    int i;
+    for (i = 0; i < 1024; i++) {
+      try {
+        out.write(data);
+        out.flush();
+      } catch (IOException e) {
+        break;
+      }
+    }
+    assertEquals(512f, i, 10f); // Halfway +/- 1%
+  }
+
+  @Test public void disconnectResponseHalfway() throws IOException {
     server.enqueue(new MockResponse()
         .setBody("ab")
         .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
index e80d121..fa585b8 100644
--- a/okcurl/pom.xml
+++ b/okcurl/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okcurl</artifactId>
diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
index dbc51f3..65c2cd0 100644
--- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -203,7 +203,7 @@
     }
     String bodyData = data;
 
-    String mimeType = "application/x-form-urlencoded";
+    String mimeType = "application/x-www-form-urlencoded";
     if (headers != null) {
       for (String header : headers) {
         String[] parts = header.split(":", -1);
diff --git a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
index 0e2e3ae..d6961e1 100644
--- a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -45,7 +45,7 @@
     RequestBody body = request.body();
     assertEquals("POST", request.method());
     assertEquals("http://example.com/", request.urlString());
-    assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+    assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
     assertEquals("foo", bodyAsString(body));
   }
 
@@ -54,7 +54,7 @@
     RequestBody body = request.body();
     assertEquals("PUT", request.method());
     assertEquals("http://example.com/", request.urlString());
-    assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+    assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
     assertEquals("foo", bodyAsString(body));
   }
 
diff --git a/okhttp-android-support/pom.xml b/okhttp-android-support/pom.xml
index f514808..0cb0f7e 100644
--- a/okhttp-android-support/pom.xml
+++ b/okhttp-android-support/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-android-support</artifactId>
diff --git a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
index 1d956b2..89570cc 100644
--- a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
@@ -821,21 +821,17 @@
       throw throwRequestSslAccessException();
     }
 
-    // ANDROID-BEGIN
-    // @Override public long getContentLengthLong() {
-    //   return delegate.getContentLengthLong();
-    // }
-    // ANDROID-END
+    @Override public long getContentLengthLong() {
+      return delegate.getContentLengthLong();
+    }
 
     @Override public void setFixedLengthStreamingMode(long contentLength) {
       delegate.setFixedLengthStreamingMode(contentLength);
     }
 
-    // ANDROID-BEGIN
-    // @Override public long getHeaderFieldLong(String field, long defaultValue) {
-    //   return delegate.getHeaderFieldLong(field, defaultValue);
-    // }
-    // ANDROID-END
+    @Override public long getHeaderFieldLong(String field, long defaultValue) {
+      return delegate.getHeaderFieldLong(field, defaultValue);
+    }
   }
 
   private static RuntimeException throwRequestModificationException() {
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index 74ff837..4da1a4e 100644
--- a/okhttp-apache/pom.xml
+++ b/okhttp-apache/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-apache</artifactId>
diff --git a/okhttp-hpacktests/src/test/resources/hpack-test-case b/okhttp-hpacktests/src/test/resources/hpack-test-case
new file mode 160000
index 0000000..a5652bc
--- /dev/null
+++ b/okhttp-hpacktests/src/test/resources/hpack-test-case
@@ -0,0 +1 @@
+Subproject commit a5652bc2bc3d2a992f39446369fb004a72e881d4
diff --git a/okhttp-logging-interceptor/README.md b/okhttp-logging-interceptor/README.md
new file mode 100644
index 0000000..a16bbd2
--- /dev/null
+++ b/okhttp-logging-interceptor/README.md
@@ -0,0 +1,49 @@
+Logging Interceptor
+===================
+
+An [OkHttp interceptor][1] which logs HTTP request and response data.
+
+```java
+OkHttpClient client = new OkHttpClient();
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
+logging.setLevel(Level.BASIC);
+client.interceptors().add(logging);
+```
+
+You can change the log level at any time by calling `setLevel`.
+
+To log to a custom location, pass a `Logger` instance to the constructor.
+```java
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new Logger() {
+  @Override public void log(String message) {
+    Timber.tag("OkHttp").d(message);
+  }
+});
+```
+
+**Warning**: The logs generated by this interceptor when using the `HEADERS` or `BODY` levels has
+the potential to leak sensitive information such as "Authorization" or "Cookie" headers and the
+contents of request and response bodies. This data should only be logged in a controlled way or in
+a non-production environment.
+
+
+Download
+--------
+
+Get via Maven:
+```xml
+<dependency>
+  <groupId>com.squareup.okhttp</groupId>
+  <artifactId>logging-interceptor</artifactId>
+  <version>(insert latest version)</version>
+</dependency>
+```
+
+or via Gradle 
+```groovy
+compile 'com.squareup.okhttp:logging-interceptor:(insert latest version)'
+```
+
+
+
+ [1]: https://github.com/square/okhttp/wiki/Interceptors
diff --git a/okhttp-logging-interceptor/pom.xml b/okhttp-logging-interceptor/pom.xml
new file mode 100644
index 0000000..efd552c
--- /dev/null
+++ b/okhttp-logging-interceptor/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.squareup.okhttp</groupId>
+    <artifactId>parent</artifactId>
+    <version>2.7.5</version>
+  </parent>
+
+  <artifactId>logging-interceptor</artifactId>
+  <name>OkHttp Logging Interceptor</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp-testing-support</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
new file mode 100644
index 0000000..402eee0
--- /dev/null
+++ b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2015 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.logging;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Headers;
+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.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.BufferedSource;
+
+/**
+ * An OkHttp interceptor which logs request and response information. Can be applied as an
+ * {@linkplain OkHttpClient#interceptors() application interceptor} or as a
+ * {@linkplain OkHttpClient#networkInterceptors() network interceptor}.
+ * <p>
+ * The format of the logs created by this class should not be considered stable and may change
+ * slightly between releases. If you need a stable logging format, use your own interceptor.
+ */
+public final class HttpLoggingInterceptor implements Interceptor {
+  private static final Charset UTF8 = Charset.forName("UTF-8");
+
+  public enum Level {
+    /** No logs. */
+    NONE,
+    /**
+     * Logs request and response lines.
+     * <p>
+     * Example:
+     * <pre>{@code
+     * --> POST /greeting HTTP/1.1 (3-byte body)
+     *
+     * <-- HTTP/1.1 200 OK (22ms, 6-byte body)
+     * }</pre>
+     */
+    BASIC,
+    /**
+     * Logs request and response lines and their respective headers.
+     * <p>
+     * Example:
+     * <pre>{@code
+     * --> POST /greeting HTTP/1.1
+     * Host: example.com
+     * Content-Type: plain/text
+     * Content-Length: 3
+     * --> END POST
+     *
+     * <-- HTTP/1.1 200 OK (22ms)
+     * Content-Type: plain/text
+     * Content-Length: 6
+     * <-- END HTTP
+     * }</pre>
+     */
+    HEADERS,
+    /**
+     * Logs request and response lines and their respective headers and bodies (if present).
+     * <p>
+     * Example:
+     * <pre>{@code
+     * --> POST /greeting HTTP/1.1
+     * Host: example.com
+     * Content-Type: plain/text
+     * Content-Length: 3
+     *
+     * Hi?
+     * --> END GET
+     *
+     * <-- HTTP/1.1 200 OK (22ms)
+     * Content-Type: plain/text
+     * Content-Length: 6
+     *
+     * Hello!
+     * <-- END HTTP
+     * }</pre>
+     */
+    BODY
+  }
+
+  public interface Logger {
+    void log(String message);
+
+    /** A {@link Logger} defaults output appropriate for the current platform. */
+    Logger DEFAULT = new Logger() {
+      @Override public void log(String message) {
+        Platform.get().log(message);
+      }
+    };
+  }
+
+  public HttpLoggingInterceptor() {
+    this(Logger.DEFAULT);
+  }
+
+  public HttpLoggingInterceptor(Logger logger) {
+    this.logger = logger;
+  }
+
+  private final Logger logger;
+
+  private volatile Level level = Level.NONE;
+
+  /** Change the level at which this interceptor logs. */
+  public HttpLoggingInterceptor setLevel(Level level) {
+    if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
+    this.level = level;
+    return this;
+  }
+
+  public Level getLevel() {
+    return level;
+  }
+
+  @Override public Response intercept(Chain chain) throws IOException {
+    Level level = this.level;
+
+    Request request = chain.request();
+    if (level == Level.NONE) {
+      return chain.proceed(request);
+    }
+
+    boolean logBody = level == Level.BODY;
+    boolean logHeaders = logBody || level == Level.HEADERS;
+
+    RequestBody requestBody = request.body();
+    boolean hasRequestBody = requestBody != null;
+
+    Connection connection = chain.connection();
+    Protocol protocol = connection != null ? connection.getProtocol() : Protocol.HTTP_1_1;
+    String requestStartMessage =
+        "--> " + request.method() + ' ' + request.httpUrl() + ' ' + protocol(protocol);
+    if (!logHeaders && hasRequestBody) {
+      requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
+    }
+    logger.log(requestStartMessage);
+
+    if (logHeaders) {
+      if (hasRequestBody) {
+        // Request body headers are only present when installed as a network interceptor. Force
+        // them to be included (when available) so there values are known.
+        if (requestBody.contentType() != null) {
+          logger.log("Content-Type: " + requestBody.contentType());
+        }
+        if (requestBody.contentLength() != -1) {
+          logger.log("Content-Length: " + requestBody.contentLength());
+        }
+      }
+
+      Headers headers = request.headers();
+      for (int i = 0, count = headers.size(); i < count; i++) {
+        String name = headers.name(i);
+        // Skip headers from the request body as they are explicitly logged above.
+        if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
+          logger.log(name + ": " + headers.value(i));
+        }
+      }
+
+      if (!logBody || !hasRequestBody) {
+        logger.log("--> END " + request.method());
+      } else if (bodyEncoded(request.headers())) {
+        logger.log("--> END " + request.method() + " (encoded body omitted)");
+      } else {
+        Buffer buffer = new Buffer();
+        requestBody.writeTo(buffer);
+
+        Charset charset = UTF8;
+        MediaType contentType = requestBody.contentType();
+        if (contentType != null) {
+          contentType.charset(UTF8);
+        }
+
+        logger.log("");
+        logger.log(buffer.readString(charset));
+
+        logger.log("--> END " + request.method()
+            + " (" + requestBody.contentLength() + "-byte body)");
+      }
+    }
+
+    long startNs = System.nanoTime();
+    Response response = chain.proceed(request);
+    long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
+
+    ResponseBody responseBody = response.body();
+    logger.log("<-- " + protocol(response.protocol()) + ' ' + response.code() + ' '
+        + response.message() + " (" + tookMs + "ms"
+        + (!logHeaders ? ", " + responseBody.contentLength() + "-byte body" : "") + ')');
+
+    if (logHeaders) {
+      Headers headers = response.headers();
+      for (int i = 0, count = headers.size(); i < count; i++) {
+        logger.log(headers.name(i) + ": " + headers.value(i));
+      }
+
+      if (!logBody || !HttpEngine.hasBody(response)) {
+        logger.log("<-- END HTTP");
+      } else if (bodyEncoded(response.headers())) {
+        logger.log("<-- END HTTP (encoded body omitted)");
+      } else {
+        BufferedSource source = responseBody.source();
+        source.request(Long.MAX_VALUE); // Buffer the entire body.
+        Buffer buffer = source.buffer();
+
+        Charset charset = UTF8;
+        MediaType contentType = responseBody.contentType();
+        if (contentType != null) {
+          charset = contentType.charset(UTF8);
+        }
+
+        if (responseBody.contentLength() != 0) {
+          logger.log("");
+          logger.log(buffer.clone().readString(charset));
+        }
+
+        logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
+      }
+    }
+
+    return response;
+  }
+
+  private boolean bodyEncoded(Headers headers) {
+    String contentEncoding = headers.get("Content-Encoding");
+    return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
+  }
+
+  private static String protocol(Protocol protocol) {
+    return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
+  }
+}
diff --git a/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
new file mode 100644
index 0000000..dbd1e84
--- /dev/null
+++ b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2015 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.logging;
+
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.logging.HttpLoggingInterceptor.Level;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ByteString;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class HttpLoggingInterceptorTest {
+  private static final MediaType PLAIN = MediaType.parse("text/plain; charset=utf-8");
+
+  @Rule public final MockWebServer server = new MockWebServer();
+
+  private final OkHttpClient client = new OkHttpClient();
+  private String host;
+  private HttpUrl url;
+
+  private final LogRecorder networkLogs = new LogRecorder();
+  private final HttpLoggingInterceptor networkInterceptor =
+      new HttpLoggingInterceptor(networkLogs);
+
+  private final LogRecorder applicationLogs = new LogRecorder();
+  private final HttpLoggingInterceptor applicationInterceptor =
+      new HttpLoggingInterceptor(applicationLogs);
+
+  private void setLevel(Level level) {
+    networkInterceptor.setLevel(level);
+    applicationInterceptor.setLevel(level);
+  }
+
+  @Before public void setUp() {
+    client.networkInterceptors().add(networkInterceptor);
+    client.interceptors().add(applicationInterceptor);
+    client.setConnectionPool(null);
+
+    host = server.getHostName() + ":" + server.getPort();
+    url = server.url("/");
+  }
+
+  @Test public void levelGetter() {
+    // The default is NONE.
+    assertEquals(Level.NONE, applicationInterceptor.getLevel());
+
+    for (Level level : Level.values()) {
+      applicationInterceptor.setLevel(level);
+      assertEquals(level, applicationInterceptor.getLevel());
+    }
+  }
+
+  @Test public void setLevelShouldPreventNullValue() {
+    try {
+      applicationInterceptor.setLevel(null);
+      fail();
+    } catch (NullPointerException expected) {
+      assertEquals("level == null. Use Level.NONE instead.", expected.getMessage());
+    }
+  }
+
+  @Test public void setLevelShouldReturnSameInstanceOfInterceptor() {
+    for (Level level : Level.values()) {
+      assertSame(applicationInterceptor, applicationInterceptor.setLevel(level));
+    }
+  }
+
+  @Test public void none() throws IOException {
+    server.enqueue(new MockResponse());
+    client.newCall(request().build()).execute();
+
+    applicationLogs.assertNoMoreLogs();
+    networkLogs.assertNoMoreLogs();
+  }
+
+  @Test public void basicGet() throws IOException {
+    setLevel(Level.BASIC);
+
+    server.enqueue(new MockResponse());
+    client.newCall(request().build()).execute();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void basicPost() throws IOException {
+    setLevel(Level.BASIC);
+
+    server.enqueue(new MockResponse());
+    client.newCall(request().post(RequestBody.create(PLAIN, "Hi?")).build()).execute();
+
+    applicationLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void basicResponseBody() throws IOException {
+    setLevel(Level.BASIC);
+
+    server.enqueue(new MockResponse()
+        .setBody("Hello!")
+        .setHeader("Content-Type", PLAIN));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void headersGet() throws IOException {
+    setLevel(Level.HEADERS);
+
+    server.enqueue(new MockResponse());
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void headersPost() throws IOException {
+    setLevel(Level.HEADERS);
+
+    server.enqueue(new MockResponse());
+    Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+    Response response = client.newCall(request).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void headersPostNoContentType() throws IOException {
+    setLevel(Level.HEADERS);
+
+    server.enqueue(new MockResponse());
+    Request request = request().post(RequestBody.create(null, "Hi?")).build();
+    Response response = client.newCall(request).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void headersPostNoLength() throws IOException {
+    setLevel(Level.HEADERS);
+
+    server.enqueue(new MockResponse());
+    RequestBody body = new RequestBody() {
+      @Override public MediaType contentType() {
+        return PLAIN;
+      }
+
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        sink.writeUtf8("Hi!");
+      }
+    };
+    Response response = client.newCall(request().post(body).build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("Transfer-Encoding: chunked")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END POST")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void headersResponseBody() throws IOException {
+    setLevel(Level.HEADERS);
+
+    server.enqueue(new MockResponse()
+        .setBody("Hello!")
+        .setHeader("Content-Type", PLAIN));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 6")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 6")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyGet() throws IOException {
+    setLevel(Level.BODY);
+
+    server.enqueue(new MockResponse());
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyGet204() throws IOException {
+    setLevel(Level.BODY);
+    bodyGetNoBody(204);
+  }
+
+  @Test public void bodyGet205() throws IOException {
+    setLevel(Level.BODY);
+    bodyGetNoBody(205);
+  }
+
+  private void bodyGetNoBody(int code) throws IOException {
+    server.enqueue(new MockResponse()
+        .setStatus("HTTP/1.1 " + code + " No Content"));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyPost() throws IOException {
+    setLevel(Level.BODY);
+
+    server.enqueue(new MockResponse());
+    Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+    Response response = client.newCall(request).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("")
+        .assertLogEqual("Hi?")
+        .assertLogEqual("--> END POST (3-byte body)")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> POST " + url + " HTTP/1.1")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogEqual("Content-Length: 3")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("")
+        .assertLogEqual("Hi?")
+        .assertLogEqual("--> END POST (3-byte body)")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 0")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (0-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyResponseBody() throws IOException {
+    setLevel(Level.BODY);
+
+    server.enqueue(new MockResponse()
+        .setBody("Hello!")
+        .setHeader("Content-Type", PLAIN));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 6")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("")
+        .assertLogEqual("Hello!")
+        .assertLogEqual("<-- END HTTP (6-byte body)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Length: 6")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("")
+        .assertLogEqual("Hello!")
+        .assertLogEqual("<-- END HTTP (6-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyResponseBodyChunked() throws IOException {
+    setLevel(Level.BODY);
+
+    server.enqueue(new MockResponse()
+        .setChunkedBody("Hello!", 2)
+        .setHeader("Content-Type", PLAIN));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Transfer-encoding: chunked")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("")
+        .assertLogEqual("Hello!")
+        .assertLogEqual("<-- END HTTP (6-byte body)")
+        .assertNoMoreLogs();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Transfer-encoding: chunked")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("")
+        .assertLogEqual("Hello!")
+        .assertLogEqual("<-- END HTTP (6-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  @Test public void bodyResponseNotIdentityEncoded() throws IOException {
+    setLevel(Level.BODY);
+
+    server.enqueue(new MockResponse()
+        .setHeader("Content-Encoding", "gzip")
+        .setHeader("Content-Type", PLAIN)
+        .setBody(new Buffer().write(ByteString.decodeBase64(
+            "H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA="))));
+    Response response = client.newCall(request().build()).execute();
+    response.body().close();
+
+    networkLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("Host: " + host)
+        .assertLogEqual("Connection: Keep-Alive")
+        .assertLogEqual("Accept-Encoding: gzip")
+        .assertLogMatch("User-Agent: okhttp/.+")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Encoding: gzip")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("Content-Length: \\d+")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("<-- END HTTP (encoded body omitted)")
+        .assertNoMoreLogs();
+
+    applicationLogs
+        .assertLogEqual("--> GET " + url + " HTTP/1.1")
+        .assertLogEqual("--> END GET")
+        .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+        .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+        .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+        .assertLogMatch("OkHttp-Received-Millis: \\d+")
+        .assertLogEqual("")
+        .assertLogEqual("Hello, Hello, Hello")
+        .assertLogEqual("<-- END HTTP (19-byte body)")
+        .assertNoMoreLogs();
+  }
+
+  private Request.Builder request() {
+    return new Request.Builder().url(url);
+  }
+
+  private static class LogRecorder implements HttpLoggingInterceptor.Logger {
+    private final List<String> logs = new ArrayList<>();
+    private int index;
+
+    LogRecorder assertLogEqual(String expected) {
+      assertTrue("No more messages found", index < logs.size());
+      String actual = logs.get(index++);
+      assertEquals(expected, actual);
+      return this;
+    }
+
+    LogRecorder assertLogMatch(String pattern) {
+      assertTrue("No more messages found", index < logs.size());
+      String actual = logs.get(index++);
+      assertTrue("<" + actual + "> did not match pattern <" + pattern + ">",
+          Pattern.matches(pattern, actual));
+      return this;
+    }
+
+    void assertNoMoreLogs() {
+      assertTrue("More messages remain: " + logs.subList(index, logs.size()), index == logs.size());
+    }
+
+    @Override public void log(String message) {
+      logs.add(message);
+    }
+  }
+}
diff --git a/okhttp-testing-support/pom.xml b/okhttp-testing-support/pom.xml
index 654b0e3..42b4701 100644
--- a/okhttp-testing-support/pom.xml
+++ b/okhttp-testing-support/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-testing-support</artifactId>
diff --git a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
index 3a043cb..6498fe8 100644
--- a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
+++ b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
@@ -18,32 +18,95 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import okio.Buffer;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
 import okio.Sink;
 import okio.Source;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
 /** A simple file system where all files are held in memory. Not safe for concurrent use. */
-public final class InMemoryFileSystem implements FileSystem {
+public final class InMemoryFileSystem implements FileSystem, TestRule {
   private final Map<File, Buffer> files = new LinkedHashMap<>();
+  private final Map<Source, File> openSources = new IdentityHashMap<>();
+  private final Map<Sink, File> openSinks = new IdentityHashMap<>();
+
+  @Override public Statement apply(final Statement base, Description description) {
+    return new Statement() {
+      @Override public void evaluate() throws Throwable {
+        base.evaluate();
+        ensureResourcesClosed();
+      }
+    };
+  }
+
+  public void ensureResourcesClosed() {
+    List<String> openResources = new ArrayList<>();
+    for (File file : openSources.values()) {
+      openResources.add("Source for " + file);
+    }
+    for (File file : openSinks.values()) {
+      openResources.add("Sink for " + file);
+    }
+    if (!openResources.isEmpty()) {
+      StringBuilder builder = new StringBuilder("Resources acquired but not closed:");
+      for (String resource : openResources) {
+        builder.append("\n * ").append(resource);
+      }
+      throw new IllegalStateException(builder.toString());
+    }
+  }
 
   @Override public Source source(File file) throws FileNotFoundException {
     Buffer result = files.get(file);
     if (result == null) throw new FileNotFoundException();
-    return result.clone();
+
+    final Source source = result.clone();
+    openSources.put(source, file);
+
+    return new ForwardingSource(source) {
+      @Override public void close() throws IOException {
+        openSources.remove(source);
+        super.close();
+      }
+    };
   }
 
   @Override public Sink sink(File file) throws FileNotFoundException {
-    Buffer result = new Buffer();
-    files.put(file, result);
-    return result;
+    return sink(file, false);
   }
 
   @Override public Sink appendingSink(File file) throws FileNotFoundException {
-    Buffer result = files.get(file);
-    return result != null ? result : sink(file);
+    return sink(file, true);
+  }
+
+  private Sink sink(File file, boolean appending) {
+    Buffer result = null;
+    if (appending) {
+      result = files.get(file);
+    }
+    if (result == null) {
+      result = new Buffer();
+    }
+    files.put(file, result);
+
+    final Sink sink = result;
+    openSinks.put(sink, file);
+
+    return new ForwardingSink(sink) {
+      @Override public void close() throws IOException {
+        openSinks.remove(sink);
+        super.close();
+      }
+    };
   }
 
   @Override public void delete(File file) throws IOException {
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
index 2bb1982..911afd2 100644
--- a/okhttp-tests/pom.xml
+++ b/okhttp-tests/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-tests</artifactId>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
index 44c39a8..1e623f0 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
@@ -26,6 +26,7 @@
 import static org.junit.Assert.assertFalse;
 
 public final class AddressTest {
+  private Dns dns = Dns.SYSTEM;
   private SocketFactory socketFactory = SocketFactory.getDefault();
   private Authenticator authenticator = AuthenticatorAdapter.INSTANCE;
   private List<Protocol> protocols = Util.immutableList(Protocol.HTTP_1_1);
@@ -33,18 +34,18 @@
   private RecordingProxySelector proxySelector = new RecordingProxySelector();
 
   @Test public void equalsAndHashcode() throws Exception {
-    Address a = new Address("square.com", 80, socketFactory, null, null, null,
+    Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
         authenticator, null, protocols, connectionSpecs, proxySelector);
-    Address b = new Address("square.com", 80, socketFactory, null, null, null,
+    Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
         authenticator, null, protocols, connectionSpecs, proxySelector);
     assertEquals(a, b);
     assertEquals(a.hashCode(), b.hashCode());
   }
 
   @Test public void differentProxySelectorsAreDifferent() throws Exception {
-    Address a = new Address("square.com", 80, socketFactory, null, null, null,
+    Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
         authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
-    Address b = new Address("square.com", 80, socketFactory, null, null, null,
+    Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
         authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
     assertFalse(a.equals(b));
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
index b9e1d50..c762862 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
@@ -19,7 +19,6 @@
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
 import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -76,9 +75,9 @@
 
   @Rule public MockWebServer server = new MockWebServer();
   @Rule public MockWebServer server2 = new MockWebServer();
+  @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
 
   private final SSLContext sslContext = SslContextBuilder.localhost();
-  private final FileSystem fileSystem = new InMemoryFileSystem();
   private final OkHttpClient client = new OkHttpClient();
   private Cache cache;
   private final CookieManager cookieManager = new CookieManager();
@@ -93,6 +92,7 @@
   @After public void tearDown() throws Exception {
     ResponseCache.setDefault(null);
     CookieHandler.setDefault(null);
+    cache.delete();
   }
 
   /**
@@ -266,7 +266,7 @@
     Principal localPrincipal = response1.handshake().localPrincipal();
 
     Response response2 = client.newCall(request).execute(); // Cached!
-    assertEquals("ABC", response2.body().source().readUtf8());
+    assertEquals("ABC", response2.body().string());
 
     assertEquals(2, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
@@ -462,6 +462,26 @@
     assertEquals("b", get(url).body().string());
   }
 
+  /** https://github.com/square/okhttp/issues/2198 */
+  @Test public void cachedRedirect() throws IOException {
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Cache-Control: max-age=60")
+        .addHeader("Location: /bar"));
+    server.enqueue(new MockResponse()
+        .setBody("ABC"));
+    server.enqueue(new MockResponse()
+        .setBody("ABC"));
+
+    Request request1 = new Request.Builder().url(server.url("/")).build();
+    Response response1 = client.newCall(request1).execute();
+    assertEquals("ABC", response1.body().string());
+
+    Request request2 = new Request.Builder().url(server.url("/")).build();
+    Response response2 = client.newCall(request2).execute();
+    assertEquals("ABC", response2.body().string());
+  }
+
   @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
     testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
   }
@@ -1007,7 +1027,7 @@
 
     assertEquals("A", get(server.url("/")).body().string());
     assertEquals("A", get(server.url("/")).body().string());
-    assertEquals(1, client.getConnectionPool().getConnectionCount());
+    assertEquals(1, client.getConnectionPool().getIdleConnectionCount());
   }
 
   @Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1873,6 +1893,7 @@
 
     Response response = get(server.url("/"));
     assertEquals("A", response.header(""));
+    assertEquals("body", response.body().string());
   }
 
   /**
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index 051eae4..34ca42a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -15,14 +15,13 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.DoubleInetAddressNetwork;
-import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.DoubleInetAddressDns;
 import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.Version;
-import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.http.FakeDns;
 import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.Dispatcher;
 import com.squareup.okhttp.mockwebserver.MockResponse;
@@ -88,9 +87,9 @@
   @Rule public final TestRule timeout = new Timeout(30_000);
   @Rule public final MockWebServer server = new MockWebServer();
   @Rule public final MockWebServer server2 = new MockWebServer();
+  @Rule public final InMemoryFileSystem fileSystem = new InMemoryFileSystem();
 
   private SSLContext sslContext = SslContextBuilder.localhost();
-  private FileSystem fileSystem = new InMemoryFileSystem();
   private OkHttpClient client = new OkHttpClient();
   private RecordingCallback callback = new RecordingCallback();
   private TestLogHandler logHandler = new TestLogHandler();
@@ -172,14 +171,48 @@
         .assertNotSuccessful();
   }
 
+  @Test public void get_HTTP_2() throws Exception {
+    enableProtocol(Protocol.HTTP_2);
+    get();
+  }
+
+  @Test public void get_HTTPS() throws Exception {
+    enableTls();
+    get();
+  }
+
   @Test public void get_SPDY_3() throws Exception {
     enableProtocol(Protocol.SPDY_3);
     get();
   }
 
-  @Test public void get_HTTP_2() throws Exception {
+  @Test public void repeatedHeaderNames() throws Exception {
+    server.enqueue(new MockResponse()
+        .addHeader("B", "123")
+        .addHeader("B", "234"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .addHeader("A", "345")
+        .addHeader("A", "456")
+        .build();
+
+    executeSynchronously(request)
+        .assertCode(200)
+        .assertHeader("B", "123", "234");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals(Arrays.asList("345", "456"), recordedRequest.getHeaders().values("A"));
+  }
+
+  @Test public void repeatedHeaderNames_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    repeatedHeaderNames();
+  }
+
+  @Test public void repeatedHeaderNames_HTTP_2() throws Exception {
     enableProtocol(Protocol.HTTP_2);
-    get();
+    repeatedHeaderNames();
   }
 
   @Test public void getWithRequestBody() throws Exception {
@@ -212,8 +245,8 @@
     assertNull(recordedRequest.getHeader("Content-Length"));
   }
 
-  @Test public void head_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void head_HTTPS() throws Exception {
+    enableTls();
     head();
   }
 
@@ -222,6 +255,11 @@
     head();
   }
 
+  @Test public void head_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    head();
+  }
+
   @Test public void post() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -241,8 +279,8 @@
     assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void post_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void post_HTTPS() throws Exception {
+    enableTls();
     post();
   }
 
@@ -251,6 +289,11 @@
     post();
   }
 
+  @Test public void post_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    post();
+  }
+
   @Test public void postZeroLength() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -270,8 +313,8 @@
     assertEquals(null, recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void postZeroLength_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void postZerolength_HTTPS() throws Exception {
+    enableTls();
     postZeroLength();
   }
 
@@ -280,12 +323,17 @@
     postZeroLength();
   }
 
+  @Test public void postZeroLength_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    postZeroLength();
+  }
+
   @Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception {
     postBodyRetransmittedAfterAuthorizationFail("abc");
   }
 
-  @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+    enableTls();
     postBodyRetransmittedAfterAuthorizationFail("abc");
   }
 
@@ -294,13 +342,18 @@
     postBodyRetransmittedAfterAuthorizationFail("abc");
   }
 
+  @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    postBodyRetransmittedAfterAuthorizationFail("abc");
+  }
+
   /** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */
   @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail() throws Exception {
     postBodyRetransmittedAfterAuthorizationFail("");
   }
 
-  @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+    enableTls();
     postBodyRetransmittedAfterAuthorizationFail("");
   }
 
@@ -309,6 +362,11 @@
     postBodyRetransmittedAfterAuthorizationFail("");
   }
 
+  @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    postBodyRetransmittedAfterAuthorizationFail("");
+  }
+
   private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception {
     server.enqueue(new MockResponse().setResponseCode(401));
     server.enqueue(new MockResponse());
@@ -385,8 +443,8 @@
     assertEquals(null, recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void delete_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void delete_HTTPS() throws Exception {
+    enableTls();
     delete();
   }
 
@@ -395,6 +453,11 @@
     delete();
   }
 
+  @Test public void delete_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    delete();
+  }
+
   @Test public void deleteWithRequestBody() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -431,8 +494,8 @@
     assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void put_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void put_HTTPS() throws Exception {
+    enableTls();
     put();
   }
 
@@ -441,6 +504,11 @@
     put();
   }
 
+  @Test public void put_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    put();
+  }
+
   @Test public void patch() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -460,13 +528,18 @@
     assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void patch_SPDY_3() throws Exception {
-    enableProtocol(Protocol.SPDY_3);
+  @Test public void patch_HTTP_2() throws Exception {
+    enableProtocol(Protocol.HTTP_2);
     patch();
   }
 
-  @Test public void patch_HTTP_2() throws Exception {
-    enableProtocol(Protocol.HTTP_2);
+  @Test public void patch_HTTPS() throws Exception {
+    enableTls();
+    patch();
+  }
+
+  @Test public void patch_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
     patch();
   }
 
@@ -497,7 +570,8 @@
         .build();
 
     Call call = client.newCall(request);
-    call.execute();
+    Response response = call.execute();
+    response.body().close();
 
     try {
       call.execute();
@@ -675,17 +749,19 @@
       long elapsedNanos = System.nanoTime() - startNanos;
       long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
       assertTrue(String.format("Timed out: %sms", elapsedMillis), elapsedMillis < 500);
+    } finally {
+      bodySource.close();
     }
   }
 
-  // https://github.com/square/okhttp/issues/442
+  /** https://github.com/square/okhttp/issues/442 */
   @Test public void timeoutsNotRetried() throws Exception {
     server.enqueue(new MockResponse()
         .setSocketPolicy(SocketPolicy.NO_RESPONSE));
     server.enqueue(new MockResponse()
         .setBody("unreachable!"));
 
-    Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+    client.setDns(new DoubleInetAddressDns());
     client.setReadTimeout(100, TimeUnit.MILLISECONDS);
 
     Request request = new Request.Builder().url(server.url("/")).build();
@@ -697,6 +773,20 @@
     }
   }
 
+  /** https://github.com/square/okhttp/issues/1801 */
+  @Test public void asyncCallEngineInitialized() throws Exception {
+    OkHttpClient c = new OkHttpClient();
+    c.interceptors().add(new Interceptor() {
+      @Override public Response intercept(Chain chain) throws IOException {
+        throw new IOException();
+      }
+    });
+    Request request = new Request.Builder().url(server.url("/")).build();
+    c.newCall(request).enqueue(callback);
+    RecordedResponse response = callback.await(request.httpUrl());
+    assertEquals(request, response.request);
+  }
+
   @Test public void reusedSinksGetIndependentTimeoutInstances() throws Exception {
     server.enqueue(new MockResponse());
     server.enqueue(new MockResponse());
@@ -764,27 +854,21 @@
   }
 
   @Test public void tls() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
+    enableTls();
     server.enqueue(new MockResponse()
         .setBody("abc")
         .addHeader("Content-Type: text/plain"));
 
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(new RecordingHostnameVerifier());
-
     executeSynchronously(new Request.Builder().url(server.url("/")).build())
         .assertHandshake();
   }
 
   @Test public void tls_Async() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
+    enableTls();
     server.enqueue(new MockResponse()
         .setBody("abc")
         .addHeader("Content-Type: text/plain"));
 
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(new RecordingHostnameVerifier());
-
     Request request = new Request.Builder()
         .url(server.url("/"))
         .build();
@@ -794,25 +878,28 @@
   }
 
   @Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception {
-    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+    server.enqueue(new MockResponse().setBody("seed connection pool"));
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
     server.enqueue(new MockResponse().setBody("retry success"));
 
-    Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+    client.setDns(new DoubleInetAddressDns());
     assertTrue(client.getRetryOnConnectionFailure());
 
     Request request = new Request.Builder().url(server.url("/")).build();
-    Response response = client.newCall(request).execute();
-    assertEquals("retry success", response.body().string());
+    executeSynchronously(request).assertBody("seed connection pool");
+    executeSynchronously(request).assertBody("retry success");
   }
 
   @Test public void noRecoverWhenRetryOnConnectionFailureIsFalse() throws Exception {
-    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+    server.enqueue(new MockResponse().setBody("seed connection pool"));
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
     server.enqueue(new MockResponse().setBody("unreachable!"));
 
-    Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+    client.setDns(new DoubleInetAddressDns());
     client.setRetryOnConnectionFailure(false);
 
     Request request = new Request.Builder().url(server.url("/")).build();
+    executeSynchronously(request).assertBody("seed connection pool");
     try {
       // If this succeeds, too many requests were made.
       client.newCall(request).execute();
@@ -828,7 +915,7 @@
 
     suppressTlsFallbackScsv(client);
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+    client.setDns(new SingleInetAddressDns());
 
     executeSynchronously(new Request.Builder().url(server.url("/")).build())
         .assertBody("abc");
@@ -850,7 +937,7 @@
         new RecordingSSLSocketFactory(sslContext.getSocketFactory());
     client.setSslSocketFactory(clientSocketFactory);
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+    client.setDns(new SingleInetAddressDns());
 
     Request request = new Request.Builder().url(server.url("/")).build();
     try {
@@ -890,7 +977,7 @@
 
     suppressTlsFallbackScsv(client);
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+    client.setDns(new SingleInetAddressDns());
 
     Request request = new Request.Builder().url(server.url("/")).build();
     try {
@@ -920,26 +1007,24 @@
   }
 
   @Test public void setFollowSslRedirectsFalse() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
-    server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location: http://square.com"));
+    enableTls();
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: http://square.com"));
 
     client.setFollowSslRedirects(false);
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(new RecordingHostnameVerifier());
 
     Request request = new Request.Builder().url(server.url("/")).build();
     Response response = client.newCall(request).execute();
     assertEquals(301, response.code());
+    response.body().close();
   }
 
   @Test public void matchingPinnedCertificate() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
+    enableTls();
     server.enqueue(new MockResponse());
     server.enqueue(new MockResponse());
 
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(new RecordingHostnameVerifier());
-
     // Make a first request without certificate pinning. Use it to collect certificates to pin.
     Request request1 = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request1).execute();
@@ -947,21 +1032,20 @@
     for (Certificate certificate : response1.handshake().peerCertificates()) {
       certificatePinnerBuilder.add(server.getHostName(), CertificatePinner.pin(certificate));
     }
+    response1.body().close();
 
     // Make another request with certificate pinning. It should complete normally.
     client.setCertificatePinner(certificatePinnerBuilder.build());
     Request request2 = new Request.Builder().url(server.url("/")).build();
     Response response2 = client.newCall(request2).execute();
     assertNotSame(response2.handshake(), response1.handshake());
+    response2.body().close();
   }
 
   @Test public void unmatchingPinnedCertificate() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
+    enableTls();
     server.enqueue(new MockResponse());
 
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(new RecordingHostnameVerifier());
-
     // Pin publicobject.com's cert.
     client.setCertificatePinner(new CertificatePinner.Builder()
         .add(server.getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -1293,6 +1377,27 @@
     assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
   }
 
+  @Test public void propfindRedirectsToPropfind() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: /page2")
+        .setBody("This page has moved!"));
+    server.enqueue(new MockResponse().setBody("Page 2"));
+
+    Response response = client.newCall(new Request.Builder()
+        .url(server.url("/page1"))
+        .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), "Request Body"))
+        .build()).execute();
+    assertEquals("Page 2", response.body().string());
+
+    RecordedRequest page1 = server.takeRequest();
+    assertEquals("PROPFIND /page1 HTTP/1.1", page1.getRequestLine());
+    assertEquals("Request Body", page1.getBody().readUtf8());
+
+    RecordedRequest page2 = server.takeRequest();
+    assertEquals("PROPFIND /page2 HTTP/1.1", page2.getRequestLine());
+  }
+
   @Test public void redirectsDoNotIncludeTooManyCookies() throws Exception {
     server2.enqueue(new MockResponse().setBody("Page 2"));
     server.enqueue(new MockResponse()
@@ -1511,13 +1616,13 @@
   }
 
   @Test public void cancelTagImmediatelyAfterEnqueue() throws Exception {
+    server.enqueue(new MockResponse());
     Call call = client.newCall(new Request.Builder()
         .url(server.url("/a"))
         .tag("request")
         .build());
     call.enqueue(callback);
     client.cancel("request");
-    assertEquals(0, server.getRequestCount());
     callback.await(server.url("/a")).assertFailure("Canceled");
   }
 
@@ -1559,6 +1664,11 @@
     }
   }
 
+  @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTPS() throws Exception {
+    enableTls();
+    cancelInFlightBeforeResponseReadThrowsIOE();
+  }
+
   @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception {
     enableProtocol(Protocol.HTTP_2);
     cancelInFlightBeforeResponseReadThrowsIOE();
@@ -1596,6 +1706,11 @@
     callback.await(requestB.httpUrl()).assertFailure("Canceled");
   }
 
+  @Test public void canceledBeforeIOSignalsOnFailure_HTTPS() throws Exception {
+    enableTls();
+    canceledBeforeIOSignalsOnFailure();
+  }
+
   @Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception {
     enableProtocol(Protocol.HTTP_2);
     canceledBeforeIOSignalsOnFailure();
@@ -1623,6 +1738,11 @@
         "Socket closed");
   }
 
+  @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTPS() throws Exception {
+    enableTls();
+    canceledBeforeResponseReadSignalsOnFailure();
+  }
+
   @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTP_2() throws Exception {
     enableProtocol(Protocol.HTTP_2);
     canceledBeforeResponseReadSignalsOnFailure();
@@ -1670,6 +1790,12 @@
     assertFalse(failureRef.get());
   }
 
+  @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTPS()
+      throws Exception {
+    enableTls();
+    canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce();
+  }
+
   @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2()
       throws Exception {
     enableProtocol(Protocol.HTTP_2);
@@ -1729,6 +1855,22 @@
         .assertRequestHeader("Accept-Encoding", "gzip");
   }
 
+  /** https://github.com/square/okhttp/issues/1927 */
+  @Test public void gzipResponseAfterAuthenticationChallenge() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(401));
+    server.enqueue(new MockResponse()
+        .setBody(gzip("abcabcabc"))
+        .addHeader("Content-Encoding: gzip"));
+    client.setAuthenticator(new RecordingOkAuthenticator("password"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    executeSynchronously(request)
+        .assertBody("abcabcabc");
+  }
+
   @Test public void asyncResponseCanBeConsumedLater() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
     server.enqueue(new MockResponse().setBody("def"));
@@ -1842,6 +1984,178 @@
         .assertHeader("", "ef");
   }
 
+  @Test public void customDns() throws Exception {
+    // Configure a DNS that returns our MockWebServer for every hostname.
+    FakeDns dns = new FakeDns();
+    dns.addresses(Dns.SYSTEM.lookup(server.url("/").host()));
+    client.setDns(dns);
+
+    server.enqueue(new MockResponse());
+    Request request = new Request.Builder()
+        .url(server.url("/").newBuilder().host("android.com").build())
+        .build();
+    executeSynchronously(request).assertCode(200);
+
+    dns.assertRequests("android.com");
+  }
+
+  /** We had a bug where failed HTTP/2 calls could break the entire connection. */
+  @Test public void failingCallsDoNotInterfereWithConnection() throws Exception {
+    enableProtocol(Protocol.HTTP_2);
+
+    server.enqueue(new MockResponse().setBody("Response 1"));
+    server.enqueue(new MockResponse().setBody("Response 2"));
+
+    RequestBody requestBody = new RequestBody() {
+      @Override public MediaType contentType() {
+        return null;
+      }
+
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        sink.writeUtf8("abc");
+        sink.flush();
+
+        makeFailingCall();
+
+        sink.writeUtf8("def");
+        sink.flush();
+      }
+    };
+    Call call = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .post(requestBody)
+        .build());
+    assertEquals("Response 1", call.execute().body().string());
+  }
+
+  /** Test which headers are sent unencrypted to the HTTP proxy. */
+  @Test public void proxyConnectOmitsApplicationHeaders() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(new MockResponse()
+        .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+        .clearHeaders());
+    server.enqueue(new MockResponse()
+        .setBody("encrypted response from the origin server"));
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setProxy(server.toProxyAddress());
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+    client.setHostnameVerifier(hostnameVerifier);
+
+    Request request = new Request.Builder()
+        .url("https://android.com/foo")
+        .header("Private", "Secret")
+        .header("User-Agent", "App 1.0")
+        .build();
+    Response response = client.newCall(request).execute();
+    assertEquals("encrypted response from the origin server", response.body().string());
+
+    RecordedRequest connect = server.takeRequest();
+    assertNull(connect.getHeader("Private"));
+    assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
+    assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
+    assertEquals("android.com", connect.getHeader("Host"));
+
+    RecordedRequest get = server.takeRequest();
+    assertEquals("Secret", get.getHeader("Private"));
+    assertEquals("App 1.0", get.getHeader("User-Agent"));
+
+    assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+  }
+
+  /** Respond to a proxy authorization challenge. */
+  @Test public void proxyAuthenticateOnConnect() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(new MockResponse()
+        .setResponseCode(407)
+        .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
+    server.enqueue(new MockResponse()
+        .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+        .clearHeaders());
+    server.enqueue(new MockResponse()
+        .setBody("response body"));
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setProxy(server.toProxyAddress());
+    client.setAuthenticator(new RecordingOkAuthenticator("password"));
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    Request request = new Request.Builder()
+        .url("https://android.com/foo")
+        .build();
+    Response response = client.newCall(request).execute();
+    assertEquals("response body", response.body().string());
+
+    RecordedRequest connect1 = server.takeRequest();
+    assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+    assertNull(connect1.getHeader("Proxy-Authorization"));
+
+    RecordedRequest connect2 = server.takeRequest();
+    assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+    assertEquals("password", connect2.getHeader("Proxy-Authorization"));
+
+    RecordedRequest get = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+    assertNull(get.getHeader("Proxy-Authorization"));
+  }
+
+  /**
+   * Confirm that we don't send the Proxy-Authorization header from the request to the proxy server.
+   * We used to have that behavior but it is problematic because unrelated requests end up sharing
+   * credentials. Worse, that approach leaks proxy credentials to the origin server.
+   */
+  @Test public void noProactiveProxyAuthorization() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(new MockResponse()
+        .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+        .clearHeaders());
+    server.enqueue(new MockResponse()
+        .setBody("response body"));
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setProxy(server.toProxyAddress());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    Request request = new Request.Builder()
+        .url("https://android.com/foo")
+        .header("Proxy-Authorization", "password")
+        .build();
+    Response response = client.newCall(request).execute();
+    assertEquals("response body", response.body().string());
+
+    RecordedRequest connect = server.takeRequest();
+    assertNull(connect.getHeader("Proxy-Authorization"));
+
+    RecordedRequest get = server.takeRequest();
+    assertEquals("password", get.getHeader("Proxy-Authorization"));
+  }
+
+  private void makeFailingCall() {
+    RequestBody requestBody = new RequestBody() {
+      @Override public MediaType contentType() {
+        return null;
+      }
+
+      @Override public long contentLength() throws IOException {
+        return 1;
+      }
+
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        throw new IOException("write body fail!");
+      }
+    };
+    Call call = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .post(requestBody)
+        .build());
+    try {
+      call.execute();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("write body fail!", expected.getMessage());
+    }
+  }
+
   private RecordedResponse executeSynchronously(Request request) throws IOException {
     Response response = client.newCall(request).execute();
     return new RecordedResponse(request, response, null, response.body().string(), null);
@@ -1852,11 +2166,15 @@
    * -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317}
    */
   private void enableProtocol(Protocol protocol) {
+    enableTls();
+    client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+    server.setProtocols(client.getProtocols());
+  }
+
+  private void enableTls() {
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
     server.useHttps(sslContext.getSocketFactory(), false);
-    server.setProtocols(client.getProtocols());
   }
 
   private Buffer gzip(String data) throws IOException {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
index 91b5a59..7f40cd5 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
@@ -15,12 +15,10 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.SslContextBuilder;
 import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
 import java.util.Set;
 import javax.net.ssl.SSLPeerUnverifiedException;
+import com.squareup.okhttp.internal.HeldCertificate;
 import okio.ByteString;
 import org.junit.Test;
 
@@ -32,39 +30,35 @@
 import static org.junit.Assert.fail;
 
 public final class CertificatePinnerTest {
-  static SslContextBuilder sslContextBuilder;
+  static HeldCertificate certA1;
+  static String certA1Pin;
+  static ByteString certA1PinBase64;
 
-  static KeyPair keyPairA;
-  static X509Certificate keypairACertificate1;
-  static String keypairACertificate1Pin;
-  static ByteString keypairACertificate1PinBase64;
+  static HeldCertificate certB1;
+  static String certB1Pin;
+  static ByteString certB1PinBase64;
 
-  static KeyPair keyPairB;
-  static X509Certificate keypairBCertificate1;
-  static String keypairBCertificate1Pin;
-  static ByteString keypairBCertificate1PinBase64;
-
-  static KeyPair keyPairC;
-  static X509Certificate keypairCCertificate1;
-  static String keypairCCertificate1Pin;
+  static HeldCertificate certC1;
+  static String certC1Pin;
 
   static {
     try {
-      sslContextBuilder = new SslContextBuilder("example.com");
+      certA1 = new HeldCertificate.Builder()
+          .serialNumber("100")
+          .build();
+      certA1Pin = CertificatePinner.pin(certA1.certificate);
+      certA1PinBase64 = pinToBase64(certA1Pin);
 
-      keyPairA = sslContextBuilder.generateKeyPair();
-      keypairACertificate1 = sslContextBuilder.selfSignedCertificate(keyPairA, "1");
-      keypairACertificate1Pin = CertificatePinner.pin(keypairACertificate1);
-      keypairACertificate1PinBase64 = pinToBase64(keypairACertificate1Pin);
+      certB1 = new HeldCertificate.Builder()
+          .serialNumber("200")
+          .build();
+      certB1Pin = CertificatePinner.pin(certB1.certificate);
+      certB1PinBase64 = pinToBase64(certB1Pin);
 
-      keyPairB = sslContextBuilder.generateKeyPair();
-      keypairBCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairB, "1");
-      keypairBCertificate1Pin = CertificatePinner.pin(keypairBCertificate1);
-      keypairBCertificate1PinBase64 = pinToBase64(keypairBCertificate1Pin);
-
-      keyPairC = sslContextBuilder.generateKeyPair();
-      keypairCCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairC, "1");
-      keypairCCertificate1Pin = CertificatePinner.pin(keypairCCertificate1);
+      certC1 = new HeldCertificate.Builder()
+          .serialNumber("300")
+          .build();
+      certC1Pin = CertificatePinner.pin(certC1.certificate);
     } catch (GeneralSecurityException e) {
       throw new AssertionError(e);
     }
@@ -94,40 +88,46 @@
 
   /** Multiple certificates generated from the same keypair have the same pin. */
   @Test public void sameKeypairSamePin() throws Exception {
-    X509Certificate keypairACertificate2 = sslContextBuilder.selfSignedCertificate(keyPairA, "2");
-    String keypairACertificate2Pin = CertificatePinner.pin(keypairACertificate2);
+    HeldCertificate heldCertificateA2 = new HeldCertificate.Builder()
+        .keyPair(certA1.keyPair)
+        .serialNumber("101")
+        .build();
+    String keypairACertificate2Pin = CertificatePinner.pin(heldCertificateA2.certificate);
 
-    X509Certificate keypairBCertificate2 = sslContextBuilder.selfSignedCertificate(keyPairB, "2");
-    String keypairBCertificate2Pin = CertificatePinner.pin(keypairBCertificate2);
+    HeldCertificate heldCertificateB2 = new HeldCertificate.Builder()
+        .keyPair(certB1.keyPair)
+        .serialNumber("201")
+        .build();
+    String keypairBCertificate2Pin = CertificatePinner.pin(heldCertificateB2.certificate);
 
-    assertTrue(keypairACertificate1Pin.equals(keypairACertificate2Pin));
-    assertTrue(keypairBCertificate1Pin.equals(keypairBCertificate2Pin));
-    assertFalse(keypairACertificate1Pin.equals(keypairBCertificate1Pin));
+    assertTrue(certA1Pin.equals(keypairACertificate2Pin));
+    assertTrue(certB1Pin.equals(keypairBCertificate2Pin));
+    assertFalse(certA1Pin.equals(certB1Pin));
   }
 
   @Test public void successfulCheck() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("example.com", keypairACertificate1Pin)
+        .add("example.com", certA1Pin)
         .build();
 
-    certificatePinner.check("example.com", keypairACertificate1);
+    certificatePinner.check("example.com", certA1.certificate);
   }
 
   @Test public void successfulMatchAcceptsAnyMatchingCertificate() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("example.com", keypairBCertificate1Pin)
+        .add("example.com", certB1Pin)
         .build();
 
-    certificatePinner.check("example.com", keypairACertificate1, keypairBCertificate1);
+    certificatePinner.check("example.com", certA1.certificate, certB1.certificate);
   }
 
   @Test public void unsuccessfulCheck() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("example.com", keypairACertificate1Pin)
+        .add("example.com", certA1Pin)
         .build();
 
     try {
-      certificatePinner.check("example.com", keypairBCertificate1);
+      certificatePinner.check("example.com", certB1.certificate);
       fail();
     } catch (SSLPeerUnverifiedException expected) {
     }
@@ -135,51 +135,51 @@
 
   @Test public void multipleCertificatesForOneHostname() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+        .add("example.com", certA1Pin, certB1Pin)
         .build();
 
-    certificatePinner.check("example.com", keypairACertificate1);
-    certificatePinner.check("example.com", keypairBCertificate1);
+    certificatePinner.check("example.com", certA1.certificate);
+    certificatePinner.check("example.com", certB1.certificate);
   }
 
   @Test public void multipleHostnamesForOneCertificate() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("example.com", keypairACertificate1Pin)
-        .add("www.example.com", keypairACertificate1Pin)
+        .add("example.com", certA1Pin)
+        .add("www.example.com", certA1Pin)
         .build();
 
-    certificatePinner.check("example.com", keypairACertificate1);
-    certificatePinner.check("www.example.com", keypairACertificate1);
+    certificatePinner.check("example.com", certA1.certificate);
+    certificatePinner.check("www.example.com", certA1.certificate);
   }
 
   @Test public void absentHostnameMatches() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder().build();
-    certificatePinner.check("example.com", keypairACertificate1);
+    certificatePinner.check("example.com", certA1.certificate);
   }
 
   @Test public void successfulCheckForWildcardHostname() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
+        .add("*.example.com", certA1Pin)
         .build();
 
-    certificatePinner.check("a.example.com", keypairACertificate1);
+    certificatePinner.check("a.example.com", certA1.certificate);
   }
 
   @Test public void successfulMatchAcceptsAnyMatchingCertificateForWildcardHostname() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairBCertificate1Pin)
+        .add("*.example.com", certB1Pin)
         .build();
 
-    certificatePinner.check("a.example.com", keypairACertificate1, keypairBCertificate1);
+    certificatePinner.check("a.example.com", certA1.certificate, certB1.certificate);
   }
 
   @Test public void unsuccessfulCheckForWildcardHostname() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
+        .add("*.example.com", certA1Pin)
         .build();
 
     try {
-      certificatePinner.check("a.example.com", keypairBCertificate1);
+      certificatePinner.check("a.example.com", certB1.certificate);
       fail();
     } catch (SSLPeerUnverifiedException expected) {
     }
@@ -187,31 +187,31 @@
 
   @Test public void multipleCertificatesForOneWildcardHostname() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+        .add("*.example.com", certA1Pin, certB1Pin)
         .build();
 
-    certificatePinner.check("a.example.com", keypairACertificate1);
-    certificatePinner.check("a.example.com", keypairBCertificate1);
+    certificatePinner.check("a.example.com", certA1.certificate);
+    certificatePinner.check("a.example.com", certB1.certificate);
   }
 
   @Test public void successfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
-        .add("a.example.com", keypairBCertificate1Pin)
+        .add("*.example.com", certA1Pin)
+        .add("a.example.com", certB1Pin)
         .build();
 
-    certificatePinner.check("a.example.com", keypairACertificate1);
-    certificatePinner.check("a.example.com", keypairBCertificate1);
+    certificatePinner.check("a.example.com", certA1.certificate);
+    certificatePinner.check("a.example.com", certB1.certificate);
   }
 
   @Test public void unsuccessfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
-        .add("a.example.com", keypairBCertificate1Pin)
+        .add("*.example.com", certA1Pin)
+        .add("a.example.com", certB1Pin)
         .build();
 
     try {
-      certificatePinner.check("a.example.com", keypairCCertificate1);
+      certificatePinner.check("a.example.com", certC1.certificate);
       fail();
     } catch (SSLPeerUnverifiedException expected) {
     }
@@ -219,32 +219,32 @@
 
   @Test public void successfulFindMatchingPins() {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("first.com", keypairACertificate1Pin, keypairBCertificate1Pin)
-        .add("second.com", keypairCCertificate1Pin)
+        .add("first.com", certA1Pin, certB1Pin)
+        .add("second.com", certC1Pin)
         .build();
 
-    Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
-    Set<ByteString> matchedPins  = certificatePinner.findMatchingPins("first.com");
+    Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+    Set<ByteString> matchedPins = certificatePinner.findMatchingPins("first.com");
 
     assertEquals(expectedPins, matchedPins);
   }
 
   @Test public void successfulFindMatchingPinsForWildcardAndDirectCertificates() {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
-        .add("a.example.com", keypairBCertificate1Pin)
-        .add("b.example.com", keypairCCertificate1Pin)
+        .add("*.example.com", certA1Pin)
+        .add("a.example.com", certB1Pin)
+        .add("b.example.com", certC1Pin)
         .build();
 
-    Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
-    Set<ByteString> matchedPins  = certificatePinner.findMatchingPins("a.example.com");
+    Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+    Set<ByteString> matchedPins = certificatePinner.findMatchingPins("a.example.com");
 
     assertEquals(expectedPins, matchedPins);
   }
 
   @Test public void wildcardHostnameShouldNotMatchThroughDot() throws Exception {
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
-        .add("*.example.com", keypairACertificate1Pin)
+        .add("*.example.com", certA1Pin)
         .build();
 
     assertNull(certificatePinner.findMatchingPins("example.com"));
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index d528c7a..e845eec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2015 Square, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,570 +15,197 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.RecordingProxySelector;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.testing.RecordingHostnameVerifier;
-import java.io.IOException;
-import java.net.InetAddress;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Executor;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
 import javax.net.SocketFactory;
-import javax.net.ssl.SSLContext;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public final class ConnectionPoolTest {
-  static {
-    Internal.initializeInstanceForTests();
-  }
-
-  private static final List<ConnectionSpec> CONNECTION_SPECS = Util.immutableList(
-      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
-
-  private static final int KEEP_ALIVE_DURATION_MS = 5000;
-
-  private SSLContext sslContext = SslContextBuilder.localhost();
-  private MockWebServer spdyServer;
-  private InetSocketAddress spdySocketAddress;
-  private Address spdyAddress;
-
-  private MockWebServer httpServer;
-  private Address httpAddress;
-  private InetSocketAddress httpSocketAddress;
-
-  private ConnectionPool pool;
-  private FakeExecutor cleanupExecutor;
-  private Connection httpA;
-  private Connection httpB;
-  private Connection httpC;
-  private Connection httpD;
-  private Connection httpE;
-  private Connection spdyA;
-
-  private Object owner;
-
-  @Before public void setUp() throws Exception {
-    setUp(2);
-  }
-
-  private void setUp(int poolSize) throws Exception {
-    SocketFactory socketFactory = SocketFactory.getDefault();
-    RecordingProxySelector proxySelector = new RecordingProxySelector();
-
-    spdyServer = new MockWebServer();
-    httpServer = new MockWebServer();
-    spdyServer.useHttps(sslContext.getSocketFactory(), false);
-
-    httpServer.start();
-    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
-        null, null, AuthenticatorAdapter.INSTANCE, null,
-        Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), CONNECTION_SPECS, proxySelector);
-    httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
-        httpServer.getPort());
-
-    spdyServer.start();
-    spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
-        sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT,
-        AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1),
-        CONNECTION_SPECS, proxySelector);
-    spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
-        spdyServer.getPort());
-
-    Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress);
-    Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress);
-    pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
-    // Disable the automatic execution of the cleanup.
-    cleanupExecutor = new FakeExecutor();
-    pool.replaceCleanupExecutorForTests(cleanupExecutor);
-    httpA = new Connection(pool, httpRoute);
-    httpA.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    httpB = new Connection(pool, httpRoute);
-    httpB.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    httpC = new Connection(pool, httpRoute);
-    httpC.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    httpD = new Connection(pool, httpRoute);
-    httpD.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    httpE = new Connection(pool, httpRoute);
-    httpE.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    spdyA = new Connection(pool, spdyRoute);
-    spdyA.connect(20000, 20000, 2000, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-
-    owner = new Object();
-    httpA.setOwner(owner);
-    httpB.setOwner(owner);
-    httpC.setOwner(owner);
-    httpD.setOwner(owner);
-    httpE.setOwner(owner);
-  }
-
-  @After public void tearDown() throws Exception {
-    httpServer.shutdown();
-    spdyServer.shutdown();
-
-    Util.closeQuietly(httpA.getSocket());
-    Util.closeQuietly(httpB.getSocket());
-    Util.closeQuietly(httpC.getSocket());
-    Util.closeQuietly(httpD.getSocket());
-    Util.closeQuietly(httpE.getSocket());
-    Util.closeQuietly(spdyA.getSocket());
-  }
-
-  private void resetWithPoolSize(int poolSize) throws Exception {
-    tearDown();
-    setUp(poolSize);
-  }
-
-  @Test public void poolSingleHttpConnection() throws Exception {
-    resetWithPoolSize(1);
-    Connection connection = pool.get(httpAddress);
-    assertNull(connection);
-
-    connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
-    connection.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-    connection.setOwner(owner);
-    assertEquals(0, pool.getConnectionCount());
-
-    pool.recycle(connection);
-    assertNull(connection.getOwner());
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getMultiplexedConnectionCount());
-
-    Connection recycledConnection = pool.get(httpAddress);
-    assertNull(connection.getOwner());
-    assertEquals(connection, recycledConnection);
-    assertTrue(recycledConnection.isAlive());
-
-    recycledConnection = pool.get(httpAddress);
-    assertNull(recycledConnection);
-  }
-
-  @Test public void getDoesNotScheduleCleanup() {
-    Connection connection = pool.get(httpAddress);
-    assertNull(connection);
-    cleanupExecutor.assertExecutionScheduled(false);
-  }
-
-  @Test public void recycleSchedulesCleanup() {
-    cleanupExecutor.assertExecutionScheduled(false);
-    pool.recycle(httpA);
-    cleanupExecutor.assertExecutionScheduled(true);
-  }
-
-  @Test public void shareSchedulesCleanup() {
-    cleanupExecutor.assertExecutionScheduled(false);
-    pool.share(spdyA);
-    cleanupExecutor.assertExecutionScheduled(true);
-  }
-
-  @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-    pool.recycle(httpC);
-    assertPooled(pool, httpC, httpB, httpA);
-
-    pool.performCleanup();
-    assertPooled(pool, httpC, httpB);
-  }
-
-  @Test public void getSpdyConnection() throws Exception {
-    pool.share(spdyA);
-    assertSame(spdyA, pool.get(spdyAddress));
-    assertPooled(pool, spdyA);
-  }
-
-  @Test public void getHttpConnection() throws Exception {
-    pool.recycle(httpA);
-    assertSame(httpA, pool.get(httpAddress));
-    assertPooled(pool);
-  }
-
-  @Test public void expiredConnectionNotReturned() throws Exception {
-    pool.recycle(httpA);
-
-    // Allow enough time to pass so that the connection is now expired.
-    Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
-
-    // The connection is held, but will not be returned.
-    assertNull(pool.get(httpAddress));
-    assertPooled(pool, httpA);
-
-    // The connection must be cleaned up.
-    pool.performCleanup();
-    assertPooled(pool);
-  }
-
-  @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-    pool.recycle(httpC);
-    pool.recycle(httpD);
-    assertPooled(pool, httpD, httpC, httpB, httpA);
-
-    pool.performCleanup();
-    assertPooled(pool, httpD, httpC);
-  }
-
-  @Test public void expiredConnectionsAreEvicted() throws Exception {
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-
-    // Allow enough time to pass so that the connections are now expired.
-    Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
-    assertPooled(pool, httpB, httpA);
-
-    // The connections must be cleaned up.
-    pool.performCleanup();
-    assertPooled(pool);
-  }
-
-  @Test public void nonAliveConnectionNotReturned() throws Exception {
-    pool.recycle(httpA);
-
-    // Close the connection. It is an ex-connection. It has ceased to be.
-    httpA.getSocket().close();
-    assertPooled(pool, httpA);
-    assertNull(pool.get(httpAddress));
-
-    // The connection must be cleaned up.
-    pool.performCleanup();
-    assertPooled(pool);
-  }
-
-  @Test public void differentAddressConnectionNotReturned() throws Exception {
-    pool.recycle(httpA);
-    assertNull(pool.get(spdyAddress));
-    assertPooled(pool, httpA);
-  }
-
-  @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
-    pool.share(spdyA);
-    pool.recycle(httpA);
-    assertPooled(pool, httpA, spdyA);
-    assertSame(spdyA, pool.get(spdyAddress));
-    assertPooled(pool, spdyA, httpA);
-  }
-
-  @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-    assertSame(httpA, pool.get(httpAddress));
-  }
-
-  @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
-    httpA.getSocket().shutdownInput();
-    pool.recycle(httpA); // Should close httpA.
-    assertTrue(httpA.getSocket().isClosed());
-
-    // The pool should remain empty, and there is no need to schedule a cleanup.
-    assertPooled(pool);
-    cleanupExecutor.assertExecutionScheduled(false);
-  }
-
-  @Test public void shareHttpConnectionFails() throws Exception {
-    try {
-      pool.share(httpA);
-      fail();
-    } catch (IllegalArgumentException expected) {
+  private final Runnable emptyRunnable = new Runnable() {
+    @Override public void run() {
     }
-    // The pool should remain empty, and there is no need to schedule a cleanup.
-    assertPooled(pool);
-    cleanupExecutor.assertExecutionScheduled(false);
-  }
+  };
 
-  @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
-    pool.recycle(spdyA);
-    // The pool should remain empty, and there is no need to schedule the cleanup.
-    assertPooled(pool);
-    cleanupExecutor.assertExecutionScheduled(false);
-  }
+  private final Address addressA = newAddress("a");
+  private final Route routeA1 = newRoute(addressA);
+  private final Address addressB = newAddress("b");
+  private final Route routeB1 = newRoute(addressB);
+  private final Address addressC = newAddress("c");
+  private final Route routeC1 = newRoute(addressC);
 
-  @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
-    pool.share(spdyA);
-    assertPooled(pool, spdyA); // Connection should be in the pool.
+  @Test public void connectionsEvictedWhenIdleLongEnough() throws Exception {
+    ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+    pool.setCleanupRunnableForTest(emptyRunnable);
 
-    Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
-    pool.performCleanup();
-    assertPooled(pool, spdyA); // Connection should still be in the pool.
+    RealConnection c1 = newConnection(pool, routeA1, 50L);
 
-    Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
-    pool.performCleanup();
-    assertPooled(pool); // Connection should have been removed.
-  }
+    // Running at time 50, the pool returns that nothing can be evicted until time 150.
+    assertEquals(100L, pool.cleanup(50L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
 
-  @Test public void validateIdleHttpConnectionTimeout() throws Exception {
-    pool.recycle(httpA);
-    assertPooled(pool, httpA); // Connection should be in the pool.
-    cleanupExecutor.assertExecutionScheduled(true);
+    // Running at time 60, the pool returns that nothing can be evicted until time 150.
+    assertEquals(90L, pool.cleanup(60L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
 
-    Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
-    pool.performCleanup();
-    assertPooled(pool, httpA); // Connection should still be in the pool.
+    // Running at time 149, the pool returns that nothing can be evicted until time 150.
+    assertEquals(1L, pool.cleanup(149L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
 
-    Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
-    pool.performCleanup();
-    assertPooled(pool); // Connection should have been removed.
-  }
-
-  @Test public void maxConnections() throws IOException, InterruptedException {
-    // Pool should be empty.
+    // Running at time 150, the pool evicts.
+    assertEquals(0, pool.cleanup(150L));
     assertEquals(0, pool.getConnectionCount());
+    assertTrue(c1.socket.isClosed());
 
-    // http A should be added to the pool.
-    pool.recycle(httpA);
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getMultiplexedConnectionCount());
-
-    // http B should be added to the pool.
-    pool.recycle(httpB);
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getMultiplexedConnectionCount());
-
-    // http C should be added
-    pool.recycle(httpC);
-    assertEquals(3, pool.getConnectionCount());
-    assertEquals(3, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getSpdyConnectionCount());
-
-    pool.performCleanup();
-
-    // http A should be removed by cleanup.
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getMultiplexedConnectionCount());
-
-    // spdy A should be added
-    pool.share(spdyA);
-    assertEquals(3, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getSpdyConnectionCount());
-
-    pool.performCleanup();
-
-    // http B should be removed by cleanup.
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // http C should be returned.
-    Connection recycledHttpConnection = pool.get(httpAddress);
-    recycledHttpConnection.setOwner(owner);
-    assertNotNull(recycledHttpConnection);
-    assertTrue(recycledHttpConnection.isAlive());
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(0, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // spdy A will be returned but also kept in the pool.
-    Connection sharedSpdyConnection = pool.get(spdyAddress);
-    assertNotNull(sharedSpdyConnection);
-    assertEquals(spdyA, sharedSpdyConnection);
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(0, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // http C should be added to the pool
-    pool.recycle(httpC);
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // An http connection should be removed from the pool.
-    recycledHttpConnection = pool.get(httpAddress);
-    assertNotNull(recycledHttpConnection);
-    assertTrue(recycledHttpConnection.isAlive());
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(0, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // spdy A will be returned but also kept in the pool.
-    sharedSpdyConnection = pool.get(spdyAddress);
-    assertEquals(spdyA, sharedSpdyConnection);
-    assertNotNull(sharedSpdyConnection);
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(0, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // http D should be added to the pool.
-    pool.recycle(httpD);
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // http E should be added to the pool.
-    pool.recycle(httpE);
-    assertEquals(3, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getSpdyConnectionCount());
-
-    pool.performCleanup();
-
-    // spdy A should be removed from the pool by cleanup.
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(0, pool.getMultiplexedConnectionCount());
-  }
-
-  @Test public void connectionCleanup() throws Exception {
-    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
-
-    // Add 3 connections to the pool.
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-    pool.share(spdyA);
-
-    // Give the cleanup callable time to run and settle down.
-    Thread.sleep(100);
-
-    // Kill http A.
-    Util.closeQuietly(httpA.getSocket());
-
-    assertEquals(3, pool.getConnectionCount());
-    assertEquals(2, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getSpdyConnectionCount());
-
-    // Http A should be removed.
-    pool.performCleanup();
-    assertPooled(pool, spdyA, httpB);
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getMultiplexedConnectionCount());
-
-    // Now let enough time pass for the connections to expire.
-    Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
-
-    // All remaining connections should be removed.
-    pool.performCleanup();
+    // Running again, the pool reports that no further runs are necessary.
+    assertEquals(-1, pool.cleanup(150L));
     assertEquals(0, pool.getConnectionCount());
+    assertTrue(c1.socket.isClosed());
   }
 
-  @Test public void maxIdleConnectionsLimitEnforced() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+  @Test public void inUseConnectionsNotEvicted() throws Exception {
+    ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+    pool.setCleanupRunnableForTest(emptyRunnable);
 
-    // Hit the max idle connections limit of 2.
-    pool.recycle(httpA);
-    pool.recycle(httpB);
-    Thread.sleep(100); // Give the cleanup callable time to run.
-    assertPooled(pool, httpB, httpA);
+    RealConnection c1 = newConnection(pool, routeA1, 50L);
+    StreamAllocation streamAllocation = new StreamAllocation(pool, addressA);
+    streamAllocation.acquire(c1);
 
-    // Adding httpC bumps httpA.
-    pool.recycle(httpC);
-    Thread.sleep(100); // Give the cleanup callable time to run.
-    assertPooled(pool, httpC, httpB);
+    // Running at time 50, the pool returns that nothing can be evicted until time 150.
+    assertEquals(100L, pool.cleanup(50L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
 
-    // Adding httpD bumps httpB.
-    pool.recycle(httpD);
-    Thread.sleep(100); // Give the cleanup callable time to run.
-    assertPooled(pool, httpD, httpC);
+    // Running at time 60, the pool returns that nothing can be evicted until time 160.
+    assertEquals(100L, pool.cleanup(60L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
 
-    // Adding httpE bumps httpC.
-    pool.recycle(httpE);
-    Thread.sleep(100); // Give the cleanup callable time to run.
-    assertPooled(pool, httpE, httpD);
+    // Running at time 160, the pool returns that nothing can be evicted until time 260.
+    assertEquals(100L, pool.cleanup(160L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
   }
 
-  @Test public void evictAllConnections() throws Exception {
-    resetWithPoolSize(10);
-    pool.recycle(httpA);
-    Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool.
-    pool.recycle(httpB);
-    pool.share(spdyA);
-    int connectionCount = pool.getConnectionCount();
-    assertTrue(connectionCount == 2 || connectionCount == 3);
+  @Test public void cleanupPrioritizesEarliestEviction() throws Exception {
+    ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+    pool.setCleanupRunnableForTest(emptyRunnable);
 
-    pool.evictAll();
+    RealConnection c1 = newConnection(pool, routeA1, 75L);
+    RealConnection c2 = newConnection(pool, routeB1, 50L);
+
+    // Running at time 75, the pool returns that nothing can be evicted until time 150.
+    assertEquals(75L, pool.cleanup(75L));
+    assertEquals(2, pool.getConnectionCount());
+
+    // Running at time 149, the pool returns that nothing can be evicted until time 150.
+    assertEquals(1L, pool.cleanup(149L));
+    assertEquals(2, pool.getConnectionCount());
+
+    // Running at time 150, the pool evicts c2.
+    assertEquals(0L, pool.cleanup(150L));
+    assertEquals(1, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
+    assertTrue(c2.socket.isClosed());
+
+    // Running at time 150, the pool returns that nothing can be evicted until time 175.
+    assertEquals(25L, pool.cleanup(150L));
+    assertEquals(1, pool.getConnectionCount());
+
+    // Running at time 175, the pool evicts c1.
+    assertEquals(0L, pool.cleanup(175L));
     assertEquals(0, pool.getConnectionCount());
+    assertTrue(c1.socket.isClosed());
+    assertTrue(c2.socket.isClosed());
   }
 
-  @Test public void closeIfOwnedBy() throws Exception {
-    httpA.closeIfOwnedBy(owner);
-    assertFalse(httpA.isAlive());
-    assertFalse(httpA.clearOwner());
+  @Test public void oldestConnectionsEvictedIfIdleLimitExceeded() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+    pool.setCleanupRunnableForTest(emptyRunnable);
+
+    RealConnection c1 = newConnection(pool, routeA1, 50L);
+    RealConnection c2 = newConnection(pool, routeB1, 75L);
+
+    // With 2 connections, there's no need to evict until the connections time out.
+    assertEquals(50L, pool.cleanup(100L));
+    assertEquals(2, pool.getConnectionCount());
+    assertFalse(c1.socket.isClosed());
+    assertFalse(c2.socket.isClosed());
+
+    // Add a third connection
+    RealConnection c3 = newConnection(pool, routeC1, 75L);
+
+    // The third connection bounces the first.
+    assertEquals(0L, pool.cleanup(100L));
+    assertEquals(2, pool.getConnectionCount());
+    assertTrue(c1.socket.isClosed());
+    assertFalse(c2.socket.isClosed());
+    assertFalse(c3.socket.isClosed());
   }
 
-  @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
-    httpA.closeIfOwnedBy(new Object());
-    assertTrue(httpA.isAlive());
-    assertTrue(httpA.clearOwner());
+  @Test public void leakedAllocation() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+    pool.setCleanupRunnableForTest(emptyRunnable);
+
+    RealConnection c1 = newConnection(pool, routeA1, 0L);
+    allocateAndLeakAllocation(pool, c1);
+
+    awaitGarbageCollection();
+    assertEquals(0L, pool.cleanup(100L));
+    assertEquals(Collections.emptyList(), c1.allocations);
+
+    assertTrue(c1.noNewStreams); // Can't allocate once a leak has been detected.
   }
 
-  @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
-    try {
-      spdyA.closeIfOwnedBy(owner);
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-  }
-
-  @Test public void cleanupRunnableStopsEventually() throws Exception {
-    pool.recycle(httpA);
-    pool.share(spdyA);
-    assertPooled(pool, spdyA, httpA);
-
-    // The cleanup should terminate once the pool is empty again.
-    cleanupExecutor.fakeExecute();
-    assertPooled(pool);
-
-    cleanupExecutor.assertExecutionScheduled(false);
-
-    // Adding a new connection should cause the cleanup to start up again.
-    pool.recycle(httpB);
-
-    cleanupExecutor.assertExecutionScheduled(true);
-
-    // The cleanup should terminate once the pool is empty again.
-    cleanupExecutor.fakeExecute();
-    assertPooled(pool);
-  }
-
-  private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
-    assertEquals(Arrays.asList(connections), pool.getConnections());
+  /** Use a helper method so there's no hidden reference remaining on the stack. */
+  private void allocateAndLeakAllocation(ConnectionPool pool, RealConnection connection) {
+    StreamAllocation leak = new StreamAllocation(pool, connection.getRoute().getAddress());
+    leak.acquire(connection);
   }
 
   /**
-   * An executor that does not actually execute anything by default. See
-   * {@link #fakeExecute()}.
+   * See FinalizationTester for discussion on how to best trigger GC in tests.
+   * https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
+   * java/lang/ref/FinalizationTester.java
    */
-  private static class FakeExecutor implements Executor {
+  private void awaitGarbageCollection() throws InterruptedException {
+    Runtime.getRuntime().gc();
+    Thread.sleep(100);
+    System.runFinalization();
+  }
 
-    private Runnable runnable;
-
-    @Override
-    public void execute(Runnable runnable) {
-      // This is a bonus assertion for the invariant: At no time should two runnables be scheduled.
-      assertNull(this.runnable);
-      this.runnable = runnable;
+  private RealConnection newConnection(ConnectionPool pool, Route route, long idleAtNanos) {
+    RealConnection connection = new RealConnection(route);
+    connection.idleAtNanos = idleAtNanos;
+    connection.socket = new Socket();
+    synchronized (pool) {
+      pool.put(connection);
     }
+    return connection;
+  }
 
-    public void assertExecutionScheduled(boolean expected) {
-      assertEquals(expected, runnable != null);
-    }
+  private Address newAddress(String name) {
+    return new Address(name, 1, Dns.SYSTEM, SocketFactory.getDefault(), null, null, null,
+        new RecordingOkAuthenticator("password"), null, Collections.<Protocol>emptyList(),
+        Collections.<ConnectionSpec>emptyList(),
+        ProxySelector.getDefault());
+  }
 
-    /**
-     * Executes the runnable.
-     */
-    public void fakeExecute() {
-      Runnable toRun = this.runnable;
-      this.runnable = null;
-      toRun.run();
-    }
+  private Route newRoute(Address address) {
+    return new Route(address, Proxy.NO_PROXY,
+        InetSocketAddress.createUnresolved(address.url().host(), address.url().port()));
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
new file mode 100644
index 0000000..f445dac
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2015 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.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import static org.junit.Assert.assertEquals;
+
+public final class ConnectionReuseTest {
+  @Rule public final TestRule timeout = new Timeout(30_000);
+  @Rule public final MockWebServer server = new MockWebServer();
+
+  private SSLContext sslContext = SslContextBuilder.localhost();
+  private OkHttpClient client = new OkHttpClient();
+
+  @Test public void connectionsAreReused() throws Exception {
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionReused(request, request);
+  }
+
+  @Test public void connectionsAreReusedWithHttp2() throws Exception {
+    enableHttp2();
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionReused(request, request);
+  }
+
+  @Test public void connectionsAreNotReusedWithRequestConnectionClose() throws Exception {
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request requestA = new Request.Builder()
+        .url(server.url("/"))
+        .header("Connection", "close")
+        .build();
+    Request requestB = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionNotReused(requestA, requestB);
+  }
+
+  @Test public void connectionsAreNotReusedWithResponseConnectionClose() throws Exception {
+    server.enqueue(new MockResponse()
+        .addHeader("Connection", "close")
+        .setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request requestA = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Request requestB = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionNotReused(requestA, requestB);
+  }
+
+  @Test public void connectionsAreNotReusedWithUnknownLengthResponseBody() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("a")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)
+        .clearHeaders());
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionNotReused(request, request);
+  }
+
+  @Test public void connectionsAreNotReusedIfPoolIsSizeZero() throws Exception {
+    client.setConnectionPool(new ConnectionPool(0, 5000));
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    assertConnectionNotReused(request, request);
+  }
+
+  @Test public void connectionsReusedWithRedirectEvenIfPoolIsSizeZero() throws Exception {
+    client.setConnectionPool(new ConnectionPool(0, 5000));
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Response response = client.newCall(request).execute();
+    assertEquals("b", response.body().string());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void connectionsNotReusedWithRedirectIfDiscardingResponseIsSlow() throws Exception {
+    client.setConnectionPool(new ConnectionPool(0, 5000));
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .setBodyDelay(1, TimeUnit.SECONDS)
+        .setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Response response = client.newCall(request).execute();
+    assertEquals("b", response.body().string());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void silentRetryWhenIdempotentRequestFailsOnReusedConnection() throws Exception {
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+
+    Response responseA = client.newCall(request).execute();
+    assertEquals("a", responseA.body().string());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+
+    Response responseB = client.newCall(request).execute();
+    assertEquals("b", responseB.body().string());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void staleConnectionNotReusedForNonIdempotentRequest() throws Exception {
+    server.enqueue(new MockResponse().setBody("a")
+        .setSocketPolicy(SocketPolicy.SHUTDOWN_OUTPUT_AT_END));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request requestA = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Response responseA = client.newCall(requestA).execute();
+    assertEquals("a", responseA.body().string());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+
+    Request requestB = new Request.Builder()
+        .url(server.url("/"))
+        .post(RequestBody.create(MediaType.parse("text/plain"), "b"))
+        .build();
+    Response responseB = client.newCall(requestB).execute();
+    assertEquals("b", responseB.body().string());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void http2ConnectionsAreSharedBeforeResponseIsConsumed() throws Exception {
+    enableHttp2();
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Response response1 = client.newCall(request).execute();
+    Response response2 = client.newCall(request).execute();
+    response1.body().string(); // Discard the response body.
+    response2.body().string(); // Discard the response body.
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void connectionsAreEvicted() throws Exception {
+    server.enqueue(new MockResponse().setBody("a"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    client.setConnectionPool(new ConnectionPool(5, 250, TimeUnit.MILLISECONDS));
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+
+    Response response1 = client.newCall(request).execute();
+    assertEquals("a", response1.body().string());
+
+    // Give the thread pool a chance to evict.
+    Thread.sleep(500);
+
+    Response response2 = client.newCall(request).execute();
+    assertEquals("b", response2.body().string());
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+  }
+
+  private void enableHttp2() {
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    client.setProtocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1));
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.setProtocols(client.getProtocols());
+  }
+
+  private void assertConnectionReused(Request... requests) throws Exception {
+    for (int i = 0; i < requests.length; i++) {
+      Response response = client.newCall(requests[i]).execute();
+      response.body().string(); // Discard the response body.
+      assertEquals(i, server.takeRequest().getSequenceNumber());
+    }
+  }
+
+  private void assertConnectionNotReused(Request... requests) throws Exception {
+    for (Request request : requests) {
+      Response response = client.newCall(request).execute();
+      response.body().string(); // Discard the response body.
+      assertEquals(0, server.takeRequest().getSequenceNumber());
+    }
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
index 7833cca..3b390e8 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
@@ -15,29 +15,49 @@
  */
 package com.squareup.okhttp;
 
-import org.junit.Test;
-
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
+import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public final class ConnectionSpecTest {
+  @Test public void noTlsVersions() throws Exception {
+    try {
+      new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+          .tlsVersions(new TlsVersion[0])
+          .build();
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertEquals("At least one TLS version is required", expected.getMessage());
+    }
+  }
 
-  @Test
-  public void cleartextBuilder() throws Exception {
+  @Test public void noCipherSuites() throws Exception {
+    try {
+      new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+          .cipherSuites(new CipherSuite[0])
+          .build();
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertEquals("At least one cipher suite is required", expected.getMessage());
+    }
+  }
+
+  @Test public void cleartextBuilder() throws Exception {
     ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build();
     assertFalse(cleartextSpec.isTls());
   }
 
-  @Test
-  public void tlsBuilder_explicitCiphers() throws Exception {
+  @Test public void tlsBuilder_explicitCiphers() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
         .tlsVersions(TlsVersion.TLS_1_2)
@@ -48,8 +68,7 @@
     assertTrue(tlsSpec.supportsTlsExtensions());
   }
 
-  @Test
-  public void tlsBuilder_defaultCiphers() throws Exception {
+  @Test public void tlsBuilder_defaultCiphers() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .tlsVersions(TlsVersion.TLS_1_2)
         .supportsTlsExtensions(true)
@@ -59,8 +78,7 @@
     assertTrue(tlsSpec.supportsTlsExtensions());
   }
 
-  @Test
-  public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
+  @Test public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .tlsVersions(TlsVersion.TLS_1_2)
         .supportsTlsExtensions(false)
@@ -79,17 +97,16 @@
     assertTrue(tlsSpec.isCompatible(socket));
     tlsSpec.apply(socket, false /* isFallback */);
 
-    assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+    assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
 
     Set<String> expectedCipherSet =
-        createSet(
+        set(
             CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
             CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
     assertEquals(expectedCipherSet, expectedCipherSet);
   }
 
-  @Test
-  public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
+  @Test public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .tlsVersions(TlsVersion.TLS_1_2)
         .supportsTlsExtensions(false)
@@ -108,10 +125,10 @@
     assertTrue(tlsSpec.isCompatible(socket));
     tlsSpec.apply(socket, true /* isFallback */);
 
-    assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+    assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
 
     Set<String> expectedCipherSet =
-        createSet(
+        set(
             CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
             CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
     if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
@@ -120,8 +137,7 @@
     assertEquals(expectedCipherSet, expectedCipherSet);
   }
 
-  @Test
-  public void tls_explicitCiphers() throws Exception {
+  @Test public void tls_explicitCiphers() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
         .tlsVersions(TlsVersion.TLS_1_2)
@@ -141,17 +157,16 @@
     assertTrue(tlsSpec.isCompatible(socket));
     tlsSpec.apply(socket, true /* isFallback */);
 
-    assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+    assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
 
-    Set<String> expectedCipherSet = createSet(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
+    Set<String> expectedCipherSet = set(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
     if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
       expectedCipherSet.add("TLS_FALLBACK_SCSV");
     }
     assertEquals(expectedCipherSet, expectedCipherSet);
   }
 
-  @Test
-  public void tls_stringCiphersAndVersions() throws Exception {
+  @Test public void tls_stringCiphersAndVersions() throws Exception {
     // Supporting arbitrary input strings allows users to enable suites and versions that are not
     // yet known to the library, but are supported by the platform.
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@@ -160,7 +175,7 @@
         .build();
   }
 
-  public void tls_missingRequiredCipher() throws Exception {
+  @Test public void tls_missingRequiredCipher() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
         .tlsVersions(TlsVersion.TLS_1_2)
@@ -185,8 +200,43 @@
     assertFalse(tlsSpec.isCompatible(socket));
   }
 
-  @Test
-  public void tls_missingTlsVersion() throws Exception {
+  @Test public void allEnabledCipherSuites() throws Exception {
+    ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .allEnabledCipherSuites()
+        .build();
+    assertNull(tlsSpec.cipherSuites());
+
+    SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+    sslSocket.setEnabledCipherSuites(new String[] {
+        CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+        CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+    });
+
+    tlsSpec.apply(sslSocket, false);
+    assertEquals(Arrays.asList(
+            CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+            CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName),
+        Arrays.asList(sslSocket.getEnabledCipherSuites()));
+  }
+
+  @Test public void allEnabledTlsVersions() throws Exception {
+    ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .allEnabledTlsVersions()
+        .build();
+    assertNull(tlsSpec.tlsVersions());
+
+    SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+    sslSocket.setEnabledProtocols(new String[] {
+        TlsVersion.SSL_3_0.javaName(),
+        TlsVersion.TLS_1_1.javaName()
+    });
+
+    tlsSpec.apply(sslSocket, false);
+    assertEquals(Arrays.asList(TlsVersion.SSL_3_0.javaName(), TlsVersion.TLS_1_1.javaName()),
+        Arrays.asList(sslSocket.getEnabledProtocols()));
+  }
+
+  @Test public void tls_missingTlsVersion() throws Exception {
     ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
         .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
         .tlsVersions(TlsVersion.TLS_1_2)
@@ -206,7 +256,48 @@
     assertFalse(tlsSpec.isCompatible(socket));
   }
 
-  private static Set<String> createSet(String... values) {
-    return new LinkedHashSet<String>(Arrays.asList(values));
+  @Test public void equalsAndHashCode() throws Exception {
+    ConnectionSpec allCipherSuites = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .allEnabledCipherSuites()
+        .build();
+    ConnectionSpec allTlsVersions = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .allEnabledTlsVersions()
+        .build();
+
+    Set<Object> set = new CopyOnWriteArraySet<>();
+    assertTrue(set.add(ConnectionSpec.MODERN_TLS));
+    assertTrue(set.add(ConnectionSpec.COMPATIBLE_TLS));
+    assertTrue(set.add(ConnectionSpec.CLEARTEXT));
+    assertTrue(set.add(allTlsVersions));
+    assertTrue(set.add(allCipherSuites));
+
+    assertTrue(set.remove(ConnectionSpec.MODERN_TLS));
+    assertTrue(set.remove(ConnectionSpec.COMPATIBLE_TLS));
+    assertTrue(set.remove(ConnectionSpec.CLEARTEXT));
+    assertTrue(set.remove(allTlsVersions));
+    assertTrue(set.remove(allCipherSuites));
+    assertTrue(set.isEmpty());
+  }
+
+  @Test public void allEnabledToString() throws Exception {
+    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .allEnabledTlsVersions()
+        .allEnabledCipherSuites()
+        .build();
+    assertEquals("ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], "
+        + "supportsTlsExtensions=true)", connectionSpec.toString());
+  }
+
+  @Test public void simpleToString() throws Exception {
+    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+        .tlsVersions(TlsVersion.TLS_1_2)
+        .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
+        .build();
+    assertEquals("ConnectionSpec(cipherSuites=[TLS_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], "
+        + "supportsTlsExtensions=true)", connectionSpec.toString());
+  }
+
+  private static <T> Set<T> set(T... values) {
+    return new LinkedHashSet<>(Arrays.asList(values));
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
index 38a5de8..f72bd1a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
@@ -35,51 +35,43 @@
     this.delegate = delegate;
   }
 
-  @Override
-  public SSLSocket createSocket() throws IOException {
+  @Override public SSLSocket createSocket() throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket();
     return configureSocket(sslSocket);
   }
 
-  @Override
-  public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+  @Override public SSLSocket createSocket(String host, int port) throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
     return configureSocket(sslSocket);
   }
 
-  @Override
-  public SSLSocket createSocket(String host, int port, InetAddress localAddress, int localPort)
-      throws IOException, UnknownHostException {
+  @Override public SSLSocket createSocket(
+      String host, int port, InetAddress localAddress, int localPort) throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
     return configureSocket(sslSocket);
   }
 
-  @Override
-  public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+  @Override public SSLSocket createSocket(InetAddress host, int port) throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
     return configureSocket(sslSocket);
   }
 
-  @Override
-  public SSLSocket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
-      throws IOException {
+  @Override public SSLSocket createSocket(
+      InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
     return configureSocket(sslSocket);
   }
 
-  @Override
-  public String[] getDefaultCipherSuites() {
+  @Override public String[] getDefaultCipherSuites() {
     return delegate.getDefaultCipherSuites();
   }
 
-  @Override
-  public String[] getSupportedCipherSuites() {
+  @Override public String[] getSupportedCipherSuites() {
     return delegate.getSupportedCipherSuites();
   }
 
-  @Override
-  public SSLSocket createSocket(Socket socket, String host, int port, boolean autoClose)
-      throws IOException {
+  @Override public SSLSocket createSocket(
+      Socket socket, String host, int port, boolean autoClose) throws IOException {
     SSLSocket sslSocket = (SSLSocket) delegate.createSocket(socket, host, port, autoClose);
     return configureSocket(sslSocket);
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
index d45ac10..466126d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
@@ -692,6 +692,8 @@
         HttpUrl.parse("http://host/%/b").pathSegments());
     assertEquals(Arrays.asList("%"),
         HttpUrl.parse("http://host/%").pathSegments());
+    assertEquals(Arrays.asList("%00"),
+        HttpUrl.parse("http://github.com/%%30%30").pathSegments());
   }
 
   @Test public void malformedUtf8Encoding() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
index d8454ac..e70c908 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
@@ -135,7 +135,7 @@
     Interceptor interceptor = new Interceptor() {
       @Override public Response intercept(Chain chain) throws IOException {
         Address address = chain.connection().getRoute().getAddress();
-        String sameHost = address.getRfc2732Host();
+        String sameHost = address.getUriHost();
         int differentPort = address.getUriPort() + 1;
         return chain.proceed(chain.request().newBuilder()
             .url(HttpUrl.parse("http://" + sameHost + ":" + differentPort + "/"))
@@ -210,6 +210,35 @@
     assertEquals("abcabcabc", response.body().string());
   }
 
+  @Test public void networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
+    server.enqueue(new MockResponse());
+
+    client.networkInterceptors().add(new Interceptor() {
+      @Override
+      public Response intercept(Chain chain) throws IOException {
+        Request originalRequest = chain.request();
+        MediaType mediaType = MediaType.parse("text/plain");
+        RequestBody body = RequestBody.create(mediaType, "abc");
+        return chain.proceed(originalRequest.newBuilder()
+            .method("POST", body)
+            .header("Content-Type", mediaType.toString())
+            .header("Content-Length", Long.toString(body.contentLength()))
+            .build());
+      }
+    });
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .get()
+        .build();
+
+    client.newCall(request).execute();
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("POST", recordedRequest.getMethod());
+    assertEquals("abc", recordedRequest.getBody().readUtf8());
+  }
+
   @Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
     rewriteRequestToServer(client.interceptors());
   }
@@ -362,7 +391,8 @@
 
     client.interceptors().add(new Interceptor() {
       @Override public Response intercept(Chain chain) throws IOException {
-        chain.proceed(chain.request());
+        Response response1 = chain.proceed(chain.request());
+        response1.body().close();
         return chain.proceed(chain.request());
       }
     });
@@ -468,14 +498,6 @@
     }
   }
 
-  @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
-    interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
-  }
-
-  @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
-    interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
-  }
-
   @Test public void networkInterceptorModifiedRequestIsReturned() throws IOException {
     server.enqueue(new MockResponse());
 
@@ -500,6 +522,14 @@
     assertEquals("intercepted request", response.networkResponse().request().header("User-Agent"));
   }
 
+  @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+    interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
+  }
+
+  @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+    interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
+  }
+
   /**
    * When an interceptor throws an unexpected exception, asynchronous callers are left hanging. The
    * exception goes to the uncaught exception handler.
@@ -525,6 +555,57 @@
     assertEquals("boom!", executor.takeException().getMessage());
   }
 
+  @Test public void applicationInterceptorReturnsNull() throws Exception {
+    server.enqueue(new MockResponse());
+
+    Interceptor interceptor = new Interceptor() {
+      @Override public Response intercept(Chain chain) throws IOException {
+        chain.proceed(chain.request());
+        return null;
+      }
+    };
+    client.interceptors().add(interceptor);
+
+    ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+    client.setDispatcher(new Dispatcher(executor));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    try {
+      client.newCall(request).execute();
+      fail();
+    } catch (NullPointerException expected) {
+      assertEquals("application interceptor " + interceptor
+          + " returned null", expected.getMessage());
+    }
+  }
+
+  @Test public void networkInterceptorReturnsNull() throws Exception {
+    server.enqueue(new MockResponse());
+
+    Interceptor interceptor = new Interceptor() {
+      @Override public Response intercept(Chain chain) throws IOException {
+        chain.proceed(chain.request());
+        return null;
+      }
+    };
+    client.networkInterceptors().add(interceptor);
+
+    ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+    client.setDispatcher(new Dispatcher(executor));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    try {
+      client.newCall(request).execute();
+      fail();
+    } catch (NullPointerException expected) {
+      assertEquals("network interceptor " + interceptor + " returned null", expected.getMessage());
+    }
+  }
+
   private RequestBody uppercase(final RequestBody original) {
     return new RequestBody() {
       @Override public MediaType contentType() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
index 9d65147..f2447ec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
@@ -20,7 +20,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import okio.Buffer;
 
 /**
  * Records received HTTP responses so they can be later retrieved by tests.
@@ -36,11 +35,8 @@
   }
 
   @Override public synchronized void onResponse(Response response) throws IOException {
-    Buffer buffer = new Buffer();
-    ResponseBody body = response.body();
-    body.source().readAll(buffer);
-
-    responses.add(new RecordedResponse(response.request(), response, null, buffer.readUtf8(), null));
+    String body = response.body().string();
+    responses.add(new RecordedResponse(response.request(), response, null, body, null));
     notifyAll();
   }
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
index e2a5532..651ea8b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
@@ -41,6 +41,8 @@
  * See <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928</a>.
  */
 public final class SocksProxy {
+  public final String HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS = "onlyProxyCanResolveMe.org";
+
   private static final int VERSION_5 = 5;
   private static final int METHOD_NONE = 0xff;
   private static final int METHOD_NO_AUTHENTICATION_REQUIRED = 0;
@@ -156,7 +158,10 @@
       case ADDRESS_TYPE_DOMAIN_NAME:
         int domainNameLength = fromSource.readByte() & 0xff;
         String domainName = fromSource.readUtf8(domainNameLength);
-        toAddress = InetAddress.getByName(domainName);
+        // Resolve HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS to localhost.
+        toAddress = domainName.equalsIgnoreCase(HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+            ? InetAddress.getByName("localhost")
+            : InetAddress.getByName(domainName);
         break;
 
       default:
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
index 377ff83..0d6e9fd 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
@@ -85,4 +85,23 @@
 
     assertEquals(1, socksProxy.connectionCount());
   }
+
+  @Test public void checkRemoteDNSResolve() throws Exception {
+    // This testcase will fail if the target is resolved locally instead of through the proxy.
+    server.enqueue(new MockResponse().setBody("abc"));
+
+    OkHttpClient client = new OkHttpClient()
+        .setProxy(socksProxy.proxy());
+
+    HttpUrl url = server.url("/")
+        .newBuilder()
+        .host(socksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+        .build();
+
+    Request request = new Request.Builder().url(url).build();
+    Response response1 = client.newCall(request).execute();
+    assertEquals("abc", response1.body().string());
+
+    assertEquals(1, socksProxy.connectionCount());
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
index 7c948b8..f021249 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
@@ -17,9 +17,10 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.RecordingAuthenticator;
 import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.Version;
@@ -47,6 +48,7 @@
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.UnknownHostException;
+import java.security.SecureRandom;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -226,6 +228,7 @@
     assertEquals("d", connection.getHeaderField(1));
     assertEquals("A", connection.getHeaderFieldKey(2));
     assertEquals("e", connection.getHeaderField(2));
+    connection.getInputStream().close();
   }
 
   @Test public void serverSendsInvalidResponseHeaders() throws Exception {
@@ -318,6 +321,7 @@
     server.enqueue(new MockResponse().setBody("A"));
     connection = client.open(server.getUrl("/"));
     assertNull(connection.getErrorStream());
+    connection.getInputStream().close();
   }
 
   @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
@@ -335,8 +339,13 @@
     server.enqueue(response);
     server.enqueue(response);
 
-    assertContent("ABCDE", client.open(server.getUrl("/")), 5);
-    assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+    HttpURLConnection c1 = client.open(server.getUrl("/"));
+    assertContent("ABCDE", c1, 5);
+    HttpURLConnection c2 = client.open(server.getUrl("/"));
+    assertContent("ABCDE", c2, 5);
+
+    c1.getInputStream().close();
+    c2.getInputStream().close();
   }
 
   // Check that we recognize a few basic mime types by extension.
@@ -607,7 +616,7 @@
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
 
     suppressTlsFallbackScsv(client.client());
-    Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+    client.client().setDns(new SingleInetAddressDns());
 
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
     connection = client.open(server.getUrl("/foo"));
@@ -883,7 +892,7 @@
 
     // Configure a single IP address for the host and a single configuration, so we only need one
     // failure to fail permanently.
-    Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+    client.client().setDns(new SingleInetAddressDns());
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
     client.client().setConnectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS));
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
@@ -926,8 +935,8 @@
 
     RecordedRequest connect = server.takeRequest();
     assertNull(connect.getHeader("Private"));
-    assertEquals("bar", connect.getHeader("Proxy-Authorization"));
-    assertEquals("baz", connect.getHeader("User-Agent"));
+    assertNull(connect.getHeader("Proxy-Authorization"));
+    assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
     assertEquals("android.com", connect.getHeader("Host"));
     assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
 
@@ -1023,6 +1032,7 @@
       fail("Expected a connection closed exception");
     } catch (IOException expected) {
     }
+    in.close();
   }
 
   @Test public void disconnectBeforeConnect() throws IOException {
@@ -1085,6 +1095,7 @@
     } catch (IOException expected) {
     }
     assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
+    in.close();
     assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/")));
   }
 
@@ -1108,6 +1119,7 @@
     assertEquals(401, conn.getResponseCode());
     assertEquals(401, conn.getResponseCode());
     assertEquals(1, server.getRequestCount());
+    conn.getErrorStream().close();
   }
 
   @Test public void nonHexChunkSize() throws IOException {
@@ -1121,6 +1133,7 @@
       fail();
     } catch (IOException e) {
     }
+    connection.getInputStream().close();
   }
 
   @Test public void malformedChunkSize() throws IOException {
@@ -1133,6 +1146,8 @@
       readAscii(connection.getInputStream(), Integer.MAX_VALUE);
       fail();
     } catch (IOException e) {
+    } finally {
+      connection.getInputStream().close();
     }
   }
 
@@ -1156,6 +1171,8 @@
       readAscii(connection.getInputStream(), Integer.MAX_VALUE);
       fail();
     } catch (IOException e) {
+    } finally {
+      connection.getInputStream().close();
     }
   }
 
@@ -1286,7 +1303,7 @@
 
     HttpURLConnection connection = client.open(server.getUrl("/"));
     assertContent("{}", connection);
-    assertEquals(0, client.client().getConnectionPool().getConnectionCount());
+    assertEquals(0, client.client().getConnectionPool().getIdleConnectionCount());
   }
 
   @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
@@ -1358,6 +1375,7 @@
     OutputStream outputStream = connection.getOutputStream();
     outputStream.write(body.getBytes("US-ASCII"));
     assertEquals(200, connection.getResponseCode());
+    connection.getInputStream().close();
 
     RecordedRequest request = server.takeRequest();
     assertEquals(body, request.getBody().readUtf8());
@@ -1511,7 +1529,8 @@
     int responseCode = proxy ? 407 : 401;
     RecordingAuthenticator authenticator = new RecordingAuthenticator(null);
     Authenticator.setDefault(authenticator);
-    MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(responseCode)
+    MockResponse pleaseAuthenticate = new MockResponse()
+        .setResponseCode(responseCode)
         .addHeader(authHeader)
         .setBody("Please authenticate.");
     server.enqueue(pleaseAuthenticate);
@@ -1523,6 +1542,7 @@
       connection = client.open(server.getUrl("/"));
     }
     assertEquals(responseCode, connection.getResponseCode());
+    connection.getErrorStream().close();
     return authenticator.calls;
   }
 
@@ -1968,7 +1988,7 @@
 
     assertContent("This is the 2nd server!", client.open(server.getUrl("/a")));
 
-    assertEquals(Arrays.asList(server.getUrl("/a").toURI(), server2.getUrl("/b").toURI()),
+    assertEquals(Arrays.asList(server.getUrl("/").toURI(), server2.getUrl("/").toURI()),
         proxySelectionRequests);
   }
 
@@ -2190,9 +2210,9 @@
 
   @Test public void httpsWithCustomTrustManager() throws Exception {
     RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
-    RecordingTrustManager trustManager = new RecordingTrustManager();
+    RecordingTrustManager trustManager = new RecordingTrustManager(sslContext);
     SSLContext sc = SSLContext.getInstance("TLS");
-    sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
+    sc.init(null, new TrustManager[] {trustManager}, new SecureRandom());
 
     client.client().setHostnameVerifier(hostnameVerifier);
     client.client().setSslSocketFactory(sc.getSocketFactory());
@@ -2206,8 +2226,7 @@
     assertContent("DEF", client.open(url));
     assertContent("GHI", client.open(url));
 
-    assertEquals(Arrays.asList("verify " + server.getHostName()),
-        hostnameVerifier.calls);
+    assertEquals(Arrays.asList("verify " + server.getHostName()), hostnameVerifier.calls);
     assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.getHostName() + " 1]"),
         trustManager.calls);
   }
@@ -2232,6 +2251,7 @@
       fail();
     } catch (SocketTimeoutException expected) {
     }
+    in.close();
   }
 
   /** Confirm that an unacknowledged write times out. */
@@ -2500,6 +2520,7 @@
     } catch (NullPointerException expected) {
     }
     assertNull(connection.getContent(new Class[] { getClass() }));
+    connection.getInputStream().close();
   }
 
   @Test public void getOutputStreamOnGetFails() throws Exception {
@@ -2510,6 +2531,7 @@
       fail();
     } catch (ProtocolException expected) {
     }
+    connection.getInputStream().close();
   }
 
   @Test public void getOutputAfterGetInputStreamFails() throws Exception {
@@ -2538,6 +2560,7 @@
       fail();
     } catch (IllegalStateException expected) {
     }
+    connection.getInputStream().close();
   }
 
   @Test public void clientSendsContentLength() throws Exception {
@@ -2550,24 +2573,28 @@
     assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
     RecordedRequest request = server.takeRequest();
     assertEquals("3", request.getHeader("Content-Length"));
+    connection.getInputStream().close();
   }
 
   @Test public void getContentLengthConnects() throws Exception {
     server.enqueue(new MockResponse().setBody("ABC"));
     connection = client.open(server.getUrl("/"));
     assertEquals(3, connection.getContentLength());
+    connection.getInputStream().close();
   }
 
   @Test public void getContentTypeConnects() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC"));
     connection = client.open(server.getUrl("/"));
     assertEquals("text/plain", connection.getContentType());
+    connection.getInputStream().close();
   }
 
   @Test public void getContentEncodingConnects() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC"));
     connection = client.open(server.getUrl("/"));
     assertEquals("identity", connection.getContentEncoding());
+    connection.getInputStream().close();
   }
 
   // http://b/4361656
@@ -2797,6 +2824,7 @@
     connection = client.open(server.getUrl("/"));
     connection.getResponseCode();
     assertEquals("A", connection.getHeaderField(""));
+    connection.getInputStream().close();
   }
 
   @Test public void requestHeaderValidationIsStrict() throws Exception {
@@ -3380,9 +3408,14 @@
 
   private static class RecordingTrustManager implements X509TrustManager {
     private final List<String> calls = new ArrayList<String>();
+    private final X509TrustManager delegate;
+
+    public RecordingTrustManager(SSLContext sslContext) {
+      this.delegate = Platform.get().trustManager(sslContext.getSocketFactory());
+    }
 
     public X509Certificate[] getAcceptedIssuers() {
-      return new X509Certificate[] { };
+      return delegate.getAcceptedIssuers();
     }
 
     public void checkClientTrusted(X509Certificate[] chain, String authType)
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
similarity index 71%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
index 4934b42..759ac5d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
@@ -15,16 +15,19 @@
  */
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.Dns;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * A network that always resolves two IP addresses per host. Use this when testing route selection
  * fallbacks to guarantee that a fallback address is available.
  */
-public class DoubleInetAddressNetwork implements Network {
-  @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
-    InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
-    return new InetAddress[] { allInetAddresses[0], allInetAddresses[0] };
+public class DoubleInetAddressDns implements Dns {
+  @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+    List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+    return Arrays.asList(addresses.get(0), addresses.get(0));
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
similarity index 72%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
index beb48cb..43cbe63 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
@@ -15,17 +15,20 @@
  */
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.Dns;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * A network that resolves only one IP address per host. Use this when testing
  * route selection fallbacks to prevent the host machine's various IP addresses
  * from interfering.
  */
-public class SingleInetAddressNetwork implements Network {
-  @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
-    InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
-    return new InetAddress[] { allInetAddresses[0] };
+public class SingleInetAddressDns implements Dns {
+  @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+    List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+    return Collections.singletonList(addresses.get(0));
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
index 24c512d..1e00b4b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
@@ -443,7 +443,8 @@
 
     String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
     Socket socket = peer.openSocket();
-    FramedConnection connection = new FramedConnection.Builder(true, socket)
+    FramedConnection connection = new FramedConnection.Builder(true)
+        .socket(socket)
         .pushObserver(IGNORE)
         .protocol(HTTP_2.getProtocol())
         .build();
@@ -488,7 +489,8 @@
 
   private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
       throws IOException {
-    return new FramedConnection.Builder(true, peer.openSocket())
+    return new FramedConnection.Builder(true)
+        .socket(peer.openSocket())
         .pushObserver(IGNORE)
         .protocol(variant.getProtocol());
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
similarity index 65%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
index 91ba56c..7947c03 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
@@ -13,13 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.PushPromise;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.net.HttpURLConnection;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -78,4 +79,36 @@
     assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
     assertEquals("bar", pushedRequest.getHeader("foo"));
   }
+
+  /**
+   * Push a setting that permits up to 2 concurrent streams, then make 3 concurrent requests and
+   * confirm that the third concurrent request prepared a new connection.
+   */
+  @Test public void settingsLimitsMaxConcurrentStreams() throws Exception {
+    Settings settings = new Settings();
+    settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 2);
+
+    // Read & write a full request to confirm settings are accepted.
+    server.enqueue(new MockResponse().withSettings(settings));
+    HttpURLConnection settingsConnection = client.open(server.getUrl("/"));
+    assertContent("", settingsConnection, Integer.MAX_VALUE);
+
+    server.enqueue(new MockResponse().setBody("ABC"));
+    server.enqueue(new MockResponse().setBody("DEF"));
+    server.enqueue(new MockResponse().setBody("GHI"));
+
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    connection1.connect();
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    connection2.connect();
+    HttpURLConnection connection3 = client.open(server.getUrl("/"));
+    connection3.connect();
+    assertContent("ABC", connection1, Integer.MAX_VALUE);
+    assertContent("DEF", connection2, Integer.MAX_VALUE);
+    assertContent("GHI", connection3, Integer.MAX_VALUE);
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // Settings connection.
+    assertEquals(1, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+    assertEquals(2, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection!
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
similarity index 94%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
index 4020bf4..5e9bf61 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
similarity index 99%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
index 2d52eee..1f7d999 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Cache;
 import com.squareup.okhttp.ConnectionPool;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
index 26d4986..752e92b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
@@ -150,15 +150,18 @@
 
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(FramedStream stream) throws IOException {
+    FramedConnection.Listener handler = new FramedConnection.Listener() {
+      @Override public void onStream(FramedStream stream) throws IOException {
         receiveCount.incrementAndGet();
         assertEquals(pushHeaders, stream.getRequestHeaders());
         assertEquals(null, stream.getErrorCode());
         stream.reply(headerEntries("b", "banana"), true);
       }
     };
-    new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+    new FramedConnection.Builder(true)
+        .socket(peer.openSocket())
+        .listener(handler)
+        .build();
 
     // verify the peer received what was expected
     MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -178,14 +181,14 @@
 
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(FramedStream stream) throws IOException {
+    FramedConnection.Listener listener = new FramedConnection.Listener() {
+      @Override public void onStream(FramedStream stream) throws IOException {
         stream.reply(headerEntries("b", "banana"), false);
         receiveCount.incrementAndGet();
       }
     };
 
-    connectionBuilder(peer, SPDY3).handler(handler).build();
+    connectionBuilder(peer, SPDY3).listener(listener).build();
 
     // verify the peer received what was expected
     MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -254,7 +257,7 @@
 
   @Test public void serverSendsSettingsToClient() throws Exception {
     // write the mocking script
-    Settings settings = new Settings();
+    final Settings settings = new Settings();
     settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10);
     peer.sendFrame().settings(settings);
     peer.sendFrame().ping(false, 2, 0);
@@ -262,12 +265,24 @@
     peer.play();
 
     // play it back
-    FramedConnection connection = connection(peer, SPDY3);
+    final AtomicInteger maxConcurrentStreams = new AtomicInteger();
+    FramedConnection.Listener listener = new FramedConnection.Listener() {
+      @Override public void onStream(FramedStream stream) throws IOException {
+        throw new AssertionError();
+      }
+      @Override public void onSettings(FramedConnection connection) {
+        maxConcurrentStreams.set(connection.maxConcurrentStreams());
+      }
+    };
+    FramedConnection connection = connectionBuilder(peer, SPDY3)
+        .listener(listener)
+        .build();
 
     peer.takeFrame(); // Guarantees that the peer Settings frame has been processed.
     synchronized (connection) {
       assertEquals(10, connection.peerSettings.getMaxConcurrentStreams(-1));
     }
+    assertEquals(10, maxConcurrentStreams.get());
   }
 
   @Test public void multipleSettingsFramesAreMerged() throws Exception {
@@ -613,15 +628,18 @@
 
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(FramedStream stream) throws IOException {
+    FramedConnection.Listener listener = new FramedConnection.Listener() {
+      @Override public void onStream(FramedStream stream) throws IOException {
         receiveCount.incrementAndGet();
         assertEquals(headerEntries("a", "android"), stream.getRequestHeaders());
         assertEquals(null, stream.getErrorCode());
         stream.reply(headerEntries("c", "cola"), true);
       }
     };
-    new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+    new FramedConnection.Builder(true)
+        .socket(peer.openSocket())
+        .listener(listener)
+        .build();
 
     // verify the peer received what was expected
     MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -1315,7 +1333,8 @@
 
     String longString = ByteString.of(randomBytes(2048)).base64();
     Socket socket = peer.openSocket();
-    FramedConnection connection = new FramedConnection.Builder(true, socket)
+    FramedConnection connection = new FramedConnection.Builder(true)
+        .socket(socket)
         .protocol(SPDY3.getProtocol())
         .build();
     socket.shutdownOutput();
@@ -1343,7 +1362,8 @@
 
   private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
       throws IOException {
-    return new FramedConnection.Builder(true, peer.openSocket())
+    return new FramedConnection.Builder(true)
+        .socket(peer.openSocket())
         .protocol(variant.getProtocol());
   }
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
new file mode 100644
index 0000000..03d4347
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.Dns;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public final class FakeDns implements Dns {
+  private List<String> requestedHosts = new ArrayList<>();
+  private List<InetAddress> addresses = Collections.emptyList();
+
+  /** Sets the addresses to be returned by this fake DNS service. */
+  public FakeDns addresses(List<InetAddress> addresses) {
+    this.addresses = new ArrayList<>(addresses);
+    return this;
+  }
+
+  /** Sets the service to throw when a hostname is requested. */
+  public FakeDns unknownHost() {
+    this.addresses = Collections.emptyList();
+    return this;
+  }
+
+  public InetAddress address(int index) {
+    return addresses.get(index);
+  }
+
+  @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+    requestedHosts.add(hostname);
+    if (addresses.isEmpty()) throw new UnknownHostException();
+    return addresses;
+  }
+
+  public void assertRequests(String... expectedHosts) {
+    assertEquals(Arrays.asList(expectedHosts), requestedHosts);
+    requestedHosts.clear();
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
index 1f5ad6d..ea6e024 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
@@ -26,7 +26,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
 import org.junit.Test;
 
 import static com.squareup.okhttp.TestUtil.headerEntries;
@@ -42,24 +41,20 @@
         ":status", "200 OK",
         ":version", "HTTP/1.1");
     Request request = new Request.Builder().url("http://square.com/").build();
-    Response response =
-        FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+    Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
     Headers headers = response.headers();
-    assertEquals(4, headers.size());
+    assertEquals(3, headers.size());
     assertEquals(Protocol.SPDY_3, response.protocol());
     assertEquals(200, response.code());
     assertEquals("OK", response.message());
     assertEquals("no-cache, no-store", headers.get("cache-control"));
     assertEquals("Cookie2", headers.get("set-cookie"));
-    assertEquals(Protocol.SPDY_3.toString(), headers.get(OkHeaders.SELECTED_PROTOCOL));
-    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
-    assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
-    assertEquals("cache-control", headers.name(1));
-    assertEquals("no-cache, no-store", headers.value(1));
+    assertEquals("cache-control", headers.name(0));
+    assertEquals("no-cache, no-store", headers.value(0));
+    assertEquals("set-cookie", headers.name(1));
+    assertEquals("Cookie1", headers.value(1));
     assertEquals("set-cookie", headers.name(2));
-    assertEquals("Cookie1", headers.value(2));
-    assertEquals("set-cookie", headers.name(3));
-    assertEquals("Cookie2", headers.value(3));
+    assertEquals("Cookie2", headers.value(2));
     assertNull(headers.get(":status"));
     assertNull(headers.get(":version"));
   }
@@ -70,12 +65,9 @@
         ":version", "HTTP/1.1",
         "connection", "close");
     Request request = new Request.Builder().url("http://square.com/").build();
-    Response response =
-        FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+    Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
     Headers headers = response.headers();
-    assertEquals(1, headers.size());
-    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
-    assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
+    assertEquals(0, headers.size());
   }
 
   @Test public void readNameValueBlockDropsForbiddenHeadersHttp2() throws IOException {
@@ -84,15 +76,14 @@
         ":version", "HTTP/1.1",
         "connection", "close");
     Request request = new Request.Builder().url("http://square.com/").build();
-    Response response = FramedTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
-        .request(request).build();
+    Response response = Http2xStream.readHttp2HeadersList(headerBlock).request(request).build();
     Headers headers = response.headers();
     assertEquals(1, headers.size());
-    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
-    assertEquals(Protocol.HTTP_2.toString(), headers.value(0));
+    assertEquals(":version", headers.name(0));
+    assertEquals("HTTP/1.1", headers.value(0));
   }
 
-  @Test public void toNameValueBlock() {
+  @Test public void spdy3HeadersList() {
     Request request = new Request.Builder()
         .url("http://square.com/")
         .header("cache-control", "no-cache, no-store")
@@ -100,8 +91,7 @@
         .addHeader("set-cookie", "Cookie2")
         .header(":status", "200 OK")
         .build();
-    List<Header> headerBlock =
-        FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
+    List<Header> headerBlock = Http2xStream.spdy3HeadersList(request);
     List<Header> expected = headerEntries(
         ":method", "GET",
         ":path", "/",
@@ -114,7 +104,7 @@
     assertEquals(expected, headerBlock);
   }
 
-  @Test public void toNameValueBlockDropsForbiddenHeadersSpdy3() {
+  @Test public void spdy3HeadersListDropsForbiddenHeadersSpdy3() {
     Request request = new Request.Builder()
         .url("http://square.com/")
         .header("Connection", "close")
@@ -126,10 +116,10 @@
         ":version", "HTTP/1.1",
         ":host", "square.com",
         ":scheme", "http");
-    assertEquals(expected, FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
+    assertEquals(expected, Http2xStream.spdy3HeadersList(request));
   }
 
-  @Test public void toNameValueBlockDropsForbiddenHeadersHttp2() {
+  @Test public void http2HeadersListDropsForbiddenHeadersHttp2() {
     Request request = new Request.Builder()
         .url("http://square.com/")
         .header("Connection", "upgrade")
@@ -140,8 +130,7 @@
         ":path", "/",
         ":authority", "square.com",
         ":scheme", "http");
-    assertEquals(expected,
-        FramedTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
+    assertEquals(expected, Http2xStream.http2HeadersList(request));
   }
 
   @Test public void ofTrims() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index bb8d082..8560e0f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -17,14 +17,9 @@
 
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Authenticator;
-import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.ConnectionSpec;
-import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
 import com.squareup.okhttp.internal.RouteDatabase;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
@@ -57,14 +52,14 @@
       ConnectionSpec.CLEARTEXT);
 
   private static final int proxyAPort = 1001;
-  private static final String proxyAHost = "proxyA";
+  private static final String proxyAHost = "proxya";
   private static final Proxy proxyA =
-      new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort));
+      new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyAHost, proxyAPort));
   private static final int proxyBPort = 1002;
-  private static final String proxyBHost = "proxyB";
+  private static final String proxyBHost = "proxyb";
   private static final Proxy proxyB =
-      new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
-  private String uriHost = "hostA";
+      new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyBHost, proxyBPort));
+  private String uriHost = "hosta";
   private int uriPort = 1003;
 
   private SocketFactory socketFactory;
@@ -76,43 +71,20 @@
   private final List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1);
   private final FakeDns dns = new FakeDns();
   private final RecordingProxySelector proxySelector = new RecordingProxySelector();
-  private OkHttpClient client;
-  private RouteDatabase routeDatabase;
-  private Request httpRequest;
-  private Request httpsRequest;
+  private RouteDatabase routeDatabase = new RouteDatabase();
 
   @Before public void setUp() throws Exception {
     socketFactory = SocketFactory.getDefault();
     hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
-
-    client = new OkHttpClient()
-        .setAuthenticator(authenticator)
-        .setProxySelector(proxySelector)
-        .setSocketFactory(socketFactory)
-        .setSslSocketFactory(sslSocketFactory)
-        .setHostnameVerifier(hostnameVerifier)
-        .setProtocols(protocols)
-        .setConnectionSpecs(connectionSpecs)
-        .setConnectionPool(ConnectionPool.getDefault());
-    Internal.instance.setNetwork(client, dns);
-
-    routeDatabase = Internal.instance.routeDatabase(client);
-
-    httpRequest = new Request.Builder()
-        .url("http://" + uriHost + ":" + uriPort + "/path")
-        .build();
-    httpsRequest = new Request.Builder()
-        .url("https://" + uriHost + ":" + uriPort + "/path")
-        .build();
   }
 
   @Test public void singleRoute() throws Exception {
     Address address = httpAddress();
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(255, 1));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
 
     assertFalse(routeSelector.hasNext());
@@ -125,14 +97,14 @@
 
   @Test public void singleRouteReturnsFailedRoute() throws Exception {
     Address address = httpAddress();
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
+    dns.addresses(makeFakeAddresses(255, 1));
     Route route = routeSelector.next();
     routeDatabase.failed(route);
-    routeSelector = RouteSelector.get(address, httpRequest, client);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    routeSelector = new RouteSelector(address, routeDatabase);
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     assertFalse(routeSelector.hasNext());
     try {
       routeSelector.next();
@@ -142,15 +114,14 @@
   }
 
   @Test public void explicitProxyTriesThatProxysAddressesOnly() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
-        proxyA, protocols, connectionSpecs, proxySelector);
-    client.setProxy(proxyA);
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+    Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+        authenticator, proxyA, protocols, connectionSpecs, proxySelector);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 2);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+    dns.addresses(makeFakeAddresses(255, 2));
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
 
     assertFalse(routeSelector.hasNext());
     dns.assertRequests(proxyAHost);
@@ -158,15 +129,14 @@
   }
 
   @Test public void explicitDirectProxy() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
-        NO_PROXY, protocols, connectionSpecs, proxySelector);
-    client.setProxy(NO_PROXY);
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+    Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+        authenticator, NO_PROXY, protocols, connectionSpecs, proxySelector);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 2);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+    dns.addresses(makeFakeAddresses(255, 2));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
 
     assertFalse(routeSelector.hasNext());
     dns.assertRequests(uriHost);
@@ -177,12 +147,12 @@
     Address address = httpAddress();
 
     proxySelector.proxies = null;
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
-    proxySelector.assertRequests(httpRequest.uri());
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+    proxySelector.assertRequests(address.url().uri());
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(255, 1));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
 
     assertFalse(routeSelector.hasNext());
@@ -190,16 +160,16 @@
 
   @Test public void proxySelectorReturnsNoProxies() throws Exception {
     Address address = httpAddress();
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 2);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+    dns.addresses(makeFakeAddresses(255, 2));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
 
     assertFalse(routeSelector.hasNext());
     dns.assertRequests(uriHost);
-    proxySelector.assertRequests(httpRequest.uri());
+    proxySelector.assertRequests(address.url().uri());
   }
 
   @Test public void proxySelectorReturnsMultipleProxies() throws Exception {
@@ -207,26 +177,26 @@
 
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
-    proxySelector.assertRequests(httpRequest.uri());
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+    proxySelector.assertRequests(address.url().uri());
 
     // First try the IP addresses of the first proxy, in sequence.
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 2);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+    dns.addresses(makeFakeAddresses(255, 2));
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
     dns.assertRequests(proxyAHost);
 
     // Next try the IP address of the second proxy.
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(254, 1);
-    assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+    dns.addresses(makeFakeAddresses(254, 1));
+    assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
     dns.assertRequests(proxyBHost);
 
     // Finally try the only IP address of the origin server.
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(253, 1);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(253, 1));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
 
     assertFalse(routeSelector.hasNext());
@@ -236,13 +206,13 @@
     Address address = httpAddress();
 
     proxySelector.proxies.add(NO_PROXY);
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
-    proxySelector.assertRequests(httpRequest.uri());
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+    proxySelector.assertRequests(address.url().uri());
 
     // Only the origin server will be attempted.
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(255, 1));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
 
     assertFalse(routeSelector.hasNext());
@@ -254,16 +224,16 @@
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
     proxySelector.proxies.add(proxyA);
-    RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
-    proxySelector.assertRequests(httpRequest.uri());
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+    proxySelector.assertRequests(address.url().uri());
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+    dns.addresses(makeFakeAddresses(255, 1));
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
     dns.assertRequests(proxyAHost);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = null;
+    dns.unknownHost();
     try {
       routeSelector.next();
       fail();
@@ -272,13 +242,13 @@
     dns.assertRequests(proxyBHost);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(255, 1);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+    dns.addresses(makeFakeAddresses(255, 1));
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
     dns.assertRequests(proxyAHost);
 
     assertTrue(routeSelector.hasNext());
-    dns.inetAddresses = makeFakeAddresses(254, 1);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(254, 1));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
 
     assertFalse(routeSelector.hasNext());
@@ -288,36 +258,35 @@
     Address address = httpsAddress();
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
-    RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     // Proxy A
-    dns.inetAddresses = makeFakeAddresses(255, 2);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+    dns.addresses(makeFakeAddresses(255, 2));
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
     dns.assertRequests(proxyAHost);
-    assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+    assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
 
     // Proxy B
-    dns.inetAddresses = makeFakeAddresses(254, 2);
-    assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+    dns.addresses(makeFakeAddresses(254, 2));
+    assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
     dns.assertRequests(proxyBHost);
-    assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort);
+    assertRoute(routeSelector.next(), address, proxyB, dns.address(1), proxyBPort);
 
     // Origin
-    dns.inetAddresses = makeFakeAddresses(253, 2);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+    dns.addresses(makeFakeAddresses(253, 2));
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
     dns.assertRequests(uriHost);
-    assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+    assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
 
     assertFalse(routeSelector.hasNext());
   }
 
   @Test public void failedRoutesAreLast() throws Exception {
     Address address = httpsAddress();
-    client.setProxy(Proxy.NO_PROXY);
-    RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+    RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
 
     final int numberOfAddresses = 2;
-    dns.inetAddresses = makeFakeAddresses(255, numberOfAddresses);
+    dns.addresses(makeFakeAddresses(255, numberOfAddresses));
 
     // Extract the regular sequence of routes from selector.
     List<Route> regularRoutes = new ArrayList<>();
@@ -330,7 +299,7 @@
     // Add first regular route as failed.
     routeDatabase.failed(regularRoutes.get(0));
     // Reset selector
-    routeSelector = RouteSelector.get(address, httpsRequest, client);
+    routeSelector = new RouteSelector(address, routeDatabase);
 
     List<Route> routesWithFailedRoute = new ArrayList<>();
     while (routeSelector.hasNext()) {
@@ -370,41 +339,25 @@
 
   /** Returns an address that's without an SSL socket factory or hostname verifier. */
   private Address httpAddress() {
-    return new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator, null,
+    return new Address(uriHost, uriPort, dns, socketFactory, null, null, null, authenticator, null,
         protocols, connectionSpecs, proxySelector);
   }
 
   private Address httpsAddress() {
-    return new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+    return new Address(uriHost, uriPort, dns, socketFactory, sslSocketFactory,
         hostnameVerifier, null, authenticator, null, protocols, connectionSpecs, proxySelector);
   }
 
-  private static InetAddress[] makeFakeAddresses(int prefix, int count) {
+  private static List<InetAddress> makeFakeAddresses(int prefix, int count) {
     try {
-      InetAddress[] result = new InetAddress[count];
+      List<InetAddress> result = new ArrayList<>();
       for (int i = 0; i < count; i++) {
-        result[i] =
-            InetAddress.getByAddress(new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i });
+        result.add(InetAddress.getByAddress(
+            new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i }));
       }
       return result;
     } catch (UnknownHostException e) {
       throw new AssertionError();
     }
   }
-
-  private static class FakeDns implements Network {
-    List<String> requestedHosts = new ArrayList<>();
-    InetAddress[] inetAddresses;
-
-    @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
-      requestedHosts.add(host);
-      if (inetAddresses == null) throw new UnknownHostException();
-      return inetAddresses;
-    }
-
-    public void assertRequests(String... expectedHosts) {
-      assertEquals(Arrays.asList(expectedHosts), requestedHosts);
-      requestedHosts.clear();
-    }
-  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
new file mode 100644
index 0000000..951d9e6
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import com.squareup.okhttp.internal.HeldCertificate;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class CertificateChainCleanerTest {
+  @Test public void normalizeSingleSelfSignedCertificate() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(root), council.clean(list(root)));
+  }
+
+  @Test public void normalizeUnknownSelfSignedCertificate() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex());
+
+    try {
+      council.clean(list(root));
+      fail();
+    } catch (SSLPeerUnverifiedException expected) {
+    }
+  }
+
+  @Test public void orderedChainOfCertificatesWithRoot() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(root)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(certA)
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(certB, certA, root), council.clean(list(certB, certA, root)));
+  }
+
+  @Test public void orderedChainOfCertificatesWithoutRoot() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(root)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(certA)
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(certB, certA, root), council.clean(list(certB, certA))); // Root is added!
+  }
+
+  @Test public void unorderedChainOfCertificatesWithRoot() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(root)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(certA)
+        .build();
+    HeldCertificate certC = new HeldCertificate.Builder()
+        .serialNumber("4")
+        .issuedBy(certB)
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, root, certB)));
+  }
+
+  @Test public void unorderedChainOfCertificatesWithoutRoot() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(root)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(certA)
+        .build();
+    HeldCertificate certC = new HeldCertificate.Builder()
+        .serialNumber("4")
+        .issuedBy(certB)
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, certB)));
+  }
+
+  @Test public void unrelatedCertificatesAreOmitted() throws Exception {
+    HeldCertificate root = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(root)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(certA)
+        .build();
+    HeldCertificate certUnnecessary = new HeldCertificate.Builder()
+        .serialNumber("4")
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(root.certificate));
+    assertEquals(list(certB, certA, root),
+        council.clean(list(certB, certUnnecessary, certA, root)));
+  }
+
+  @Test public void chainGoesAllTheWayToSelfSignedRoot() throws Exception {
+    HeldCertificate selfSigned = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate trusted = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .issuedBy(selfSigned)
+        .build();
+    HeldCertificate certA = new HeldCertificate.Builder()
+        .serialNumber("3")
+        .issuedBy(trusted)
+        .build();
+    HeldCertificate certB = new HeldCertificate.Builder()
+        .serialNumber("4")
+        .issuedBy(certA)
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(selfSigned.certificate, trusted.certificate));
+    assertEquals(list(certB, certA, trusted, selfSigned),
+        council.clean(list(certB, certA)));
+    assertEquals(list(certB, certA, trusted, selfSigned),
+        council.clean(list(certB, certA, trusted)));
+    assertEquals(list(certB, certA, trusted, selfSigned),
+        council.clean(list(certB, certA, trusted, selfSigned)));
+  }
+
+  @Test public void trustedRootNotSelfSigned() throws Exception {
+    HeldCertificate unknownSigner = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .build();
+    HeldCertificate trusted = new HeldCertificate.Builder()
+        .issuedBy(unknownSigner)
+        .serialNumber("2")
+        .build();
+    HeldCertificate intermediateCa = new HeldCertificate.Builder()
+        .issuedBy(trusted)
+        .serialNumber("3")
+        .build();
+    HeldCertificate certificate = new HeldCertificate.Builder()
+        .issuedBy(intermediateCa)
+        .serialNumber("4")
+        .build();
+
+    CertificateChainCleaner council = new CertificateChainCleaner(
+        new RealTrustRootIndex(trusted.certificate));
+    assertEquals(list(certificate, intermediateCa, trusted),
+        council.clean(list(certificate, intermediateCa)));
+    assertEquals(list(certificate, intermediateCa, trusted),
+        council.clean(list(certificate, intermediateCa, trusted)));
+  }
+
+  @Test public void chainMaxLength() throws Exception {
+    List<HeldCertificate> heldCertificates = chainOfLength(10);
+    List<Certificate> certificates = new ArrayList<>();
+    for (HeldCertificate heldCertificate : heldCertificates) {
+      certificates.add(heldCertificate.certificate);
+    }
+
+    X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+    CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+    assertEquals(certificates, council.clean(certificates));
+    assertEquals(certificates, council.clean(certificates.subList(0, 9)));
+  }
+
+  @Test public void chainTooLong() throws Exception {
+    List<HeldCertificate> heldCertificates = chainOfLength(11);
+    List<Certificate> certificates = new ArrayList<>();
+    for (HeldCertificate heldCertificate : heldCertificates) {
+      certificates.add(heldCertificate.certificate);
+    }
+
+    X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+    CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+    try {
+      council.clean(certificates);
+      fail();
+    } catch (SSLPeerUnverifiedException expected) {
+    }
+  }
+
+  /** Returns a chain starting at the leaf certificate and progressing to the root. */
+  private List<HeldCertificate> chainOfLength(int length) throws GeneralSecurityException {
+    List<HeldCertificate> result = new ArrayList<>();
+    for (int i = 1; i <= length; i++) {
+      result.add(0, new HeldCertificate.Builder()
+          .issuedBy(!result.isEmpty() ? result.get(0) : null)
+          .serialNumber(Integer.toString(i))
+          .build());
+    }
+    return result;
+  }
+
+  private List<Certificate> list(HeldCertificate... heldCertificates) {
+    List<Certificate> result = new ArrayList<>();
+    for (HeldCertificate heldCertificate : heldCertificates) {
+      result.add(heldCertificate.certificate);
+    }
+    return result;
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
new file mode 100644
index 0000000..5144dd2
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.HeldCertificate;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CertificatePinnerChainValidationTest {
+  @Rule public final MockWebServer server = new MockWebServer();
+
+  /** The pinner should pull the root certificate from the trust manager. */
+  @Test public void pinRootNotPresentInChain() throws Exception {
+    HeldCertificate rootCa = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .ca(3)
+        .commonName("root")
+        .build();
+    HeldCertificate intermediateCa = new HeldCertificate.Builder()
+        .issuedBy(rootCa)
+        .ca(2)
+        .serialNumber("2")
+        .commonName("intermediate_ca")
+        .build();
+    HeldCertificate certificate = new HeldCertificate.Builder()
+        .issuedBy(intermediateCa)
+        .serialNumber("3")
+        .commonName(server.getHostName())
+        .build();
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add(server.getHostName(), CertificatePinner.pin(rootCa.certificate))
+        .build();
+    SSLContext clientContext = new SslContextBuilder()
+        .addTrustedCertificate(rootCa.certificate)
+        .build();
+    OkHttpClient client = new OkHttpClient()
+        .setSslSocketFactory(clientContext.getSocketFactory())
+        .setHostnameVerifier(new RecordingHostnameVerifier())
+        .setCertificatePinner(certificatePinner);
+
+    SSLContext serverSslContext = new SslContextBuilder()
+        .certificateChain(certificate, intermediateCa)
+        .build();
+    server.useHttps(serverSslContext.getSocketFactory(), false);
+
+    // The request should complete successfully.
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    Call call1 = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .build());
+    Response response1 = call1.execute();
+    assertEquals("abc", response1.body().string());
+
+    // Confirm that a second request also succeeds. This should detect caching problems.
+    server.enqueue(new MockResponse()
+        .setBody("def")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    Call call2 = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .build());
+    Response response2 = call2.execute();
+    assertEquals("def", response2.body().string());
+  }
+
+  /** The pinner should accept an intermediate from the server's chain. */
+  @Test public void pinIntermediatePresentInChain() throws Exception {
+    HeldCertificate rootCa = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .ca(3)
+        .commonName("root")
+        .build();
+    HeldCertificate intermediateCa = new HeldCertificate.Builder()
+        .issuedBy(rootCa)
+        .ca(2)
+        .serialNumber("2")
+        .commonName("intermediate_ca")
+        .build();
+    HeldCertificate certificate = new HeldCertificate.Builder()
+        .issuedBy(intermediateCa)
+        .serialNumber("3")
+        .commonName(server.getHostName())
+        .build();
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add(server.getHostName(), CertificatePinner.pin(intermediateCa.certificate))
+        .build();
+    SSLContext clientContext = new SslContextBuilder()
+        .addTrustedCertificate(rootCa.certificate)
+        .build();
+    OkHttpClient client = new OkHttpClient()
+        .setSslSocketFactory(clientContext.getSocketFactory())
+        .setHostnameVerifier(new RecordingHostnameVerifier())
+        .setCertificatePinner(certificatePinner);
+
+    SSLContext serverSslContext = new SslContextBuilder()
+        .certificateChain(certificate, intermediateCa)
+        .build();
+    server.useHttps(serverSslContext.getSocketFactory(), false);
+
+    // The request should complete successfully.
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    Call call1 = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .build());
+    Response response1 = call1.execute();
+    assertEquals("abc", response1.body().string());
+
+    // Confirm that a second request also succeeds. This should detect caching problems.
+    server.enqueue(new MockResponse()
+        .setBody("def")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    Call call2 = client.newCall(new Request.Builder()
+        .url(server.url("/"))
+        .build());
+    Response response2 = call2.execute();
+    assertEquals("def", response2.body().string());
+  }
+
+  @Test public void unrelatedPinnedLeafCertificateInChain() throws Exception {
+    // Start with a trusted root CA certificate.
+    HeldCertificate rootCa = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .ca(3)
+        .commonName("root")
+        .build();
+
+    // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+    // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+    // certificate.
+    HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+        .issuedBy(rootCa)
+        .ca(2)
+        .serialNumber("2")
+        .commonName("good_intermediate_ca")
+        .build();
+    HeldCertificate goodCertificate = new HeldCertificate.Builder()
+        .issuedBy(goodIntermediateCa)
+        .serialNumber("3")
+        .commonName(server.getHostName())
+        .build();
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add(server.getHostName(), CertificatePinner.pin(goodCertificate.certificate))
+        .build();
+    SSLContext clientContext = new SslContextBuilder()
+        .addTrustedCertificate(rootCa.certificate)
+        .build();
+    OkHttpClient client = new OkHttpClient()
+        .setSslSocketFactory(clientContext.getSocketFactory())
+        .setHostnameVerifier(new RecordingHostnameVerifier())
+        .setCertificatePinner(certificatePinner);
+
+    // Add a bad intermediate CA and have that issue a rogue certificate for localhost. Prepare
+    // an SSL context for an attacking webserver. It includes both these rogue certificates plus the
+    // trusted good certificate above. The attack is that by including the good certificate in the
+    // chain, we may trick the certificate pinner into accepting the rouge certificate.
+    HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+        .issuedBy(rootCa)
+        .ca(2)
+        .serialNumber("4")
+        .commonName("bad_intermediate_ca")
+        .build();
+    HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+        .serialNumber("5")
+        .issuedBy(compromisedIntermediateCa)
+        .commonName(server.getHostName())
+        .build();
+    SSLContext serverSslContext = new SslContextBuilder()
+        .certificateChain(rogueCertificate, compromisedIntermediateCa, goodCertificate, rootCa)
+        .build();
+    server.useHttps(serverSslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+
+    // Make a request from client to server. It should succeed certificate checks (unfortunately the
+    // rogue CA is trusted) but it should fail certificate pinning.
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Call call = client.newCall(request);
+    try {
+      call.execute();
+      fail();
+    } catch (SSLPeerUnverifiedException expected) {
+      // Certificate pinning fails!
+      String message = expected.getMessage();
+      assertTrue(message, message.startsWith("Certificate pinning failure!"));
+    }
+  }
+
+  @Test public void unrelatedPinnedIntermediateCertificateInChain() throws Exception {
+    // Start with two root CA certificates, one is good and the other is compromised.
+    HeldCertificate rootCa = new HeldCertificate.Builder()
+        .serialNumber("1")
+        .ca(3)
+        .commonName("root")
+        .build();
+    HeldCertificate compromisedRootCa = new HeldCertificate.Builder()
+        .serialNumber("2")
+        .ca(3)
+        .commonName("compromised_root")
+        .build();
+
+    // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+    // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+    // certificate.
+    HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+        .issuedBy(rootCa)
+        .ca(2)
+        .serialNumber("3")
+        .commonName("intermediate_ca")
+        .build();
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add(server.getHostName(), CertificatePinner.pin(goodIntermediateCa.certificate))
+        .build();
+    SSLContext clientContext = new SslContextBuilder()
+        .addTrustedCertificate(rootCa.certificate)
+        .addTrustedCertificate(compromisedRootCa.certificate)
+        .build();
+    OkHttpClient client = new OkHttpClient()
+        .setSslSocketFactory(clientContext.getSocketFactory())
+        .setHostnameVerifier(new RecordingHostnameVerifier())
+        .setCertificatePinner(certificatePinner);
+
+    // The attacker compromises the root CA, issues an intermediate with the same common name
+    // "intermediate_ca" as the good CA. This signs a rogue certificate for localhost. The server
+    // serves the good CAs certificate in the chain, which means the certificate pinner sees a
+    // different set of certificates than the SSL verifier.
+    HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+        .issuedBy(compromisedRootCa)
+        .ca(2)
+        .serialNumber("4")
+        .commonName("intermediate_ca")
+        .build();
+    HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+        .serialNumber("5")
+        .issuedBy(compromisedIntermediateCa)
+        .commonName(server.getHostName())
+        .build();
+    SSLContext serverSslContext = new SslContextBuilder()
+        .certificateChain(
+            rogueCertificate, goodIntermediateCa, compromisedIntermediateCa, compromisedRootCa)
+        .build();
+    server.useHttps(serverSslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+
+    // Make a request from client to server. It should succeed certificate checks (unfortunately the
+    // rogue CA is trusted) but it should fail certificate pinning.
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+    Call call = client.newCall(request);
+    try {
+      call.execute();
+      fail();
+    } catch (SSLHandshakeException expected) {
+      // On Android, the handshake fails before the certificate pinner runs.
+      String message = expected.getMessage();
+      assertTrue(message, message.contains("Could not validate certificate"));
+    } catch (SSLPeerUnverifiedException expected) {
+      // On OpenJDK, the handshake succeeds but the certificate pinner fails.
+      String message = expected.getMessage();
+      assertTrue(message, message.startsWith("Certificate pinning failure!"));
+    }
+  }
+}
diff --git a/okhttp-urlconnection/pom.xml b/okhttp-urlconnection/pom.xml
index be60560..c11c67f 100644
--- a/okhttp-urlconnection/pom.xml
+++ b/okhttp-urlconnection/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-urlconnection</artifactId>
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
index 454f6c3..2398de5 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
@@ -40,6 +40,7 @@
 import com.squareup.okhttp.internal.http.RetryableSink;
 import com.squareup.okhttp.internal.http.RouteException;
 import com.squareup.okhttp.internal.http.StatusLine;
+import com.squareup.okhttp.internal.http.StreamAllocation;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -73,7 +74,7 @@
  *
  * <h3>What does 'connected' mean?</h3>
  * This class inherits a {@code connected} field from the superclass. That field
- * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * is <strong>not</strong> used to indicate whether this URLConnection is
  * currently connected. Instead, it indicates whether a connection has ever been
  * attempted. Once a connection has been attempted, certain properties (request
  * header fields, request method, etc.) are immutable.
@@ -131,7 +132,7 @@
     // Calling disconnect() before a connection exists should have no effect.
     if (httpEngine == null) return;
 
-    httpEngine.disconnect();
+    httpEngine.cancel();
 
     // This doesn't close the stream because doing so would require all stream
     // access to be synchronized. It's expected that the thread using the
@@ -161,9 +162,9 @@
     if (responseHeaders == null) {
       Response response = getResponse().getResponse();
       Headers headers = response.headers();
-
       responseHeaders = headers.newBuilder()
-          .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response))
+          .add(OkHeaders.SELECTED_PROTOCOL, response.protocol().toString())
+          .add(OkHeaders.RESPONSE_SOURCE, responseSourceHeader(response))
           .build();
     }
     return responseHeaders;
@@ -335,8 +336,9 @@
     }
   }
 
-  private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody,
-      Response priorResponse) throws MalformedURLException, UnknownHostException {
+  private HttpEngine newHttpEngine(String method, StreamAllocation streamAllocation,
+      RetryableSink requestBody, Response priorResponse)
+      throws MalformedURLException, UnknownHostException {
     // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
     RequestBody placeholderBody = HttpMethod.requiresRequestBody(method)
         ? EMPTY_REQUEST_BODY
@@ -380,7 +382,7 @@
       engineClient = client.clone().setCache(null);
     }
 
-    return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null,
+    return new HttpEngine(engineClient, request, bufferRequestBody, true, false, streamAllocation,
         requestBody, priorResponse);
   }
 
@@ -410,7 +412,7 @@
       Request followUp = httpEngine.followUpRequest();
 
       if (followUp == null) {
-        httpEngine.releaseConnection();
+        httpEngine.releaseStreamAllocation();
         return httpEngine;
       }
 
@@ -434,12 +436,13 @@
         throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
       }
 
+      StreamAllocation streamAllocation = httpEngine.close();
       if (!httpEngine.sameConnection(followUp.httpUrl())) {
-        httpEngine.releaseConnection();
+        streamAllocation.release();
+        streamAllocation = null;
       }
 
-      Connection connection = httpEngine.close();
-      httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody,
+      httpEngine = newHttpEngine(followUp.method(), streamAllocation, (RetryableSink) requestBody,
           response);
     }
   }
@@ -450,18 +453,24 @@
    * retried. Throws an exception if the request failed permanently.
    */
   private boolean execute(boolean readResponse) throws IOException {
+    boolean releaseConnection = true;
     if (urlFilter != null) {
       urlFilter.checkURLPermitted(httpEngine.getRequest().url());
     }
     try {
       httpEngine.sendRequest();
-      route = httpEngine.getRoute();
-      handshake = httpEngine.getConnection() != null
-          ? httpEngine.getConnection().getHandshake()
-          : null;
+      Connection connection = httpEngine.getConnection();
+      if (connection != null) {
+        route = connection.getRoute();
+        handshake = connection.getHandshake();
+      } else {
+        route = null;
+        handshake = null;
+      }
       if (readResponse) {
         httpEngine.readResponse();
       }
+      releaseConnection = false;
 
       return true;
     } catch (RequestException e) {
@@ -473,6 +482,7 @@
       // The attempt to connect via a route failed. The request will not have been sent.
       HttpEngine retryEngine = httpEngine.recover(e);
       if (retryEngine != null) {
+        releaseConnection = false;
         httpEngine = retryEngine;
         return false;
       }
@@ -485,6 +495,7 @@
       // An attempt to communicate with a server failed. The request may have been sent.
       HttpEngine retryEngine = httpEngine.recover(e);
       if (retryEngine != null) {
+        releaseConnection = false;
         httpEngine = retryEngine;
         return false;
       }
@@ -492,6 +503,12 @@
       // Give up; recovery is not possible.
       httpEngineFailure = e;
       throw e;
+    } finally {
+      // We're throwing an unchecked exception. Release any resources.
+      if (releaseConnection) {
+        StreamAllocation streamAllocation = httpEngine.close();
+        streamAllocation.release();
+      }
     }
   }
 
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
index 27e4d23..140b4d2 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
@@ -68,19 +68,15 @@
     return delegate.client.getSslSocketFactory();
   }
 
-  // ANDROID-BEGIN
-  //  @Override public long getContentLengthLong() {
-  //    return delegate.getContentLengthLong();
-  //  }
-  // ANDROID-END
+  @Override public long getContentLengthLong() {
+    return delegate.getContentLengthLong();
+  }
 
   @Override public void setFixedLengthStreamingMode(long contentLength) {
     delegate.setFixedLengthStreamingMode(contentLength);
   }
 
-  // ANDROID-BEGIN
-  // @Override public long getHeaderFieldLong(String field, long defaultValue) {
-  //   return delegate.getHeaderFieldLong(field, defaultValue);
-  // }
-  // ANDROID-END
+  @Override public long getHeaderFieldLong(String field, long defaultValue) {
+    return delegate.getHeaderFieldLong(field, defaultValue);
+  }
 }
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
index 983dd57..7e47918 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
@@ -1,5 +1,6 @@
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.http.OkHeaders;
 import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.URLFilter;
 import com.squareup.okhttp.internal.io.FileSystem;
@@ -16,6 +17,8 @@
 import java.util.Locale;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
+import okio.BufferedSource;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -28,16 +31,22 @@
 
 public class OkUrlFactoryTest {
   @Rule public MockWebServer server = new MockWebServer();
+  @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
 
-  private FileSystem fileSystem = new InMemoryFileSystem();
   private OkUrlFactory factory;
+  private Cache cache;
 
   @Before public void setUp() throws IOException {
     OkHttpClient client = new OkHttpClient();
-    client.setCache(new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem));
+    cache = new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem);
+    client.setCache(cache);
     factory = new OkUrlFactory(client);
   }
 
+  @After public void tearDown() throws IOException {
+    cache.delete();
+  }
+
   /**
    * Response code 407 should only come from proxy servers. Android's client
    * throws if it is sent by an origin server.
@@ -66,6 +75,7 @@
 
     HttpURLConnection connection = factory.open(server.getUrl("/"));
     assertResponseHeader(connection, "NETWORK 404");
+    connection.getErrorStream().close();
   }
 
   @Test public void conditionalCacheHitResponseSourceHeaders() throws Exception {
@@ -166,13 +176,14 @@
   }
 
   private void assertResponseBody(HttpURLConnection connection, String expected) throws Exception {
-    String actual = buffer(source(connection.getInputStream())).readString(US_ASCII);
+    BufferedSource source = buffer(source(connection.getInputStream()));
+    String actual = source.readString(US_ASCII);
+    source.close();
     assertEquals(expected, actual);
   }
 
   private void assertResponseHeader(HttpURLConnection connection, String expected) {
-    final String headerFieldPrefix = Platform.get().getPrefix();
-    assertEquals(expected, connection.getHeaderField(headerFieldPrefix + "-Response-Source"));
+    assertEquals(expected, connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
   }
 
   private void assertResponseCode(HttpURLConnection connection, int expected) throws IOException {
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
index 0af815b..66bf7c2 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
@@ -19,7 +19,6 @@
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
 import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -81,9 +80,9 @@
 
   @Rule public MockWebServer server = new MockWebServer();
   @Rule public MockWebServer server2 = new MockWebServer();
+  @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
 
   private final SSLContext sslContext = SslContextBuilder.localhost();
-  private final FileSystem fileSystem = new InMemoryFileSystem();
   private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
   private Cache cache;
   private final CookieManager cookieManager = new CookieManager();
@@ -98,6 +97,7 @@
   @After public void tearDown() throws Exception {
     ResponseCache.setDefault(null);
     CookieHandler.setDefault(null);
+    cache.delete();
   }
 
   @Test public void responseCacheAccessWithOkHttpMember() throws IOException {
@@ -855,7 +855,7 @@
 
     assertEquals("A", readAscii(client.open(server.getUrl("/"))));
     assertEquals("A", readAscii(client.open(server.getUrl("/"))));
-    assertEquals(1, client.client().getConnectionPool().getConnectionCount());
+    assertEquals(1, client.client().getConnectionPool().getIdleConnectionCount());
   }
 
   @Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1586,6 +1586,7 @@
 
     HttpURLConnection connection = client.open(server.getUrl("/"));
     assertEquals("A", connection.getHeaderField(""));
+    assertEquals("body", readAscii(connection));
   }
 
   /**
diff --git a/okhttp-ws-tests/fuzzingserver-config.json b/okhttp-ws-tests/fuzzingserver-config.json
new file mode 100644
index 0000000..99e06ab
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-config.json
@@ -0,0 +1,153 @@
+{
+  "url": "ws://127.0.0.1:9001",
+  "outdir": "./target/fuzzingserver-report",
+  "cases": ["*"],
+  "exclude-cases": [
+    "6.1.1",
+    "6.1.2",
+    "6.1.3",
+    "6.2.1",
+    "6.2.2",
+    "6.2.3",
+    "6.2.4",
+    "6.3.1",
+    "6.3.2",
+    "6.4.1",
+    "6.4.2",
+    "6.4.3",
+    "6.4.4",
+    "6.5.1",
+    "6.5.2",
+    "6.5.3",
+    "6.5.4",
+    "6.5.5",
+    "6.6.1",
+    "6.6.2",
+    "6.6.3",
+    "6.6.4",
+    "6.6.5",
+    "6.6.6",
+    "6.6.7",
+    "6.6.8",
+    "6.6.9",
+    "6.6.10",
+    "6.6.11",
+    "6.7.1",
+    "6.7.2",
+    "6.7.3",
+    "6.7.4",
+    "6.8.1",
+    "6.8.2",
+    "6.9.1",
+    "6.9.2",
+    "6.9.3",
+    "6.9.4",
+    "6.10.1",
+    "6.10.2",
+    "6.10.3",
+    "6.11.1",
+    "6.11.2",
+    "6.11.3",
+    "6.11.4",
+    "6.11.5",
+    "6.12.1",
+    "6.12.2",
+    "6.12.3",
+    "6.12.4",
+    "6.12.5",
+    "6.12.6",
+    "6.12.7",
+    "6.12.8",
+    "6.13.1",
+    "6.13.2",
+    "6.13.3",
+    "6.13.4",
+    "6.13.5",
+    "6.14.1",
+    "6.14.2",
+    "6.14.3",
+    "6.14.4",
+    "6.14.5",
+    "6.14.6",
+    "6.14.7",
+    "6.14.8",
+    "6.14.9",
+    "6.14.10",
+    "6.15.1",
+    "6.16.1",
+    "6.16.2",
+    "6.16.3",
+    "6.17.1",
+    "6.17.2",
+    "6.17.3",
+    "6.17.4",
+    "6.17.5",
+    "6.18.1",
+    "6.18.2",
+    "6.18.3",
+    "6.18.4",
+    "6.18.5",
+    "6.19.1",
+    "6.19.2",
+    "6.19.3",
+    "6.19.4",
+    "6.19.5",
+    "6.20.1",
+    "6.20.2",
+    "6.20.3",
+    "6.20.4",
+    "6.20.5",
+    "6.20.6",
+    "6.20.7",
+    "6.21.1",
+    "6.21.2",
+    "6.21.3",
+    "6.21.4",
+    "6.21.5",
+    "6.21.6",
+    "6.21.7",
+    "6.21.8",
+    "6.22.1",
+    "6.22.2",
+    "6.22.3",
+    "6.22.4",
+    "6.22.5",
+    "6.22.6",
+    "6.22.7",
+    "6.22.8",
+    "6.22.9",
+    "6.22.10",
+    "6.22.11",
+    "6.22.12",
+    "6.22.13",
+    "6.22.14",
+    "6.22.15",
+    "6.22.16",
+    "6.22.17",
+    "6.22.18",
+    "6.22.19",
+    "6.22.20",
+    "6.22.21",
+    "6.22.22",
+    "6.22.23",
+    "6.22.24",
+    "6.22.25",
+    "6.22.26",
+    "6.22.27",
+    "6.22.28",
+    "6.22.29",
+    "6.22.30",
+    "6.22.31",
+    "6.22.32",
+    "6.22.33",
+    "6.22.34",
+    "6.23.1",
+    "6.23.2",
+    "6.23.3",
+    "6.23.4",
+    "6.23.5",
+    "6.23.6",
+    "6.23.7"
+  ],
+  "exclude-agent-cases": {}
+}
diff --git a/okhttp-ws-tests/fuzzingserver-expected.txt b/okhttp-ws-tests/fuzzingserver-expected.txt
new file mode 100644
index 0000000..f4a3305
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-expected.txt
@@ -0,0 +1,376 @@
+"1.1.1 OK"
+"1.1.2 OK"
+"1.1.3 OK"
+"1.1.4 OK"
+"1.1.5 OK"
+"1.1.6 OK"
+"1.1.7 OK"
+"1.1.8 OK"
+"1.2.1 OK"
+"1.2.2 OK"
+"1.2.3 OK"
+"1.2.4 OK"
+"1.2.5 OK"
+"1.2.6 OK"
+"1.2.7 OK"
+"1.2.8 OK"
+"10.1.1 OK"
+"12.1.1 UNIMPLEMENTED"
+"12.1.10 UNIMPLEMENTED"
+"12.1.11 UNIMPLEMENTED"
+"12.1.12 UNIMPLEMENTED"
+"12.1.13 UNIMPLEMENTED"
+"12.1.14 UNIMPLEMENTED"
+"12.1.15 UNIMPLEMENTED"
+"12.1.16 UNIMPLEMENTED"
+"12.1.17 UNIMPLEMENTED"
+"12.1.18 UNIMPLEMENTED"
+"12.1.2 UNIMPLEMENTED"
+"12.1.3 UNIMPLEMENTED"
+"12.1.4 UNIMPLEMENTED"
+"12.1.5 UNIMPLEMENTED"
+"12.1.6 UNIMPLEMENTED"
+"12.1.7 UNIMPLEMENTED"
+"12.1.8 UNIMPLEMENTED"
+"12.1.9 UNIMPLEMENTED"
+"12.2.1 UNIMPLEMENTED"
+"12.2.10 UNIMPLEMENTED"
+"12.2.11 UNIMPLEMENTED"
+"12.2.12 UNIMPLEMENTED"
+"12.2.13 UNIMPLEMENTED"
+"12.2.14 UNIMPLEMENTED"
+"12.2.15 UNIMPLEMENTED"
+"12.2.16 UNIMPLEMENTED"
+"12.2.17 UNIMPLEMENTED"
+"12.2.18 UNIMPLEMENTED"
+"12.2.2 UNIMPLEMENTED"
+"12.2.3 UNIMPLEMENTED"
+"12.2.4 UNIMPLEMENTED"
+"12.2.5 UNIMPLEMENTED"
+"12.2.6 UNIMPLEMENTED"
+"12.2.7 UNIMPLEMENTED"
+"12.2.8 UNIMPLEMENTED"
+"12.2.9 UNIMPLEMENTED"
+"12.3.1 UNIMPLEMENTED"
+"12.3.10 UNIMPLEMENTED"
+"12.3.11 UNIMPLEMENTED"
+"12.3.12 UNIMPLEMENTED"
+"12.3.13 UNIMPLEMENTED"
+"12.3.14 UNIMPLEMENTED"
+"12.3.15 UNIMPLEMENTED"
+"12.3.16 UNIMPLEMENTED"
+"12.3.17 UNIMPLEMENTED"
+"12.3.18 UNIMPLEMENTED"
+"12.3.2 UNIMPLEMENTED"
+"12.3.3 UNIMPLEMENTED"
+"12.3.4 UNIMPLEMENTED"
+"12.3.5 UNIMPLEMENTED"
+"12.3.6 UNIMPLEMENTED"
+"12.3.7 UNIMPLEMENTED"
+"12.3.8 UNIMPLEMENTED"
+"12.3.9 UNIMPLEMENTED"
+"12.4.1 UNIMPLEMENTED"
+"12.4.10 UNIMPLEMENTED"
+"12.4.11 UNIMPLEMENTED"
+"12.4.12 UNIMPLEMENTED"
+"12.4.13 UNIMPLEMENTED"
+"12.4.14 UNIMPLEMENTED"
+"12.4.15 UNIMPLEMENTED"
+"12.4.16 UNIMPLEMENTED"
+"12.4.17 UNIMPLEMENTED"
+"12.4.18 UNIMPLEMENTED"
+"12.4.2 UNIMPLEMENTED"
+"12.4.3 UNIMPLEMENTED"
+"12.4.4 UNIMPLEMENTED"
+"12.4.5 UNIMPLEMENTED"
+"12.4.6 UNIMPLEMENTED"
+"12.4.7 UNIMPLEMENTED"
+"12.4.8 UNIMPLEMENTED"
+"12.4.9 UNIMPLEMENTED"
+"12.5.1 UNIMPLEMENTED"
+"12.5.10 UNIMPLEMENTED"
+"12.5.11 UNIMPLEMENTED"
+"12.5.12 UNIMPLEMENTED"
+"12.5.13 UNIMPLEMENTED"
+"12.5.14 UNIMPLEMENTED"
+"12.5.15 UNIMPLEMENTED"
+"12.5.16 UNIMPLEMENTED"
+"12.5.17 UNIMPLEMENTED"
+"12.5.18 UNIMPLEMENTED"
+"12.5.2 UNIMPLEMENTED"
+"12.5.3 UNIMPLEMENTED"
+"12.5.4 UNIMPLEMENTED"
+"12.5.5 UNIMPLEMENTED"
+"12.5.6 UNIMPLEMENTED"
+"12.5.7 UNIMPLEMENTED"
+"12.5.8 UNIMPLEMENTED"
+"12.5.9 UNIMPLEMENTED"
+"13.1.1 UNIMPLEMENTED"
+"13.1.10 UNIMPLEMENTED"
+"13.1.11 UNIMPLEMENTED"
+"13.1.12 UNIMPLEMENTED"
+"13.1.13 UNIMPLEMENTED"
+"13.1.14 UNIMPLEMENTED"
+"13.1.15 UNIMPLEMENTED"
+"13.1.16 UNIMPLEMENTED"
+"13.1.17 UNIMPLEMENTED"
+"13.1.18 UNIMPLEMENTED"
+"13.1.2 UNIMPLEMENTED"
+"13.1.3 UNIMPLEMENTED"
+"13.1.4 UNIMPLEMENTED"
+"13.1.5 UNIMPLEMENTED"
+"13.1.6 UNIMPLEMENTED"
+"13.1.7 UNIMPLEMENTED"
+"13.1.8 UNIMPLEMENTED"
+"13.1.9 UNIMPLEMENTED"
+"13.2.1 UNIMPLEMENTED"
+"13.2.10 UNIMPLEMENTED"
+"13.2.11 UNIMPLEMENTED"
+"13.2.12 UNIMPLEMENTED"
+"13.2.13 UNIMPLEMENTED"
+"13.2.14 UNIMPLEMENTED"
+"13.2.15 UNIMPLEMENTED"
+"13.2.16 UNIMPLEMENTED"
+"13.2.17 UNIMPLEMENTED"
+"13.2.18 UNIMPLEMENTED"
+"13.2.2 UNIMPLEMENTED"
+"13.2.3 UNIMPLEMENTED"
+"13.2.4 UNIMPLEMENTED"
+"13.2.5 UNIMPLEMENTED"
+"13.2.6 UNIMPLEMENTED"
+"13.2.7 UNIMPLEMENTED"
+"13.2.8 UNIMPLEMENTED"
+"13.2.9 UNIMPLEMENTED"
+"13.3.1 UNIMPLEMENTED"
+"13.3.10 UNIMPLEMENTED"
+"13.3.11 UNIMPLEMENTED"
+"13.3.12 UNIMPLEMENTED"
+"13.3.13 UNIMPLEMENTED"
+"13.3.14 UNIMPLEMENTED"
+"13.3.15 UNIMPLEMENTED"
+"13.3.16 UNIMPLEMENTED"
+"13.3.17 UNIMPLEMENTED"
+"13.3.18 UNIMPLEMENTED"
+"13.3.2 UNIMPLEMENTED"
+"13.3.3 UNIMPLEMENTED"
+"13.3.4 UNIMPLEMENTED"
+"13.3.5 UNIMPLEMENTED"
+"13.3.6 UNIMPLEMENTED"
+"13.3.7 UNIMPLEMENTED"
+"13.3.8 UNIMPLEMENTED"
+"13.3.9 UNIMPLEMENTED"
+"13.4.1 UNIMPLEMENTED"
+"13.4.10 UNIMPLEMENTED"
+"13.4.11 UNIMPLEMENTED"
+"13.4.12 UNIMPLEMENTED"
+"13.4.13 UNIMPLEMENTED"
+"13.4.14 UNIMPLEMENTED"
+"13.4.15 UNIMPLEMENTED"
+"13.4.16 UNIMPLEMENTED"
+"13.4.17 UNIMPLEMENTED"
+"13.4.18 UNIMPLEMENTED"
+"13.4.2 UNIMPLEMENTED"
+"13.4.3 UNIMPLEMENTED"
+"13.4.4 UNIMPLEMENTED"
+"13.4.5 UNIMPLEMENTED"
+"13.4.6 UNIMPLEMENTED"
+"13.4.7 UNIMPLEMENTED"
+"13.4.8 UNIMPLEMENTED"
+"13.4.9 UNIMPLEMENTED"
+"13.5.1 UNIMPLEMENTED"
+"13.5.10 UNIMPLEMENTED"
+"13.5.11 UNIMPLEMENTED"
+"13.5.12 UNIMPLEMENTED"
+"13.5.13 UNIMPLEMENTED"
+"13.5.14 UNIMPLEMENTED"
+"13.5.15 UNIMPLEMENTED"
+"13.5.16 UNIMPLEMENTED"
+"13.5.17 UNIMPLEMENTED"
+"13.5.18 UNIMPLEMENTED"
+"13.5.2 UNIMPLEMENTED"
+"13.5.3 UNIMPLEMENTED"
+"13.5.4 UNIMPLEMENTED"
+"13.5.5 UNIMPLEMENTED"
+"13.5.6 UNIMPLEMENTED"
+"13.5.7 UNIMPLEMENTED"
+"13.5.8 UNIMPLEMENTED"
+"13.5.9 UNIMPLEMENTED"
+"13.6.1 UNIMPLEMENTED"
+"13.6.10 UNIMPLEMENTED"
+"13.6.11 UNIMPLEMENTED"
+"13.6.12 UNIMPLEMENTED"
+"13.6.13 UNIMPLEMENTED"
+"13.6.14 UNIMPLEMENTED"
+"13.6.15 UNIMPLEMENTED"
+"13.6.16 UNIMPLEMENTED"
+"13.6.17 UNIMPLEMENTED"
+"13.6.18 UNIMPLEMENTED"
+"13.6.2 UNIMPLEMENTED"
+"13.6.3 UNIMPLEMENTED"
+"13.6.4 UNIMPLEMENTED"
+"13.6.5 UNIMPLEMENTED"
+"13.6.6 UNIMPLEMENTED"
+"13.6.7 UNIMPLEMENTED"
+"13.6.8 UNIMPLEMENTED"
+"13.6.9 UNIMPLEMENTED"
+"13.7.1 UNIMPLEMENTED"
+"13.7.10 UNIMPLEMENTED"
+"13.7.11 UNIMPLEMENTED"
+"13.7.12 UNIMPLEMENTED"
+"13.7.13 UNIMPLEMENTED"
+"13.7.14 UNIMPLEMENTED"
+"13.7.15 UNIMPLEMENTED"
+"13.7.16 UNIMPLEMENTED"
+"13.7.17 UNIMPLEMENTED"
+"13.7.18 UNIMPLEMENTED"
+"13.7.2 UNIMPLEMENTED"
+"13.7.3 UNIMPLEMENTED"
+"13.7.4 UNIMPLEMENTED"
+"13.7.5 UNIMPLEMENTED"
+"13.7.6 UNIMPLEMENTED"
+"13.7.7 UNIMPLEMENTED"
+"13.7.8 UNIMPLEMENTED"
+"13.7.9 UNIMPLEMENTED"
+"2.1 OK"
+"2.10 OK"
+"2.11 OK"
+"2.2 OK"
+"2.3 OK"
+"2.4 OK"
+"2.5 OK"
+"2.6 OK"
+"2.7 OK"
+"2.8 OK"
+"2.9 OK"
+"3.1 OK"
+"3.2 NON-STRICT"
+"3.3 NON-STRICT"
+"3.4 NON-STRICT"
+"3.5 OK"
+"3.6 OK"
+"3.7 OK"
+"4.1.1 OK"
+"4.1.2 OK"
+"4.1.3 NON-STRICT"
+"4.1.4 NON-STRICT"
+"4.1.5 OK"
+"4.2.1 OK"
+"4.2.2 OK"
+"4.2.3 NON-STRICT"
+"4.2.4 NON-STRICT"
+"4.2.5 OK"
+"5.1 OK"
+"5.10 OK"
+"5.11 OK"
+"5.12 OK"
+"5.13 OK"
+"5.14 OK"
+"5.15 NON-STRICT"
+"5.16 OK"
+"5.17 OK"
+"5.18 OK"
+"5.19 OK"
+"5.2 OK"
+"5.20 OK"
+"5.3 OK"
+"5.4 OK"
+"5.5 OK"
+"5.6 OK"
+"5.7 OK"
+"5.8 OK"
+"5.9 OK"
+"7.1.1 OK"
+"7.1.2 OK"
+"7.1.3 OK"
+"7.1.4 OK"
+"7.1.5 FAILED"
+"7.1.6 INFORMATIONAL"
+"7.13.1 INFORMATIONAL"
+"7.13.2 INFORMATIONAL"
+"7.3.1 OK"
+"7.3.2 OK"
+"7.3.3 OK"
+"7.3.4 OK"
+"7.3.5 OK"
+"7.3.6 OK"
+"7.5.1 FAILED"
+"7.7.1 OK"
+"7.7.10 OK"
+"7.7.11 OK"
+"7.7.12 OK"
+"7.7.13 OK"
+"7.7.2 OK"
+"7.7.3 OK"
+"7.7.4 OK"
+"7.7.5 OK"
+"7.7.6 OK"
+"7.7.7 OK"
+"7.7.8 OK"
+"7.7.9 OK"
+"7.9.1 OK"
+"7.9.10 OK"
+"7.9.11 OK"
+"7.9.12 OK"
+"7.9.13 OK"
+"7.9.2 OK"
+"7.9.3 OK"
+"7.9.4 OK"
+"7.9.5 OK"
+"7.9.6 OK"
+"7.9.7 OK"
+"7.9.8 OK"
+"7.9.9 OK"
+"9.1.1 OK"
+"9.1.2 OK"
+"9.1.3 OK"
+"9.1.4 OK"
+"9.1.5 OK"
+"9.1.6 OK"
+"9.2.1 OK"
+"9.2.2 OK"
+"9.2.3 OK"
+"9.2.4 OK"
+"9.2.5 OK"
+"9.2.6 OK"
+"9.3.1 OK"
+"9.3.2 OK"
+"9.3.3 OK"
+"9.3.4 OK"
+"9.3.5 OK"
+"9.3.6 OK"
+"9.3.7 OK"
+"9.3.8 OK"
+"9.3.9 OK"
+"9.4.1 OK"
+"9.4.2 OK"
+"9.4.3 OK"
+"9.4.4 OK"
+"9.4.5 OK"
+"9.4.6 OK"
+"9.4.7 OK"
+"9.4.8 OK"
+"9.4.9 OK"
+"9.5.1 OK"
+"9.5.2 OK"
+"9.5.3 OK"
+"9.5.4 OK"
+"9.5.5 OK"
+"9.5.6 OK"
+"9.6.1 OK"
+"9.6.2 OK"
+"9.6.3 OK"
+"9.6.4 OK"
+"9.6.5 OK"
+"9.6.6 OK"
+"9.7.1 OK"
+"9.7.2 OK"
+"9.7.3 OK"
+"9.7.4 OK"
+"9.7.5 OK"
+"9.7.6 OK"
+"9.8.1 OK"
+"9.8.2 OK"
+"9.8.3 OK"
+"9.8.4 OK"
+"9.8.5 OK"
+"9.8.6 OK"
diff --git a/okhttp-ws-tests/fuzzingserver-test.sh b/okhttp-ws-tests/fuzzingserver-test.sh
new file mode 100755
index 0000000..af89a42
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-test.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+which wstest
+if [ $? != 0 ]; then
+  echo "Run 'pip install autobahntestsuite', maybe with 'sudo'."
+  exit 1
+fi
+which jq
+if [ $? != 0 ]; then
+  echo "Run 'brew install jq'"
+  exit 1
+fi
+
+trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
+
+set -ex
+
+wstest -m fuzzingserver -s fuzzingserver-config.json &
+sleep 2 # wait for wstest to start
+
+java -jar target/okhttp-ws-tests-*-jar-with-dependencies.jar
+
+jq '.[] as $in | $in | keys[] | . + " " + $in[.].behavior' target/fuzzingserver-report/index.json > target/fuzzingserver-actual.txt
+
+diff fuzzingserver-expected.txt target/fuzzingserver-actual.txt
diff --git a/okhttp-ws-tests/fuzzingserver-update-expected.sh b/okhttp-ws-tests/fuzzingserver-update-expected.sh
new file mode 100755
index 0000000..56592c9
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-update-expected.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+if [ ! -f target/fuzzingserver-actual.txt ]; then
+  echo "File not found. Did you run the Autobahn test script?"
+  exit 1
+fi
+
+cp target/fuzzingserver-actual.txt fuzzingserver-expected.txt
diff --git a/okhttp-ws-tests/pom.xml b/okhttp-ws-tests/pom.xml
index c7d1778..1802304 100644
--- a/okhttp-ws-tests/pom.xml
+++ b/okhttp-ws-tests/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-ws-tests</artifactId>
@@ -15,15 +15,16 @@
   <dependencies>
     <dependency>
       <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp-ws</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
       <artifactId>okhttp-testing-support</artifactId>
       <version>${project.version}</version>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>com.squareup.okhttp</groupId>
-      <artifactId>okhttp-ws</artifactId>
-      <version>${project.version}</version>
-    </dependency>
 
     <dependency>
       <groupId>junit</groupId>
@@ -40,6 +41,28 @@
 
   <build>
     <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <configuration>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+          <archive>
+            <manifest>
+              <mainClass>com.squareup.okhttp.ws.AutobahnTester</mainClass>
+            </manifest>
+          </archive>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
       <!-- Do not deploy this as an artifact to Maven central. -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
similarity index 77%
rename from okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
rename to okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
index a592624..08f7beb 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
+++ b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
@@ -17,7 +17,9 @@
 
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.Version;
 import java.io.IOException;
 import java.util.concurrent.CountDownLatch;
@@ -29,6 +31,9 @@
 import okio.Buffer;
 import okio.BufferedSource;
 
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
+
 /**
  * Exercises the web socket implementation against the
  * <a href="http://autobahn.ws/testsuite/">Autobahn Testsuite</a>.
@@ -64,28 +69,34 @@
 
   private void runTest(final long number, final long count) throws IOException {
     final CountDownLatch latch = new CountDownLatch(1);
-    newWebSocket("/runCase?case=" + number + "&agent=" + Version.userAgent()) //
+    final AtomicLong startNanos = new AtomicLong();
+    newWebSocket("/runCase?case=" + number + "&agent=okhttp") //
         .enqueue(new WebSocketListener() {
           private final ExecutorService sendExecutor = Executors.newSingleThreadExecutor();
           private WebSocket webSocket;
 
           @Override public void onOpen(WebSocket webSocket, Response response) {
-            System.out.println("Executing test case " + number + "/" + count);
             this.webSocket = webSocket;
+
+            System.out.println("Executing test case " + number + "/" + count);
+            startNanos.set(System.nanoTime());
           }
 
-          @Override public void onMessage(BufferedSource payload, final WebSocket.PayloadType type)
-              throws IOException {
-            final Buffer buffer = new Buffer();
-            payload.readAll(buffer);
-            payload.close();
-
+          @Override public void onMessage(final ResponseBody message) throws IOException {
+            final RequestBody response;
+            if (message.contentType() == TEXT) {
+              response = RequestBody.create(TEXT, message.string());
+            } else {
+              BufferedSource source = message.source();
+              response = RequestBody.create(BINARY, source.readByteString());
+              source.close();
+            }
             sendExecutor.execute(new Runnable() {
               @Override public void run() {
                 try {
-                  webSocket.sendMessage(type, buffer);
+                  webSocket.sendMessage(response);
                 } catch (IOException e) {
-                  e.printStackTrace();
+                  e.printStackTrace(System.out);
                 }
               }
             });
@@ -100,16 +111,21 @@
           }
 
           @Override public void onFailure(IOException e, Response response) {
+            e.printStackTrace(System.out);
             latch.countDown();
           }
         });
     try {
-      if (!latch.await(10, TimeUnit.SECONDS)) {
-        throw new IllegalStateException("Timed out waiting for count.");
+      if (!latch.await(30, TimeUnit.SECONDS)) {
+        throw new IllegalStateException("Timed out waiting for test " + number + " to finish.");
       }
     } catch (InterruptedException e) {
       throw new AssertionError();
     }
+
+    long endNanos = System.nanoTime();
+    long tookMs = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos.get());
+    System.out.println("Took " + tookMs + "ms");
   }
 
   private long getTestCount() throws IOException {
@@ -120,10 +136,9 @@
       @Override public void onOpen(WebSocket webSocket, Response response) {
       }
 
-      @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
-          throws IOException {
-        countRef.set(payload.readDecimalLong());
-        payload.close();
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        countRef.set(message.source().readDecimalLong());
+        message.close();
       }
 
       @Override public void onPong(Buffer payload) {
@@ -158,8 +173,7 @@
       @Override public void onOpen(WebSocket webSocket, Response response) {
       }
 
-      @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
-          throws IOException {
+      @Override public void onMessage(ResponseBody message) throws IOException {
       }
 
       @Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
index 241376d..5cc0c25 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
@@ -15,23 +15,28 @@
  */
 package com.squareup.okhttp.internal.ws;
 
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.ws.WebSocketRecorder;
+import java.io.Closeable;
 import java.io.IOException;
 import java.net.ProtocolException;
 import java.util.Random;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import okio.Buffer;
 import okio.BufferedSink;
+import okio.BufferedSource;
 import okio.ByteString;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -42,34 +47,43 @@
   // zero effect on the behavior of the WebSocket API which is why tests are only written once
   // from the perspective of a single peer.
 
-  private final Executor clientExecutor = Executors.newSingleThreadExecutor();
+  private final Executor clientExecutor = new SynchronousExecutor();
   private RealWebSocket client;
   private boolean clientConnectionCloseThrows;
   private boolean clientConnectionClosed;
-  private final Buffer client2Server = new Buffer();
+  private final MemorySocket client2Server = new MemorySocket();
   private final WebSocketRecorder clientListener = new WebSocketRecorder();
 
-  private final Executor serverExecutor = Executors.newSingleThreadExecutor();
+  private final Executor serverExecutor = new SynchronousExecutor();
   private RealWebSocket server;
-  private final Buffer server2client = new Buffer();
+  private boolean serverConnectionClosed;
+  private final MemorySocket server2client = new MemorySocket();
   private final WebSocketRecorder serverListener = new WebSocketRecorder();
 
   @Before public void setUp() {
     Random random = new Random(0);
     String url = "http://example.com/websocket";
 
-    client = new RealWebSocket(true, server2client, client2Server, random, clientExecutor,
-        clientListener, url) {
-      @Override protected void closeConnection() throws IOException {
+    client = new RealWebSocket(true, server2client.source(), client2Server.sink(), random,
+        clientExecutor, clientListener, url) {
+      @Override protected void close() throws IOException {
+        if (clientConnectionClosed) {
+          throw new AssertionError("Already closed");
+        }
         clientConnectionClosed = true;
+
         if (clientConnectionCloseThrows) {
           throw new IOException("Oops!");
         }
       }
     };
-    server = new RealWebSocket(false, client2Server, server2client, random, serverExecutor,
-        serverListener, url) {
-      @Override protected void closeConnection() throws IOException {
+    server = new RealWebSocket(false, client2Server.source(), server2client.sink(), random,
+        serverExecutor, serverListener, url) {
+      @Override protected void close() throws IOException {
+        if (serverConnectionClosed) {
+          throw new AssertionError("Already closed");
+        }
+        serverConnectionClosed = true;
       }
     };
   }
@@ -79,36 +93,82 @@
     serverListener.assertExhausted();
   }
 
+  @Test public void nullMessageThrows() throws IOException {
+    try {
+      client.sendMessage(null);
+      fail();
+    } catch (NullPointerException e) {
+      assertEquals("message == null", e.getMessage());
+    }
+  }
+
   @Test public void textMessage() throws IOException {
-    client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+    client.sendMessage(RequestBody.create(TEXT, "Hello!"));
     server.readMessage();
     serverListener.assertTextMessage("Hello!");
   }
 
   @Test public void binaryMessage() throws IOException {
-    client.sendMessage(BINARY, new Buffer().writeUtf8("Hello!"));
+    client.sendMessage(RequestBody.create(BINARY, "Hello!"));
     server.readMessage();
     serverListener.assertBinaryMessage(new byte[] { 'H', 'e', 'l', 'l', 'o', '!' });
   }
 
+  @Test public void missingContentTypeThrows() throws IOException {
+    try {
+      client.sendMessage(RequestBody.create(null, "Hey!"));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.",
+          e.getMessage());
+    }
+  }
+
+  @Test public void unknownContentTypeThrows() throws IOException {
+    try {
+      client.sendMessage(RequestBody.create(MediaType.parse("text/plain"), "Hey!"));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals(
+          "Unknown message content type: text/plain. Must use WebSocket.TEXT or WebSocket.BINARY.",
+          e.getMessage());
+    }
+  }
+
   @Test public void streamingMessage() throws IOException {
-    BufferedSink sink = client.newMessageSink(TEXT);
-    sink.writeUtf8("Hel").flush();
-    sink.writeUtf8("lo!").flush();
-    sink.close();
+    RequestBody message = new RequestBody() {
+      @Override public MediaType contentType() {
+        return TEXT;
+      }
+
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        sink.writeUtf8("Hel").flush();
+        sink.writeUtf8("lo!").flush();
+        sink.close();
+      }
+    };
+    client.sendMessage(message);
     server.readMessage();
     serverListener.assertTextMessage("Hello!");
   }
 
   @Test public void streamingMessageCanInterleavePing() throws IOException, InterruptedException {
-    BufferedSink sink = client.newMessageSink(TEXT);
-    sink.writeUtf8("Hel").flush();
-    client.sendPing(new Buffer().writeUtf8("Pong?"));
-    sink.writeUtf8("lo!").flush();
-    sink.close();
+    RequestBody message = new RequestBody() {
+      @Override public MediaType contentType() {
+        return TEXT;
+      }
+
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        sink.writeUtf8("Hel").flush();
+        client.sendPing(new Buffer().writeUtf8("Pong?"));
+        sink.writeUtf8("lo!").flush();
+        sink.close();
+      }
+    };
+
+    client.sendMessage(message);
     server.readMessage();
     serverListener.assertTextMessage("Hello!");
-    waitForExecutor(serverExecutor); // Pong write happens asynchronously.
     client.readMessage();
     clientListener.assertPong(new Buffer().writeUtf8("Pong?"));
   }
@@ -116,7 +176,6 @@
   @Test public void pingWritesPong() throws IOException, InterruptedException {
     client.sendPing(new Buffer().writeUtf8("Hello!"));
     server.readMessage(); // Read the ping, write the pong.
-    waitForExecutor(serverExecutor); // Pong write happens asynchronously.
     client.readMessage(); // Read the pong.
     clientListener.assertPong(new Buffer().writeUtf8("Hello!"));
   }
@@ -151,20 +210,62 @@
       assertEquals("closed", e.getMessage());
     }
     try {
-      client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
-      fail();
-    } catch (IllegalStateException e) {
-      assertEquals("closed", e.getMessage());
-    }
-    try {
-      client.newMessageSink(TEXT);
+      client.sendMessage(RequestBody.create(TEXT, "Hello!"));
       fail();
     } catch (IllegalStateException e) {
       assertEquals("closed", e.getMessage());
     }
   }
 
-  @Test public void serverCloseThenWritingThrows() throws IOException {
+  @Test public void socketClosedDuringPingKillsWebSocket() throws IOException {
+    client2Server.close();
+
+    try {
+      client.sendPing(new Buffer().writeUtf8("Ping!"));
+      fail();
+    } catch (IOException ignored) {
+    }
+
+    // A failed write prevents further use of the WebSocket instance.
+    try {
+      client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+      fail();
+    } catch (IllegalStateException e) {
+      assertEquals("must call close()", e.getMessage());
+    }
+    try {
+      client.sendPing(new Buffer().writeUtf8("Ping!"));
+      fail();
+    } catch (IllegalStateException e) {
+      assertEquals("must call close()", e.getMessage());
+    }
+  }
+
+  @Test public void socketClosedDuringMessageKillsWebSocket() throws IOException {
+    client2Server.close();
+
+    try {
+      client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+      fail();
+    } catch (IOException ignored) {
+    }
+
+    // A failed write prevents further use of the WebSocket instance.
+    try {
+      client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+      fail();
+    } catch (IllegalStateException e) {
+      assertEquals("must call close()", e.getMessage());
+    }
+    try {
+      client.sendPing(new Buffer().writeUtf8("Ping!"));
+      fail();
+    } catch (IllegalStateException e) {
+      assertEquals("must call close()", e.getMessage());
+    }
+  }
+
+  @Test public void serverCloseThenWritingPingThrows() throws IOException {
     server.close(1000, "Hello!");
     client.readMessage();
     clientListener.assertClose(1000, "Hello!");
@@ -175,12 +276,26 @@
     } catch (IOException e) {
       assertEquals("closed", e.getMessage());
     }
+  }
+
+  @Test public void serverCloseThenWritingMessageThrows() throws IOException {
+    server.close(1000, "Hello!");
+    client.readMessage();
+    clientListener.assertClose(1000, "Hello!");
+
     try {
-      client.sendMessage(TEXT, new Buffer().writeUtf8("Hi!"));
+      client.sendMessage(RequestBody.create(TEXT, "Hi!"));
       fail();
     } catch (IOException e) {
       assertEquals("closed", e.getMessage());
     }
+  }
+
+  @Test public void serverCloseThenWritingCloseThrows() throws IOException {
+    server.close(1000, "Hello!");
+    client.readMessage();
+    clientListener.assertClose(1000, "Hello!");
+
     try {
       client.close(1000, "Bye!");
       fail();
@@ -190,33 +305,34 @@
   }
 
   @Test public void serverCloseWhileWritingThrows() throws IOException {
-    // Start writing data.
-    BufferedSink sink = client.newMessageSink(TEXT);
-    sink.writeUtf8("Hel").flush();
+    RequestBody message = new RequestBody() {
+      @Override public MediaType contentType() {
+        return TEXT;
+      }
 
-    server.close(1000, "Hello!");
-    client.readMessage();
-    clientListener.assertClose(1000, "Hello!");
+      @Override public void writeTo(BufferedSink sink) throws IOException {
+        // Start writing data.
+        sink.writeUtf8("Hel").flush();
 
-    try {
-      sink.writeUtf8("lo!").emit(); // No writing to the underlying sink.
-      fail();
-    } catch (IOException e) {
-      assertEquals("closed", e.getMessage());
-      sink.buffer().clear();
-    }
-    try {
-      sink.flush(); // No flushing.
-      fail();
-    } catch (IOException e) {
-      assertEquals("closed", e.getMessage());
-    }
-    try {
-      sink.close(); // No closing because this requires writing a frame.
-      fail();
-    } catch (IOException e) {
-      assertEquals("closed", e.getMessage());
-    }
+        server.close(1000, "Hello!");
+        client.readMessage();
+        clientListener.assertClose(1000, "Hello!");
+
+        try {
+          sink.flush(); // No flushing.
+          fail();
+        } catch (IOException e) {
+          assertEquals("closed", e.getMessage());
+        }
+        try {
+          sink.close(); // No closing because this requires writing a frame.
+          fail();
+        } catch (IOException e) {
+          assertEquals("closed", e.getMessage());
+        }
+      }
+    };
+    client.sendMessage(message);
   }
 
   @Test public void clientCloseClosesConnection() throws IOException {
@@ -225,8 +341,7 @@
     server.readMessage(); // Read client close, send server close.
     serverListener.assertClose(1000, "Hello!");
 
-    client.readMessage(); // Read server close.
-    waitForExecutor(clientExecutor); // Close happens asynchronously.
+    client.readMessage(); // Read server close, close connection.
     assertTrue(clientConnectionClosed);
     clientListener.assertClose(1000, "Hello!");
   }
@@ -235,8 +350,8 @@
     server.close(1000, "Hello!");
 
     client.readMessage(); // Read server close, send client close, close connection.
-    clientListener.assertClose(1000, "Hello!");
     assertTrue(clientConnectionClosed);
+    clientListener.assertClose(1000, "Hello!");
 
     server.readMessage();
     serverListener.assertClose(1000, "Hello!");
@@ -248,8 +363,7 @@
     client.close(1000, "Hi!");
     assertFalse(clientConnectionClosed);
 
-    client.readMessage(); // Read close, should NOT send close.
-    waitForExecutor(clientExecutor); // Close happens asynchronously.
+    client.readMessage(); // Read close, close connection close.
     assertTrue(clientConnectionClosed);
     clientListener.assertClose(1000, "Hello!");
 
@@ -261,7 +375,7 @@
   }
 
   @Test public void serverCloseBreaksReadMessageLoop() throws IOException {
-    server.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+    server.sendMessage(RequestBody.create(TEXT, "Hello!"));
     server.close(1000, "Bye!");
     assertTrue(client.readMessage());
     clientListener.assertTextMessage("Hello!");
@@ -269,12 +383,12 @@
     clientListener.assertClose(1000, "Bye!");
   }
 
-  @Test public void protocolErrorBeforeCloseSendsClose() {
-    server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+  @Test public void protocolErrorBeforeCloseSendsClose() throws IOException {
+    server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
 
-    client.readMessage(); // Detects error, send close.
-    clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+    client.readMessage(); // Detects error, send close, close connection.
     assertTrue(clientConnectionClosed);
+    clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
 
     server.readMessage();
     serverListener.assertClose(1002, "");
@@ -282,14 +396,41 @@
 
   @Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException {
     client.close(1000, "Hello!");
-    server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+    assertFalse(clientConnectionClosed); // Not closed until close reply is received.
+    server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
 
-    client.readMessage();
-    clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+    client.readMessage(); // Detects error, closes connection immediately since close already sent.
     assertTrue(clientConnectionClosed);
+    clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
 
     server.readMessage();
     serverListener.assertClose(1000, "Hello!");
+
+    serverListener.assertExhausted(); // Client should not have sent second close.
+  }
+
+  @Test public void closeThrowingClosesConnection() {
+    client2Server.close();
+
+    try {
+      client.close(1000, null);
+      fail();
+    } catch (IOException ignored) {
+    }
+    assertTrue(clientConnectionClosed);
+  }
+
+  @Test public void closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() throws IOException {
+    client2Server.close();
+    clientConnectionCloseThrows = true;
+
+    try {
+      client.close(1000, "Bye!");
+      fail();
+    } catch (IOException e) {
+      assertNotEquals("Oops!", e.getMessage());
+    }
+    assertTrue(clientConnectionClosed);
   }
 
   @Test public void peerConnectionCloseThrowingDoesNotPropagate() throws IOException {
@@ -297,26 +438,72 @@
 
     server.close(1000, "Bye!");
     client.readMessage();
-    clientListener.assertClose(1000, "Bye!");
     assertTrue(clientConnectionClosed);
+    clientListener.assertClose(1000, "Bye!");
 
     server.readMessage();
     serverListener.assertClose(1000, "Bye!");
   }
 
-  private static void waitForExecutor(Executor executor) {
-    final CountDownLatch latch = new CountDownLatch(1);
-    executor.execute(new Runnable() {
-      @Override public void run() {
-        latch.countDown();
-      }
-    });
-    try {
-      if (!latch.await(10, TimeUnit.SECONDS)) {
-        throw new IllegalStateException("Timed out waiting for executor.");
-      }
-    } catch (InterruptedException e) {
-      Thread.currentThread().interrupt();
+  static final class MemorySocket implements Closeable {
+    private final Buffer buffer = new Buffer();
+    private boolean closed;
+
+    @Override public void close() {
+      closed = true;
+    }
+
+    Buffer raw() {
+      return buffer;
+    }
+
+    BufferedSource source() {
+      return Okio.buffer(new Source() {
+        @Override public long read(Buffer sink, long byteCount) throws IOException {
+          if (closed) throw new IOException("closed");
+          return buffer.read(sink, byteCount);
+        }
+
+        @Override public Timeout timeout() {
+          return Timeout.NONE;
+        }
+
+        @Override public void close() throws IOException {
+          closed = true;
+        }
+      });
+    }
+
+    BufferedSink sink() {
+      return Okio.buffer(new Sink() {
+        @Override public void write(Buffer source, long byteCount) throws IOException {
+          if (closed) throw new IOException("closed");
+          buffer.write(source, byteCount);
+        }
+
+        @Override public void flush() throws IOException {
+        }
+
+        @Override public Timeout timeout() {
+          return Timeout.NONE;
+        }
+
+        @Override public void close() throws IOException {
+          closed = true;
+        }
+      });
     }
   }
+
+  static final class SynchronousExecutor implements Executor {
+    @Override public void execute(Runnable command) {
+      command.run();
+    }
+  }
+
+  // ANDROID-BEGIN Android uses JUnit 4.10 which does not have assertNotEquals()
+  private static void assertNotEquals(Object o1, Object o2) {
+    org.junit.Assert.assertFalse(o1 == o2 || (o1 != null && o1.equals(o2)));
+  }
+  // ANDROID-END
 }
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
index 1674511..213bda5 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
@@ -15,22 +15,24 @@
  */
 package com.squareup.okhttp.internal.ws;
 
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.ws.WebSocketRecorder;
 import java.io.EOFException;
 import java.io.IOException;
 import java.net.ProtocolException;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
 import okio.Buffer;
 import okio.BufferedSource;
 import okio.ByteString;
 import org.junit.After;
 import org.junit.Test;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
 import static com.squareup.okhttp.ws.WebSocketRecorder.MessageDelegate;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public final class WebSocketReaderTest {
@@ -151,11 +153,12 @@
 
     final Buffer sink = new Buffer();
     callback.setNextMessageDelegate(new MessageDelegate() {
-      @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-        payload.readFully(sink, 3); // Read "Hel"
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        BufferedSource source = message.source();
+        source.readFully(sink, 3); // Read "Hel"
         data.write(ByteString.decodeHex("5158")); // lo
-        payload.readFully(sink, 2); // Read "lo"
-        payload.close();
+        source.readFully(sink, 2); // Read "lo"
+        source.close();
       }
     });
     serverReader.processNextFrame();
@@ -251,8 +254,8 @@
   @Test public void noCloseErrors() throws IOException {
     data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
     callback.setNextMessageDelegate(new MessageDelegate() {
-      @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-        payload.readAll(new Buffer());
+      @Override public void onMessage(ResponseBody body) throws IOException {
+        body.source().readAll(new Buffer());
       }
     });
     try {
@@ -269,9 +272,9 @@
 
     final Buffer sink = new Buffer();
     callback.setNextMessageDelegate(new MessageDelegate() {
-      @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-        payload.read(sink, 3);
-        payload.close();
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        message.source().read(sink, 3);
+        message.close();
       }
     });
 
@@ -291,9 +294,9 @@
 
     final Buffer sink = new Buffer();
     callback.setNextMessageDelegate(new MessageDelegate() {
-      @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-        payload.read(sink, 2);
-        payload.close();
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        message.source().read(sink, 2);
+        message.close();
       }
     });
 
@@ -311,10 +314,10 @@
 
     final AtomicReference<Exception> exception = new AtomicReference<>();
     callback.setNextMessageDelegate(new MessageDelegate() {
-      @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-        payload.close();
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        message.close();
         try {
-          payload.readAll(new Buffer());
+          message.source().readAll(new Buffer());
           fail();
         } catch (IllegalStateException e) {
           exception.set(e);
@@ -341,7 +344,17 @@
   @Test public void emptyCloseCallsCallback() throws IOException {
     data.write(ByteString.decodeHex("8800")); // Empty close
     clientReader.processNextFrame();
-    callback.assertClose(0, "");
+    callback.assertClose(1000, "");
+  }
+
+  @Test public void closeLengthOfOneThrows() throws IOException {
+    data.write(ByteString.decodeHex("880100")); // Close with invalid 1-byte payload
+    try {
+      clientReader.processNextFrame();
+      fail();
+    } catch (ProtocolException e) {
+      assertEquals("Malformed close payload length of 1.", e.getMessage());
+    }
   }
 
   @Test public void closeCallsCallback() throws IOException {
@@ -350,6 +363,44 @@
     callback.assertClose(1000, "Hello");
   }
 
+  @Test public void closeOutOfRangeThrows() throws IOException {
+    data.write(ByteString.decodeHex("88020001")); // Close with code 1
+    try {
+      clientReader.processNextFrame();
+      fail();
+    } catch (ProtocolException e) {
+      assertEquals("Code must be in range [1000,5000): 1", e.getMessage());
+    }
+    data.write(ByteString.decodeHex("88021388")); // Close with code 5000
+    try {
+      clientReader.processNextFrame();
+      fail();
+    } catch (ProtocolException e) {
+      assertEquals("Code must be in range [1000,5000): 5000", e.getMessage());
+    }
+  }
+
+  @Test public void closeReservedSetThrows() throws IOException {
+    data.write(ByteString.decodeHex("880203ec")); // Close with code 1004
+    data.write(ByteString.decodeHex("880203ed")); // Close with code 1005
+    data.write(ByteString.decodeHex("880203ee")); // Close with code 1006
+    for (int i = 1012; i <= 2999; i++) {
+      data.write(ByteString.decodeHex("8802" + String.format("%04X", i))); // Close with code 'i'
+    }
+
+    int count = 0;
+    for (; !data.exhausted(); count++) {
+      try {
+        clientReader.processNextFrame();
+        fail();
+      } catch (ProtocolException e) {
+        String message = e.getMessage();
+        assertTrue(message, Pattern.matches("Code \\d+ is reserved and may not be used.", message));
+      }
+    }
+    assertEquals(1991, count);
+  }
+
   private byte[] binaryData(int length) {
     byte[] junk = new byte[length];
     random.nextBytes(junk);
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
index a98e6bb..741b33f 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
@@ -17,16 +17,22 @@
 
 import java.io.EOFException;
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Random;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.ByteString;
-import org.junit.After;
+import okio.Okio;
+import okio.Sink;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
@@ -35,28 +41,27 @@
   private final Buffer data = new Buffer();
   private final Random random = new Random(0);
 
+  /**
+   * Check all data as verified inside of the test. We do this in a rule instead of @After so that
+   * exceptions thrown from the test do not cause this check to fail.
+   */
+  @Rule public final TestRule noDataLeftBehind = new TestRule() {
+    @Override public Statement apply(final Statement base, Description description) {
+      return new Statement() {
+        @Override public void evaluate() throws Throwable {
+          base.evaluate();
+          assertEquals("Data not empty", "", data.readByteString().hex());
+        }
+      };
+    }
+  };
+
   // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test.
   private final WebSocketWriter serverWriter = new WebSocketWriter(false, data, random);
   private final WebSocketWriter clientWriter = new WebSocketWriter(true, data, random);
 
-  @After public void tearDown() throws IOException {
-    assertEquals("Data not empty", "", data.readByteString().hex());
-  }
-
-  @Test public void serverSendSimpleHello() throws IOException {
-    Buffer payload = new Buffer().writeUtf8("Hello");
-    serverWriter.sendMessage(TEXT, payload);
-    assertData("810548656c6c6f");
-  }
-
-  @Test public void clientSendSimpleHello() throws IOException {
-    Buffer payload = new Buffer().writeUtf8("Hello");
-    clientWriter.sendMessage(TEXT, payload);
-    assertData("818560b420bb28d14cd70f");
-  }
-
-  @Test public void serverStreamSimpleHello() throws IOException {
-    BufferedSink sink = serverWriter.newMessageSink(TEXT);
+  @Test public void serverTextMessage() throws IOException {
+    BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
 
     sink.writeUtf8("Hel").flush();
     assertData("010348656c");
@@ -68,19 +73,34 @@
     assertData("8000");
   }
 
-  @Test public void serverStreamCloseFlushes() throws IOException {
-    BufferedSink sink = serverWriter.newMessageSink(TEXT);
+  @Test public void closeFlushes() throws IOException {
+    BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
 
     sink.writeUtf8("Hel").flush();
     assertData("010348656c");
 
     sink.writeUtf8("lo").close();
-    assertData("00026c6f");
-    assertData("8000");
+    assertData("80026c6f");
   }
 
-  @Test public void clientStreamSimpleHello() throws IOException {
-    BufferedSink sink = clientWriter.newMessageSink(TEXT);
+  @Test public void noWritesAfterClose() throws IOException {
+    Sink sink = serverWriter.newMessageSink(OPCODE_TEXT);
+
+    sink.close();
+    assertData("8100");
+
+    Buffer payload = new Buffer().writeUtf8("Hello");
+    try {
+      // Write to the unbuffered sink as BufferedSink keeps its own closed state.
+      sink.write(payload, payload.size());
+      fail();
+    } catch (IOException e) {
+      assertEquals("closed", e.getMessage());
+    }
+  }
+
+  @Test public void clientTextMessage() throws IOException {
+    BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_TEXT));
 
     sink.writeUtf8("Hel").flush();
     assertData("018360b420bb28d14c");
@@ -92,87 +112,84 @@
     assertData("80807acb933d");
   }
 
-  @Test public void serverSendBinary() throws IOException {
-    byte[] payload = binaryData(100);
-    serverWriter.sendMessage(BINARY, new Buffer().write(payload));
-    assertData("8264");
-    assertData(payload);
-  }
+  @Test public void serverBinaryMessage() throws IOException {
+    BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_BINARY));
 
-  @Test public void serverSendBinaryShort() throws IOException {
-    byte[] payload = binaryData(0xffff);
-    serverWriter.sendMessage(BINARY, new Buffer().write(payload));
-    assertData("827effff");
-    assertData(payload);
-  }
-
-  @Test public void serverSendBinaryLong() throws IOException {
-    byte[] payload = binaryData(65537);
-    serverWriter.sendMessage(BINARY, new Buffer().write(payload));
-    assertData("827f0000000000010001");
-    assertData(payload);
-  }
-
-  @Test public void clientSendBinary() throws IOException {
-    byte[] payload = binaryData(100);
-    clientWriter.sendMessage(BINARY, new Buffer().write(payload));
-    assertData("82e4");
-
-    byte[] maskKey = new byte[4];
-    random.setSeed(0); // Reset the seed so we can mask the payload.
-    random.nextBytes(maskKey);
-    toggleMask(payload, payload.length, maskKey, 0);
-
-    assertData(maskKey);
-    assertData(payload);
-  }
-
-  @Test public void serverStreamBinary() throws IOException {
-    byte[] payload = binaryData(100);
-    BufferedSink sink = serverWriter.newMessageSink(BINARY);
-
-    sink.write(payload, 0, 50).flush();
+    sink.write(binaryData(50)).flush();
     assertData("0232");
-    assertData(Arrays.copyOfRange(payload, 0, 50));
+    assertData(binaryData(50));
 
-    sink.write(payload, 50, 50).flush();
+    sink.write(binaryData(50)).flush();
     assertData("0032");
-    assertData(Arrays.copyOfRange(payload, 50, 100));
+    assertData(binaryData(50));
 
     sink.close();
     assertData("8000");
   }
 
-  @Test public void clientStreamBinary() throws IOException {
+  @Test public void serverMessageLengthShort() throws IOException {
+    Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
+
+    // Create a payload which will overflow the normal payload byte size.
+    Buffer payload = new Buffer();
+    while (payload.completeSegmentByteCount() <= PAYLOAD_BYTE_MAX) {
+      payload.writeByte('0');
+    }
+    long byteCount = payload.completeSegmentByteCount();
+
+    // Write directly to the unbuffered sink. This ensures it will become single frame.
+    sink.write(payload.clone(), byteCount);
+    assertData("027e"); // 'e' == 4-byte follow-up length.
+    assertData(String.format("%04X", payload.completeSegmentByteCount()));
+    assertData(payload.readByteArray());
+
+    sink.close();
+    assertData("8000");
+  }
+
+  @Test public void serverMessageLengthLong() throws IOException {
+    Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
+
+    // Create a payload which will overflow the normal and short payload byte size.
+    Buffer payload = new Buffer();
+    while (payload.completeSegmentByteCount() <= PAYLOAD_SHORT_MAX) {
+      payload.writeByte('0');
+    }
+    long byteCount = payload.completeSegmentByteCount();
+
+    // Write directly to the unbuffered sink. This ensures it will become single frame.
+    sink.write(payload.clone(), byteCount);
+    assertData("027f"); // 'f' == 16-byte follow-up length.
+    assertData(String.format("%016X", byteCount));
+    assertData(payload.readByteArray(byteCount));
+
+    sink.close();
+    assertData("8000");
+  }
+
+  @Test public void clientBinary() throws IOException {
     byte[] maskKey1 = new byte[4];
     random.nextBytes(maskKey1);
     byte[] maskKey2 = new byte[4];
     random.nextBytes(maskKey2);
-    byte[] maskKey3 = new byte[4];
-    random.nextBytes(maskKey3);
 
     random.setSeed(0); // Reset the seed so real data matches.
 
-    byte[] payload = binaryData(100);
-    BufferedSink sink = clientWriter.newMessageSink(BINARY);
+    BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_BINARY));
 
-    sink.write(payload, 0, 50).flush();
-    byte[] part1 = Arrays.copyOfRange(payload, 0, 50);
+    byte[] part1 = binaryData(50);
+    sink.write(part1).flush();
     toggleMask(part1, 50, maskKey1, 0);
     assertData("02b2");
     assertData(maskKey1);
     assertData(part1);
 
-    sink.write(payload, 50, 50).flush();
-    byte[] part2 = Arrays.copyOfRange(payload, 50, 100);
+    byte[] part2 = binaryData(50);
+    sink.write(part2).close();
     toggleMask(part2, 50, maskKey2, 0);
-    assertData("00b2");
+    assertData("80b2");
     assertData(maskKey2);
     assertData(part2);
-
-    sink.close();
-    assertData("8080");
-    assertData(maskKey3);
   }
 
   @Test public void serverEmptyClose() throws IOException {
@@ -287,26 +304,16 @@
     }
   }
 
-  @Test public void twoWritersThrows() {
-    clientWriter.newMessageSink(TEXT);
+  @Test public void twoMessageSinksThrows() {
+    clientWriter.newMessageSink(OPCODE_TEXT);
     try {
-      clientWriter.newMessageSink(TEXT);
+      clientWriter.newMessageSink(OPCODE_TEXT);
       fail();
     } catch (IllegalStateException e) {
       assertEquals("Another message writer is active. Did you call close()?", e.getMessage());
     }
   }
 
-  @Test public void writeWhileWriterThrows() throws IOException {
-    clientWriter.newMessageSink(TEXT);
-    try {
-      clientWriter.sendMessage(TEXT, new Buffer());
-      fail();
-    } catch (IllegalStateException e) {
-      assertEquals("A message writer is active. Did you call close()?", e.getMessage());
-    }
-  }
-
   private void assertData(String hex) throws EOFException {
     ByteString expected = ByteString.decodeHex(hex);
     ByteString actual = data.readByteString(expected.size());
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
index 895eb1f..bbc908c 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
@@ -17,7 +17,9 @@
 
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -30,13 +32,11 @@
 import java.util.concurrent.atomic.AtomicReference;
 import javax.net.ssl.SSLContext;
 import okio.Buffer;
-import okio.BufferedSink;
-import okio.BufferedSource;
 import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
 
 public final class WebSocketCallTest {
   @Rule public final MockWebServer server = new MockWebServer();
@@ -64,7 +64,7 @@
     server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
 
     WebSocket webSocket = awaitWebSocket();
-    webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+    webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
     serverListener.assertTextMessage("Hello, WebSockets!");
   }
 
@@ -74,43 +74,7 @@
         new Thread() {
           @Override public void run() {
             try {
-              webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
-            } catch (IOException e) {
-              throw new AssertionError(e);
-            }
-          }
-        }.start();
-      }
-    };
-    server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
-    awaitWebSocket();
-    listener.assertTextMessage("Hello, WebSockets!");
-  }
-
-  @Test public void clientStreamingMessage() throws IOException {
-    WebSocketRecorder serverListener = new WebSocketRecorder();
-    server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
-    WebSocket webSocket = awaitWebSocket();
-    BufferedSink sink = webSocket.newMessageSink(TEXT);
-    sink.writeUtf8("Hello, ").flush();
-    sink.writeUtf8("WebSockets!").flush();
-    sink.close();
-
-    serverListener.assertTextMessage("Hello, WebSockets!");
-  }
-
-  @Test public void serverStreamingMessage() throws IOException {
-    WebSocketListener serverListener = new EmptyWebSocketListener() {
-      @Override public void onOpen(final WebSocket webSocket, Response response) {
-        new Thread() {
-          @Override public void run() {
-            try {
-              BufferedSink sink = webSocket.newMessageSink(TEXT);
-              sink.writeUtf8("Hello, ").flush();
-              sink.writeUtf8("WebSockets!").flush();
-              sink.close();
+              webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
             } catch (IOException e) {
               throw new AssertionError(e);
             }
@@ -233,7 +197,7 @@
         .build();
 
     WebSocket webSocket = awaitWebSocket(request1);
-    webSocket.sendMessage(TEXT, new Buffer().writeUtf8("abc"));
+    webSocket.sendMessage(RequestBody.create(TEXT, "abc"));
     serverListener.assertTextMessage("abc");
   }
 
@@ -255,9 +219,8 @@
         latch.countDown();
       }
 
-      @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
-          throws IOException {
-        listener.onMessage(payload, type);
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        listener.onMessage(message);
       }
 
       @Override public void onPong(Buffer payload) {
@@ -290,8 +253,7 @@
     @Override public void onOpen(WebSocket webSocket, Response response) {
     }
 
-    @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
-        throws IOException {
+    @Override public void onMessage(ResponseBody message) throws IOException {
     }
 
     @Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
index 56b3810..3e82e05 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
@@ -15,24 +15,25 @@
  */
 package com.squareup.okhttp.ws;
 
+import com.squareup.okhttp.MediaType;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.ws.WebSocketReader;
 import java.io.IOException;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import okio.Buffer;
-import okio.BufferedSource;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 public final class WebSocketRecorder implements WebSocketReader.FrameCallback, WebSocketListener {
   public interface MessageDelegate {
-    void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException;
+    void onMessage(ResponseBody message) throws IOException;
   }
 
   private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
@@ -46,16 +47,15 @@
   @Override public void onOpen(WebSocket webSocket, Response response) {
   }
 
-  @Override public void onMessage(BufferedSource source, WebSocket.PayloadType type)
-      throws IOException {
+  @Override public void onMessage(ResponseBody message) throws IOException {
     if (delegate != null) {
-      delegate.onMessage(source, type);
+      delegate.onMessage(message);
       delegate = null;
     } else {
-      Message message = new Message(type);
-      source.readAll(message.buffer);
-      source.close();
-      events.add(message);
+      Message event = new Message(message.contentType());
+      message.source().readAll(event.buffer);
+      message.close();
+      events.add(event);
     }
   }
 
@@ -87,28 +87,48 @@
     }
   }
 
-  public void assertTextMessage(String payload) {
+  public void assertTextMessage(String payload) throws IOException {
     Message message = new Message(TEXT);
     message.buffer.writeUtf8(payload);
-    assertEquals(message, nextEvent());
+    Object actual = nextEvent();
+    if (actual instanceof IOException) {
+      throw (IOException) actual;
+    }
+    assertEquals(message, actual);
   }
 
-  public void assertBinaryMessage(byte[] payload) {
+  public void assertBinaryMessage(byte[] payload) throws IOException {
     Message message = new Message(BINARY);
     message.buffer.write(payload);
-    assertEquals(message, nextEvent());
+    Object actual = nextEvent();
+    if (actual instanceof IOException) {
+      throw (IOException) actual;
+    }
+    assertEquals(message, actual);
   }
 
-  public void assertPing(Buffer payload) {
-    assertEquals(new Ping(payload), nextEvent());
+  public void assertPing(Buffer payload) throws IOException {
+    Object actual = nextEvent();
+    if (actual instanceof IOException) {
+      throw (IOException) actual;
+    }
+    assertEquals(new Ping(payload), actual);
   }
 
-  public void assertPong(Buffer payload) {
-    assertEquals(new Pong(payload), nextEvent());
+  public void assertPong(Buffer payload) throws IOException {
+    Object actual = nextEvent();
+    if (actual instanceof IOException) {
+      throw (IOException) actual;
+    }
+    assertEquals(new Pong(payload), actual);
   }
 
-  public void assertClose(int code, String reason) {
-    assertEquals(new Close(code, reason), nextEvent());
+  public void assertClose(int code, String reason) throws IOException {
+    Object actual = nextEvent();
+    if (actual instanceof IOException) {
+      throw (IOException) actual;
+    }
+    assertEquals(new Close(code, reason), actual);
   }
 
   public void assertFailure(Class<? extends IOException> cls, String message) {
@@ -125,25 +145,25 @@
   }
 
   private static class Message {
-    public final WebSocket.PayloadType type;
+    public final MediaType mediaType;
     public final Buffer buffer = new Buffer();
 
-    private Message(WebSocket.PayloadType type) {
-      this.type = type;
+    private Message(MediaType mediaType) {
+      this.mediaType = mediaType;
     }
 
     @Override public String toString() {
-      return "Message[" + type + " " + buffer + "]";
+      return "Message[" + mediaType + " " + buffer + "]";
     }
 
     @Override public int hashCode() {
-      return type.hashCode() * 37 + buffer.hashCode();
+      return mediaType.hashCode() * 37 + buffer.hashCode();
     }
 
     @Override public boolean equals(Object obj) {
       if (obj instanceof Message) {
         Message other = (Message) obj;
-        return type == other.type && buffer.equals(other.buffer);
+        return mediaType.equals(other.mediaType) && buffer.equals(other.buffer);
       }
       return false;
     }
diff --git a/okhttp-ws/pom.xml b/okhttp-ws/pom.xml
index 81f8afd..688b538 100644
--- a/okhttp-ws/pom.xml
+++ b/okhttp-ws/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp-ws</artifactId>
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
index 8d6b7c4..ea55b5a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
@@ -15,6 +15,9 @@
  */
 package com.squareup.okhttp.internal.ws;
 
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.NamedRunnable;
 import com.squareup.okhttp.ws.WebSocket;
 import com.squareup.okhttp.ws.WebSocketListener;
@@ -22,14 +25,17 @@
 import java.net.ProtocolException;
 import java.util.Random;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.BufferedSource;
+import okio.Okio;
 
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
 import static com.squareup.okhttp.internal.ws.WebSocketReader.FrameCallback;
 
 public abstract class RealWebSocket implements WebSocket {
-  /** A close code which indicates that the peer encountered a protocol exception. */
   private static final int CLOSE_PROTOCOL_EXCEPTION = 1002;
 
   private final WebSocketWriter writer;
@@ -38,10 +44,13 @@
 
   /** True after calling {@link #close(int, String)}. No writes are allowed afterward. */
   private volatile boolean writerSentClose;
+  /** True after {@link IOException}. {@link #close(int, String)} becomes only valid call. */
+  private boolean writerWantsClose;
   /** True after a close frame was read by the reader. No frames will follow it. */
-  private volatile boolean readerSentClose;
-  /** Lock required to negotiate closing the connection. */
-  private final Object closeLock = new Object();
+  private boolean readerSentClose;
+
+  /** True after calling {@link #close()} to free connection resources. */
+  private final AtomicBoolean connectionClosed = new AtomicBoolean();
 
   public RealWebSocket(boolean isClient, BufferedSource source, BufferedSink sink, Random random,
       final Executor replyExecutor, final WebSocketListener listener, final String url) {
@@ -49,8 +58,8 @@
 
     writer = new WebSocketWriter(isClient, sink, random);
     reader = new WebSocketReader(isClient, source, new FrameCallback() {
-      @Override public void onMessage(BufferedSource source, PayloadType type) throws IOException {
-        listener.onMessage(source, type);
+      @Override public void onMessage(ResponseBody message) throws IOException {
+        listener.onMessage(message);
       }
 
       @Override public void onPing(final Buffer buffer) {
@@ -69,17 +78,10 @@
       }
 
       @Override public void onClose(final int code, final String reason) {
-        final boolean writeCloseResponse;
-        synchronized (closeLock) {
-          readerSentClose = true;
-
-          // If the writer has not indicated a desire to close we will write a close response.
-          writeCloseResponse = !writerSentClose;
-        }
-
+        readerSentClose = true;
         replyExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Close Reply", url) {
           @Override protected void execute() {
-            peerClose(code, reason, writeCloseResponse);
+            peerClose(code, reason);
           }
         });
       }
@@ -100,57 +102,96 @@
     }
   }
 
-  @Override public BufferedSink newMessageSink(PayloadType type) {
+  @Override public void sendMessage(RequestBody message) throws IOException {
+    if (message == null) throw new NullPointerException("message == null");
     if (writerSentClose) throw new IllegalStateException("closed");
-    return writer.newMessageSink(type);
-  }
+    if (writerWantsClose) throw new IllegalStateException("must call close()");
 
-  @Override public void sendMessage(PayloadType type, Buffer payload) throws IOException {
-    if (writerSentClose) throw new IllegalStateException("closed");
-    writer.sendMessage(type, payload);
+    MediaType contentType = message.contentType();
+    if (contentType == null) {
+      throw new IllegalArgumentException(
+          "Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.");
+    }
+    String contentSubtype = contentType.subtype();
+
+    int formatOpcode;
+    if (WebSocket.TEXT.subtype().equals(contentSubtype)) {
+      formatOpcode = OPCODE_TEXT;
+    } else if (WebSocket.BINARY.subtype().equals(contentSubtype)) {
+      formatOpcode = OPCODE_BINARY;
+    } else {
+      throw new IllegalArgumentException("Unknown message content type: "
+          + contentType.type() + "/" + contentType.subtype() // Omit any implicitly added charset.
+          + ". Must use WebSocket.TEXT or WebSocket.BINARY.");
+    }
+
+    BufferedSink sink = Okio.buffer(writer.newMessageSink(formatOpcode));
+    try {
+      message.writeTo(sink);
+      sink.close();
+    } catch (IOException e) {
+      writerWantsClose = true;
+      throw e;
+    }
   }
 
   @Override public void sendPing(Buffer payload) throws IOException {
     if (writerSentClose) throw new IllegalStateException("closed");
-    writer.writePing(payload);
+    if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+    try {
+      writer.writePing(payload);
+    } catch (IOException e) {
+      writerWantsClose = true;
+      throw e;
+    }
   }
 
   /** Send an unsolicited pong with the specified payload. */
   public void sendPong(Buffer payload) throws IOException {
     if (writerSentClose) throw new IllegalStateException("closed");
-    writer.writePong(payload);
+    if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+    try {
+      writer.writePong(payload);
+    } catch (IOException e) {
+      writerWantsClose = true;
+      throw e;
+    }
   }
 
   @Override public void close(int code, String reason) throws IOException {
     if (writerSentClose) throw new IllegalStateException("closed");
+    writerSentClose = true;
 
-    boolean closeConnection;
-    synchronized (closeLock) {
-      writerSentClose = true;
-
-      // If the reader has also indicated a desire to close we will close the connection.
-      closeConnection = readerSentClose;
-    }
-
-    writer.writeClose(code, reason);
-
-    if (closeConnection) {
-      closeConnection();
+    try {
+      writer.writeClose(code, reason);
+    } catch (IOException e) {
+      if (connectionClosed.compareAndSet(false, true)) {
+        // Try to close the connection without masking the original exception.
+        try {
+          close();
+        } catch (IOException ignored) {
+        }
+      }
+      throw e;
     }
   }
 
   /** Replies and closes this web socket when a close frame is read from the peer. */
-  private void peerClose(int code, String reason, boolean writeCloseResponse) {
-    if (writeCloseResponse) {
+  private void peerClose(int code, String reason) {
+    if (!writerSentClose) {
       try {
         writer.writeClose(code, reason);
       } catch (IOException ignored) {
       }
     }
 
-    try {
-      closeConnection();
-    } catch (IOException ignored) {
+    if (connectionClosed.compareAndSet(false, true)) {
+      try {
+        close();
+      } catch (IOException ignored) {
+      }
     }
 
     listener.onClose(code, reason);
@@ -158,32 +199,24 @@
 
   /** Called on the reader thread when an error occurs. */
   private void readerErrorClose(IOException e) {
-    boolean writeCloseResponse;
-    synchronized (closeLock) {
-      readerSentClose = true;
-
-      // If the writer has not closed we will close the connection.
-      writeCloseResponse = !writerSentClose;
-    }
-
-    if (writeCloseResponse) {
-      if (e instanceof ProtocolException) {
-        // For protocol exceptions, try to inform the server of such.
-        try {
-          writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
-        } catch (IOException ignored) {
-        }
+    // For protocol exceptions, try to inform the server of such.
+    if (!writerSentClose && e instanceof ProtocolException) {
+      try {
+        writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
+      } catch (IOException ignored) {
       }
     }
 
-    try {
-      closeConnection();
-    } catch (IOException ignored) {
+    if (connectionClosed.compareAndSet(false, true)) {
+      try {
+        close();
+      } catch (IOException ignored) {
+      }
     }
 
     listener.onFailure(e, null);
   }
 
-  /** Perform any tear-down work on the connection (close the socket, recycle, etc.). */
-  protected abstract void closeConnection() throws IOException;
+  /** Perform any tear-down work (close the connection, shutdown executors). */
+  protected abstract void close() throws IOException;
 }
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
index 2b93398..0778278 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
@@ -68,14 +68,16 @@
   static final int OPCODE_CONTROL_PONG = 0xa;
 
   /**
-   * Maximum length of frame payload. Larger payloads, if supported, can use the special values
-   * {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
+   * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the
+   * special values {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
    */
-  static final int PAYLOAD_MAX = 125;
+  static final long PAYLOAD_BYTE_MAX = 125L;
   /**
    * Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the unsigned length.
    */
   static final int PAYLOAD_SHORT = 126;
+  /** Maximum length of a frame payload to be denoted as {@link #PAYLOAD_SHORT}. */
+  static final long PAYLOAD_SHORT_MAX = 0xffffL;
   /**
    * Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the unsigned
    * length.
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
index ce548b1..d81785a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
@@ -15,6 +15,9 @@
  */
 package com.squareup.okhttp.internal.ws;
 
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.ws.WebSocket;
 import java.io.EOFException;
 import java.io.IOException;
 import java.net.ProtocolException;
@@ -24,7 +27,6 @@
 import okio.Source;
 import okio.Timeout;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV1;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV2;
@@ -40,7 +42,7 @@
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_FLAG_CONTROL;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
 import static java.lang.Integer.toHexString;
@@ -50,7 +52,7 @@
  */
 public final class WebSocketReader {
   public interface FrameCallback {
-    void onMessage(BufferedSource source, PayloadType type) throws IOException;
+    void onMessage(ResponseBody body) throws IOException;
     void onPing(Buffer buffer);
     void onPong(Buffer buffer);
     void onClose(int code, String reason);
@@ -145,8 +147,8 @@
     }
     frameBytesRead = 0;
 
-    if (isControlFrame && frameLength > PAYLOAD_MAX) {
-      throw new ProtocolException("Control frame must be less than " + PAYLOAD_MAX + "B.");
+    if (isControlFrame && frameLength > PAYLOAD_BYTE_MAX) {
+      throw new ProtocolException("Control frame must be less than " + PAYLOAD_BYTE_MAX + "B.");
     }
 
     if (isMasked) {
@@ -182,18 +184,23 @@
         frameCallback.onPong(buffer);
         break;
       case OPCODE_CONTROL_CLOSE:
-        int code = 0;
+        int code = 1000;
         String reason = "";
         if (buffer != null) {
-          if (buffer.size() < 2) {
-            throw new ProtocolException("Close payload must be at least two bytes.");
-          }
-          code = buffer.readShort();
-          if (code < 1000 || code >= 5000) {
-            throw new ProtocolException("Code must be in range [1000,5000): " + code);
-          }
+          long bufferSize = buffer.size();
+          if (bufferSize == 1) {
+            throw new ProtocolException("Malformed close payload length of 1.");
+          } else if (bufferSize != 0) {
+            code = buffer.readShort();
+            if (code < 1000 || code >= 5000) {
+              throw new ProtocolException("Code must be in range [1000,5000): " + code);
+            }
+            if ((code >= 1004 && code <= 1006) || (code >= 1012 && code <= 2999)) {
+              throw new ProtocolException("Code " + code + " is reserved and may not be used.");
+            }
 
-          reason = buffer.readUtf8();
+            reason = buffer.readUtf8();
+          }
         }
         frameCallback.onClose(code, reason);
         closed = true;
@@ -204,20 +211,35 @@
   }
 
   private void readMessageFrame() throws IOException {
-    PayloadType type;
+    final MediaType type;
     switch (opcode) {
       case OPCODE_TEXT:
-        type = PayloadType.TEXT;
+        type = WebSocket.TEXT;
         break;
       case OPCODE_BINARY:
-        type = PayloadType.BINARY;
+        type = WebSocket.BINARY;
         break;
       default:
         throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
     }
 
+    final BufferedSource source = Okio.buffer(framedMessageSource);
+    ResponseBody body = new ResponseBody() {
+      @Override public MediaType contentType() {
+        return type;
+      }
+
+      @Override public long contentLength() throws IOException {
+        return -1;
+      }
+
+      @Override public BufferedSource source() throws IOException {
+        return source;
+      }
+    };
+
     messageClosed = false;
-    frameCallback.onMessage(Okio.buffer(framedMessageSource), type);
+    frameCallback.onMessage(body);
     if (!messageClosed) {
       throw new IllegalStateException("Listener failed to call close on message payload.");
     }
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
index fc5de75..feece7a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
@@ -20,42 +20,41 @@
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.BufferedSource;
-import okio.Okio;
 import okio.Sink;
 import okio.Timeout;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B1_FLAG_MASK;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTINUATION;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_CLOSE;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PING;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
 import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
 
 /**
  * An <a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a>-compatible WebSocket frame writer.
  * <p>
  * This class is partially thread safe. Only a single "main" thread should be sending messages via
- * calls to {@link #newMessageSink} or {@link #sendMessage} as well as any calls to
- * {@link #writePing} or {@link #writeClose}. Other threads may call {@link #writePing},
- * {@link #writePong}, or {@link #writeClose} which will interleave on the wire with frames from
- * the main thread.
+ * calls to {@link #newMessageSink}, {@link #writePing}, or {@link #writeClose}. Other threads may
+ * call {@link #writePing}, {@link #writePong}, or {@link #writeClose} which will interleave on the
+ * wire with frames from the "main" sending thread.
  */
 public final class WebSocketWriter {
   private final boolean isClient;
-  /** Writes must be guarded by synchronizing on this instance! */
-  private final BufferedSink sink;
   private final Random random;
 
+  /** Writes must be guarded by synchronizing on 'this'. */
+  private final BufferedSink sink;
+  /** Access must be guarded by synchronizing on 'this'. */
+  private boolean writerClosed;
+
+  private final Buffer buffer = new Buffer();
   private final FrameSink frameSink = new FrameSink();
 
-  private boolean closed;
   private boolean activeWriter;
 
   private final byte[] maskKey;
@@ -75,15 +74,15 @@
 
   /** Send a ping with the supplied {@code payload}. Payload may be {@code null} */
   public void writePing(Buffer payload) throws IOException {
-    synchronized (sink) {
-      writeControlFrame(OPCODE_CONTROL_PING, payload);
+    synchronized (this) {
+      writeControlFrameSynchronized(OPCODE_CONTROL_PING, payload);
     }
   }
 
   /** Send a pong with the supplied {@code payload}. Payload may be {@code null} */
   public void writePong(Buffer payload) throws IOException {
-    synchronized (sink) {
-      writeControlFrame(OPCODE_CONTROL_PONG, payload);
+    synchronized (this) {
+      writeControlFrameSynchronized(OPCODE_CONTROL_PONG, payload);
     }
   }
 
@@ -108,21 +107,23 @@
       }
     }
 
-    synchronized (sink) {
-      writeControlFrame(OPCODE_CONTROL_CLOSE, payload);
-      closed = true;
+    synchronized (this) {
+      writeControlFrameSynchronized(OPCODE_CONTROL_CLOSE, payload);
+      writerClosed = true;
     }
   }
 
-  private void writeControlFrame(int opcode, Buffer payload) throws IOException {
-    if (closed) throw new IOException("closed");
+  private void writeControlFrameSynchronized(int opcode, Buffer payload) throws IOException {
+    assert Thread.holdsLock(this);
+
+    if (writerClosed) throw new IOException("closed");
 
     int length = 0;
     if (payload != null) {
       length = (int) payload.size();
-      if (length > PAYLOAD_MAX) {
+      if (length > PAYLOAD_BYTE_MAX) {
         throw new IllegalArgumentException(
-            "Payload size must be less than or equal to " + PAYLOAD_MAX);
+            "Payload size must be less than or equal to " + PAYLOAD_BYTE_MAX);
       }
     }
 
@@ -138,7 +139,7 @@
       sink.write(maskKey);
 
       if (payload != null) {
-        writeAllMasked(payload, length);
+        writeMaskedSynchronized(payload, length);
       }
     } else {
       sink.writeByte(b1);
@@ -148,93 +149,70 @@
       }
     }
 
-    sink.flush();
+    sink.emit();
   }
 
   /**
    * Stream a message payload as a series of frames. This allows control frames to be interleaved
    * between parts of the message.
    */
-  public BufferedSink newMessageSink(PayloadType type) {
-    if (type == null) throw new NullPointerException("type == null");
+  public Sink newMessageSink(int formatOpcode) {
     if (activeWriter) {
       throw new IllegalStateException("Another message writer is active. Did you call close()?");
     }
     activeWriter = true;
 
-    frameSink.payloadType = type;
+    // Reset FrameSink state for a new writer.
+    frameSink.formatOpcode = formatOpcode;
     frameSink.isFirstFrame = true;
-    return Okio.buffer(frameSink);
+    frameSink.closed = false;
+
+    return frameSink;
   }
 
-  /**
-   * Send a message payload as a single frame. This will block any control frames that need sent
-   * until it is completed.
-   */
-  public void sendMessage(PayloadType type, Buffer payload) throws IOException {
-    if (type == null) throw new NullPointerException("type == null");
-    if (payload == null) throw new NullPointerException("payload == null");
-    if (activeWriter) {
-      throw new IllegalStateException("A message writer is active. Did you call close()?");
+  private void writeMessageFrameSynchronized(int formatOpcode, long byteCount, boolean isFirstFrame,
+      boolean isFinal) throws IOException {
+    assert Thread.holdsLock(this);
+
+    if (writerClosed) throw new IOException("closed");
+
+    int b0 = isFirstFrame ? formatOpcode : OPCODE_CONTINUATION;
+    if (isFinal) {
+      b0 |= B0_FLAG_FIN;
     }
-    writeFrame(type, payload, payload.size(), true /* first frame */, true /* final */);
+    sink.writeByte(b0);
+
+    int b1 = 0;
+    if (isClient) {
+      b1 |= B1_FLAG_MASK;
+      random.nextBytes(maskKey);
+    }
+    if (byteCount <= PAYLOAD_BYTE_MAX) {
+      b1 |= (int) byteCount;
+      sink.writeByte(b1);
+    } else if (byteCount <= PAYLOAD_SHORT_MAX) {
+      b1 |= PAYLOAD_SHORT;
+      sink.writeByte(b1);
+      sink.writeShort((int) byteCount);
+    } else {
+      b1 |= PAYLOAD_LONG;
+      sink.writeByte(b1);
+      sink.writeLong(byteCount);
+    }
+
+    if (isClient) {
+      sink.write(maskKey);
+      writeMaskedSynchronized(buffer, byteCount);
+    } else {
+      sink.write(buffer, byteCount);
+    }
+
+    sink.emit();
   }
 
-  private void writeFrame(PayloadType payloadType, Buffer source, long byteCount,
-      boolean isFirstFrame, boolean isFinal) throws IOException {
-    if (closed) throw new IOException("closed");
+  private void writeMaskedSynchronized(BufferedSource source, long byteCount) throws IOException {
+    assert Thread.holdsLock(this);
 
-    int opcode = OPCODE_CONTINUATION;
-    if (isFirstFrame) {
-      switch (payloadType) {
-        case TEXT:
-          opcode = OPCODE_TEXT;
-          break;
-        case BINARY:
-          opcode = OPCODE_BINARY;
-          break;
-        default:
-          throw new IllegalStateException("Unknown payload type: " + payloadType);
-      }
-    }
-
-    synchronized (sink) {
-      int b0 = opcode;
-      if (isFinal) {
-        b0 |= B0_FLAG_FIN;
-      }
-      sink.writeByte(b0);
-
-      int b1 = 0;
-      if (isClient) {
-        b1 |= B1_FLAG_MASK;
-        random.nextBytes(maskKey);
-      }
-      if (byteCount <= PAYLOAD_MAX) {
-        b1 |= (int) byteCount;
-        sink.writeByte(b1);
-      } else if (byteCount <= 0xffffL) { // Unsigned short.
-        b1 |= PAYLOAD_SHORT;
-        sink.writeByte(b1);
-        sink.writeShort((int) byteCount);
-      } else {
-        b1 |= PAYLOAD_LONG;
-        sink.writeByte(b1);
-        sink.writeLong(byteCount);
-      }
-
-      if (isClient) {
-        sink.write(maskKey);
-        writeAllMasked(source, byteCount);
-      } else {
-        sink.write(source, byteCount);
-      }
-
-      sink.flush();
-    }
-  }
-
-  private void writeAllMasked(BufferedSource source, long byteCount) throws IOException {
     long written = 0;
     while (written < byteCount) {
       int toRead = (int) Math.min(byteCount, maskBuffer.length);
@@ -247,20 +225,31 @@
   }
 
   private final class FrameSink implements Sink {
-    private PayloadType payloadType;
+    private int formatOpcode;
     private boolean isFirstFrame;
+    private boolean closed;
 
     @Override public void write(Buffer source, long byteCount) throws IOException {
-      writeFrame(payloadType, source, byteCount, isFirstFrame, false /* final */);
-      isFirstFrame = false;
+      if (closed) throw new IOException("closed");
+
+      buffer.write(source, byteCount);
+
+      long emitCount = buffer.completeSegmentByteCount();
+      if (emitCount > 0) {
+        synchronized (WebSocketWriter.this) {
+          writeMessageFrameSynchronized(formatOpcode, emitCount, isFirstFrame, false /* final */);
+        }
+        isFirstFrame = false;
+      }
     }
 
     @Override public void flush() throws IOException {
       if (closed) throw new IOException("closed");
 
-      synchronized (sink) {
-        sink.flush();
+      synchronized (WebSocketWriter.this) {
+        writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, false /* final */);
       }
+      isFirstFrame = false;
     }
 
     @Override public Timeout timeout() {
@@ -271,21 +260,10 @@
     @Override public void close() throws IOException {
       if (closed) throw new IOException("closed");
 
-      int length = 0;
-
-      synchronized (sink) {
-        sink.writeByte(B0_FLAG_FIN | OPCODE_CONTINUATION);
-
-        if (isClient) {
-          sink.writeByte(B1_FLAG_MASK | length);
-          random.nextBytes(maskKey);
-          sink.write(maskKey);
-        } else {
-          sink.writeByte(length);
-        }
-        sink.flush();
+      synchronized (WebSocketWriter.this) {
+        writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, true /* final */);
       }
-
+      closed = true;
       activeWriter = false;
     }
   }
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
index 4cf2f42..a3eebe7 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
@@ -15,40 +15,35 @@
  */
 package com.squareup.okhttp.ws;
 
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
 import java.io.IOException;
 import okio.Buffer;
-import okio.BufferedSink;
 
 /** Blocking interface to connect and write to a web socket. */
 public interface WebSocket {
-  /** The format of a message payload. */
-  enum PayloadType {
-    /** UTF8-encoded text data. */
-    TEXT,
-    /** Arbitrary binary data. */
-    BINARY
-  }
+  /** A {@link MediaType} indicating UTF-8 text frames should be used when sending the message. */
+  MediaType TEXT = MediaType.parse("application/vnd.okhttp.websocket+text; charset=utf-8");
+  /** A {@link MediaType} indicating binary frames should be used when sending the message. */
+  MediaType BINARY = MediaType.parse("application/vnd.okhttp.websocket+binary");
 
   /**
-   * Stream a message payload to the server of the specified {code type}.
-   * <p>
-   * You must call {@link BufferedSink#close() close()} to complete the message. Calls to
-   * {@link BufferedSink#flush() flush()} write a frame fragment. The message may be empty.
+   * Send a message payload to the server.
    *
+   * <p>The {@linkplain RequestBody#contentType() content type} of {@code message} should be either
+   * {@link #TEXT} or {@link #BINARY}.
+   *
+   * @throws IOException if unable to write the message. Clients must call {@link #close} when this
+   * happens to ensure resources are cleaned up.
    * @throws IllegalStateException if not connected, already closed, or another writer is active.
    */
-  BufferedSink newMessageSink(WebSocket.PayloadType type);
-
-  /**
-   * Send a message payload to the server of the specified {@code type}.
-   *
-   * @throws IllegalStateException if not connected, already closed, or another writer is active.
-   */
-  void sendMessage(WebSocket.PayloadType type, Buffer payload) throws IOException;
+  void sendMessage(RequestBody message) throws IOException;
 
   /**
    * Send a ping to the server with optional payload.
    *
+   * @throws IOException if unable to write the ping.  Clients must call {@link #close} when this
+   * happens to ensure resources are cleaned up.
    * @throws IllegalStateException if already closed.
    */
   void sendPing(Buffer payload) throws IOException;
@@ -62,6 +57,7 @@
    * It is an error to call this method before calling close on an active writer. Calling this
    * method more than once has no effect.
    *
+   * @throws IOException if unable to write the close message. Resources will still be freed.
    * @throws IllegalStateException if already closed.
    */
   void close(int code, String reason) throws IOException;
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
index 46ee8a1..5950850 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
@@ -17,12 +17,12 @@
 
 import com.squareup.okhttp.Call;
 import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.StreamAllocation;
 import com.squareup.okhttp.internal.ws.RealWebSocket;
 import com.squareup.okhttp.internal.ws.WebSocketProtocol;
 import java.io.IOException;
@@ -30,11 +30,9 @@
 import java.security.SecureRandom;
 import java.util.Collections;
 import java.util.Random;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.ThreadPoolExecutor;
-import okio.BufferedSink;
-import okio.BufferedSource;
 import okio.ByteString;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -47,7 +45,6 @@
     return new WebSocketCall(client, request);
   }
 
-  private final Request request;
   private final Call call;
   private final Random random;
   private final String key;
@@ -78,7 +75,6 @@
         .header("Sec-WebSocket-Key", key)
         .header("Sec-WebSocket-Version", "13")
         .build();
-    this.request = request;
 
     call = client.newCall(request);
   }
@@ -118,11 +114,9 @@
     call.cancel();
   }
 
-  private void createWebSocket(Response response, WebSocketListener listener)
-      throws IOException {
+  private void createWebSocket(Response response, WebSocketListener listener) throws IOException {
     if (response.code() != 101) {
-      // TODO call.engine.releaseConnection();
-      Internal.instance.callEngineReleaseConnection(call);
+      Util.closeQuietly(response.body());
       throw new ProtocolException("Expected HTTP 101 response but was '"
           + response.code()
           + " "
@@ -150,21 +144,9 @@
           + "'");
     }
 
-    // TODO connection = call.engine.getConnection();
-    Connection connection = Internal.instance.callEngineGetConnection(call);
-    // TODO if (!connection.clearOwner()) {
-    if (!Internal.instance.clearOwner(connection)) {
-      throw new IllegalStateException("Unable to take ownership of connection.");
-    }
-
-    BufferedSource source = Internal.instance.connectionRawSource(connection);
-    BufferedSink sink = Internal.instance.connectionRawSink(connection);
-
-    final RealWebSocket webSocket =
-        ConnectionWebSocket.create(response, connection, source, sink, random, listener);
-
-    // TODO connection.setOwner(webSocket);
-    Internal.instance.connectionSetOwner(connection, webSocket);
+    StreamAllocation streamAllocation = Internal.instance.callEngineGetStreamAllocation(call);
+    RealWebSocket webSocket = StreamWebSocket.create(
+        streamAllocation, response, random, listener);
 
     listener.onOpen(webSocket, response);
 
@@ -173,30 +155,33 @@
   }
 
   // Keep static so that the WebSocketCall instance can be garbage collected.
-  private static class ConnectionWebSocket extends RealWebSocket {
-    static RealWebSocket create(Response response, Connection connection, BufferedSource source,
-        BufferedSink sink, Random random, WebSocketListener listener) {
+  private static class StreamWebSocket extends RealWebSocket {
+    static RealWebSocket create(StreamAllocation streamAllocation, Response response,
+        Random random, WebSocketListener listener) {
       String url = response.request().urlString();
       ThreadPoolExecutor replyExecutor =
           new ThreadPoolExecutor(1, 1, 1, SECONDS, new LinkedBlockingDeque<Runnable>(),
               Util.threadFactory(String.format("OkHttp %s WebSocket", url), true));
       replyExecutor.allowCoreThreadTimeOut(true);
 
-      return new ConnectionWebSocket(connection, source, sink, random, replyExecutor, listener,
-          url);
+      return new StreamWebSocket(streamAllocation, random, replyExecutor, listener, url);
     }
 
-    private final Connection connection;
+    private final StreamAllocation streamAllocation;
+    private final ExecutorService replyExecutor;
 
-    private ConnectionWebSocket(Connection connection, BufferedSource source, BufferedSink sink,
-        Random random, Executor replyExecutor, WebSocketListener listener, String url) {
-      super(true /* is client */, source, sink, random, replyExecutor, listener, url);
-      this.connection = connection;
+    private StreamWebSocket(StreamAllocation streamAllocation,
+        Random random, ExecutorService replyExecutor, WebSocketListener listener, String url) {
+      super(true /* is client */, streamAllocation.connection().source,
+          streamAllocation.connection().sink, random, replyExecutor, listener, url);
+      this.streamAllocation = streamAllocation;
+      this.replyExecutor = replyExecutor;
     }
 
-    @Override protected void closeConnection() throws IOException {
-      // TODO connection.closeIfOwnedBy(this);
-      Internal.instance.closeIfOwnedBy(connection, this);
+    @Override protected void close() throws IOException {
+      replyExecutor.shutdown();
+      streamAllocation.noNewStreams();
+      streamAllocation.streamFinished(streamAllocation.stream());
     }
   }
 }
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
index 8941b74..5a5a8b1 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
@@ -16,11 +16,9 @@
 package com.squareup.okhttp.ws;
 
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import java.io.IOException;
 import okio.Buffer;
-import okio.BufferedSource;
-
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
 
 /** Listener for server-initiated messages on a connected {@link WebSocket}. */
 public interface WebSocketListener {
@@ -50,8 +48,11 @@
    * <p>Implementations <strong>must</strong> call {@code source.close()} before returning. This
    * indicates completion of parsing the message payload and will consume any remaining bytes in
    * the message.
+   *
+   * <p>The {@linkplain ResponseBody#contentType() content type} of {@code message} will be either
+   * {@link WebSocket#TEXT} or {@link WebSocket#BINARY} which indicates the format of the message.
    */
-  void onMessage(BufferedSource payload, PayloadType type) throws IOException;
+  void onMessage(ResponseBody message) throws IOException;
 
   /**
    * Called when a server pong is received. This is usually a result of calling {@link
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 5cd1187..3254b30 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>okhttp</artifactId>
@@ -17,6 +17,11 @@
       <groupId>com.squareup.okio</groupId>
       <artifactId>okio</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.android</groupId>
+      <artifactId>android</artifactId>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java
index cd3687b..9efdf28 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Address.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Address.java
@@ -30,57 +30,90 @@
  * this is the server's hostname and port. If an explicit proxy is requested (or
  * {@linkplain Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
  * that proxy information. For secure connections the address also includes the
- * SSL socket factory and hostname verifier.
+ * SSL socket factory, hostname verifier, and certificate pinner.
  *
  * <p>HTTP requests that share the same {@code Address} may also share the same
  * {@link Connection}.
  */
 public final class Address {
-  final Proxy proxy;
-  final String uriHost;
-  final int uriPort;
+  final HttpUrl url;
+  final Dns dns;
   final SocketFactory socketFactory;
-  final SSLSocketFactory sslSocketFactory;
-  final HostnameVerifier hostnameVerifier;
-  final CertificatePinner certificatePinner;
   final Authenticator authenticator;
   final List<Protocol> protocols;
   final List<ConnectionSpec> connectionSpecs;
   final ProxySelector proxySelector;
+  final Proxy proxy;
+  final SSLSocketFactory sslSocketFactory;
+  final HostnameVerifier hostnameVerifier;
+  final CertificatePinner certificatePinner;
 
-  public Address(String uriHost, int uriPort, SocketFactory socketFactory,
+  public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
       SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
       CertificatePinner certificatePinner, Authenticator authenticator, Proxy proxy,
       List<Protocol> protocols, List<ConnectionSpec> connectionSpecs, ProxySelector proxySelector) {
-    if (uriHost == null) throw new NullPointerException("uriHost == null");
-    if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
-    if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
-    if (protocols == null) throw new IllegalArgumentException("protocols == null");
-    if (proxySelector == null) throw new IllegalArgumentException("proxySelector == null");
-    this.proxy = proxy;
-    this.uriHost = uriHost;
-    this.uriPort = uriPort;
+    this.url = new HttpUrl.Builder()
+        .scheme(sslSocketFactory != null ? "https" : "http")
+        .host(uriHost)
+        .port(uriPort)
+        .build();
+
+    if (dns == null) throw new IllegalArgumentException("dns == null");
+    this.dns = dns;
+
+    if (socketFactory == null) throw new IllegalArgumentException("socketFactory == null");
     this.socketFactory = socketFactory;
+
+    if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
+    this.authenticator = authenticator;
+
+    if (protocols == null) throw new IllegalArgumentException("protocols == null");
+    this.protocols = Util.immutableList(protocols);
+
+    if (connectionSpecs == null) throw new IllegalArgumentException("connectionSpecs == null");
+    this.connectionSpecs = Util.immutableList(connectionSpecs);
+
+    if (proxySelector == null) throw new IllegalArgumentException("proxySelector == null");
+    this.proxySelector = proxySelector;
+
+    this.proxy = proxy;
     this.sslSocketFactory = sslSocketFactory;
     this.hostnameVerifier = hostnameVerifier;
     this.certificatePinner = certificatePinner;
-    this.authenticator = authenticator;
-    this.protocols = Util.immutableList(protocols);
-    this.connectionSpecs = Util.immutableList(connectionSpecs);
-    this.proxySelector = proxySelector;
   }
 
-  /** Returns the hostname of the origin server. */
-  public String getRfc2732Host() {
-    return uriHost;
+  /**
+   * Returns a URL with the hostname and port of the origin server. The path, query, and fragment of
+   * this URL are always empty, since they are not significant for planning a route.
+   */
+  public HttpUrl url() {
+    return url;
+  }
+
+  /**
+   * Returns the hostname of the origin server.
+   *
+   * @deprecated prefer {@code address.url().host()}.
+   */
+  @Deprecated
+  public String getUriHost() {
+    return url.host();
   }
 
   /**
    * Returns the port of the origin server; typically 80 or 443. Unlike
    * may {@code getPort()} accessors, this method never returns -1.
+   *
+   * @deprecated prefer {@code address.url().port()}.
    */
+  @Deprecated
   public int getUriPort() {
-    return uriPort;
+    return url.port();
+  }
+
+  /** Returns the service that will be used to resolve IP addresses for hostnames. */
+  public Dns getDns() {
+    return dns;
   }
 
   /** Returns the socket factory for new connections. */
@@ -88,25 +121,7 @@
     return socketFactory;
   }
 
-  /**
-   * Returns the SSL socket factory, or null if this is not an HTTPS
-   * address.
-   */
-  public SSLSocketFactory getSslSocketFactory() {
-    return sslSocketFactory;
-  }
-
-  /**
-   * Returns the hostname verifier, or null if this is not an HTTPS
-   * address.
-   */
-  public HostnameVerifier getHostnameVerifier() {
-    return hostnameVerifier;
-  }
-
-  /**
-   * Returns the client's authenticator. This method never returns null.
-   */
+  /** Returns the client's authenticator. */
   public Authenticator getAuthenticator() {
     return authenticator;
   }
@@ -124,14 +139,6 @@
   }
 
   /**
-   * Returns this address's explicitly-specified HTTP proxy, or null to
-   * delegate to the {@linkplain #getProxySelector proxy selector}.
-   */
-  public Proxy getProxy() {
-    return proxy;
-  }
-
-  /**
    * Returns this address's proxy selector. Only used if the proxy is null. If none of this
    * selector's proxies are reachable, a direct connection will be attempted.
    */
@@ -140,8 +147,24 @@
   }
 
   /**
-   * Returns this address's certificate pinner. Only used for secure connections.
+   * Returns this address's explicitly-specified HTTP proxy, or null to
+   * delegate to the {@linkplain #getProxySelector proxy selector}.
    */
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  /** Returns the SSL socket factory, or null if this is not an HTTPS address. */
+  public SSLSocketFactory getSslSocketFactory() {
+    return sslSocketFactory;
+  }
+
+  /** Returns the hostname verifier, or null if this is not an HTTPS address. */
+  public HostnameVerifier getHostnameVerifier() {
+    return hostnameVerifier;
+  }
+
+  /** Returns this address's certificate pinner, or null if this is not an HTTPS address. */
   public CertificatePinner getCertificatePinner() {
     return certificatePinner;
   }
@@ -149,32 +172,32 @@
   @Override public boolean equals(Object other) {
     if (other instanceof Address) {
       Address that = (Address) other;
-      return equal(this.proxy, that.proxy)
-          && this.uriHost.equals(that.uriHost)
-          && this.uriPort == that.uriPort
+      return this.url.equals(that.url)
+          && this.dns.equals(that.dns)
+          && this.authenticator.equals(that.authenticator)
+          && this.protocols.equals(that.protocols)
+          && this.connectionSpecs.equals(that.connectionSpecs)
+          && this.proxySelector.equals(that.proxySelector)
+          && equal(this.proxy, that.proxy)
           && equal(this.sslSocketFactory, that.sslSocketFactory)
           && equal(this.hostnameVerifier, that.hostnameVerifier)
-          && equal(this.certificatePinner, that.certificatePinner)
-          && equal(this.authenticator, that.authenticator)
-          && equal(this.protocols, that.protocols)
-          && equal(this.connectionSpecs, that.connectionSpecs)
-          && equal(this.proxySelector, that.proxySelector);
+          && equal(this.certificatePinner, that.certificatePinner);
     }
     return false;
   }
 
   @Override public int hashCode() {
     int result = 17;
-    result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
-    result = 31 * result + uriHost.hashCode();
-    result = 31 * result + uriPort;
-    result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
-    result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
-    result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
+    result = 31 * result + url.hashCode();
+    result = 31 * result + dns.hashCode();
     result = 31 * result + authenticator.hashCode();
     result = 31 * result + protocols.hashCode();
     result = 31 * result + connectionSpecs.hashCode();
     result = 31 * result + proxySelector.hashCode();
+    result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+    result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
+    result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+    result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
     return result;
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index 33561ba..651bd0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -19,6 +19,7 @@
 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;
@@ -119,7 +120,15 @@
    */
   public void cancel() {
     canceled = true;
-    if (engine != null) engine.disconnect();
+    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() {
@@ -172,7 +181,8 @@
           // Do not signal the callback twice!
           logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
         } else {
-          responseCallback.onFailure(engine.getRequest(), e);
+          Request request = engine == null ? originalRequest : engine.getRequest();
+          responseCallback.onFailure(request, e);
         }
       } finally {
         client.getDispatcher().finished(this);
@@ -215,14 +225,22 @@
     }
 
     @Override public Response proceed(Request request) throws IOException {
+      // If there's another interceptor in the chain, call that.
       if (index < client.interceptors().size()) {
-        // There's another interceptor in the chain. Call that.
         Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
-        return client.interceptors().get(index).intercept(chain);
-      } else {
-        // No more interceptors. Do HTTP.
-        return getResponse(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);
     }
   }
 
@@ -254,18 +272,20 @@
     }
 
     // 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, null);
+    engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
 
     int followUpCount = 0;
     while (true) {
       if (canceled) {
-        engine.releaseConnection();
+        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();
@@ -273,6 +293,7 @@
         // 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;
         }
@@ -282,12 +303,19 @@
         // 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();
@@ -295,22 +323,25 @@
 
       if (followUp == null) {
         if (!forWebSocket) {
-          engine.releaseConnection();
+          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())) {
-        engine.releaseConnection();
+        streamAllocation.release();
+        streamAllocation = null;
       }
 
-      Connection connection = engine.close();
       request = followUp;
-      engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
+      engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
           response);
     }
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
index 15a2952..bd3df19 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
@@ -54,7 +54,7 @@
  *
  *     String hostname = "publicobject.com";
  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
- *         .add(hostname, "sha1/BOGUSPIN")
+ *         .add(hostname, "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
  *         .build();
  *     OkHttpClient client = new OkHttpClient();
  *     client.setCertificatePinner(certificatePinner);
@@ -74,7 +74,7 @@
  *     sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
  *     sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
  *   Pinned certificates for publicobject.com:
- *     sha1/BOGUSPIN
+ *     sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=
  *   at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
  *   at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
  *   at com.squareup.okhttp.Connection.connect(Connection.java)
@@ -135,7 +135,7 @@
   private final Map<String, Set<ByteString>> hostnameToPins;
 
   private CertificatePinner(Builder builder) {
-    hostnameToPins = Util.immutableMap(builder.hostnameToPins);
+    this.hostnameToPins = Util.immutableMap(builder.hostnameToPins);
   }
 
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index a778747..203b510 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -16,500 +16,69 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.ConnectionSpecSelector;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.framed.FramedConnection;
-import com.squareup.okhttp.internal.http.FramedTransport;
-import com.squareup.okhttp.internal.http.HttpConnection;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.HttpTransport;
-import com.squareup.okhttp.internal.http.OkHeaders;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
-import java.net.Proxy;
 import java.net.Socket;
-import java.net.UnknownServiceException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
-import okio.Source;
-
-import static com.squareup.okhttp.internal.Util.closeQuietly;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
 
 /**
- * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be
- * used for multiple HTTP request/response exchanges. Connections may be direct
- * to the origin server or via a proxy.
+ * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be used for multiple
+ * HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
  *
- * <p>Typically instances of this class are created, connected and exercised
- * automatically by the HTTP client. Applications may use this class to monitor
- * HTTP connections as members of a {@linkplain ConnectionPool connection pool}.
+ * <p>Typically instances of this class are created, connected and exercised automatically by the
+ * HTTP client. Applications may use this class to monitor HTTP connections as members of a
+ * {@linkplain ConnectionPool connection pool}.
  *
- * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
- * which isn't so much a connection as a single request/response exchange.
+ * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, which isn't so much a
+ * connection as a single request/response exchange.
  *
  * <h3>Modern TLS</h3>
- * There are tradeoffs when selecting which options to include when negotiating
- * a secure connection to a remote host. Newer TLS options are quite useful:
+ * There are tradeoffs when selecting which options to include when negotiating a secure connection
+ * to a remote host. Newer TLS options are quite useful:
  * <ul>
- *   <li>Server Name Indication (SNI) enables one IP address to negotiate secure
- *       connections for multiple domain names.
- *   <li>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port
- *       (443) to be used for different HTTP and SPDY protocols.
+ *   <li>Server Name Indication (SNI) enables one IP address to negotiate secure connections for
+ *       multiple domain names.
+ *   <li>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for
+ *       different HTTP and SPDY protocols.
  * </ul>
- * Unfortunately, older HTTPS servers refuse to connect when such options are
- * presented. Rather than avoiding these options entirely, this class allows a
- * connection to be attempted with modern options and then retried without them
- * should the attempt fail.
+ * Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than
+ * avoiding these options entirely, this class allows a connection to be attempted with modern
+ * options and then retried without them should the attempt fail.
+ *
+ * <h3>Connection Reuse</h3>
+ * <p>Each connection can carry a varying number streams, depending on the underlying protocol being
+ * used. HTTP/1.x connections can carry either zero or one streams. HTTP/2 connections can carry any
+ * number of streams, dynamically configured with {@code SETTINGS_MAX_CONCURRENT_STREAMS}. A
+ * connection currently carrying zero streams is an idle stream. We keep it alive because reusing an
+ * existing connection is typically faster than establishing a new one.
+ *
+ * <p>When a single logical call requires multiple streams due to redirects or authorization
+ * challenges, we prefer to use the same physical connection for all streams in the sequence. There
+ * are potential performance and behavior consequences to this preference. To support this feature,
+ * this class separates <i>allocations</i> from <i>streams</i>. An allocation is created by a call,
+ * used for one or more streams, and then released. An allocated connection won't be stolen by
+ * other calls while a redirect or authorization challenge is being handled.
+ *
+ * <p>When the maximum concurrent streams limit is reduced, some allocations will be rescinded.
+ * Attempting to create new streams on these allocations will fail.
+ *
+ * <p>Note that an allocation may be released before its stream is completed. This is intended to
+ * make bookkeeping easier for the caller: releasing the allocation as soon as the terminal stream
+ * has been found. But only complete the stream once its data stream has been exhausted.
  */
-public final class Connection {
-  private final ConnectionPool pool;
-  private final Route route;
-
-  private Socket socket;
-  private boolean connected = false;
-  private HttpConnection httpConnection;
-  private FramedConnection framedConnection;
-  private Protocol protocol = Protocol.HTTP_1_1;
-  private long idleStartTimeNs;
-  private Handshake handshake;
-  private int recycleCount;
-
-  /**
-   * The object that owns this connection. Null if it is shared (for SPDY),
-   * belongs to a pool, or has been discarded. Guarded by {@code pool}, which
-   * clears the owner when an incoming connection is recycled.
-   */
-  private Object owner;
-
-  public Connection(ConnectionPool pool, Route route) {
-    this.pool = pool;
-    this.route = route;
-  }
-
-  Object getOwner() {
-    synchronized (pool) {
-      return owner;
-    }
-  }
-
-  void setOwner(Object owner) {
-    if (isFramed()) return; // Framed connections are shared.
-    synchronized (pool) {
-      if (this.owner != null) throw new IllegalStateException("Connection already has an owner!");
-      this.owner = owner;
-    }
-  }
-
-  /**
-   * Attempts to clears the owner of this connection. Returns true if the owner
-   * was cleared and the connection can be pooled or reused. This will return
-   * false if the connection cannot be pooled or reused, such as if it was
-   * closed with {@link #closeIfOwnedBy}.
-   */
-  boolean clearOwner() {
-    synchronized (pool) {
-      if (owner == null) {
-        // No owner? Don't reuse this connection.
-        return false;
-      }
-
-      owner = null;
-      return true;
-    }
-  }
-
-  /**
-   * Closes this connection if it is currently owned by {@code owner}. This also
-   * strips the ownership of the connection so it cannot be pooled or reused.
-   */
-  void closeIfOwnedBy(Object owner) throws IOException {
-    if (isFramed()) throw new IllegalStateException();
-    synchronized (pool) {
-      if (this.owner != owner) {
-        return; // Wrong owner. Perhaps a late disconnect?
-      }
-
-      this.owner = null; // Drop the owner so the connection won't be reused.
-    }
-
-    // Don't close() inside the synchronized block.
-    if (socket != null) {
-      socket.close();
-    }
-  }
-
-  void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
-      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
-    if (connected) throw new IllegalStateException("already connected");
-
-    RouteException routeException = null;
-    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
-    Proxy proxy = route.getProxy();
-    Address address = route.getAddress();
-
-    if (route.address.getSslSocketFactory() == null
-        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
-      throw new RouteException(new UnknownServiceException(
-          "CLEARTEXT communication not supported: " + connectionSpecs));
-    }
-
-    while (!connected) {
-      try {
-        socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
-            ? address.getSocketFactory().createSocket()
-            : new Socket(proxy);
-        connectSocket(connectTimeout, readTimeout, writeTimeout, request,
-            connectionSpecSelector);
-        connected = true; // Success!
-      } catch (IOException e) {
-        Util.closeQuietly(socket);
-        socket = null;
-
-        if (routeException == null) {
-          routeException = new RouteException(e);
-        } else {
-          routeException.addConnectException(e);
-        }
-
-        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
-          throw routeException;
-        }
-      }
-    }
-  }
-
-  /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
-  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
-      Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
-    socket.setSoTimeout(readTimeout);
-    Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
-
-    if (route.address.getSslSocketFactory() != null) {
-      connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
-    }
-
-    if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
-      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
-      framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket)
-          .protocol(protocol).build();
-      framedConnection.sendConnectionPreface();
-    } else {
-      httpConnection = new HttpConnection(pool, this, socket);
-    }
-  }
-
-  private void connectTls(int readTimeout, int writeTimeout, Request request,
-      ConnectionSpecSelector connectionSpecSelector) throws IOException {
-    if (route.requiresTunnel()) {
-      createTunnel(readTimeout, writeTimeout, request);
-    }
-
-    Address address = route.getAddress();
-    SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
-    boolean success = false;
-    SSLSocket sslSocket = null;
-    try {
-      // Create the wrapper over the connected socket.
-      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
-          socket, address.getRfc2732Host(), address.getUriPort(), true /* autoClose */);
-
-      // Configure the socket's ciphers, TLS versions, and extensions.
-      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
-      if (connectionSpec.supportsTlsExtensions()) {
-        Platform.get().configureTlsExtensions(
-            sslSocket, address.getRfc2732Host(), address.getProtocols());
-      }
-
-      // Force handshake. This can throw!
-      sslSocket.startHandshake();
-      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
-
-      // Verify that the socket's certificates are acceptable for the target host.
-      if (!address.getHostnameVerifier().verify(address.getRfc2732Host(), sslSocket.getSession())) {
-        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
-        throw new SSLPeerUnverifiedException("Hostname " + address.getRfc2732Host() + " not verified:"
-            + "\n    certificate: " + CertificatePinner.pin(cert)
-            + "\n    DN: " + cert.getSubjectDN().getName()
-            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
-      }
-
-      // Check that the certificate pinner is satisfied by the certificates presented.
-      address.getCertificatePinner().check(address.getRfc2732Host(),
-          unverifiedHandshake.peerCertificates());
-
-      // Success! Save the handshake and the ALPN protocol.
-      String maybeProtocol = connectionSpec.supportsTlsExtensions()
-          ? Platform.get().getSelectedProtocol(sslSocket)
-          : null;
-      protocol = maybeProtocol != null
-          ? Protocol.get(maybeProtocol)
-          : Protocol.HTTP_1_1;
-      handshake = unverifiedHandshake;
-      socket = sslSocket;
-      success = true;
-    } catch (AssertionError e) {
-      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
-      throw e;
-    } finally {
-      if (sslSocket != null) {
-        Platform.get().afterHandshake(sslSocket);
-      }
-      if (!success) {
-        closeQuietly(sslSocket);
-      }
-    }
-  }
-
-  /**
-   * To make an HTTPS connection over an HTTP proxy, send an unencrypted
-   * CONNECT request to create the proxy connection. This may need to be
-   * retried if the proxy requires authorization.
-   */
-  private void createTunnel(int readTimeout, int writeTimeout, Request request) throws IOException {
-    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
-    Request tunnelRequest = createTunnelRequest(request);
-    HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
-    tunnelConnection.setTimeouts(readTimeout, writeTimeout);
-    HttpUrl url = tunnelRequest.httpUrl();
-    String requestLine = "CONNECT " + url.rfc2732host() + ":" + url.port() + " HTTP/1.1";
-    while (true) {
-      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
-      tunnelConnection.flush();
-      Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
-      // The response body from a CONNECT should be empty, but if it is not then we should consume
-      // it before proceeding.
-      long contentLength = OkHeaders.contentLength(response);
-      if (contentLength == -1L) {
-        contentLength = 0L;
-      }
-      Source body = tunnelConnection.newFixedLengthSource(contentLength);
-      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
-      body.close();
-
-      switch (response.code()) {
-        case HTTP_OK:
-          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
-          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
-          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
-          // that it will almost certainly fail because the proxy has sent unexpected data.
-          if (tunnelConnection.bufferSize() > 0) {
-            throw new IOException("TLS tunnel buffered too many bytes!");
-          }
-          return;
-
-        case HTTP_PROXY_AUTH:
-          tunnelRequest = OkHeaders.processAuthHeader(
-              route.getAddress().getAuthenticator(), response, route.getProxy());
-          if (tunnelRequest != null) continue;
-          throw new IOException("Failed to authenticate with proxy");
-
-        default:
-          throw new IOException(
-              "Unexpected response code for CONNECT: " + response.code());
-      }
-    }
-  }
-
-  /**
-   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
-   * no tunnel is necessary. Everything in the tunnel request is sent
-   * unencrypted to the proxy server, so tunnels include only the minimum set of
-   * headers. This avoids sending potentially sensitive data like HTTP cookies
-   * to the proxy unencrypted.
-   */
-  private Request createTunnelRequest(Request request) throws IOException {
-    HttpUrl tunnelUrl = new HttpUrl.Builder()
-        .scheme("https")
-        .host(request.httpUrl().host())
-        .port(request.httpUrl().port())
-        .build();
-    Request.Builder result = new Request.Builder()
-        .url(tunnelUrl)
-        .header("Host", Util.hostHeader(tunnelUrl))
-        .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
-
-    // Copy over the User-Agent header if it exists.
-    String userAgent = request.header("User-Agent");
-    if (userAgent != null) {
-      result.header("User-Agent", userAgent);
-    }
-
-    // Copy over the Proxy-Authorization header if it exists.
-    String proxyAuthorization = request.header("Proxy-Authorization");
-    if (proxyAuthorization != null) {
-      result.header("Proxy-Authorization", proxyAuthorization);
-    }
-
-    return result.build();
-  }
-
-  /**
-   * Connects this connection if it isn't already. This creates tunnels, shares
-   * the connection with the connection pool, and configures timeouts.
-   */
-  void connectAndSetOwner(OkHttpClient client, Object owner, Request request)
-      throws RouteException {
-    setOwner(owner);
-
-    if (!isConnected()) {
-      List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
-      connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
-          request, connectionSpecs, client.getRetryOnConnectionFailure());
-      if (isFramed()) {
-        client.getConnectionPool().share(this);
-      }
-      client.routeDatabase().connected(getRoute());
-    }
-
-    setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
-  }
-
-  /** Returns true if {@link #connect} has been attempted on this connection. */
-  boolean isConnected() {
-    return connected;
-  }
-
+public interface Connection {
   /** Returns the route used by this connection. */
-  public Route getRoute() {
-    return route;
-  }
+  Route getRoute();
 
   /**
    * Returns the socket that this connection uses, or null if the connection
    * is not currently connected.
    */
-  public Socket getSocket() {
-    return socket;
-  }
+  Socket getSocket();
 
-  BufferedSource rawSource() {
-    if (httpConnection == null) throw new UnsupportedOperationException();
-    return httpConnection.rawSource();
-  }
-
-  BufferedSink rawSink() {
-    if (httpConnection == null) throw new UnsupportedOperationException();
-    return httpConnection.rawSink();
-  }
-
-  /** Returns true if this connection is alive. */
-  boolean isAlive() {
-    return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
-  }
+  Handshake getHandshake();
 
   /**
-   * Returns true if we are confident that we can read data from this
-   * connection. This is more expensive and more accurate than {@link
-   * #isAlive()}; callers should check {@link #isAlive()} first.
+   * Returns the protocol negotiated by this connection, or {@link Protocol#HTTP_1_1} if no protocol
+   * has been negotiated. This method returns {@link Protocol#HTTP_1_1} even if the remote peer is
+   * using {@link Protocol#HTTP_1_0}.
    */
-  boolean isReadable() {
-    if (httpConnection != null) return httpConnection.isReadable();
-    return true; // Framed connections, and connections before connect() are both optimistic.
-  }
-
-  void resetIdleStartTime() {
-    if (framedConnection != null) throw new IllegalStateException("framedConnection != null");
-    this.idleStartTimeNs = System.nanoTime();
-  }
-
-  /** Returns true if this connection is idle. */
-  boolean isIdle() {
-    return framedConnection == null || framedConnection.isIdle();
-  }
-
-  /**
-   * Returns the time in ns when this connection became idle. Undefined if
-   * this connection is not idle.
-   */
-  long getIdleStartTimeNs() {
-    return framedConnection == null ? idleStartTimeNs : framedConnection.getIdleStartTimeNs();
-  }
-
-  public Handshake getHandshake() {
-    return handshake;
-  }
-
-  /** Returns the transport appropriate for this connection. */
-  Transport newTransport(HttpEngine httpEngine) throws IOException {
-    return (framedConnection != null)
-        ? new FramedTransport(httpEngine, framedConnection)
-        : new HttpTransport(httpEngine, httpConnection);
-  }
-
-  /**
-   * Returns true if this is a SPDY connection. Such connections can be used
-   * in multiple HTTP requests simultaneously.
-   */
-  boolean isFramed() {
-    return framedConnection != null;
-  }
-
-  /**
-   * Returns the protocol negotiated by this connection, or {@link
-   * Protocol#HTTP_1_1} if no protocol has been negotiated.
-   */
-  public Protocol getProtocol() {
-    return protocol;
-  }
-
-  /**
-   * Sets the protocol negotiated by this connection. Typically this is used
-   * when an HTTP/1.1 request is sent and an HTTP/1.0 response is received.
-   */
-  void setProtocol(Protocol protocol) {
-    if (protocol == null) throw new IllegalArgumentException("protocol == null");
-    this.protocol = protocol;
-  }
-
-  void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis)
-      throws RouteException {
-    if (!connected) throw new IllegalStateException("setTimeouts - not connected");
-
-    // Don't set timeouts on shared SPDY connections.
-    if (httpConnection != null) {
-      try {
-        socket.setSoTimeout(readTimeoutMillis);
-      } catch (IOException e) {
-        throw new RouteException(e);
-      }
-      httpConnection.setTimeouts(readTimeoutMillis, writeTimeoutMillis);
-    }
-  }
-
-  void incrementRecycleCount() {
-    recycleCount++;
-  }
-
-  /**
-   * Returns the number of times this connection has been returned to the
-   * connection pool.
-   */
-  int recycleCount() {
-    return recycleCount;
-  }
-
-  @Override public String toString() {
-    return "Connection{"
-        + route.address.uriHost + ":" + route.address.uriPort
-        + ", proxy="
-        + route.proxy
-        + " hostAddress="
-        + route.inetSocketAddress.getAddress().getHostAddress()
-        + " cipherSuite="
-        + (handshake != null ? handshake.cipherSuite() : "none")
-        + " protocol="
-        + protocol
-        + '}';
-  }
+  Protocol getProtocol();
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index da3ac73..6f8efc4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -16,13 +16,17 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
 import com.squareup.okhttp.internal.Util;
-import java.net.SocketException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.lang.ref.Reference;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.Deque;
+import java.util.Iterator;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -31,8 +35,8 @@
 /**
  * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
  * requests that share the same {@link com.squareup.okhttp.Address} may share a
- * {@link com.squareup.okhttp.Connection}. This class implements the policy of
- * which connections to keep open for future use.
+ * {@link Connection}. This class implements the policy of which connections to
+ * keep open for future use.
  *
  * <p>The {@link #getDefault() system-wide default} uses system properties for
  * tuning parameters:
@@ -60,7 +64,8 @@
     String keepAlive = System.getProperty("http.keepAlive");
     String keepAliveDuration = System.getProperty("http.keepAliveDuration");
     String maxIdleConnections = System.getProperty("http.maxConnections");
-    long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
+    long keepAliveDurationMs = keepAliveDuration != null
+        ? Long.parseLong(keepAliveDuration)
         : DEFAULT_KEEP_ALIVE_DURATION_MS;
     if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
       systemDefault = new ConnectionPool(0, keepAliveDurationMs);
@@ -71,43 +76,73 @@
     }
   }
 
-  /** The maximum number of idle connections for each address. */
-  private final int maxIdleConnections;
-  private final long keepAliveDurationNs;
-
-  private final LinkedList<Connection> connections = new LinkedList<>();
-
   /**
    * A background thread is used to cleanup expired connections. There will be, at most, a single
-   * thread running per connection pool.
-   *
-   * <p>A {@link ThreadPoolExecutor} is used and not a
-   * {@link java.util.concurrent.ScheduledThreadPoolExecutor}; ScheduledThreadPoolExecutors do not
-   * shrink. This executor shrinks the thread pool after a period of inactivity, and starts threads
-   * as needed. Delays are instead handled by the {@link #connectionsCleanupRunnable}. It is
-   * important that the {@link #connectionsCleanupRunnable} stops eventually, otherwise it will pin
-   * the thread, and thus the connection pool, in memory.
+   * thread running per connection pool. We use a thread pool executor because it can shrink to
+   * zero threads, permitting this pool to be garbage collected.
    */
-  private Executor executor = new ThreadPoolExecutor(
+  private final Executor executor = new ThreadPoolExecutor(
       0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
       new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
 
-  private final Runnable connectionsCleanupRunnable = new Runnable() {
+  /** The maximum number of idle connections for each address. */
+  private final int maxIdleConnections;
+  private final long keepAliveDurationNs;
+  private Runnable cleanupRunnable = new Runnable() {
     @Override public void run() {
-      runCleanupUntilPoolIsEmpty();
+      while (true) {
+        long waitNanos = cleanup(System.nanoTime());
+        if (waitNanos == -1) return;
+        if (waitNanos > 0) {
+          long waitMillis = waitNanos / 1000000L;
+          waitNanos -= (waitMillis * 1000000L);
+          synchronized (ConnectionPool.this) {
+            try {
+              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
+            } catch (InterruptedException ignored) {
+            }
+          }
+        }
+      }
     }
   };
 
+  private final Deque<RealConnection> connections = new ArrayDeque<>();
+  final RouteDatabase routeDatabase = new RouteDatabase();
+
   public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+    this(maxIdleConnections, keepAliveDurationMs, TimeUnit.MILLISECONDS);
+  }
+
+  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
     this.maxIdleConnections = maxIdleConnections;
-    this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
+
+    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
+    if (keepAliveDuration <= 0) {
+      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
+    }
   }
 
   public static ConnectionPool getDefault() {
     return systemDefault;
   }
 
-  /** Returns total number of connections in the pool. */
+  /** Returns the number of idle connections in the pool. */
+  public synchronized int getIdleConnectionCount() {
+    int total = 0;
+    for (RealConnection connection : connections) {
+      if (connection.allocations.isEmpty()) total++;
+    }
+    return total;
+  }
+
+  /**
+   * Returns total number of connections in the pool. Note that prior to OkHttp 2.7 this included
+   * only idle connections and SPDY connections. In OkHttp 2.7 this includes all connections, both
+   * active and inactive. Use {@link #getIdleConnectionCount()} to count connections not currently
+   * in use.
+   */
   public synchronized int getConnectionCount() {
     return connections.size();
   }
@@ -121,8 +156,8 @@
   /** Returns total number of multiplexed connections in the pool. */
   public synchronized int getMultiplexedConnectionCount() {
     int total = 0;
-    for (Connection connection : connections) {
-      if (connection.isFramed()) total++;
+    for (RealConnection connection : connections) {
+      if (connection.isMultiplexed()) total++;
     }
     return total;
   }
@@ -133,205 +168,156 @@
   }
 
   /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
-  public synchronized Connection get(Address address) {
-    Connection foundConnection = null;
-    for (ListIterator<Connection> i = connections.listIterator(connections.size());
-        i.hasPrevious(); ) {
-      Connection connection = i.previous();
-      if (!connection.getRoute().getAddress().equals(address)
-          || !connection.isAlive()
-          || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
-        continue;
+  RealConnection get(Address address, StreamAllocation streamAllocation) {
+    assert (Thread.holdsLock(this));
+    for (RealConnection connection : connections) {
+      // TODO(jwilson): this is awkward. We're already holding a lock on 'this', and
+      //     connection.allocationLimit() may also lock the FramedConnection.
+      if (connection.allocations.size() < connection.allocationLimit()
+          && address.equals(connection.getRoute().address)
+          && !connection.noNewStreams) {
+        streamAllocation.acquire(connection);
+        return connection;
       }
-      i.remove();
-      if (!connection.isFramed()) {
-        try {
-          Platform.get().tagSocket(connection.getSocket());
-        } catch (SocketException e) {
-          Util.closeQuietly(connection.getSocket());
-          // When unable to tag, skip recycling and close
-          Platform.get().logW("Unable to tagSocket(): " + e);
+    }
+    return null;
+  }
+
+  void put(RealConnection connection) {
+    assert (Thread.holdsLock(this));
+    if (connections.isEmpty()) {
+      executor.execute(cleanupRunnable);
+    }
+    connections.add(connection);
+  }
+
+  /**
+   * Notify this pool that {@code connection} has become idle. Returns true if the connection
+   * has been removed from the pool and should be closed.
+   */
+  boolean connectionBecameIdle(RealConnection connection) {
+    assert (Thread.holdsLock(this));
+    if (connection.noNewStreams || maxIdleConnections == 0) {
+      connections.remove(connection);
+      return true;
+    } else {
+      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
+      return false;
+    }
+  }
+
+  /** Close and remove all idle connections in the pool. */
+  public void evictAll() {
+    List<RealConnection> evictedConnections = new ArrayList<>();
+    synchronized (this) {
+      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+        RealConnection connection = i.next();
+        if (connection.allocations.isEmpty()) {
+          connection.noNewStreams = true;
+          evictedConnections.add(connection);
+          i.remove();
+        }
+      }
+    }
+
+    for (RealConnection connection : evictedConnections) {
+      Util.closeQuietly(connection.getSocket());
+    }
+  }
+
+  /**
+   * Performs maintenance on this pool, evicting the connection that has been idle the longest if
+   * either it has exceeded the keep alive limit or the idle connections limit.
+   *
+   * <p>Returns the duration in nanos to sleep until the next scheduled call to this method.
+   * Returns -1 if no further cleanups are required.
+   */
+  long cleanup(long now) {
+    int inUseConnectionCount = 0;
+    int idleConnectionCount = 0;
+    RealConnection longestIdleConnection = null;
+    long longestIdleDurationNs = Long.MIN_VALUE;
+
+    // Find either a connection to evict, or the time that the next eviction is due.
+    synchronized (this) {
+      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+        RealConnection connection = i.next();
+
+        // If the connection is in use, keep searching.
+        if (pruneAndGetAllocationCount(connection, now) > 0) {
+          inUseConnectionCount++;
           continue;
         }
-      }
-      foundConnection = connection;
-      break;
-    }
 
-    if (foundConnection != null && foundConnection.isFramed()) {
-      connections.addFirst(foundConnection); // Add it back after iteration.
-    }
+        idleConnectionCount++;
 
-    return foundConnection;
-  }
-
-  /**
-   * Gives {@code connection} to the pool. The pool may store the connection,
-   * or close it, as its policy describes.
-   *
-   * <p>It is an error to use {@code connection} after calling this method.
-   */
-  void recycle(Connection connection) {
-    if (connection.isFramed()) {
-      return;
-    }
-
-    if (!connection.clearOwner()) {
-      return; // This connection isn't eligible for reuse.
-    }
-
-    if (!connection.isAlive()) {
-      Util.closeQuietly(connection.getSocket());
-      return;
-    }
-
-    try {
-      Platform.get().untagSocket(connection.getSocket());
-    } catch (SocketException e) {
-      // When unable to remove tagging, skip recycling and close.
-      Platform.get().logW("Unable to untagSocket(): " + e);
-      Util.closeQuietly(connection.getSocket());
-      return;
-    }
-
-    synchronized (this) {
-      addConnection(connection);
-      connection.incrementRecycleCount();
-      connection.resetIdleStartTime();
-    }
-  }
-
-  private void addConnection(Connection connection) {
-    boolean empty = connections.isEmpty();
-    connections.addFirst(connection);
-    if (empty) {
-      executor.execute(connectionsCleanupRunnable);
-    } else {
-      notifyAll();
-    }
-  }
-
-  /**
-   * Shares the SPDY connection with the pool. Callers to this method may
-   * continue to use {@code connection}.
-   */
-  void share(Connection connection) {
-    if (!connection.isFramed()) throw new IllegalArgumentException();
-    if (!connection.isAlive()) return;
-    synchronized (this) {
-      addConnection(connection);
-    }
-  }
-
-  /** Close and remove all connections in the pool. */
-  public void evictAll() {
-    List<Connection> toEvict;
-    synchronized (this) {
-      toEvict = new ArrayList<>(connections);
-      connections.clear();
-      notifyAll();
-    }
-
-    for (int i = 0, size = toEvict.size(); i < size; i++) {
-      Util.closeQuietly(toEvict.get(i).getSocket());
-    }
-  }
-
-  private void runCleanupUntilPoolIsEmpty() {
-    while (true) {
-      if (!performCleanup()) return; // Halt cleanup.
-    }
-  }
-
-  /**
-   * Attempts to make forward progress on connection eviction. There are three possible outcomes:
-   *
-   * <h3>The pool is empty.</h3>
-   * In this case, this method returns false and the eviction job should exit because there are no
-   * further cleanup tasks coming. (If additional connections are added to the pool, another cleanup
-   * job must be enqueued.)
-   *
-   * <h3>Connections were evicted.</h3>
-   * At least one connections was eligible for immediate eviction and was evicted. The method
-   * returns true and cleanup should continue.
-   *
-   * <h3>We waited to evict.</h3>
-   * None of the pooled connections were eligible for immediate eviction. Instead, we waited until
-   * either a connection became eligible for eviction, or the connections list changed. In either
-   * case, the method returns true and cleanup should continue.
-   */
-  // VisibleForTesting
-  boolean performCleanup() {
-    List<Connection> evictableConnections;
-
-    synchronized (this) {
-      if (connections.isEmpty()) return false; // Halt cleanup.
-
-      evictableConnections = new ArrayList<>();
-      int idleConnectionCount = 0;
-      long now = System.nanoTime();
-      long nanosUntilNextEviction = keepAliveDurationNs;
-
-      // Collect connections eligible for immediate eviction.
-      for (ListIterator<Connection> i = connections.listIterator(connections.size());
-          i.hasPrevious(); ) {
-        Connection connection = i.previous();
-        long nanosUntilEviction = connection.getIdleStartTimeNs() + keepAliveDurationNs - now;
-        if (nanosUntilEviction <= 0 || !connection.isAlive()) {
-          i.remove();
-          evictableConnections.add(connection);
-        } else if (connection.isIdle()) {
-          idleConnectionCount++;
-          nanosUntilNextEviction = Math.min(nanosUntilNextEviction, nanosUntilEviction);
+        // If the connection is ready to be evicted, we're done.
+        long idleDurationNs = now - connection.idleAtNanos;
+        if (idleDurationNs > longestIdleDurationNs) {
+          longestIdleDurationNs = idleDurationNs;
+          longestIdleConnection = connection;
         }
       }
 
-      // If the pool has too many idle connections, gather more! Oldest to newest.
-      for (ListIterator<Connection> i = connections.listIterator(connections.size());
-          i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
-        Connection connection = i.previous();
-        if (connection.isIdle()) {
-          evictableConnections.add(connection);
-          i.remove();
-          --idleConnectionCount;
-        }
-      }
+      if (longestIdleDurationNs >= this.keepAliveDurationNs
+          || idleConnectionCount > this.maxIdleConnections) {
+        // We've found a connection to evict. Remove it from the list, then close it below (outside
+        // of the synchronized block).
+        connections.remove(longestIdleConnection);
 
-      // If there's nothing to evict, wait. (This will be interrupted if connections are added.)
-      if (evictableConnections.isEmpty()) {
-        try {
-          long millisUntilNextEviction = nanosUntilNextEviction / (1000 * 1000);
-          long remainderNanos = nanosUntilNextEviction - millisUntilNextEviction * (1000 * 1000);
-          this.wait(millisUntilNextEviction, (int) remainderNanos);
-          return true; // Cleanup continues.
-        } catch (InterruptedException ignored) {
-        }
+      } else if (idleConnectionCount > 0) {
+        // A connection will be ready to evict soon.
+        return keepAliveDurationNs - longestIdleDurationNs;
+
+      } else if (inUseConnectionCount > 0) {
+        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
+        return keepAliveDurationNs;
+
+      } else {
+        // No connections, idle or in use.
+        return -1;
       }
     }
 
-    // Actually do the eviction. Note that we avoid synchronized() when closing sockets.
-    for (int i = 0, size = evictableConnections.size(); i < size; i++) {
-      Connection expiredConnection = evictableConnections.get(i);
-      Util.closeQuietly(expiredConnection.getSocket());
+    Util.closeQuietly(longestIdleConnection.getSocket());
+
+    // Cleanup again immediately.
+    return 0;
+  }
+
+  /**
+   * Prunes any leaked allocations and then returns the number of remaining live allocations on
+   * {@code connection}. Allocations are leaked if the connection is tracking them but the
+   * application code has abandoned them. Leak detection is imprecise and relies on garbage
+   * collection.
+   */
+  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
+    List<Reference<StreamAllocation>> references = connection.allocations;
+    for (int i = 0; i < references.size(); ) {
+      Reference<StreamAllocation> reference = references.get(i);
+
+      if (reference.get() != null) {
+        i++;
+        continue;
+      }
+
+      // We've discovered a leaked allocation. This is an application bug.
+      Internal.logger.warning("A connection to " + connection.getRoute().getAddress().url()
+          + " was leaked. Did you forget to close a response body?");
+      references.remove(i);
+      connection.noNewStreams = true;
+
+      // If this was the last allocation, the connection is eligible for immediate eviction.
+      if (references.isEmpty()) {
+        connection.idleAtNanos = now - keepAliveDurationNs;
+        return 0;
+      }
     }
 
-    return true; // Cleanup continues.
+    return references.size();
   }
 
-  /**
-   * Replace the default {@link Executor} with a different one. Only use in tests.
-   */
-  // VisibleForTesting
-  void replaceCleanupExecutorForTests(Executor cleanupExecutor) {
-    this.executor = cleanupExecutor;
-  }
-
-  /**
-   * Returns a snapshot of the connections in this pool, ordered from newest to
-   * oldest. Only use in tests.
-   */
-  // VisibleForTesting
-  synchronized List<Connection> getConnections() {
-    return new ArrayList<>(connections);
+  void setCleanupRunnableForTest(Runnable cleanupRunnable) {
+    this.cleanupRunnable = cleanupRunnable;
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
index 5e0f7d8..af63afd 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
@@ -20,14 +20,24 @@
 import java.util.List;
 import javax.net.ssl.SSLSocket;
 
+import static com.squareup.okhttp.internal.Util.concat;
+import static com.squareup.okhttp.internal.Util.contains;
+
 /**
  * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
  * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
  * connection.
+ *
+ * <p>The TLS versions configured in a connection spec are only be used if they are also enabled in
+ * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used
+ * even if it is present on the connection spec. The same policy also applies to cipher suites.
+ *
+ * <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to
+ * defer all feature selection to the underlying SSL socket.
  */
 public final class ConnectionSpec {
 
-  // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
+  // This is a subset of the cipher suites supported in Chrome 46, current as of 2015-11-05.
   // All of these suites are available on Android 5.0; earlier releases support a subset of
   // these suites. https://github.com/square/okhttp/issues/330
   private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
@@ -43,7 +53,6 @@
       CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
       CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
       CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
-      CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
       CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
       CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
       CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
@@ -67,19 +76,11 @@
   /** Unencrypted, unauthenticated connections for {@code http:} URLs. */
   public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
 
-  final boolean tls;
-
-  /**
-   * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
-   * default set".
-   */
+  private final boolean tls;
+  private final boolean supportsTlsExtensions;
   private final String[] cipherSuites;
-
-  /** Used if tls == true. The TLS protocol versions to use. */
   private final String[] tlsVersions;
 
-  final boolean supportsTlsExtensions;
-
   private ConnectionSpec(Builder builder) {
     this.tls = builder.tls;
     this.cipherSuites = builder.cipherSuites;
@@ -92,13 +93,12 @@
   }
 
   /**
-   * Returns the cipher suites to use for a connection. This method can return {@code null} if the
-   * cipher suites enabled by default should be used.
+   * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL
+   * socket's enabled cipher suites should be used.
    */
   public List<CipherSuite> cipherSuites() {
-    if (cipherSuites == null) {
-      return null;
-    }
+    if (cipherSuites == null) return null;
+
     CipherSuite[] result = new CipherSuite[cipherSuites.length];
     for (int i = 0; i < cipherSuites.length; i++) {
       result[i] = CipherSuite.forJavaName(cipherSuites[i]);
@@ -106,7 +106,13 @@
     return Util.immutableList(result);
   }
 
+  /**
+   * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of
+   * the SSL socket's enabled TLS versions should be used.
+   */
   public List<TlsVersion> tlsVersions() {
+    if (tlsVersions == null) return null;
+
     TlsVersion[] result = new TlsVersion[tlsVersions.length];
     for (int i = 0; i < tlsVersions.length; i++) {
       result[i] = TlsVersion.forJavaName(tlsVersions[i]);
@@ -122,57 +128,40 @@
   void apply(SSLSocket sslSocket, boolean isFallback) {
     ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
 
-    sslSocket.setEnabledProtocols(specToApply.tlsVersions);
-
-    String[] cipherSuitesToEnable = specToApply.cipherSuites;
-    // null means "use default set".
-    if (cipherSuitesToEnable != null) {
-      sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
+    if (specToApply.tlsVersions != null) {
+      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
+    }
+    if (specToApply.cipherSuites != null) {
+      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
     }
   }
 
   /**
-   * Returns a copy of this that omits cipher suites and TLS versions not enabled by
-   * {@code sslSocket}.
+   * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
+   * sslSocket}.
    */
   private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
-    String[] cipherSuitesToEnable = null;
-    if (cipherSuites != null) {
-      String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
-      cipherSuitesToEnable =
-          Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
+    String[] cipherSuitesIntersection = cipherSuites != null
+        ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
+        : sslSocket.getEnabledCipherSuites();
+    String[] tlsVersionsIntersection = tlsVersions != null
+        ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
+        : sslSocket.getEnabledProtocols();
+
+    // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
+    // the SCSV cipher is added to signal that a protocol fallback has taken place.
+    if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) {
+      cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
     }
 
-    if (isFallback) {
-      // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
-      // the SCSV cipher is added to signal that a protocol fallback has taken place.
-      final String fallbackScsv = "TLS_FALLBACK_SCSV";
-      boolean socketSupportsFallbackScsv =
-          Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv);
-
-      if (socketSupportsFallbackScsv) {
-        // Add the SCSV cipher to the set of enabled cipher suites iff it is supported.
-        String[] oldEnabledCipherSuites = cipherSuitesToEnable != null
-            ? cipherSuitesToEnable
-            : sslSocket.getEnabledCipherSuites();
-        String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1];
-        System.arraycopy(oldEnabledCipherSuites, 0,
-            newEnabledCipherSuites, 0, oldEnabledCipherSuites.length);
-        newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
-        cipherSuitesToEnable = newEnabledCipherSuites;
-      }
-    }
-
-    String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
-    String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
     return new Builder(this)
-        .cipherSuites(cipherSuitesToEnable)
-        .tlsVersions(protocolsToEnable)
+        .cipherSuites(cipherSuitesIntersection)
+        .tlsVersions(tlsVersionsIntersection)
         .build();
   }
 
   /**
-   * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
+   * Returns {@code true} if the socket, as currently configured, supports this connection spec.
    * In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
    *
    * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
@@ -187,20 +176,17 @@
       return false;
     }
 
-    String[] enabledProtocols = socket.getEnabledProtocols();
-    boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
-    if (!requiredProtocolsEnabled) {
+    if (tlsVersions != null
+        && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
       return false;
     }
 
-    boolean requiredCiphersEnabled;
-    if (cipherSuites == null) {
-      requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
-    } else {
-      String[] enabledCipherSuites = socket.getEnabledCipherSuites();
-      requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
+    if (cipherSuites != null
+        && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
+      return false;
     }
-    return requiredCiphersEnabled;
+
+    return true;
   }
 
   /**
@@ -220,15 +206,6 @@
     return false;
   }
 
-  private static <T> boolean contains(T[] array, T value) {
-    for (T arrayValue : array) {
-      if (Util.equal(value, arrayValue)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   @Override public boolean equals(Object other) {
     if (!(other instanceof ConnectionSpec)) return false;
     if (other == this) return true;
@@ -256,16 +233,17 @@
   }
 
   @Override public String toString() {
-    if (tls) {
-      List<CipherSuite> cipherSuites = cipherSuites();
-      String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
-      return "ConnectionSpec(cipherSuites=" + cipherSuitesString
-          + ", tlsVersions=" + tlsVersions()
-          + ", supportsTlsExtensions=" + supportsTlsExtensions
-          + ")";
-    } else {
+    if (!tls) {
       return "ConnectionSpec()";
     }
+
+    String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]";
+    String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]";
+    return "ConnectionSpec("
+        + "cipherSuites=" + cipherSuitesString
+        + ", tlsVersions=" + tlsVersionsString
+        + ", supportsTlsExtensions=" + supportsTlsExtensions
+        + ")";
   }
 
   public static final class Builder {
@@ -285,56 +263,58 @@
       this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
     }
 
+    public Builder allEnabledCipherSuites() {
+      if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
+      this.cipherSuites = null;
+      return this;
+    }
+
     public Builder cipherSuites(CipherSuite... cipherSuites) {
       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
 
-      // Convert enums to the string names Java wants. This makes a defensive copy!
       String[] strings = new String[cipherSuites.length];
       for (int i = 0; i < cipherSuites.length; i++) {
         strings[i] = cipherSuites[i].javaName;
       }
-      this.cipherSuites = strings;
-      return this;
+      return cipherSuites(strings);
     }
 
     public Builder cipherSuites(String... cipherSuites) {
       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
 
-      if (cipherSuites == null) {
-        this.cipherSuites = null;
-      } else {
-        // This makes a defensive copy!
-        this.cipherSuites = cipherSuites.clone();
+      if (cipherSuites.length == 0) {
+        throw new IllegalArgumentException("At least one cipher suite is required");
       }
 
+      this.cipherSuites = cipherSuites.clone(); // Defensive copy.
+      return this;
+    }
+
+    public Builder allEnabledTlsVersions() {
+      if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
+      this.tlsVersions = null;
       return this;
     }
 
     public Builder tlsVersions(TlsVersion... tlsVersions) {
       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
-      if (tlsVersions.length == 0) {
-        throw new IllegalArgumentException("At least one TlsVersion is required");
-      }
 
-      // Convert enums to the string names Java wants. This makes a defensive copy!
       String[] strings = new String[tlsVersions.length];
       for (int i = 0; i < tlsVersions.length; i++) {
         strings[i] = tlsVersions[i].javaName;
       }
-      this.tlsVersions = strings;
-      return this;
+
+      return tlsVersions(strings);
     }
 
     public Builder tlsVersions(String... tlsVersions) {
       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
 
-      if (tlsVersions == null) {
-        this.tlsVersions = null;
-      } else {
-        // This makes a defensive copy!
-        this.tlsVersions = tlsVersions.clone();
+      if (tlsVersions.length == 0) {
+        throw new IllegalArgumentException("At least one TLS version is required");
       }
 
+      this.tlsVersions = tlsVersions.clone(); // Defensive copy.
       return this;
     }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index a934670..a669b94 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -125,7 +125,7 @@
       if (Util.equal(tag, call.tag())) {
         call.get().canceled = true;
         HttpEngine engine = call.get().engine;
-        if (engine != null) engine.disconnect();
+        if (engine != null) engine.cancel();
       }
     }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dns.java b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
new file mode 100644
index 0000000..1ebd392
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A domain name service that resolves IP addresses for host names. Most applications will use the
+ * {@linkplain #SYSTEM system DNS service}, which is the default. Some applications may provide
+ * their own implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4
+ * addresses, or to force a specific known IP address.
+ *
+ * <p>Implementations of this interface must be safe for concurrent use.
+ */
+public interface Dns {
+  /**
+   * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
+   * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
+   */
+  Dns SYSTEM = new Dns() {
+    @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+      if (hostname == null) throw new UnknownHostException("hostname == null");
+      return Arrays.asList(InetAddress.getAllByName(hostname));
+    }
+  };
+
+  /**
+   * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp.
+   * If a connection to an address fails, OkHttp will retry the connection with the next address
+   * until either a connection is made, the set of IP addresses is exhausted, or a limit is
+   * exceeded.
+   */
+  List<InetAddress> lookup(String hostname) throws UnknownHostException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
index 8e75e3f..6fbf5b8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
@@ -402,19 +402,6 @@
   }
 
   /**
-   * Same as {@link #host} except that literal IPv6 addresses are surrounding by square
-   * braces. For example, this method will return {@code [::1]} where {@code host} returns
-   * {@code ::1}.
-   */
-  public String rfc2732host() {
-    if (host.indexOf(':') == -1) {
-      return host;
-    }
-
-    return "[" + host + "]";
-  }
-
-  /**
    * Returns the explicitly-specified port if one was provided, or the default port for this URL's
    * scheme. For example, this returns 8443 for {@code https://square.com:8443/} and 443 for {@code
    * https://square.com/}. The result is in {@code [1..65535]}.
@@ -1640,7 +1627,7 @@
    *
    * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
    * @param strict true to encode '%' if it is not the prefix of a valid percent encoding.
-   * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded
+   * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded.
    * @param asciiOnly true to encode all non-ASCII codepoints.
    */
   static String canonicalize(String input, int pos, int limit, String encodeSet,
@@ -1702,9 +1689,9 @@
     }
   }
 
-  static String canonicalize(String input, String encodeSet, boolean alreadyEncoded, boolean strict,
-      boolean plusIsSpace, boolean asciiOnly) {
-    return canonicalize(
-        input, 0, input.length(), encodeSet, alreadyEncoded, strict, plusIsSpace, asciiOnly);
+  static String canonicalize(String input, String encodeSet, boolean alreadyEncoded,
+      boolean strict, boolean plusIsSpace, boolean asciiOnly) {
+    return canonicalize(input, 0, input.length(),
+            encodeSet, alreadyEncoded, strict, plusIsSpace, asciiOnly);
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 4ed8000..aabc2d2 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -17,15 +17,12 @@
 
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.InternalCache;
-import com.squareup.okhttp.internal.Network;
 import com.squareup.okhttp.internal.RouteDatabase;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
 import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
 import java.net.CookieHandler;
 import java.net.MalformedURLException;
 import java.net.Proxy;
@@ -41,8 +38,6 @@
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
 
 /**
  * Configures and creates HTTP connections. Most applications can use a single
@@ -64,35 +59,6 @@
 
   static {
     Internal.instance = new Internal() {
-      @Override public Transport newTransport(
-          Connection connection, HttpEngine httpEngine) throws IOException {
-        return connection.newTransport(httpEngine);
-      }
-
-      @Override public boolean clearOwner(Connection connection) {
-        return connection.clearOwner();
-      }
-
-      @Override public void closeIfOwnedBy(Connection connection, Object owner) throws IOException {
-        connection.closeIfOwnedBy(owner);
-      }
-
-      @Override public int recycleCount(Connection connection) {
-        return connection.recycleCount();
-      }
-
-      @Override public void setProtocol(Connection connection, Protocol protocol) {
-        connection.setProtocol(protocol);
-      }
-
-      @Override public void setOwner(Connection connection, HttpEngine httpEngine) {
-        connection.setOwner(httpEngine);
-      }
-
-      @Override public boolean isReadable(Connection pooled) {
-        return pooled.isReadable();
-      }
-
       @Override public void addLenient(Headers.Builder builder, String line) {
         builder.addLenient(line);
       }
@@ -109,25 +75,22 @@
         return client.internalCache();
       }
 
-      @Override public void recycle(ConnectionPool pool, Connection connection) {
-        pool.recycle(connection);
+      @Override public boolean connectionBecameIdle(
+          ConnectionPool pool, RealConnection connection) {
+        return pool.connectionBecameIdle(connection);
       }
 
-      @Override public RouteDatabase routeDatabase(OkHttpClient client) {
-        return client.routeDatabase();
+      @Override public RealConnection get(
+          ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
+        return pool.get(address, streamAllocation);
       }
 
-      @Override public Network network(OkHttpClient client) {
-        return client.network;
+      @Override public void put(ConnectionPool pool, RealConnection connection) {
+        pool.put(connection);
       }
 
-      @Override public void setNetwork(OkHttpClient client, Network network) {
-        client.network = network;
-      }
-
-      @Override public void connectAndSetOwner(OkHttpClient client, Connection connection,
-          HttpEngine owner, Request request) throws RouteException {
-        connection.connectAndSetOwner(client, owner, request);
+      @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
+        return connectionPool.routeDatabase;
       }
 
       @Override
@@ -135,24 +98,8 @@
         call.enqueue(responseCallback, forWebSocket);
       }
 
-      @Override public void callEngineReleaseConnection(Call call) throws IOException {
-        call.engine.releaseConnection();
-      }
-
-      @Override public Connection callEngineGetConnection(Call call) {
-        return call.engine.getConnection();
-      }
-
-      @Override public BufferedSource connectionRawSource(Connection connection) {
-        return connection.rawSource();
-      }
-
-      @Override public BufferedSink connectionRawSink(Connection connection) {
-        return connection.rawSink();
-      }
-
-      @Override public void connectionSetOwner(Connection connection, Object owner) {
-        connection.setOwner(owner);
+      @Override public StreamAllocation callEngineGetStreamAllocation(Call call) {
+        return call.engine.streamAllocation;
       }
 
       @Override
@@ -190,7 +137,7 @@
   private CertificatePinner certificatePinner;
   private Authenticator authenticator;
   private ConnectionPool connectionPool;
-  private Network network;
+  private Dns dns;
   private boolean followSslRedirects = true;
   private boolean followRedirects = true;
   private boolean retryOnConnectionFailure = true;
@@ -221,7 +168,7 @@
     this.certificatePinner = okHttpClient.certificatePinner;
     this.authenticator = okHttpClient.authenticator;
     this.connectionPool = okHttpClient.connectionPool;
-    this.network = okHttpClient.network;
+    this.dns = okHttpClient.dns;
     this.followSslRedirects = okHttpClient.followSslRedirects;
     this.followRedirects = okHttpClient.followRedirects;
     this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
@@ -358,6 +305,20 @@
   }
 
   /**
+   * Sets the DNS service used to lookup IP addresses for hostnames.
+   *
+   * <p>If unset, the {@link Dns#SYSTEM system-wide default} DNS will be used.
+   */
+  public OkHttpClient setDns(Dns dns) {
+    this.dns = dns;
+    return this;
+  }
+
+  public Dns getDns() {
+    return dns;
+  }
+
+  /**
    * Sets the socket factory used to create connections. OkHttp only uses
    * the parameterless {@link SocketFactory#createSocket() createSocket()}
    * method to create unconnected sockets. Overriding this method,
@@ -647,8 +608,8 @@
     if (result.connectionSpecs == null) {
       result.connectionSpecs = DEFAULT_CONNECTION_SPECS;
     }
-    if (result.network == null) {
-      result.network = Network.DEFAULT;
+    if (result.dns == null) {
+      result.dns = Dns.SYSTEM;
     }
     return result;
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 2417c13..e099267 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -189,6 +189,9 @@
     /**
      * Adds a header with {@code name} and {@code value}. Prefer this method for
      * multiply-valued headers like "Cookie".
+     *
+     * <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
+     * OkHttp may replace {@code value} with a header derived from the request body.
      */
     public Builder addHeader(String name, String value) {
       headers.add(name, value);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
index bfa95c4..512aa0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
@@ -30,7 +30,7 @@
 
   final String javaName;
 
-  private TlsVersion(String javaName) {
+  TlsVersion(String javaName) {
     this.javaName = javaName;
   }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
index 21bcbf5..03bc1c5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
@@ -15,26 +15,20 @@
  */
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Call;
 import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.ConnectionSpec;
 import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import java.io.IOException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
 import java.net.MalformedURLException;
 import java.net.UnknownHostException;
 import java.util.logging.Logger;
 import javax.net.ssl.SSLSocket;
-import okio.BufferedSink;
-import okio.BufferedSource;
 
 /**
  * Escalate internal APIs in {@code com.squareup.okhttp} so they can be used
@@ -51,21 +45,6 @@
 
   public static Internal instance;
 
-  public abstract Transport newTransport(Connection connection, HttpEngine httpEngine)
-      throws IOException;
-
-  public abstract boolean clearOwner(Connection connection);
-
-  public abstract void closeIfOwnedBy(Connection connection, Object owner) throws IOException;
-
-  public abstract int recycleCount(Connection connection);
-
-  public abstract void setProtocol(Connection connection, Protocol protocol);
-
-  public abstract void setOwner(Connection connection, HttpEngine httpEngine);
-
-  public abstract boolean isReadable(Connection pooled);
-
   public abstract void addLenient(Headers.Builder builder, String line);
 
   public abstract void addLenient(Headers.Builder builder, String name, String value);
@@ -74,16 +53,14 @@
 
   public abstract InternalCache internalCache(OkHttpClient client);
 
-  public abstract void recycle(ConnectionPool pool, Connection connection);
+  public abstract RealConnection get(
+      ConnectionPool pool, Address address, StreamAllocation streamAllocation);
 
-  public abstract RouteDatabase routeDatabase(OkHttpClient client);
+  public abstract void put(ConnectionPool pool, RealConnection connection);
 
-  public abstract Network network(OkHttpClient client);
+  public abstract boolean connectionBecameIdle(ConnectionPool pool, RealConnection connection);
 
-  public abstract void setNetwork(OkHttpClient client, Network network);
-
-  public abstract void connectAndSetOwner(OkHttpClient client, Connection connection,
-      HttpEngine owner, Request request) throws RouteException;
+  public abstract RouteDatabase routeDatabase(ConnectionPool connectionPool);
 
   public abstract void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket,
       boolean isFallback);
@@ -93,9 +70,5 @@
 
   // TODO delete the following when web sockets move into the main package.
   public abstract void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket);
-  public abstract void callEngineReleaseConnection(Call call) throws IOException;
-  public abstract Connection callEngineGetConnection(Call call);
-  public abstract BufferedSource connectionRawSource(Connection connection);
-  public abstract BufferedSink connectionRawSink(Connection connection);
-  public abstract void connectionSetOwner(Connection connection, Object owner);
+  public abstract StreamAllocation callEngineGetStreamAllocation(Call call);
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
deleted file mode 100644
index a007065..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2012 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;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * Services specific to the host device's network interface. Prefer this over {@link
- * InetAddress#getAllByName} to make code more testable.
- */
-public interface Network {
-  Network DEFAULT = new Network() {
-    @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
-      if (host == null) throw new UnknownHostException("host == null");
-      return InetAddress.getAllByName(host);
-    }
-  };
-
-  InetAddress[] resolveInetAddresses(String host) throws UnknownHostException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
index b906495..a2df181 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,8 +16,13 @@
  */
 package com.squareup.okhttp.internal;
 
+import android.util.Log;
 import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.tls.AndroidTrustRootIndex;
+import com.squareup.okhttp.internal.tls.RealTrustRootIndex;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -29,6 +34,8 @@
 import java.util.List;
 import java.util.logging.Level;
 import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
 import okio.Buffer;
 
 import static com.squareup.okhttp.internal.Internal.logger;
@@ -50,6 +57,11 @@
  * unstable.
  *
  * Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
+ *
+ * <h3>Trust Manager Extraction</h3>
+ *
+ * <p>Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust
+ * manager that was used to create an {@link SSLSocketFactory}.
  */
 public class Platform {
   private static final Platform PLATFORM = findPlatform();
@@ -73,6 +85,14 @@
   public void untagSocket(Socket socket) throws SocketException {
   }
 
+  public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+    return null;
+  }
+
+  public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+    return new RealTrustRootIndex(trustManager.getAcceptedIssuers());
+  }
+
   /**
    * Configure TLS extensions on {@code sslSocket} for {@code route}.
    *
@@ -100,15 +120,21 @@
     socket.connect(address, connectTimeout);
   }
 
+  public void log(String message) {
+    System.out.println(message);
+  }
+
   /** Attempt to match the host runtime to a capable Platform implementation. */
   private static Platform findPlatform() {
     // Attempt to find Android 2.3+ APIs.
     try {
+      Class<?> sslParametersClass;
       try {
-        Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
+        sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
       } catch (ClassNotFoundException e) {
         // Older platform before being unbundled.
-        Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+        sslParametersClass = Class.forName(
+            "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
       }
 
       OptionalMethod<Socket> setUseSessionTickets
@@ -136,25 +162,34 @@
       } catch (ClassNotFoundException | NoSuchMethodException ignored) {
       }
 
-      return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket,
-          trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols);
+      return new Android(sslParametersClass, setUseSessionTickets, setHostname,
+          trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol,
+          setAlpnProtocols);
     } catch (ClassNotFoundException ignored) {
       // This isn't an Android runtime.
     }
 
-    // Find Jetty's ALPN extension for OpenJDK.
+    // Find an Oracle JDK.
     try {
-      String negoClassName = "org.eclipse.jetty.alpn.ALPN";
-      Class<?> negoClass = Class.forName(negoClassName);
-      Class<?> providerClass = Class.forName(negoClassName + "$Provider");
-      Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
-      Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
-      Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
-      Method getMethod = negoClass.getMethod("get", SSLSocket.class);
-      Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
-      return new JdkWithJettyBootPlatform(
-          putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
-    } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+      Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl");
+
+      // Find Jetty's ALPN extension for OpenJDK.
+      try {
+        String negoClassName = "org.eclipse.jetty.alpn.ALPN";
+        Class<?> negoClass = Class.forName(negoClassName);
+        Class<?> providerClass = Class.forName(negoClassName + "$Provider");
+        Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
+        Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
+        Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
+        Method getMethod = negoClass.getMethod("get", SSLSocket.class);
+        Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
+        return new JdkWithJettyBootPlatform(sslContextClass,
+            putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
+      } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+      }
+
+      return new JdkPlatform(sslContextClass);
+    } catch (ClassNotFoundException ignored) {
     }
 
     return new Platform();
@@ -162,6 +197,9 @@
 
   /** Android 2.3 or better. */
   private static class Android extends Platform {
+    private static final int MAX_LOG_LENGTH = 4000;
+
+    private final Class<?> sslParametersClass;
     private final OptionalMethod<Socket> setUseSessionTickets;
     private final OptionalMethod<Socket> setHostname;
 
@@ -173,9 +211,11 @@
     private final OptionalMethod<Socket> getAlpnSelectedProtocol;
     private final OptionalMethod<Socket> setAlpnProtocols;
 
-    public Android(OptionalMethod<Socket> setUseSessionTickets, OptionalMethod<Socket> setHostname,
-        Method trafficStatsTagSocket, Method trafficStatsUntagSocket,
-        OptionalMethod<Socket> getAlpnSelectedProtocol, OptionalMethod<Socket> setAlpnProtocols) {
+    public Android(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
+        OptionalMethod<Socket> setHostname, Method trafficStatsTagSocket,
+        Method trafficStatsUntagSocket, OptionalMethod<Socket> getAlpnSelectedProtocol,
+        OptionalMethod<Socket> setAlpnProtocols) {
+      this.sslParametersClass = sslParametersClass;
       this.setUseSessionTickets = setUseSessionTickets;
       this.setHostname = setHostname;
       this.trafficStatsTagSocket = trafficStatsTagSocket;
@@ -188,15 +228,46 @@
         int connectTimeout) throws IOException {
       try {
         socket.connect(address, connectTimeout);
-      } catch (SecurityException se) {
+      } catch (AssertionError e) {
+        if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+        throw e;
+      } catch (SecurityException e) {
         // Before android 4.3, socket.connect could throw a SecurityException
         // if opening a socket resulted in an EACCES error.
         IOException ioException = new IOException("Exception in connect");
-        ioException.initCause(se);
+        ioException.initCause(e);
         throw ioException;
       }
     }
 
+    @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+      Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters");
+      if (context == null) {
+        // If that didn't work, try the Google Play Services SSL provider before giving up. This
+        // must be loaded by the SSLSocketFactory's class loader.
+        try {
+          Class<?> gmsSslParametersClass = Class.forName(
+              "com.google.android.gms.org.conscrypt.SSLParametersImpl", false,
+              sslSocketFactory.getClass().getClassLoader());
+          context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters");
+        } catch (ClassNotFoundException e) {
+          return null;
+        }
+      }
+
+      X509TrustManager x509TrustManager = readFieldOrNull(
+          context, X509TrustManager.class, "x509TrustManager");
+      if (x509TrustManager != null) return x509TrustManager;
+
+      return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+    }
+
+    @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+      TrustRootIndex result = AndroidTrustRootIndex.get(trustManager);
+      if (result != null) return result;
+      return super.trustRootIndex(trustManager);
+    }
+
     @Override public void configureTlsExtensions(
         SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
       // Enable SNI and session tickets.
@@ -243,20 +314,49 @@
         throw new RuntimeException(e.getCause());
       }
     }
+
+    @Override public void log(String message) {
+      // Split by line, then ensure each line can fit into Log's maximum length.
+      for (int i = 0, length = message.length(); i < length; i++) {
+        int newline = message.indexOf('\n', i);
+        newline = newline != -1 ? newline : length;
+        do {
+          int end = Math.min(newline, i + MAX_LOG_LENGTH);
+          Log.d("OkHttp", message.substring(i, end));
+          i = end;
+        } while (i < newline);
+      }
+    }
+  }
+
+  /** JDK 1.7 or better. */
+  private static class JdkPlatform extends Platform {
+    private final Class<?> sslContextClass;
+
+    public JdkPlatform(Class<?> sslContextClass) {
+      this.sslContextClass = sslContextClass;
+    }
+
+    @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+      Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context");
+      if (context == null) return null;
+      return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+    }
   }
 
   /**
    * OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
    */
-  private static class JdkWithJettyBootPlatform extends Platform {
+  private static class JdkWithJettyBootPlatform extends JdkPlatform {
     private final Method putMethod;
     private final Method getMethod;
     private final Method removeMethod;
     private final Class<?> clientProviderClass;
     private final Class<?> serverProviderClass;
 
-    public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod,
-        Class<?> clientProviderClass, Class<?> serverProviderClass) {
+    public JdkWithJettyBootPlatform(Class<?> sslContextClass, Method putMethod, Method getMethod,
+        Method removeMethod, Class<?> clientProviderClass, Class<?> serverProviderClass) {
+      super(sslContextClass);
       this.putMethod = putMethod;
       this.getMethod = getMethod;
       this.removeMethod = removeMethod;
@@ -368,4 +468,27 @@
     }
     return result.readByteArray();
   }
+
+  static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
+    for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
+      try {
+        Field field = c.getDeclaredField(fieldName);
+        field.setAccessible(true);
+        Object value = field.get(instance);
+        if (value == null || !fieldType.isInstance(value)) return null;
+        return fieldType.cast(value);
+      } catch (NoSuchFieldException ignored) {
+      } catch (IllegalAccessException e) {
+        throw new AssertionError();
+      }
+    }
+
+    // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate.
+    if (!fieldName.equals("delegate")) {
+      Object delegate = readFieldOrNull(instance, Object.class, "delegate");
+      if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName);
+    }
+
+    return null;
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
index 4000fbe..b05dc6d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -259,8 +259,8 @@
   public static String hostHeader(HttpUrl url) {
     // TODO: square braces for IPv6 ?
     return url.port() != HttpUrl.defaultPort(url.scheme())
-        ? url.rfc2732host() + ":" + url.port()
-        : url.rfc2732host();
+        ? url.host() + ":" + url.port()
+        : url.host();
   }
 
   /** Returns {@code s} with control characters and non-ASCII characters replaced with '?'. */
@@ -288,4 +288,15 @@
     return e.getCause() != null && e.getMessage() != null
         && e.getMessage().contains("getsockname failed");
   }
+
+  public static boolean contains(String[] array, String value) {
+    return Arrays.asList(array).contains(value);
+  }
+
+  public static String[] concat(String[] array, String value) {
+    String[] result = new String[array.length + 1];
+    System.arraycopy(array, 0, result, 0, array.length);
+    result[result.length - 1] = value;
+    return result;
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
index a86924b..6feb16c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
@@ -35,6 +35,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import okio.Buffer;
+import okio.BufferedSink;
 import okio.BufferedSource;
 import okio.ByteString;
 import okio.Okio;
@@ -76,10 +77,10 @@
   final boolean client;
 
   /**
-   * User code to run in response to an incoming stream. Callbacks must not be
-   * run on the callback executor.
+   * User code to run in response to incoming streams or settings. Calls to this are always invoked
+   * on {@link #executor}.
    */
-  private final IncomingStreamHandler handler;
+  private final Listener listener;
   private final Map<Integer, FramedStream> streams = new HashMap<>();
   private final String hostName;
   private int lastGoodStreamId;
@@ -111,9 +112,8 @@
   long bytesLeftInWriteWindow;
 
   /** Settings we communicate to the peer. */
-  // TODO: Do we want to dynamically adjust settings, or KISS and only set once?
-  final Settings okHttpSettings = new Settings();
-      // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max);
+  Settings okHttpSettings = new Settings();
+
   private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024;
 
   /** Settings we receive from the peer. */
@@ -132,7 +132,7 @@
     protocol = builder.protocol;
     pushObserver = builder.pushObserver;
     client = builder.client;
-    handler = builder.handler;
+    listener = builder.listener;
     // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-5.1.1
     nextStreamId = builder.client ? 1 : 2;
     if (builder.client && protocol == Protocol.HTTP_2) {
@@ -168,9 +168,9 @@
     }
     bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
     socket = builder.socket;
-    frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client);
+    frameWriter = variant.newWriter(builder.sink, client);
 
-    readerRunnable = new Reader();
+    readerRunnable = new Reader(variant.newReader(builder.source, client));
     new Thread(readerRunnable).start(); // Not a daemon thread.
   }
 
@@ -209,6 +209,10 @@
     return idleStartTimeNs != Long.MAX_VALUE;
   }
 
+  public synchronized int maxConcurrentStreams() {
+    return peerSettings.getMaxConcurrentStreams(Integer.MAX_VALUE);
+  }
+
   /**
    * Returns the time in ns when this connection became idle or Long.MAX_VALUE
    * if connection is not idle.
@@ -515,30 +519,53 @@
     }
   }
 
+  /** Merges {@code settings} into this peer's settings and sends them to the remote peer. */
+  public void setSettings(Settings settings) throws IOException {
+    synchronized (frameWriter) {
+      synchronized (this) {
+        if (shutdown) {
+          throw new IOException("shutdown");
+        }
+        okHttpSettings.merge(settings);
+        frameWriter.settings(settings);
+      }
+    }
+  }
+
   public static class Builder {
-    private String hostName;
     private Socket socket;
-    private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+    private String hostName;
+    private BufferedSource source;
+    private BufferedSink sink;
+    private Listener listener = Listener.REFUSE_INCOMING_STREAMS;
     private Protocol protocol = Protocol.SPDY_3;
     private PushObserver pushObserver = PushObserver.CANCEL;
     private boolean client;
 
-    public Builder(boolean client, Socket socket) throws IOException {
-      this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket);
-    }
-
     /**
      * @param client true if this peer initiated the connection; false if this
      *     peer accepted the connection.
      */
-    public Builder(String hostName, boolean client, Socket socket) throws IOException {
-      this.hostName = hostName;
+    public Builder(boolean client) throws IOException {
       this.client = client;
-      this.socket = socket;
     }
 
-    public Builder handler(IncomingStreamHandler handler) {
-      this.handler = handler;
+    public Builder socket(Socket socket) throws IOException {
+      return socket(socket, ((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(),
+          Okio.buffer(Okio.source(socket)), Okio.buffer(Okio.sink(socket)));
+    }
+
+    public Builder socket(
+        Socket socket, String hostName, BufferedSource source, BufferedSink sink) {
+      this.socket = socket;
+      this.hostName = hostName;
+      this.source = source;
+      this.sink = sink;
+      return this;
+    }
+
+    public Builder listener(Listener listener) {
+      this.listener = listener;
       return this;
     }
 
@@ -562,17 +589,17 @@
    * write a frame, create an async task to do so.
    */
   class Reader extends NamedRunnable implements FrameReader.Handler {
-    FrameReader frameReader;
+    final FrameReader frameReader;
 
-    private Reader() {
+    private Reader(FrameReader frameReader) {
       super("OkHttp %s", hostName);
+      this.frameReader = frameReader;
     }
 
     @Override protected void execute() {
       ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
       ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
       try {
-        frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client);
         if (!client) {
           frameReader.readConnectionPreface();
         }
@@ -645,9 +672,9 @@
           executor.execute(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
             @Override public void execute() {
               try {
-                handler.receive(newStream);
+                listener.onStream(newStream);
               } catch (IOException e) {
-                logger.log(Level.INFO, "StreamHandler failure for " + hostName, e);
+                logger.log(Level.INFO, "FramedConnection.Listener failure for " + hostName, e);
                 try {
                   newStream.close(ErrorCode.PROTOCOL_ERROR);
                 } catch (IOException ignored) {
@@ -703,6 +730,11 @@
             streamsToNotify = streams.values().toArray(new FramedStream[streams.size()]);
           }
         }
+        executor.execute(new NamedRunnable("OkHttp %s settings", hostName) {
+          @Override public void execute() {
+            listener.onSettings(FramedConnection.this);
+          }
+        });
       }
       if (streamsToNotify != null && delta != 0) {
         for (FramedStream stream : streamsToNotify) {
@@ -878,4 +910,34 @@
       }
     });
   }
+
+  /** Listener of streams and settings initiated by the peer. */
+  public abstract static class Listener {
+    public static final Listener REFUSE_INCOMING_STREAMS = new Listener() {
+      @Override public void onStream(FramedStream stream) throws IOException {
+        stream.close(ErrorCode.REFUSED_STREAM);
+      }
+    };
+
+    /**
+     * Handle a new stream from this connection's peer. Implementations should
+     * respond by either {@linkplain FramedStream#reply replying to the stream}
+     * or {@linkplain FramedStream#close closing it}. This response does not
+     * need to be synchronous.
+     */
+    public abstract void onStream(FramedStream stream) throws IOException;
+
+    /**
+     * Notification that the connection's peer's settings may have changed.
+     * Implementations should take appropriate action to handle the updated
+     * settings.
+     *
+     * <p>It is the implementation's responsibility to handle concurrent calls
+     * to this method. A remote peer that sends multiple settings frames will
+     * trigger multiple calls to this method, and those calls are not
+     * necessarily serialized.
+     */
+    public void onSettings(FramedConnection connection) {
+    }
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
deleted file mode 100644
index 57863df..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * 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.framed;
-
-import java.io.IOException;
-
-/** Listener to be notified when a connected peer creates a new stream. */
-public interface IncomingStreamHandler {
-  IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
-    @Override public void receive(FramedStream stream) throws IOException {
-      stream.close(ErrorCode.REFUSED_STREAM);
-    }
-  };
-
-  /**
-   * Handle a new stream from this connection's peer. Implementations should
-   * respond by either {@link FramedStream#reply replying to the stream} or
-   * {@link FramedStream#close closing it}. This response does not need to be
-   * synchronous.
-   */
-  void receive(FramedStream stream) throws IOException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
index a9f9b5a..8d88410 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
@@ -43,7 +43,7 @@
       if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
 
       PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
-          url.rfc2732host(), getConnectToInetAddress(proxy, url), url.port(), url.scheme(),
+          url.host(), getConnectToInetAddress(proxy, url), url.port(), url.scheme(),
           challenge.getRealm(), challenge.getScheme(), url.url(), RequestorType.SERVER);
       if (auth == null) continue;
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
deleted file mode 100644
index abeaf86..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * 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.Headers;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.framed.ErrorCode;
-import com.squareup.okhttp.internal.framed.FramedConnection;
-import com.squareup.okhttp.internal.framed.FramedStream;
-import com.squareup.okhttp.internal.framed.Header;
-import java.io.IOException;
-import java.net.ProtocolException;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import okio.ByteString;
-import okio.Okio;
-import okio.Sink;
-
-import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_HOST;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_METHOD;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
-import static com.squareup.okhttp.internal.framed.Header.VERSION;
-
-public final class FramedTransport implements Transport {
-  /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
-  private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
-      ByteString.encodeUtf8("connection"),
-      ByteString.encodeUtf8("host"),
-      ByteString.encodeUtf8("keep-alive"),
-      ByteString.encodeUtf8("proxy-connection"),
-      ByteString.encodeUtf8("transfer-encoding"));
-
-  /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
-  private static final List<ByteString> HTTP_2_PROHIBITED_HEADERS = Util.immutableList(
-      ByteString.encodeUtf8("connection"),
-      ByteString.encodeUtf8("host"),
-      ByteString.encodeUtf8("keep-alive"),
-      ByteString.encodeUtf8("proxy-connection"),
-      ByteString.encodeUtf8("te"),
-      ByteString.encodeUtf8("transfer-encoding"),
-      ByteString.encodeUtf8("encoding"),
-      ByteString.encodeUtf8("upgrade"));
-
-  private final HttpEngine httpEngine;
-  private final FramedConnection framedConnection;
-  private FramedStream stream;
-
-  public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
-    this.httpEngine = httpEngine;
-    this.framedConnection = framedConnection;
-  }
-
-  @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
-    return stream.getSink();
-  }
-
-  @Override public void writeRequestHeaders(Request request) throws IOException {
-    if (stream != null) return;
-
-    httpEngine.writingRequestHeaders();
-    boolean permitsRequestBody = httpEngine.permitsRequestBody();
-    boolean hasResponseBody = true;
-    String version = RequestLine.version(httpEngine.getConnection().getProtocol());
-    stream = framedConnection.newStream(
-        writeNameValueBlock(request, framedConnection.getProtocol(), version), permitsRequestBody,
-        hasResponseBody);
-    stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
-  }
-
-  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
-    requestBody.writeToSocket(stream.getSink());
-  }
-
-  @Override public void finishRequest() throws IOException {
-    stream.getSink().close();
-  }
-
-  @Override public Response.Builder readResponseHeaders() throws IOException {
-    return readNameValueBlock(stream.getResponseHeaders(), framedConnection.getProtocol());
-  }
-
-  /**
-   * Returns a list of alternating names and values containing a SPDY request.
-   * Names are all lowercase. No names are repeated. If any name has multiple
-   * values, they are concatenated using "\0" as a delimiter.
-   */
-  public static List<Header> writeNameValueBlock(Request request, Protocol protocol,
-      String version) {
-    Headers headers = request.headers();
-    List<Header> result = new ArrayList<>(headers.size() + 10);
-    result.add(new Header(TARGET_METHOD, request.method()));
-    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
-    String host = Util.hostHeader(request.httpUrl());
-    if (Protocol.SPDY_3 == protocol) {
-      result.add(new Header(VERSION, version));
-      result.add(new Header(TARGET_HOST, host));
-    } else if (Protocol.HTTP_2 == protocol) {
-      result.add(new Header(TARGET_AUTHORITY, host)); // Optional in HTTP/2
-    } else {
-      throw new AssertionError();
-    }
-    result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
-
-    Set<ByteString> names = new LinkedHashSet<ByteString>();
-    for (int i = 0, size = headers.size(); i < size; i++) {
-      // header names must be lowercase.
-      ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
-      String value = headers.value(i);
-
-      // Drop headers that are forbidden when layering HTTP over SPDY.
-      if (isProhibitedHeader(protocol, name)) continue;
-
-      // They shouldn't be set, but if they are, drop them. We've already written them!
-      if (name.equals(TARGET_METHOD)
-          || name.equals(TARGET_PATH)
-          || name.equals(TARGET_SCHEME)
-          || name.equals(TARGET_AUTHORITY)
-          || name.equals(TARGET_HOST)
-          || name.equals(VERSION)) {
-        continue;
-      }
-
-      // If we haven't seen this name before, add the pair to the end of the list...
-      if (names.add(name)) {
-        result.add(new Header(name, value));
-        continue;
-      }
-
-      // ...otherwise concatenate the existing values and this value.
-      for (int j = 0; j < result.size(); j++) {
-        if (result.get(j).name.equals(name)) {
-          String concatenated = joinOnNull(result.get(j).value.utf8(), value);
-          result.set(j, new Header(name, concatenated));
-          break;
-        }
-      }
-    }
-    return result;
-  }
-
-  private static String joinOnNull(String first, String second) {
-    return new StringBuilder(first).append('\0').append(second).toString();
-  }
-
-  /** Returns headers for a name value block containing a SPDY response. */
-  public static Response.Builder readNameValueBlock(List<Header> headerBlock,
-      Protocol protocol) throws IOException {
-    String status = null;
-    String version = "HTTP/1.1"; // :version present only in spdy/3.
-
-    Headers.Builder headersBuilder = new Headers.Builder();
-    headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.toString());
-    for (int i = 0, size = headerBlock.size(); i < size; i++) {
-      ByteString name = headerBlock.get(i).name;
-      String values = headerBlock.get(i).value.utf8();
-      for (int start = 0; start < values.length(); ) {
-        int end = values.indexOf('\0', start);
-        if (end == -1) {
-          end = values.length();
-        }
-        String value = values.substring(start, end);
-        if (name.equals(RESPONSE_STATUS)) {
-          status = value;
-        } else if (name.equals(VERSION)) {
-          version = value;
-        } else if (!isProhibitedHeader(protocol, name)) { // Don't write forbidden headers!
-          headersBuilder.add(name.utf8(), value);
-        }
-        start = end + 1;
-      }
-    }
-    if (status == null) throw new ProtocolException("Expected ':status' header not present");
-
-    StatusLine statusLine = StatusLine.parse(version + " " + status);
-    return new Response.Builder()
-        .protocol(protocol)
-        .code(statusLine.code)
-        .message(statusLine.message)
-        .headers(headersBuilder.build());
-  }
-
-  @Override public ResponseBody openResponseBody(Response response) throws IOException {
-    return new RealResponseBody(response.headers(), Okio.buffer(stream.getSource()));
-  }
-
-  @Override public void releaseConnectionOnIdle() {
-  }
-
-  @Override public void disconnect(HttpEngine engine) throws IOException {
-    if (stream != null) stream.close(ErrorCode.CANCEL);
-  }
-
-  @Override public boolean canReuseConnection() {
-    return true; // TODO: framedConnection.isClosed() ?
-  }
-
-  /** When true, this header should not be emitted or consumed. */
-  private static boolean isProhibitedHeader(Protocol protocol, ByteString name) {
-    if (protocol == Protocol.SPDY_3) {
-      return SPDY_3_PROHIBITED_HEADERS.contains(name);
-    } else if (protocol == Protocol.HTTP_2) {
-      return HTTP_2_PROHIBITED_HEADERS.contains(name);
-    } else {
-      throw new AssertionError(protocol);
-    }
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
similarity index 74%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
index 1bbde80..b43f0d3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
@@ -16,17 +16,16 @@
 
 package com.squareup.okhttp.internal.http;
 
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
 import java.io.EOFException;
 import java.io.IOException;
 import java.net.ProtocolException;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.BufferedSource;
@@ -38,7 +37,6 @@
 
 import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
 import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
-import static com.squareup.okhttp.internal.http.Transport.DISCARD_STREAM_TIMEOUT_MILLIS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 /**
@@ -60,7 +58,7 @@
  * #newFixedLengthSource(long) newFixedLengthSource(0)} and may skip reading and
  * closing that source.
  */
-public final class HttpConnection {
+public final class Http1xStream implements HttpStream {
   private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
   private static final int STATE_OPEN_REQUEST_BODY = 1;
   private static final int STATE_WRITING_REQUEST_BODY = 2;
@@ -69,63 +67,89 @@
   private static final int STATE_READING_RESPONSE_BODY = 5;
   private static final int STATE_CLOSED = 6;
 
-  private static final int ON_IDLE_HOLD = 0;
-  private static final int ON_IDLE_POOL = 1;
-  private static final int ON_IDLE_CLOSE = 2;
-
-  private final ConnectionPool pool;
-  private final Connection connection;
-  private final Socket socket;
+  /** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
+  private final StreamAllocation streamAllocation;
   private final BufferedSource source;
   private final BufferedSink sink;
-
+  private HttpEngine httpEngine;
   private int state = STATE_IDLE;
-  private int onIdle = ON_IDLE_HOLD;
 
-  public HttpConnection(ConnectionPool pool, Connection connection, Socket socket)
-      throws IOException {
-    this.pool = pool;
-    this.connection = connection;
-    this.socket = socket;
-    this.source = Okio.buffer(Okio.source(socket));
-    this.sink = Okio.buffer(Okio.sink(socket));
+  public Http1xStream(StreamAllocation streamAllocation, BufferedSource source, BufferedSink sink) {
+    this.streamAllocation = streamAllocation;
+    this.source = source;
+    this.sink = sink;
   }
 
-  public void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) {
-    if (readTimeoutMillis != 0) {
-      source.timeout().timeout(readTimeoutMillis, MILLISECONDS);
+  @Override public void setHttpEngine(HttpEngine httpEngine) {
+    this.httpEngine = httpEngine;
+  }
+
+  @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
+    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
+      // Stream a request body of unknown length.
+      return newChunkedSink();
     }
-    if (writeTimeoutMillis != 0) {
-      sink.timeout().timeout(writeTimeoutMillis, MILLISECONDS);
+
+    if (contentLength != -1) {
+      // Stream a request body of a known length.
+      return newFixedLengthSink(contentLength);
     }
+
+    throw new IllegalStateException(
+        "Cannot stream a request body without chunked encoding or a known content length!");
+  }
+
+  @Override public void cancel() {
+    RealConnection connection = streamAllocation.connection();
+    if (connection != null) connection.cancel();
   }
 
   /**
-   * Configure this connection to put itself back into the connection pool when
-   * the HTTP response body is exhausted.
+   * Prepares the HTTP headers and sends them to the server.
+   *
+   * <p>For streaming requests with a body, headers must be prepared
+   * <strong>before</strong> the output stream has been written to. Otherwise
+   * the body would need to be buffered!
+   *
+   * <p>For non-streaming requests with a body, headers must be prepared
+   * <strong>after</strong> the output stream has been written to and closed.
+   * This ensures that the {@code Content-Length} header field receives the
+   * proper value.
    */
-  public void poolOnIdle() {
-    onIdle = ON_IDLE_POOL;
-
-    // If we're already idle, go to the pool immediately.
-    if (state == STATE_IDLE) {
-      onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
-      Internal.instance.recycle(pool, connection);
-    }
+  @Override public void writeRequestHeaders(Request request) throws IOException {
+    httpEngine.writingRequestHeaders();
+    String requestLine = RequestLine.get(
+        request, httpEngine.getConnection().getRoute().getProxy().type());
+    writeRequest(request.headers(), requestLine);
   }
 
-  /**
-   * Configure this connection to close itself when the HTTP response body is
-   * exhausted.
-   */
-  public void closeOnIdle() throws IOException {
-    onIdle = ON_IDLE_CLOSE;
+  @Override public Response.Builder readResponseHeaders() throws IOException {
+    return readResponse();
+  }
 
-    // If we're already idle, close immediately.
-    if (state == STATE_IDLE) {
-      state = STATE_CLOSED;
-      connection.getSocket().close();
+  @Override public ResponseBody openResponseBody(Response response) throws IOException {
+    Source source = getTransferStream(response);
+    return new RealResponseBody(response.headers(), Okio.buffer(source));
+  }
+
+  private Source getTransferStream(Response response) throws IOException {
+    if (!HttpEngine.hasBody(response)) {
+      return newFixedLengthSource(0);
     }
+
+    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
+      return newChunkedSource(httpEngine);
+    }
+
+    long contentLength = OkHeaders.contentLength(response);
+    if (contentLength != -1) {
+      return newFixedLengthSource(contentLength);
+    }
+
+    // Wrap the input stream from the connection (rather than just returning
+    // "socketIn" directly here), so that we can control its use after the
+    // reference escapes.
+    return newUnknownLengthSource();
   }
 
   /** Returns true if this connection is closed. */
@@ -133,39 +157,10 @@
     return state == STATE_CLOSED;
   }
 
-  public void closeIfOwnedBy(Object owner) throws IOException {
-    Internal.instance.closeIfOwnedBy(connection, owner);
-  }
-
-  public void flush() throws IOException {
+  @Override public void finishRequest() throws IOException {
     sink.flush();
   }
 
-  /** Returns the number of buffered bytes immediately readable. */
-  public long bufferSize() {
-    return source.buffer().size();
-  }
-
-  /** Test for a stale socket. */
-  public boolean isReadable() {
-    try {
-      int readTimeout = socket.getSoTimeout();
-      try {
-        socket.setSoTimeout(1);
-        if (source.exhausted()) {
-          return false; // Stream is exhausted; socket is closed.
-        }
-        return true;
-      } finally {
-        socket.setSoTimeout(readTimeout);
-      }
-    } catch (SocketTimeoutException ignored) {
-      return true; // Read timed out; socket is good.
-    } catch (IOException e) {
-      return false; // Couldn't read; socket is closed.
-    }
-  }
-
   /** Returns bytes of a request header for sending on an HTTP transport. */
   public void writeRequest(Headers headers, String requestLine) throws IOException {
     if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
@@ -193,12 +188,8 @@
         Response.Builder responseBuilder = new Response.Builder()
             .protocol(statusLine.protocol)
             .code(statusLine.code)
-            .message(statusLine.message);
-
-        Headers.Builder headersBuilder = new Headers.Builder();
-        readHeaders(headersBuilder);
-        headersBuilder.add(OkHeaders.SELECTED_PROTOCOL, statusLine.protocol.toString());
-        responseBuilder.headers(headersBuilder.build());
+            .message(statusLine.message)
+            .headers(readHeaders());
 
         if (statusLine.code != HTTP_CONTINUE) {
           state = STATE_OPEN_RESPONSE_BODY;
@@ -207,19 +198,20 @@
       }
     } catch (EOFException e) {
       // Provide more context if the server ends the stream before sending a response.
-      IOException exception = new IOException("unexpected end of stream on " + connection
-          + " (recycle count=" + Internal.instance.recycleCount(connection) + ")");
+      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
       exception.initCause(e);
       throw exception;
     }
   }
 
-  /** Reads headers or trailers into {@code builder}. */
-  public void readHeaders(Headers.Builder builder) throws IOException {
+  /** Reads headers or trailers. */
+  public Headers readHeaders() throws IOException {
+    Headers.Builder headers = new Headers.Builder();
     // parse the result headers until the first blank line
     for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
-      Internal.instance.addLenient(builder, line);
+      Internal.instance.addLenient(headers, line);
     }
+    return headers.build();
   }
 
   public Sink newChunkedSink() {
@@ -234,7 +226,7 @@
     return new FixedLengthSink(contentLength);
   }
 
-  public void writeRequestBody(RetryableSink requestBody) throws IOException {
+  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
     if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
     state = STATE_READ_RESPONSE_HEADERS;
     requestBody.writeToSocket(sink);
@@ -254,18 +246,12 @@
 
   public Source newUnknownLengthSource() throws IOException {
     if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+    if (streamAllocation == null) throw new IllegalStateException("streamAllocation == null");
     state = STATE_READING_RESPONSE_BODY;
+    streamAllocation.noNewStreams();
     return new UnknownLengthSource();
   }
 
-  public BufferedSink rawSink() {
-    return sink;
-  }
-
-  public BufferedSource rawSource() {
-    return source;
-  }
-
   /**
    * Sets the delegate of {@code timeout} to {@link Timeout#NONE} and resets its underlying timeout
    * to the default configuration. Use this to avoid unexpected sharing of timeouts between pooled
@@ -366,36 +352,25 @@
      * Closes the cache entry and makes the socket available for reuse. This
      * should be invoked when the end of the body has been reached.
      */
-    protected final void endOfInput(boolean recyclable) throws IOException {
+    protected final void endOfInput() throws IOException {
       if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
 
       detachTimeout(timeout);
 
-      state = STATE_IDLE;
-      if (recyclable && onIdle == ON_IDLE_POOL) {
-        onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
-        Internal.instance.recycle(pool, connection);
-      } else if (onIdle == ON_IDLE_CLOSE) {
-        state = STATE_CLOSED;
-        connection.getSocket().close();
+      state = STATE_CLOSED;
+      if (streamAllocation != null) {
+        streamAllocation.streamFinished(Http1xStream.this);
       }
     }
 
-    /**
-     * Calls abort on the cache entry and disconnects the socket. This
-     * should be invoked when the connection is closed unexpectedly to
-     * invalidate the cache entry and to prevent the HTTP connection from
-     * being reused. HTTP messages are sent in serial so whenever a message
-     * cannot be read to completion, subsequent messages cannot be read
-     * either and the connection must be discarded.
-     *
-     * <p>An earlier implementation skipped the remaining bytes, but this
-     * requires that the entire transfer be completed. If the intention was
-     * to cancel the transfer, closing the connection is the only solution.
-     */
     protected final void unexpectedEndOfInput() {
-      Util.closeQuietly(connection.getSocket());
+      if (state == STATE_CLOSED) return;
+
       state = STATE_CLOSED;
+      if (streamAllocation != null) {
+        streamAllocation.noNewStreams();
+        streamAllocation.streamFinished(Http1xStream.this);
+      }
     }
   }
 
@@ -406,7 +381,7 @@
     public FixedLengthSource(long length) throws IOException {
       bytesRemaining = length;
       if (bytesRemaining == 0) {
-        endOfInput(true);
+        endOfInput();
       }
     }
 
@@ -423,7 +398,7 @@
 
       bytesRemaining -= read;
       if (bytesRemaining == 0) {
-        endOfInput(true);
+        endOfInput();
       }
       return read;
     }
@@ -487,10 +462,8 @@
       }
       if (bytesRemainingInChunk == 0L) {
         hasMoreChunks = false;
-        Headers.Builder trailersBuilder = new Headers.Builder();
-        readHeaders(trailersBuilder);
-        httpEngine.receiveHeaders(trailersBuilder.build());
-        endOfInput(true);
+        httpEngine.receiveHeaders(readHeaders());
+        endOfInput();
       }
     }
 
@@ -516,7 +489,7 @@
       long read = source.read(sink, byteCount);
       if (read == -1) {
         inputExhausted = true;
-        endOfInput(false);
+        endOfInput();
         return -1;
       }
       return read;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
new file mode 100644
index 0000000..6b8b68f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.Headers;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.framed.ErrorCode;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.framed.FramedStream;
+import com.squareup.okhttp.internal.framed.Header;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import okio.ByteString;
+import okio.ForwardingSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_HOST;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_METHOD;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
+import static com.squareup.okhttp.internal.framed.Header.VERSION;
+
+/** An HTTP stream for HTTP/2 and SPDY. */
+public final class Http2xStream implements HttpStream {
+  private static final ByteString CONNECTION = ByteString.encodeUtf8("connection");
+  private static final ByteString HOST = ByteString.encodeUtf8("host");
+  private static final ByteString KEEP_ALIVE = ByteString.encodeUtf8("keep-alive");
+  private static final ByteString PROXY_CONNECTION = ByteString.encodeUtf8("proxy-connection");
+  private static final ByteString TRANSFER_ENCODING = ByteString.encodeUtf8("transfer-encoding");
+  private static final ByteString TE = ByteString.encodeUtf8("te");
+  private static final ByteString ENCODING = ByteString.encodeUtf8("encoding");
+  private static final ByteString UPGRADE = ByteString.encodeUtf8("upgrade");
+
+  /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
+  private static final List<ByteString> SPDY_3_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+      CONNECTION,
+      HOST,
+      KEEP_ALIVE,
+      PROXY_CONNECTION,
+      TRANSFER_ENCODING,
+      TARGET_METHOD,
+      TARGET_PATH,
+      TARGET_SCHEME,
+      TARGET_AUTHORITY,
+      TARGET_HOST,
+      VERSION);
+  private static final List<ByteString> SPDY_3_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+      CONNECTION,
+      HOST,
+      KEEP_ALIVE,
+      PROXY_CONNECTION,
+      TRANSFER_ENCODING);
+
+  /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
+  private static final List<ByteString> HTTP_2_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+      CONNECTION,
+      HOST,
+      KEEP_ALIVE,
+      PROXY_CONNECTION,
+      TE,
+      TRANSFER_ENCODING,
+      ENCODING,
+      UPGRADE,
+      TARGET_METHOD,
+      TARGET_PATH,
+      TARGET_SCHEME,
+      TARGET_AUTHORITY,
+      TARGET_HOST,
+      VERSION);
+  private static final List<ByteString> HTTP_2_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+      CONNECTION,
+      HOST,
+      KEEP_ALIVE,
+      PROXY_CONNECTION,
+      TE,
+      TRANSFER_ENCODING,
+      ENCODING,
+      UPGRADE);
+
+  private final StreamAllocation streamAllocation;
+  private final FramedConnection framedConnection;
+  private HttpEngine httpEngine;
+  private FramedStream stream;
+
+  public Http2xStream(StreamAllocation streamAllocation, FramedConnection framedConnection) {
+    this.streamAllocation = streamAllocation;
+    this.framedConnection = framedConnection;
+  }
+
+  @Override public void setHttpEngine(HttpEngine httpEngine) {
+    this.httpEngine = httpEngine;
+  }
+
+  @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
+    return stream.getSink();
+  }
+
+  @Override public void writeRequestHeaders(Request request) throws IOException {
+    if (stream != null) return;
+
+    httpEngine.writingRequestHeaders();
+    boolean permitsRequestBody = httpEngine.permitsRequestBody(request);
+    List<Header> requestHeaders = framedConnection.getProtocol() == Protocol.HTTP_2
+        ? http2HeadersList(request)
+        : spdy3HeadersList(request);
+    boolean hasResponseBody = true;
+    stream = framedConnection.newStream(requestHeaders, permitsRequestBody, hasResponseBody);
+    stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
+    stream.writeTimeout().timeout(httpEngine.client.getWriteTimeout(), TimeUnit.MILLISECONDS);
+  }
+
+  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
+    requestBody.writeToSocket(stream.getSink());
+  }
+
+  @Override public void finishRequest() throws IOException {
+    stream.getSink().close();
+  }
+
+  @Override public Response.Builder readResponseHeaders() throws IOException {
+    return framedConnection.getProtocol() == Protocol.HTTP_2
+        ? readHttp2HeadersList(stream.getResponseHeaders())
+        : readSpdy3HeadersList(stream.getResponseHeaders());
+  }
+
+  /**
+   * Returns a list of alternating names and values containing a SPDY request.
+   * Names are all lowercase. No names are repeated. If any name has multiple
+   * values, they are concatenated using "\0" as a delimiter.
+   */
+  public static List<Header> spdy3HeadersList(Request request) {
+    Headers headers = request.headers();
+    List<Header> result = new ArrayList<>(headers.size() + 5);
+    result.add(new Header(TARGET_METHOD, request.method()));
+    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+    result.add(new Header(VERSION, "HTTP/1.1"));
+    result.add(new Header(TARGET_HOST, Util.hostHeader(request.httpUrl())));
+    result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
+
+    Set<ByteString> names = new LinkedHashSet<>();
+    for (int i = 0, size = headers.size(); i < size; i++) {
+      // header names must be lowercase.
+      ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
+
+      // Drop headers that are forbidden when layering HTTP over SPDY.
+      if (SPDY_3_SKIPPED_REQUEST_HEADERS.contains(name)) continue;
+
+      // If we haven't seen this name before, add the pair to the end of the list...
+      String value = headers.value(i);
+      if (names.add(name)) {
+        result.add(new Header(name, value));
+        continue;
+      }
+
+      // ...otherwise concatenate the existing values and this value.
+      for (int j = 0; j < result.size(); j++) {
+        if (result.get(j).name.equals(name)) {
+          String concatenated = joinOnNull(result.get(j).value.utf8(), value);
+          result.set(j, new Header(name, concatenated));
+          break;
+        }
+      }
+    }
+    return result;
+  }
+
+  private static String joinOnNull(String first, String second) {
+    return new StringBuilder(first).append('\0').append(second).toString();
+  }
+
+  public static List<Header> http2HeadersList(Request request) {
+    Headers headers = request.headers();
+    List<Header> result = new ArrayList<>(headers.size() + 4);
+    result.add(new Header(TARGET_METHOD, request.method()));
+    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+    result.add(new Header(TARGET_AUTHORITY, Util.hostHeader(request.httpUrl()))); // Optional.
+    result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
+
+    for (int i = 0, size = headers.size(); i < size; i++) {
+      // header names must be lowercase.
+      ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
+      if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)) {
+        result.add(new Header(name, headers.value(i)));
+      }
+    }
+    return result;
+  }
+
+  /** Returns headers for a name value block containing a SPDY response. */
+  public static Response.Builder readSpdy3HeadersList(List<Header> headerBlock) throws IOException {
+    String status = null;
+    String version = "HTTP/1.1";
+    Headers.Builder headersBuilder = new Headers.Builder();
+    for (int i = 0, size = headerBlock.size(); i < size; i++) {
+      ByteString name = headerBlock.get(i).name;
+
+      String values = headerBlock.get(i).value.utf8();
+      for (int start = 0; start < values.length(); ) {
+        int end = values.indexOf('\0', start);
+        if (end == -1) {
+          end = values.length();
+        }
+        String value = values.substring(start, end);
+        if (name.equals(RESPONSE_STATUS)) {
+          status = value;
+        } else if (name.equals(VERSION)) {
+          version = value;
+        } else if (!SPDY_3_SKIPPED_RESPONSE_HEADERS.contains(name)) {
+          headersBuilder.add(name.utf8(), value);
+        }
+        start = end + 1;
+      }
+    }
+    if (status == null) throw new ProtocolException("Expected ':status' header not present");
+
+    StatusLine statusLine = StatusLine.parse(version + " " + status);
+    return new Response.Builder()
+        .protocol(Protocol.SPDY_3)
+        .code(statusLine.code)
+        .message(statusLine.message)
+        .headers(headersBuilder.build());
+  }
+
+  /** Returns headers for a name value block containing an HTTP/2 response. */
+  public static Response.Builder readHttp2HeadersList(List<Header> headerBlock) throws IOException {
+    String status = null;
+
+    Headers.Builder headersBuilder = new Headers.Builder();
+    for (int i = 0, size = headerBlock.size(); i < size; i++) {
+      ByteString name = headerBlock.get(i).name;
+
+      String value = headerBlock.get(i).value.utf8();
+      if (name.equals(RESPONSE_STATUS)) {
+        status = value;
+      } else if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
+        headersBuilder.add(name.utf8(), value);
+      }
+    }
+    if (status == null) throw new ProtocolException("Expected ':status' header not present");
+
+    StatusLine statusLine = StatusLine.parse("HTTP/1.1 " + status);
+    return new Response.Builder()
+        .protocol(Protocol.HTTP_2)
+        .code(statusLine.code)
+        .message(statusLine.message)
+        .headers(headersBuilder.build());
+  }
+
+  @Override public ResponseBody openResponseBody(Response response) throws IOException {
+    Source source = new StreamFinishingSource(stream.getSource());
+    return new RealResponseBody(response.headers(), Okio.buffer(source));
+  }
+
+  @Override public void cancel() {
+    if (stream != null) stream.closeLater(ErrorCode.CANCEL);
+  }
+
+  class StreamFinishingSource extends ForwardingSource {
+    public StreamFinishingSource(Source delegate) {
+      super(delegate);
+    }
+
+    @Override public void close() throws IOException {
+      streamAllocation.streamFinished(Http2xStream.this);
+      super.close();
+    }
+  }
+}
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 7286428..b80f7d5 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
@@ -20,7 +20,6 @@
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.CertificatePinner;
 import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Interceptor;
@@ -36,18 +35,13 @@
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.Version;
 import java.io.IOException;
-import java.io.InterruptedIOException;
 import java.net.CookieHandler;
 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 java.util.Map;
 import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocketFactory;
 import okio.Buffer;
 import okio.BufferedSink;
@@ -111,13 +105,9 @@
 
   final OkHttpClient client;
 
-  private Connection connection;
-  private Address address;
-  private RouteSelector routeSelector;
-  private Route route;
+  public final StreamAllocation streamAllocation;
   private final Response priorResponse;
-
-  private Transport transport;
+  private HttpStream httpStream;
 
   /** The time when the request headers were written, or -1 if they haven't been written yet. */
   long sentRequestMillis = -1;
@@ -178,30 +168,20 @@
    * @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.
-   * @param connection the connection used for an intermediate response immediately prior to this
-   *     request/response pair, such as a same-host redirect. This engine assumes ownership of the
-   *     connection and must release it when it is unneeded.
-   * @param routeSelector the route selector used for a failed attempt immediately preceding this
    */
   public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
-      boolean callerWritesRequestBody, boolean forWebSocket, Connection connection,
-      RouteSelector routeSelector, RetryableSink requestBodyOut, Response priorResponse) {
+      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.connection = connection;
-    this.routeSelector = routeSelector;
+    this.streamAllocation = streamAllocation != null
+        ? streamAllocation
+        : new StreamAllocation(client.getConnectionPool(), createAddress(client, request));
     this.requestBodyOut = requestBodyOut;
     this.priorResponse = priorResponse;
-
-    if (connection != null) {
-      Internal.instance.setOwner(connection, this);
-      this.route = connection.getRoute();
-    } else {
-      this.route = null;
-    }
   }
 
   /**
@@ -218,7 +198,7 @@
    */
   public void sendRequest() throws RequestException, RouteException, IOException {
     if (cacheStrategy != null) return; // Already sent.
-    if (transport != null) throw new IllegalStateException();
+    if (httpStream != null) throw new IllegalStateException();
 
     Request request = networkRequest(userRequest);
 
@@ -241,18 +221,14 @@
     }
 
     if (networkRequest != null) {
-      // Open a connection unless we inherited one from a redirect.
-      if (connection == null) {
-        connect();
-      }
-
-      transport = Internal.instance.newTransport(connection, this);
+      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() && requestBodyOut == null) {
+      if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) {
         long contentLength = OkHeaders.contentLength(request);
         if (bufferRequestBody) {
           if (contentLength > Integer.MAX_VALUE) {
@@ -262,7 +238,7 @@
 
           if (contentLength != -1) {
             // Buffer a request body of a known length.
-            transport.writeRequestHeaders(networkRequest);
+            httpStream.writeRequestHeaders(networkRequest);
             requestBodyOut = new RetryableSink((int) contentLength);
           } else {
             // Buffer a request body of an unknown length. Don't write request
@@ -271,18 +247,12 @@
             requestBodyOut = new RetryableSink();
           }
         } else {
-          transport.writeRequestHeaders(networkRequest);
-          requestBodyOut = transport.createRequestBody(networkRequest, contentLength);
+          httpStream.writeRequestHeaders(networkRequest);
+          requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
         }
       }
 
     } else {
-      // We aren't using the network. Recycle a connection we may have inherited from a redirect.
-      if (connection != null) {
-        Internal.instance.recycle(client.getConnectionPool(), connection);
-        connection = null;
-      }
-
       if (cacheResponse != null) {
         // We have a valid cached response. Promote it to the user response immediately.
         this.userResponse = cacheResponse.newBuilder()
@@ -306,48 +276,19 @@
     }
   }
 
+  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;
   }
 
-  /** Connect to the origin server either directly or via a proxy. */
-  private void connect() throws RequestException, RouteException {
-    if (connection != null) throw new IllegalStateException();
-
-    if (routeSelector == null) {
-      address = createAddress(client, networkRequest);
-      try {
-        routeSelector = RouteSelector.get(address, networkRequest, client);
-      } catch (IOException e) {
-        throw new RequestException(e);
-      }
-    }
-
-    connection = createNextConnection();
-    Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
-    route = connection.getRoute();
-  }
-
-  private Connection createNextConnection() throws RouteException {
-    ConnectionPool pool = client.getConnectionPool();
-
-    // Always prefer pooled connections over new connections.
-    for (Connection pooled; (pooled = pool.get(address)) != null; ) {
-      if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
-        return pooled;
-      }
-      closeQuietly(pooled.getSocket());
-    }
-
-    try {
-      Route route = routeSelector.next();
-      return new Connection(pool, route);
-    } catch (IOException e) {
-      throw new RouteException(e);
-    }
-  }
 
   /**
    * Called immediately before the transport transmits HTTP request headers.
@@ -358,8 +299,8 @@
     sentRequestMillis = System.currentTimeMillis();
   }
 
-  boolean permitsRequestBody() {
-    return HttpMethod.permitsRequestBody(userRequest.method());
+  boolean permitsRequestBody(Request request) {
+    return HttpMethod.permitsRequestBody(request.method());
   }
 
   /** Returns the request body or null if this request doesn't have a body. */
@@ -393,7 +334,7 @@
   }
 
   public Connection getConnection() {
-    return connection;
+    return streamAllocation.connection();
   }
 
   /**
@@ -402,64 +343,19 @@
    * there are no more routes to try.
    */
   public HttpEngine recover(RouteException e) {
-    if (routeSelector != null && connection != null) {
-      connectFailed(routeSelector, e.getLastConnectException());
-    }
-
-    if (routeSelector == null && connection == null // No connection.
-        || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
-        || !isRecoverable(e)) {
+    if (!streamAllocation.recover(e)) {
       return null;
     }
 
-    Connection connection = close();
+    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, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
-  }
-
-  private boolean isRecoverable(RouteException e) {
-    // If the application has opted-out of recovery, don't recover.
-    if (!client.getRetryOnConnectionFailure()) {
-      return false;
-    }
-
-    // Problems with a route may mean the connection can be retried with a new route, or may
-    // indicate a client-side or server-side issue that should not be retried. To tell, we must look
-    // at the cause.
-
-    IOException ioe = e.getLastConnectException();
-
-    // If there was a protocol problem, don't recover.
-    if (ioe instanceof ProtocolException) {
-      return false;
-    }
-
-    // If there was an interruption don't recover, but if there was a timeout
-    // we should try the next route (if there is one).
-    if (ioe instanceof InterruptedIOException) {
-      return ioe instanceof SocketTimeoutException;
-    }
-
-    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
-    // again with a different route.
-    if (ioe instanceof SSLHandshakeException) {
-      // If the problem was a CertificateException from the X509TrustManager,
-      // do not retry.
-      if (ioe.getCause() instanceof CertificateException) {
-        return false;
-      }
-    }
-    if (ioe 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;
+        forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
   }
 
   /**
@@ -469,63 +365,25 @@
    * body is buffered.
    */
   public HttpEngine recover(IOException e, Sink requestBodyOut) {
-    if (routeSelector != null && connection != null) {
-      connectFailed(routeSelector, e);
-    }
-
-    boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
-    if (routeSelector == null && connection == null // No connection.
-        || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
-        || !isRecoverable(e)
-        || !canRetryRequestBody) {
+    if (!streamAllocation.recover(e, requestBodyOut)) {
       return null;
     }
 
-    Connection connection = close();
+    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, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
-  }
-
-  private void connectFailed(RouteSelector routeSelector, IOException e) {
-    // If this is a recycled connection, don't count its failure against the route.
-    if (Internal.instance.recycleCount(connection) > 0) return;
-    Route failedRoute = connection.getRoute();
-    routeSelector.connectFailed(failedRoute, e);
+        forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
   }
 
   public HttpEngine recover(IOException e) {
     return recover(e, requestBodyOut);
   }
 
-  private boolean isRecoverable(IOException e) {
-    // If the application has opted-out of recovery, don't recover.
-    if (!client.getRetryOnConnectionFailure()) {
-      return false;
-    }
-
-    // If there was a protocol problem, don't recover.
-    if (e instanceof ProtocolException) {
-      return false;
-    }
-
-    // If there was an interruption or timeout, don't recover.
-    if (e instanceof InterruptedIOException) {
-      return false;
-    }
-
-    return true;
-  }
-
-  /**
-   * Returns the route used to retrieve the response. Null if we haven't
-   * connected yet, or if no connection was necessary.
-   */
-  public Route getRoute() {
-    return route;
-  }
-
   private void maybeCache() throws IOException {
     InternalCache responseCache = Internal.instance.internalCache(client);
     if (responseCache == null) return;
@@ -551,11 +409,8 @@
    * either exhausted or closed. If it is unneeded when this is called, it will
    * be released immediately.
    */
-  public void releaseConnection() throws IOException {
-    if (transport != null && connection != null) {
-      transport.releaseConnectionOnIdle();
-    }
-    connection = null;
+  public void releaseStreamAllocation() throws IOException {
+    streamAllocation.release();
   }
 
   /**
@@ -567,25 +422,15 @@
    * 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 disconnect() {
-    try {
-      if (transport != null) {
-        transport.disconnect(this);
-      } else {
-        Connection connection = this.connection;
-        if (connection != null) {
-          Internal.instance.closeIfOwnedBy(connection, this);
-        }
-      }
-    } catch (IOException ignored) {
-    }
+  public void cancel() {
+    streamAllocation.cancel();
   }
 
   /**
-   * Release any resources held by this engine. If a connection is still held by
-   * this engine, it is returned.
+   * Release any resources held by this engine. Returns the stream allocation held by this engine,
+   * which itself must be used or released.
    */
-  public Connection close() {
+  public StreamAllocation close() {
     if (bufferedRequestBody != null) {
       // This also closes the wrapped requestBodyOut.
       closeQuietly(bufferedRequestBody);
@@ -593,31 +438,14 @@
       closeQuietly(requestBodyOut);
     }
 
-    // If this engine never achieved a response body, its connection cannot be reused.
-    if (userResponse == null) {
-      if (connection != null) closeQuietly(connection.getSocket()); // TODO: does this break SPDY?
-      connection = null;
-      return null;
+    if (userResponse != null) {
+      closeQuietly(userResponse.body());
+    } else {
+      // If this engine never achieved a response body, its stream allocation is dead.
+      streamAllocation.connectionFailed();
     }
 
-    // Close the response body. This will recycle the connection if it is eligible.
-    closeQuietly(userResponse.body());
-
-    // Close the connection if it cannot be reused.
-    if (transport != null && connection != null && !transport.canReuseConnection()) {
-      closeQuietly(connection.getSocket());
-      connection = null;
-      return null;
-    }
-
-    // Prevent this engine from disconnecting a connection it no longer owns.
-    if (connection != null && !Internal.instance.clearOwner(connection)) {
-      connection = null;
-    }
-
-    Connection result = connection;
-    connection = null;
-    return result;
+    return streamAllocation;
   }
 
   /**
@@ -694,8 +522,7 @@
       result.header("Host", Util.hostHeader(request.httpUrl()));
     }
 
-    if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0)
-        && request.header("Connection") == null) {
+    if (request.header("Connection") == null) {
       result.header("Connection", "Keep-Alive");
     }
 
@@ -742,7 +569,7 @@
     Response networkResponse;
 
     if (forWebSocket) {
-      transport.writeRequestHeaders(networkRequest);
+      httpStream.writeRequestHeaders(networkRequest);
       networkResponse = readNetworkResponse();
 
     } else if (!callerWritesRequestBody) {
@@ -763,7 +590,7 @@
               .header("Content-Length", Long.toString(contentLength))
               .build();
         }
-        transport.writeRequestHeaders(networkRequest);
+        httpStream.writeRequestHeaders(networkRequest);
       }
 
       // Write the request body to the socket.
@@ -775,7 +602,7 @@
           requestBodyOut.close();
         }
         if (requestBodyOut instanceof RetryableSink) {
-          transport.writeRequestBody((RetryableSink) requestBodyOut);
+          httpStream.writeRequestBody((RetryableSink) requestBodyOut);
         }
       }
 
@@ -795,7 +622,7 @@
             .networkResponse(stripBody(networkResponse))
             .build();
         networkResponse.body().close();
-        releaseConnection();
+        releaseStreamAllocation();
 
         // Update the cache after combining headers but before stripping the
         // Content-Encoding header (as performed by initContentStream()).
@@ -833,7 +660,7 @@
     }
 
     @Override public Connection connection() {
-      return connection;
+      return streamAllocation.connection();
     }
 
     @Override public Request request() {
@@ -848,7 +675,7 @@
         Address address = connection().getRoute().getAddress();
 
         // Confirm that the interceptor uses the connection we've already prepared.
-        if (!request.httpUrl().rfc2732host().equals(address.getRfc2732Host())
+        if (!request.httpUrl().host().equals(address.getUriHost())
             || request.httpUrl().port() != address.getUriPort()) {
           throw new IllegalStateException("network interceptor " + caller
               + " must retain the same host and port");
@@ -872,17 +699,21 @@
           throw new IllegalStateException("network interceptor " + interceptor
               + " must call proceed() exactly once");
         }
+        if (interceptedResponse == null) {
+          throw new NullPointerException("network interceptor " + interceptor
+              + " returned null");
+        }
 
         return interceptedResponse;
       }
 
-      transport.writeRequestHeaders(request);
+      httpStream.writeRequestHeaders(request);
 
       //Update the networkRequest with the possibly updated interceptor request.
       networkRequest = request;
 
-      if (permitsRequestBody() && request.body() != null) {
-        Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
+      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();
@@ -901,22 +732,26 @@
   }
 
   private Response readNetworkResponse() throws IOException {
-    transport.finishRequest();
+    httpStream.finishRequest();
 
-    Response networkResponse = transport.readResponseHeaders()
+    Response networkResponse = httpStream.readResponseHeaders()
         .request(networkRequest)
-        .handshake(connection.getHandshake())
+        .handshake(streamAllocation.connection().getHandshake())
         .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
         .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
         .build();
 
     if (!forWebSocket) {
       networkResponse = networkResponse.newBuilder()
-          .body(transport.openResponseBody(networkResponse))
+          .body(httpStream.openResponseBody(networkResponse))
           .build();
     }
 
-    Internal.instance.setProtocol(connection, networkResponse.protocol());
+    if ("close".equalsIgnoreCase(networkResponse.request().header("Connection"))
+        || "close".equalsIgnoreCase(networkResponse.header("Connection"))) {
+      streamAllocation.noNewStreams();
+    }
+
     return networkResponse;
   }
 
@@ -969,7 +804,7 @@
 
       @Override public void close() throws IOException {
         if (!cacheRequestClosed
-            && !Util.discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
+            && !Util.discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
           cacheRequestClosed = true;
           cacheRequest.abort();
         }
@@ -1051,11 +886,16 @@
    */
   public Request followUpRequest() throws IOException {
     if (userResponse == null) throw new IllegalStateException();
-    Proxy selectedProxy = getRoute() != null
-        ? getRoute().getProxy()
+    Connection connection = streamAllocation.connection();
+    Route route = connection != null
+        ? connection.getRoute()
+        : null;
+    Proxy selectedProxy = route != null
+        ? route.getProxy()
         : client.getProxy();
     int responseCode = userResponse.code();
 
+    final String method = userRequest.method();
     switch (responseCode) {
       case HTTP_PROXY_AUTH:
         if (selectedProxy.type() != Proxy.Type.HTTP) {
@@ -1069,7 +909,7 @@
       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 (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) {
+        if (!method.equals("GET") && !method.equals("HEAD")) {
             return null;
         }
         // fall-through
@@ -1093,8 +933,12 @@
 
         // Redirects don't include a request body.
         Request.Builder requestBuilder = userRequest.newBuilder();
-        if (HttpMethod.permitsRequestBody(userRequest.method())) {
-          requestBuilder.method("GET", null);
+        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");
@@ -1135,7 +979,7 @@
       certificatePinner = client.getCertificatePinner();
     }
 
-    return new Address(request.httpUrl().rfc2732host(), request.httpUrl().port(),
+    return new Address(request.httpUrl().host(), request.httpUrl().port(), client.getDns(),
         client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
         client.getAuthenticator(), client.getProxy(), client.getProtocols(),
         client.getConnectionSpecs(), client.getProxySelector());
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
index b5f2a48..b6bf700 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
@@ -20,18 +20,30 @@
     return method.equals("POST")
         || method.equals("PATCH")
         || method.equals("PUT")
-        || method.equals("DELETE");
+        || method.equals("DELETE")
+        || method.equals("MOVE");     // WebDAV
   }
 
   public static boolean requiresRequestBody(String method) {
     return method.equals("POST")
         || method.equals("PUT")
-        || method.equals("PATCH");
+        || method.equals("PATCH")
+        || method.equals("PROPPATCH") // WebDAV
+        || method.equals("REPORT");   // CalDAV/CardDAV (defined in WebDAV Versioning)
   }
 
   public static boolean permitsRequestBody(String method) {
     return requiresRequestBody(method)
-        || method.equals("DELETE"); // Permitted as spec is ambiguous.
+        || method.equals("OPTIONS")
+        || method.equals("DELETE")    // Permitted as spec is ambiguous.
+        || method.equals("PROPFIND")  // (WebDAV) without body: request <allprop/>
+        || method.equals("MKCOL")     // (WebDAV) may contain a body, but behaviour is unspecified
+        || method.equals("LOCK");     // (WebDAV) body: create lock, without body: refresh lock
+  }
+
+  public static boolean redirectsToGet(String method) {
+    // All requests but PROPFIND should redirect to a GET request.
+    return !method.equals("PROPFIND");
   }
 
   private HttpMethod() {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
similarity index 82%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
index 77f7c9e..ef1deb7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
@@ -22,7 +22,7 @@
 import java.io.IOException;
 import okio.Sink;
 
-public interface Transport {
+public interface HttpStream {
   /**
    * The timeout to use while discarding a stream of input data. Since this is
    * used for connection reuse, this timeout should be significantly less than
@@ -51,17 +51,11 @@
   /** Returns a stream that reads the response body. */
   ResponseBody openResponseBody(Response response) throws IOException;
 
-  /**
-   * Configures the response body to pool or close the socket connection when
-   * the response body is closed.
-   */
-  void releaseConnectionOnIdle() throws IOException;
-
-  void disconnect(HttpEngine engine) throws IOException;
+  void setHttpEngine(HttpEngine httpEngine);
 
   /**
-   * Returns true if the socket connection held by this transport can be reused
-   * for a follow-up exchange.
+   * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
+   * That may happen later by the connection pool thread.
    */
-  boolean canReuseConnection();
+  void cancel();
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
deleted file mode 100644
index d02e1e5..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * 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.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import java.io.IOException;
-import okio.Okio;
-import okio.Sink;
-import okio.Source;
-
-public final class HttpTransport implements Transport {
-  private final HttpEngine httpEngine;
-  private final HttpConnection httpConnection;
-
-  public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
-    this.httpEngine = httpEngine;
-    this.httpConnection = httpConnection;
-  }
-
-  @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
-    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
-      // Stream a request body of unknown length.
-      return httpConnection.newChunkedSink();
-    }
-
-    if (contentLength != -1) {
-      // Stream a request body of a known length.
-      return httpConnection.newFixedLengthSink(contentLength);
-    }
-
-    throw new IllegalStateException(
-        "Cannot stream a request body without chunked encoding or a known content length!");
-  }
-
-  @Override public void finishRequest() throws IOException {
-    httpConnection.flush();
-  }
-
-  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
-    httpConnection.writeRequestBody(requestBody);
-  }
-
-  /**
-   * Prepares the HTTP headers and sends them to the server.
-   *
-   * <p>For streaming requests with a body, headers must be prepared
-   * <strong>before</strong> the output stream has been written to. Otherwise
-   * the body would need to be buffered!
-   *
-   * <p>For non-streaming requests with a body, headers must be prepared
-   * <strong>after</strong> the output stream has been written to and closed.
-   * This ensures that the {@code Content-Length} header field receives the
-   * proper value.
-   */
-  public void writeRequestHeaders(Request request) throws IOException {
-    httpEngine.writingRequestHeaders();
-    String requestLine = RequestLine.get(request,
-        httpEngine.getConnection().getRoute().getProxy().type(),
-        httpEngine.getConnection().getProtocol());
-    httpConnection.writeRequest(request.headers(), requestLine);
-  }
-
-  @Override public Response.Builder readResponseHeaders() throws IOException {
-    return httpConnection.readResponse();
-  }
-
-  @Override public void releaseConnectionOnIdle() throws IOException {
-    if (canReuseConnection()) {
-      httpConnection.poolOnIdle();
-    } else {
-      httpConnection.closeOnIdle();
-    }
-  }
-
-  @Override public boolean canReuseConnection() {
-    // If the request specified that the connection shouldn't be reused, don't reuse it.
-    if ("close".equalsIgnoreCase(httpEngine.getRequest().header("Connection"))) {
-      return false;
-    }
-
-    // If the response specified that the connection shouldn't be reused, don't reuse it.
-    if ("close".equalsIgnoreCase(httpEngine.getResponse().header("Connection"))) {
-      return false;
-    }
-
-    if (httpConnection.isClosed()) {
-      return false;
-    }
-
-    return true;
-  }
-
-  @Override public ResponseBody openResponseBody(Response response) throws IOException {
-    Source source = getTransferStream(response);
-    return new RealResponseBody(response.headers(), Okio.buffer(source));
-  }
-
-  private Source getTransferStream(Response response) throws IOException {
-    if (!HttpEngine.hasBody(response)) {
-      return httpConnection.newFixedLengthSource(0);
-    }
-
-    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
-      return httpConnection.newChunkedSource(httpEngine);
-    }
-
-    long contentLength = OkHeaders.contentLength(response);
-    if (contentLength != -1) {
-      return httpConnection.newFixedLengthSource(contentLength);
-    }
-
-    // Wrap the input stream from the connection (rather than just returning
-    // "socketIn" directly here), so that we can control its use after the
-    // reference escapes.
-    return httpConnection.newUnknownLengthSource();
-  }
-
-  @Override public void disconnect(HttpEngine engine) throws IOException {
-    httpConnection.closeIfOwnedBy(engine);
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
index c381c47..5e41c85 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
@@ -55,6 +55,9 @@
    */
   public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
 
+  /** Synthetic response header: the location from which the response was loaded. */
+  public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
+
   private OkHeaders() {
   }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
index 89a3922..39419a8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
@@ -1,7 +1,6 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
@@ -15,7 +14,7 @@
    * to the application by {@link HttpURLConnection#getHeaderFields}, so it
    * needs to be set even if the transport is SPDY.
    */
-  static String get(Request request, Proxy.Type proxyType, Protocol protocol) {
+  static String get(Request request, Proxy.Type proxyType) {
     StringBuilder result = new StringBuilder();
     result.append(request.method());
     result.append(' ');
@@ -26,8 +25,7 @@
       result.append(requestPath(request.httpUrl()));
     }
 
-    result.append(' ');
-    result.append(version(protocol));
+    result.append(" HTTP/1.1");
     return result.toString();
   }
 
@@ -56,8 +54,4 @@
     String query = url.encodedQuery();
     return query != null ? (path + '?' + query) : path;
   }
-
-  public static String version(Protocol protocol) {
-    return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
-  }
 }
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 b210c56..3365914 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
@@ -17,11 +17,7 @@
 
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
 import com.squareup.okhttp.internal.RouteDatabase;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -41,9 +37,6 @@
  */
 public final class RouteSelector {
   private final Address address;
-  private final HttpUrl url;
-  private final Network network;
-  private final OkHttpClient client;
   private final RouteDatabase routeDatabase;
 
   /* The most recently attempted route. */
@@ -61,19 +54,11 @@
   /* State for negotiating failed routes */
   private final List<Route> postponedRoutes = new ArrayList<>();
 
-  private RouteSelector(Address address, HttpUrl url, OkHttpClient client) {
+  public RouteSelector(Address address, RouteDatabase routeDatabase) {
     this.address = address;
-    this.url = url;
-    this.client = client;
-    this.routeDatabase = Internal.instance.routeDatabase(client);
-    this.network = Internal.instance.network(client);
+    this.routeDatabase = routeDatabase;
 
-    resetNextProxy(url, address.getProxy());
-  }
-
-  public static RouteSelector get(Address address, Request request, OkHttpClient client)
-      throws IOException {
-    return new RouteSelector(address, request.httpUrl(), client);
+    resetNextProxy(address.url(), address.getProxy());
   }
 
   /**
@@ -117,7 +102,7 @@
     if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && address.getProxySelector() != null) {
       // Tell the proxy selector when we fail to connect on a fresh connection.
       address.getProxySelector().connectFailed(
-          url.uri(), failedRoute.getProxy().address(), failure);
+          address.url().uri(), failedRoute.getProxy().address(), failure);
     }
 
     routeDatabase.failed(failedRoute);
@@ -132,7 +117,7 @@
       // Try each of the ProxySelector choices until one connection succeeds. If none succeed
       // then we'll try a direct connection below.
       proxies = new ArrayList<>();
-      List<Proxy> selectedProxies = client.getProxySelector().select(url.uri());
+      List<Proxy> selectedProxies = address.getProxySelector().select(url.uri());
       if (selectedProxies != null) proxies.addAll(selectedProxies);
       // Finally try a direct connection. We only try it once!
       proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
@@ -149,7 +134,7 @@
   /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
   private Proxy nextProxy() throws IOException {
     if (!hasNextProxy()) {
-      throw new SocketException("No route to " + address.getRfc2732Host()
+      throw new SocketException("No route to " + address.getUriHost()
           + "; exhausted proxy configurations: " + proxies);
     }
     Proxy result = proxies.get(nextProxyIndex++);
@@ -165,7 +150,7 @@
     String socketHost;
     int socketPort;
     if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
-      socketHost = address.getRfc2732Host();
+      socketHost = address.getUriHost();
       socketPort = address.getUriPort();
     } else {
       SocketAddress proxyAddress = proxy.address();
@@ -183,9 +168,15 @@
           + "; port is out of range");
     }
 
-    // Try each address for best behavior in mixed IPv4/IPv6 environments.
-    for (InetAddress inetAddress : network.resolveInetAddresses(socketHost)) {
-      inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+    if (proxy.type() == Proxy.Type.SOCKS) {
+      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
+    } else {
+      // Try each address for best behavior in mixed IPv4/IPv6 environments.
+      List<InetAddress> addresses = address.getDns().lookup(socketHost);
+      for (int i = 0, size = addresses.size(); i < size; i++) {
+        InetAddress inetAddress = addresses.get(i);
+        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+      }
     }
 
     nextInetSocketAddressIndex = 0;
@@ -217,7 +208,7 @@
   /** Returns the next socket address to try. */
   private InetSocketAddress nextInetSocketAddress() throws IOException {
     if (!hasNextInetSocketAddress()) {
-      throw new SocketException("No route to " + address.getRfc2732Host()
+      throw new SocketException("No route to " + address.getUriHost()
           + "; exhausted inet socket addresses: " + inetSocketAddresses);
     }
     return inetSocketAddresses.get(nextInetSocketAddressIndex++);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
new file mode 100644
index 0000000..7d95338
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 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.Address;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.net.ProtocolException;
+import java.net.SocketTimeoutException;
+import java.security.cert.CertificateException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import okio.Sink;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * This class coordinates the relationship between three entities:
+ *
+ * <ul>
+ *   <li><strong>Connections:</strong> physical socket connections to remote servers. These are
+ *       potentially slow to establish so it is necessary to be able to cancel a connection
+ *       currently being connected.
+ *   <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
+ *       connections. Each connection has its own allocation limit, which defines how many
+ *       concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
+ *       at a time, SPDY and HTTP/2 typically carry multiple.
+ *   <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
+ *       its follow up requests. We prefer to keep all streams of a single call on the same
+ *       connection for better behavior and locality.
+ * </ul>
+ *
+ * <p>Instances of this class act on behalf of the call, using one or more streams over one or
+ * more connections. This class has APIs to release each of the above resources:
+ *
+ * <ul>
+ *   <li>{@link #noNewStreams()} prevents the connection from being used for new streams in the
+ *       future. Use this after a {@code Connection: close} header, or when the connection may be
+ *       inconsistent.
+ *   <li>{@link #streamFinished streamFinished()} releases the active stream from this allocation.
+ *       Note that only one stream may be active at a given time, so it is necessary to call {@link
+ *       #streamFinished streamFinished()} before creating a subsequent stream with {@link
+ *       #newStream newStream()}.
+ *   <li>{@link #release()} removes the call's hold on the connection. Note that this won't
+ *       immediately free the connection if there is a stream still lingering. That happens when a
+ *       call is complete but its response body has yet to be fully consumed.
+ * </ul>
+ *
+ * <p>This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have
+ * the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that
+ * stream but not the other streams sharing its connection. But if the TLS handshake is still in
+ * progress then canceling may break the entire connection.
+ */
+public final class StreamAllocation {
+  public final Address address;
+  private final ConnectionPool connectionPool;
+
+  // State guarded by connectionPool.
+  private RouteSelector routeSelector;
+  private RealConnection connection;
+  private boolean released;
+  private boolean canceled;
+  private HttpStream stream;
+
+  public StreamAllocation(ConnectionPool connectionPool, Address address) {
+    this.connectionPool = connectionPool;
+    this.address = address;
+  }
+
+  public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,
+      boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+      throws RouteException, IOException {
+    try {
+      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
+          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
+
+      HttpStream resultStream;
+      if (resultConnection.framedConnection != null) {
+        resultStream = new Http2xStream(this, resultConnection.framedConnection);
+      } else {
+        resultConnection.getSocket().setSoTimeout(readTimeout);
+        resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
+        resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
+        resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink);
+      }
+
+      synchronized (connectionPool) {
+        resultConnection.streamCount++;
+        stream = resultStream;
+        return resultStream;
+      }
+    } catch (IOException e) {
+      throw new RouteException(e);
+    }
+  }
+
+  /**
+   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
+   * until a healthy connection is found.
+   */
+  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
+      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+      throws IOException, RouteException {
+    while (true) {
+      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
+          connectionRetryEnabled);
+
+      // If this is a brand new connection, we can skip the extensive health checks.
+      synchronized (connectionPool) {
+        if (candidate.streamCount == 0) {
+          return candidate;
+        }
+      }
+
+      // Otherwise do a potentially-slow check to confirm that the pooled connection is still good.
+      if (candidate.isHealthy(doExtensiveHealthChecks)) {
+        return candidate;
+      }
+
+      connectionFailed();
+    }
+  }
+
+  /**
+   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
+   * then the pool, finally building a new connection.
+   */
+  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
+      boolean connectionRetryEnabled) throws IOException, RouteException {
+    synchronized (connectionPool) {
+      if (released) throw new IllegalStateException("released");
+      if (stream != null) throw new IllegalStateException("stream != null");
+      if (canceled) throw new IOException("Canceled");
+
+      RealConnection allocatedConnection = this.connection;
+      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
+        return allocatedConnection;
+      }
+
+      // Attempt to get a connection from the pool.
+      RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
+      if (pooledConnection != null) {
+        this.connection = pooledConnection;
+        return pooledConnection;
+      }
+
+      // Attempt to create a connection.
+      if (routeSelector == null) {
+        routeSelector = new RouteSelector(address, routeDatabase());
+      }
+    }
+
+    Route route = routeSelector.next();
+    RealConnection newConnection = new RealConnection(route);
+    acquire(newConnection);
+
+    synchronized (connectionPool) {
+      Internal.instance.put(connectionPool, newConnection);
+      this.connection = newConnection;
+      if (canceled) throw new IOException("Canceled");
+    }
+
+    newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),
+        connectionRetryEnabled);
+    routeDatabase().connected(newConnection.getRoute());
+
+    return newConnection;
+  }
+
+  public void streamFinished(HttpStream stream) {
+    synchronized (connectionPool) {
+      if (stream == null || stream != this.stream) {
+        throw new IllegalStateException("expected " + this.stream + " but was " + stream);
+      }
+    }
+    deallocate(false, false, true);
+  }
+
+  public HttpStream stream() {
+    synchronized (connectionPool) {
+      return stream;
+    }
+  }
+
+  private RouteDatabase routeDatabase() {
+    return Internal.instance.routeDatabase(connectionPool);
+  }
+
+  public synchronized RealConnection connection() {
+    return connection;
+  }
+
+  public void release() {
+    deallocate(false, true, false);
+  }
+
+  /** Forbid new streams from being created on the connection that hosts this allocation. */
+  public void noNewStreams() {
+    deallocate(true, false, false);
+  }
+
+  /**
+   * Releases resources held by this allocation. If sufficient resources are allocated, the
+   * connection will be detached or closed.
+   */
+  private void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
+    RealConnection connectionToClose = null;
+    synchronized (connectionPool) {
+      if (streamFinished) {
+        this.stream = null;
+      }
+      if (released) {
+        this.released = true;
+      }
+      if (connection != null) {
+        if (noNewStreams) {
+          connection.noNewStreams = true;
+        }
+        if (this.stream == null && (this.released || connection.noNewStreams)) {
+          release(connection);
+          if (connection.streamCount > 0) {
+            routeSelector = null;
+          }
+          if (connection.allocations.isEmpty()) {
+            connection.idleAtNanos = System.nanoTime();
+            if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
+              connectionToClose = connection;
+            }
+          }
+          connection = null;
+        }
+      }
+    }
+    if (connectionToClose != null) {
+      Util.closeQuietly(connectionToClose.getSocket());
+    }
+  }
+
+  public void cancel() {
+    HttpStream streamToCancel;
+    RealConnection connectionToCancel;
+    synchronized (connectionPool) {
+      canceled = true;
+      streamToCancel = stream;
+      connectionToCancel = connection;
+    }
+    if (streamToCancel != null) {
+      streamToCancel.cancel();
+    } else if (connectionToCancel != null) {
+      connectionToCancel.cancel();
+    }
+  }
+
+  private void connectionFailed(IOException e) {
+    synchronized (connectionPool) {
+      if (routeSelector != null) {
+        if (connection.streamCount == 0) {
+          // Record the failure on a fresh route.
+          Route failedRoute = connection.getRoute();
+          routeSelector.connectFailed(failedRoute, e);
+        } else {
+          // We saw a failure on a recycled connection, reset this allocation with a fresh route.
+          routeSelector = null;
+        }
+      }
+    }
+    connectionFailed();
+  }
+
+  /** Finish the current stream and prevent new streams from being created. */
+  public void connectionFailed() {
+    deallocate(true, false, true);
+  }
+
+  /**
+   * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
+   * {@link #release} on the same connection.
+   */
+  public void acquire(RealConnection connection) {
+    connection.allocations.add(new WeakReference<>(this));
+  }
+
+  /** Remove this allocation from the connection's list of allocations. */
+  private void release(RealConnection connection) {
+    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
+      Reference<StreamAllocation> reference = connection.allocations.get(i);
+      if (reference.get() == this) {
+        connection.allocations.remove(i);
+        return;
+      }
+    }
+    throw new IllegalStateException();
+  }
+
+  public boolean recover(RouteException e) {
+    if (connection != null) {
+      connectionFailed(e.getLastConnectException());
+    }
+
+    if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+        || !isRecoverable(e)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public boolean recover(IOException e, Sink requestBodyOut) {
+    if (connection != null) {
+      int streamCount = connection.streamCount;
+      connectionFailed(e);
+
+      if (streamCount == 1) {
+        // This isn't a recycled connection.
+        // TODO(jwilson): find a better way for this.
+        return false;
+      }
+    }
+
+    boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
+    if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+        || !isRecoverable(e)
+        || !canRetryRequestBody) {
+      return false;
+    }
+
+    return true;
+  }
+
+  private boolean isRecoverable(IOException e) {
+    // If there was a protocol problem, don't recover.
+    if (e instanceof ProtocolException) {
+      return false;
+    }
+
+    // If there was an interruption or timeout, don't recover.
+    if (e instanceof InterruptedIOException) {
+      return false;
+    }
+
+    return true;
+  }
+
+  private boolean isRecoverable(RouteException e) {
+    // Problems with a route may mean the connection can be retried with a new route, or may
+    // indicate a client-side or server-side issue that should not be retried. To tell, we must look
+    // at the cause.
+
+    IOException ioe = e.getLastConnectException();
+
+    // If there was a protocol problem, don't recover.
+    if (ioe instanceof ProtocolException) {
+      return false;
+    }
+
+    // If there was an interruption don't recover, but if there was a timeout
+    // we should try the next route (if there is one).
+    if (ioe instanceof InterruptedIOException) {
+      return ioe instanceof SocketTimeoutException;
+    }
+
+    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
+    // again with a different route.
+    if (ioe instanceof SSLHandshakeException) {
+      // If the problem was a CertificateException from the X509TrustManager,
+      // do not retry.
+      if (ioe.getCause() instanceof CertificateException) {
+        return false;
+      }
+    }
+    if (ioe 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;
+  }
+
+  @Override public String toString() {
+    return address.toString();
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
new file mode 100644
index 0000000..9ff53c1
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
@@ -0,0 +1,407 @@
+/*
+ *  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.io;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.ConnectionSpecSelector;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.http.Http1xStream;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.RouteException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.tls.CertificateChainCleaner;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.net.ConnectException;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownServiceException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.Util.closeQuietly;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+public final class RealConnection implements Connection {
+  private final Route route;
+
+  /** The low-level TCP socket. */
+  private Socket rawSocket;
+
+  /**
+   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
+   * {@link #rawSocket} itself if this connection does not use SSL.
+   */
+  public Socket socket;
+  private Handshake handshake;
+  private Protocol protocol;
+  public volatile FramedConnection framedConnection;
+  public int streamCount;
+  public BufferedSource source;
+  public BufferedSink sink;
+  public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
+  public boolean noNewStreams;
+  public long idleAtNanos = Long.MAX_VALUE;
+
+  public RealConnection(Route route) {
+    this.route = route;
+  }
+
+  public void connect(int connectTimeout, int readTimeout, int writeTimeout,
+      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
+    if (protocol != null) throw new IllegalStateException("already connected");
+
+    RouteException routeException = null;
+    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
+    Proxy proxy = route.getProxy();
+    Address address = route.getAddress();
+
+    if (route.getAddress().getSslSocketFactory() == null
+        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
+      throw new RouteException(new UnknownServiceException(
+          "CLEARTEXT communication not supported: " + connectionSpecs));
+    }
+
+    while (protocol == null) {
+      try {
+        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
+            ? address.getSocketFactory().createSocket()
+            : new Socket(proxy);
+        connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
+      } catch (IOException e) {
+        Util.closeQuietly(socket);
+        Util.closeQuietly(rawSocket);
+        socket = null;
+        rawSocket = null;
+        source = null;
+        sink = null;
+        handshake = null;
+        protocol = null;
+
+        if (routeException == null) {
+          routeException = new RouteException(e);
+        } else {
+          routeException.addConnectException(e);
+        }
+
+        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
+          throw routeException;
+        }
+      }
+    }
+  }
+
+  /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
+  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
+      ConnectionSpecSelector connectionSpecSelector) throws IOException {
+    rawSocket.setSoTimeout(readTimeout);
+    try {
+      Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
+    } catch (ConnectException e) {
+      throw new ConnectException("Failed to connect to " + route.getSocketAddress());
+    }
+    source = Okio.buffer(Okio.source(rawSocket));
+    sink = Okio.buffer(Okio.sink(rawSocket));
+
+    if (route.getAddress().getSslSocketFactory() != null) {
+      connectTls(readTimeout, writeTimeout, connectionSpecSelector);
+    } else {
+      protocol = Protocol.HTTP_1_1;
+      socket = rawSocket;
+    }
+
+    if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
+      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
+
+      FramedConnection framedConnection = new FramedConnection.Builder(true)
+          .socket(socket, route.getAddress().url().host(), source, sink)
+          .protocol(protocol)
+          .build();
+      framedConnection.sendConnectionPreface();
+
+      // Only assign the framed connection once the preface has been sent successfully.
+      this.framedConnection = framedConnection;
+    }
+  }
+
+  private void connectTls(int readTimeout, int writeTimeout,
+      ConnectionSpecSelector connectionSpecSelector) throws IOException {
+    if (route.requiresTunnel()) {
+      createTunnel(readTimeout, writeTimeout);
+    }
+
+    Address address = route.getAddress();
+    SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
+    boolean success = false;
+    SSLSocket sslSocket = null;
+    try {
+      // Create the wrapper over the connected socket.
+      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
+          rawSocket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
+
+      // Configure the socket's ciphers, TLS versions, and extensions.
+      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
+      if (connectionSpec.supportsTlsExtensions()) {
+        Platform.get().configureTlsExtensions(
+            sslSocket, address.getUriHost(), address.getProtocols());
+      }
+
+      // Force handshake. This can throw!
+      sslSocket.startHandshake();
+      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
+
+      // Verify that the socket's certificates are acceptable for the target host.
+      if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
+        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
+        throw new SSLPeerUnverifiedException("Hostname " + address.getUriHost() + " not verified:"
+            + "\n    certificate: " + CertificatePinner.pin(cert)
+            + "\n    DN: " + cert.getSubjectDN().getName()
+            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
+      }
+
+      // Check that the certificate pinner is satisfied by the certificates presented.
+      if (address.getCertificatePinner() != CertificatePinner.DEFAULT) {
+        TrustRootIndex trustRootIndex = trustRootIndex(address.getSslSocketFactory());
+        List<Certificate> certificates = new CertificateChainCleaner(trustRootIndex)
+            .clean(unverifiedHandshake.peerCertificates());
+        address.getCertificatePinner().check(address.getUriHost(), certificates);
+      }
+
+      // Success! Save the handshake and the ALPN protocol.
+      String maybeProtocol = connectionSpec.supportsTlsExtensions()
+          ? Platform.get().getSelectedProtocol(sslSocket)
+          : null;
+      socket = sslSocket;
+      source = Okio.buffer(Okio.source(socket));
+      sink = Okio.buffer(Okio.sink(socket));
+      handshake = unverifiedHandshake;
+      protocol = maybeProtocol != null
+          ? Protocol.get(maybeProtocol)
+          : Protocol.HTTP_1_1;
+      success = true;
+    } catch (AssertionError e) {
+      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+      throw e;
+    } finally {
+      if (sslSocket != null) {
+        Platform.get().afterHandshake(sslSocket);
+      }
+      if (!success) {
+        closeQuietly(sslSocket);
+      }
+    }
+  }
+
+  private static SSLSocketFactory lastSslSocketFactory;
+  private static TrustRootIndex lastTrustRootIndex;
+
+  /**
+   * Returns a trust root index for {@code sslSocketFactory}. This uses a static, single-element
+   * cache to avoid redoing reflection and SSL indexing in the common case where most SSL
+   * connections use the same SSL socket factory.
+   */
+  private static synchronized TrustRootIndex trustRootIndex(SSLSocketFactory sslSocketFactory) {
+    if (sslSocketFactory != lastSslSocketFactory) {
+      X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
+      lastTrustRootIndex = Platform.get().trustRootIndex(trustManager);
+      lastSslSocketFactory = sslSocketFactory;
+    }
+    return lastTrustRootIndex;
+  }
+
+  /**
+   * To make an HTTPS connection over an HTTP proxy, send an unencrypted
+   * CONNECT request to create the proxy connection. This may need to be
+   * retried if the proxy requires authorization.
+   */
+  private void createTunnel(int readTimeout, int writeTimeout) throws IOException {
+    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+    Request tunnelRequest = createTunnelRequest();
+    HttpUrl url = tunnelRequest.httpUrl();
+    String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1";
+    while (true) {
+      Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
+      source.timeout().timeout(readTimeout, MILLISECONDS);
+      sink.timeout().timeout(writeTimeout, MILLISECONDS);
+      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
+      tunnelConnection.finishRequest();
+      Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
+      // The response body from a CONNECT should be empty, but if it is not then we should consume
+      // it before proceeding.
+      long contentLength = OkHeaders.contentLength(response);
+      if (contentLength == -1L) {
+        contentLength = 0L;
+      }
+      Source body = tunnelConnection.newFixedLengthSource(contentLength);
+      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
+      body.close();
+
+      switch (response.code()) {
+        case HTTP_OK:
+          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
+          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
+          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
+          // that it will almost certainly fail because the proxy has sent unexpected data.
+          if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
+            throw new IOException("TLS tunnel buffered too many bytes!");
+          }
+          return;
+
+        case HTTP_PROXY_AUTH:
+          tunnelRequest = OkHeaders.processAuthHeader(
+              route.getAddress().getAuthenticator(), response, route.getProxy());
+          if (tunnelRequest != null) continue;
+          throw new IOException("Failed to authenticate with proxy");
+
+        default:
+          throw new IOException(
+              "Unexpected response code for CONNECT: " + response.code());
+      }
+    }
+  }
+
+  /**
+   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
+   * no tunnel is necessary. Everything in the tunnel request is sent
+   * unencrypted to the proxy server, so tunnels include only the minimum set of
+   * headers. This avoids sending potentially sensitive data like HTTP cookies
+   * to the proxy unencrypted.
+   */
+  private Request createTunnelRequest() throws IOException {
+    return new Request.Builder()
+        .url(route.getAddress().url())
+        .header("Host", Util.hostHeader(route.getAddress().url()))
+        .header("Proxy-Connection", "Keep-Alive")
+        .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
+        .build();
+  }
+
+  /** Returns true if {@link #connect} has been attempted on this connection. */
+  boolean isConnected() {
+    return protocol != null;
+  }
+
+  @Override public Route getRoute() {
+    return route;
+  }
+
+  public void cancel() {
+    // Close the raw socket so we don't end up doing synchronous I/O.
+    Util.closeQuietly(rawSocket);
+  }
+
+  @Override public Socket getSocket() {
+    return socket;
+  }
+
+  public int allocationLimit() {
+    FramedConnection framedConnection = this.framedConnection;
+    return framedConnection != null
+        ? framedConnection.maxConcurrentStreams()
+        : 1;
+  }
+
+  /** Returns true if this connection is ready to host new streams. */
+  public boolean isHealthy(boolean doExtensiveChecks) {
+    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
+      return false;
+    }
+
+    if (framedConnection != null) {
+      return true; // TODO: check framedConnection.shutdown.
+    }
+
+    if (doExtensiveChecks) {
+      try {
+        int readTimeout = socket.getSoTimeout();
+        try {
+          socket.setSoTimeout(1);
+          if (source.exhausted()) {
+            return false; // Stream is exhausted; socket is closed.
+          }
+          return true;
+        } finally {
+          socket.setSoTimeout(readTimeout);
+        }
+      } catch (SocketTimeoutException ignored) {
+        // Read timed out; socket is good.
+      } catch (IOException e) {
+        return false; // Couldn't read; socket is closed.
+      }
+    }
+
+    return true;
+  }
+
+  @Override public Handshake getHandshake() {
+    return handshake;
+  }
+
+  /**
+   * Returns true if this is a SPDY connection. Such connections can be used
+   * in multiple HTTP requests simultaneously.
+   */
+  public boolean isMultiplexed() {
+    return framedConnection != null;
+  }
+
+  @Override public Protocol getProtocol() {
+    return protocol != null ? protocol : Protocol.HTTP_1_1;
+  }
+
+  @Override public String toString() {
+    return "Connection{"
+        + route.getAddress().url().host() + ":" + route.getAddress().url().port()
+        + ", proxy="
+        + route.getProxy()
+        + " hostAddress="
+        + route.getSocketAddress()
+        + " cipherSuite="
+        + (handshake != null ? handshake.cipherSuite() : "none")
+        + " protocol="
+        + protocol
+        + '}';
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
new file mode 100644
index 0000000..0beba94
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A index of trusted root certificates that exploits knowledge of Android implementation details.
+ * This class is potentially much faster to initialize than {@link RealTrustRootIndex} because
+ * it doesn't need to load and index trusted CA certificates.
+ */
+public final class AndroidTrustRootIndex implements TrustRootIndex {
+  private final X509TrustManager trustManager;
+  private final Method findByIssuerAndSignatureMethod;
+
+  public AndroidTrustRootIndex(
+      X509TrustManager trustManager, Method findByIssuerAndSignatureMethod) {
+    this.findByIssuerAndSignatureMethod = findByIssuerAndSignatureMethod;
+    this.trustManager = trustManager;
+  }
+
+  @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+    try {
+      TrustAnchor trustAnchor = (TrustAnchor) findByIssuerAndSignatureMethod.invoke(
+          trustManager, cert);
+      return trustAnchor != null
+          ? trustAnchor.getTrustedCert()
+          : null;
+    } catch (IllegalAccessException e) {
+      throw new AssertionError();
+    } catch (InvocationTargetException e) {
+      return null;
+    }
+  }
+
+  public static TrustRootIndex get(X509TrustManager trustManager) {
+    // From org.conscrypt.TrustManagerImpl, we want the method with this signature:
+    // private TrustAnchor findTrustAnchorByIssuerAndSignature(X509Certificate lastCert);
+    try {
+      Method method = trustManager.getClass().getDeclaredMethod(
+          "findTrustAnchorByIssuerAndSignature", X509Certificate.class);
+      method.setAccessible(true);
+      return new AndroidTrustRootIndex(trustManager, method);
+    } catch (NoSuchMethodException e) {
+      return null;
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
new file mode 100644
index 0000000..0e53298
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
@@ -0,0 +1,117 @@
+/*
+ *  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.tls;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+/**
+ * Computes the effective certificate chain from the raw array returned by Java's built in TLS APIs.
+ * Cleaning a chain returns a list of certificates where the first element is {@code chain[0]}, each
+ * certificate is signed by the certificate that follows, and the last certificate is a trusted CA
+ * certificate.
+ *
+ * <p>Use of the chain cleaner is necessary to omit unexpected certificates that aren't relevant to
+ * the TLS handshake and to extract the trusted CA certificate for the benefit of certificate
+ * pinning.
+ *
+ * <p>This class includes code from <a href="https://conscrypt.org/">Conscrypt's</a> {@code
+ * TrustManagerImpl} and {@code TrustedCertificateIndex}.
+ */
+public final class CertificateChainCleaner {
+  /** The maximum number of signers in a chain. We use 9 for consistency with OpenSSL. */
+  private static final int MAX_SIGNERS = 9;
+
+  private final TrustRootIndex trustRootIndex;
+
+  public CertificateChainCleaner(TrustRootIndex trustRootIndex) {
+    this.trustRootIndex = trustRootIndex;
+  }
+
+  /**
+   * Returns a cleaned chain for {@code chain}.
+   *
+   * <p>This method throws if the complete chain to a trusted CA certificate cannot be constructed.
+   * This is unexpected unless the trust root index in this class has a different trust manager than
+   * what was used to establish {@code chain}.
+   */
+  public List<Certificate> clean(List<Certificate> chain) throws SSLPeerUnverifiedException {
+    Deque<Certificate> queue = new ArrayDeque<>(chain);
+    List<Certificate> result = new ArrayList<>();
+    result.add(queue.removeFirst());
+    boolean foundTrustedCertificate = false;
+
+    followIssuerChain:
+    for (int c = 0; c < MAX_SIGNERS; c++) {
+      X509Certificate toVerify = (X509Certificate) result.get(result.size() - 1);
+
+      // If this cert has been signed by a trusted cert, use that. Add the trusted certificate to
+      // the end of the chain unless it's already present. (That would happen if the first
+      // certificate in the chain is itself a self-signed and trusted CA certificate.)
+      X509Certificate trustedCert = trustRootIndex.findByIssuerAndSignature(toVerify);
+      if (trustedCert != null) {
+        if (result.size() > 1 || !toVerify.equals(trustedCert)) {
+          result.add(trustedCert);
+        }
+        if (verifySignature(trustedCert, trustedCert)) {
+          return result; // The self-signed cert is a root CA. We're done.
+        }
+        foundTrustedCertificate = true;
+        continue;
+      }
+
+      // Search for the certificate in the chain that signed this certificate. This is typically the
+      // next element in the chain, but it could be any element.
+      for (Iterator<Certificate> i = queue.iterator(); i.hasNext(); ) {
+        X509Certificate signingCert = (X509Certificate) i.next();
+        if (verifySignature(toVerify, signingCert)) {
+          i.remove();
+          result.add(signingCert);
+          continue followIssuerChain;
+        }
+      }
+
+      // We've reached the end of the chain. If any cert in the chain is trusted, we're done.
+      if (foundTrustedCertificate) {
+        return result;
+      }
+
+      // The last link isn't trusted. Fail.
+      throw new SSLPeerUnverifiedException("Failed to find a trusted cert that signed " + toVerify);
+    }
+
+    throw new SSLPeerUnverifiedException("Certificate chain too long: " + result);
+  }
+
+  /** Returns true if {@code toVerify} was signed by {@code signingCert}'s public key. */
+  private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) {
+    if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false;
+    try {
+      toVerify.verify(signingCert.getPublicKey());
+      return true;
+    } catch (GeneralSecurityException verifyFailed) {
+      return false;
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
new file mode 100644
index 0000000..885eea4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.security.auth.x500.X500Principal;
+
+public final class RealTrustRootIndex implements TrustRootIndex {
+  private final Map<X500Principal, List<X509Certificate>> subjectToCaCerts;
+
+  public RealTrustRootIndex(X509Certificate... caCerts) {
+    subjectToCaCerts = new LinkedHashMap<>();
+    for (X509Certificate caCert : caCerts) {
+      X500Principal subject = caCert.getSubjectX500Principal();
+      List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(subject);
+      if (subjectCaCerts == null) {
+        subjectCaCerts = new ArrayList<>(1);
+        subjectToCaCerts.put(subject, subjectCaCerts);
+      }
+      subjectCaCerts.add(caCert);
+    }
+  }
+
+  @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+    X500Principal issuer = cert.getIssuerX500Principal();
+    List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(issuer);
+    if (subjectCaCerts == null) return null;
+
+    for (X509Certificate caCert : subjectCaCerts) {
+      PublicKey publicKey = caCert.getPublicKey();
+      try {
+        cert.verify(publicKey);
+        return caCert;
+      } catch (Exception ignored) {
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
similarity index 65%
copy from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
copy to okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
index 4020bf4..6b0036b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2016 Square, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,13 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.tls;
 
-import com.squareup.okhttp.Protocol;
+import java.security.cert.X509Certificate;
 
-public class HttpOverSpdy3Test extends HttpOverSpdyTest {
-
-  public HttpOverSpdy3Test() {
-    super(Protocol.SPDY_3);
-  }
+public interface TrustRootIndex {
+  /** Returns the trusted CA certificate that signed {@code cert}. */
+  X509Certificate findByIssuerAndSignature(X509Certificate cert);
 }
diff --git a/okio/README.android b/okio/README.android
index a143b63..63024bd 100644
--- a/okio/README.android
+++ b/okio/README.android
@@ -5,8 +5,11 @@
 Local patches
 -------------
 
-All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
+All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END.
+
+These changes relate to okio dependencies not available on Android, such as:
   - Removal of reference to a codehause annotation used in
     okio/src/main/java/okio/DeflaterSink.java
   - Commenting of code that references APIs not present on Android.
   - Removal of test code that uses JUnit 4.11 features such as @Parameterized.Parameters
+
diff --git a/pom.xml b/pom.xml
index 7219018..4654188 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
 
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>parent</artifactId>
-  <version>2.6.0-SNAPSHOT</version>
+  <version>2.7.5</version>
   <packaging>pom</packaging>
 
   <name>OkHttp (Parent)</name>
@@ -31,6 +31,8 @@
     <module>okhttp-ws</module>
     <module>okhttp-ws-tests</module>
 
+    <module>okhttp-logging-interceptor</module>
+
     <module>okcurl</module>
     <module>mockwebserver</module>
     <module>samples</module>
@@ -52,6 +54,7 @@
     <apache.http.version>4.2.2</apache.http.version>
     <airlift.version>0.6</airlift.version>
     <guava.version>16.0</guava.version>
+    <android.version>4.1.1.4</android.version>
 
     <!-- Test Dependencies -->
     <junit.version>4.11</junit.version>
@@ -61,7 +64,7 @@
     <url>https://github.com/square/okhttp/</url>
     <connection>scm:git:https://github.com/square/okhttp.git</connection>
     <developerConnection>scm:git:git@github.com:square/okhttp.git</developerConnection>
-    <tag>HEAD</tag>
+    <tag>parent-2.7.5</tag>
   </scm>
 
   <issueManagement>
@@ -113,6 +116,11 @@
         <artifactId>guava</artifactId>
         <version>${guava.version}</version>
       </dependency>
+      <dependency>
+        <groupId>com.google.android</groupId>
+        <artifactId>android</artifactId>
+        <version>${android.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
index aebd0eb..e92d9ed 100644
--- a/samples/crawler/pom.xml
+++ b/samples/crawler/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>crawler</artifactId>
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index 55e2671..a72fd64 100644
--- a/samples/guide/pom.xml
+++ b/samples/guide/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>guide</artifactId>
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
index 8e5334a..2c6cfa0 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
@@ -40,7 +40,7 @@
     RequestBody requestBody = new MultipartBuilder()
         .type(MultipartBuilder.FORM)
         .addFormDataPart("title", "Square Logo")
-        .addFormDataPart("image", null,
+        .addFormDataPart("image", "logo-square.png",
             RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
         .build();
 
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
index d439e99..877812e 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
@@ -2,7 +2,9 @@
 
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.ws.WebSocket;
 import com.squareup.okhttp.ws.WebSocketCall;
 import com.squareup.okhttp.ws.WebSocketListener;
@@ -10,11 +12,10 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import okio.Buffer;
-import okio.BufferedSource;
+import okio.ByteString;
 
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
 
 public final class WebSocketEcho implements WebSocketListener {
   private final Executor writeExecutor = Executors.newSingleThreadExecutor();
@@ -35,9 +36,9 @@
     writeExecutor.execute(new Runnable() {
       @Override public void run() {
         try {
-          webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
-          webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
-          webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
+          webSocket.sendMessage(RequestBody.create(TEXT, "Hello..."));
+          webSocket.sendMessage(RequestBody.create(TEXT, "...World!"));
+          webSocket.sendMessage(RequestBody.create(BINARY, ByteString.decodeHex("deadbeef")));
           webSocket.close(1000, "Goodbye, World!");
         } catch (IOException e) {
           System.err.println("Unable to send messages: " + e.getMessage());
@@ -46,18 +47,13 @@
     });
   }
 
-  @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
-    switch (type) {
-      case TEXT:
-        System.out.println("MESSAGE: " + payload.readUtf8());
-        break;
-      case BINARY:
-        System.out.println("MESSAGE: " + payload.readByteString().hex());
-        break;
-      default:
-        throw new IllegalStateException("Unknown payload type: " + type);
+  @Override public void onMessage(ResponseBody message) throws IOException {
+    if (message.contentType() == TEXT) {
+      System.out.println("MESSAGE: " + message.string());
+    } else {
+      System.out.println("MESSAGE: " + message.source().readByteString().hex());
     }
-    payload.close();
+    message.close();
   }
 
   @Override public void onPong(Buffer payload) {
diff --git a/samples/pom.xml b/samples/pom.xml
index 29f1e87..77ca5f5 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <groupId>com.squareup.okhttp.sample</groupId>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index 3a1aa7d..0065b7b 100644
--- a/samples/simple-client/pom.xml
+++ b/samples/simple-client/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>simple-client</artifactId>
diff --git a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
index e616d41..d897a9a 100644
--- a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
+++ b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
@@ -5,6 +5,7 @@
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
 import java.io.Reader;
 import java.util.Collections;
 import java.util.Comparator;
@@ -34,8 +35,10 @@
     Response response = client.newCall(request).execute();
 
     // Deserialize HTTP response to concrete type.
-    Reader body = response.body().charStream();
-    List<Contributor> contributors = GSON.fromJson(body, CONTRIBUTORS.getType());
+    ResponseBody body = response.body();
+    Reader charStream = body.charStream();
+    List<Contributor> contributors = GSON.fromJson(charStream, CONTRIBUTORS.getType());
+    body.close();
 
     // Sort list by the most contributions.
     Collections.sort(contributors, new Comparator<Contributor>() {
diff --git a/samples/static-server/pom.xml b/samples/static-server/pom.xml
index f223151..75b492d 100644
--- a/samples/static-server/pom.xml
+++ b/samples/static-server/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.6.0-SNAPSHOT</version>
+    <version>2.7.5</version>
   </parent>
 
   <artifactId>static-server</artifactId>
diff --git a/website/index.html b/website/index.html
index 86695a4..ecc7c69 100644
--- a/website/index.html
+++ b/website/index.html
@@ -155,7 +155,7 @@
               </ul>
               <ul class="nav nav-pills nav-stacked secondary">
                 <li><a href="https://github.com/square/okhttp/wiki">Wiki</a></li>
-                <li><a href="javadoc/index.html">Javadoc</a></li>
+                <li><a href="2.x/okhttp/">Javadoc</a></li>
                 <li><a href="http://stackoverflow.com/questions/tagged/okhttp?sort=active">StackOverflow</a></li>
               </ul>
             </div>