am 3c0f4a28: (-s ours) Revert "DO NOT MERGE Fixing broken OkHttp CTS test"

* commit '3c0f4a28c5d5a98e15f9e9fc6a7c03bf459d5e14':
  Revert "DO NOT MERGE Fixing broken OkHttp CTS test"
diff --git a/Android.mk b/Android.mk
index 3b2cdbd..4baf397 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,13 +16,13 @@
 LOCAL_PATH := $(call my-dir)
 
 okhttp_common_src_files := $(call all-java-files-under,okhttp/src/main/java)
-okhttp_common_src_files += $(call all-java-files-under,okhttp-protocols/src/main/java)
+okhttp_common_src_files += $(call all-java-files-under,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)
 
-okhttp_test_src_files := $(call all-java-files-under,okhttp/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-protocols/src/test/java)
+okhttp_test_src_files := $(call all-java-files-under,okhttp-tests/src/test/java)
 okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/main/java)
+okhttp_test_src_files += $(call all-java-files-under,android/test/java)
 okhttp_test_src_files := $(filter-out mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java, $(okhttp_test_src_files))
 
 include $(CLEAR_VARS)
@@ -31,7 +31,7 @@
 LOCAL_SRC_FILES := $(okhttp_system_src_files)
 LOCAL_JAVACFLAGS := -encoding UTF-8
 LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := conscrypt core
+LOCAL_JAVA_LIBRARIES := core-libart conscrypt
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 include $(BUILD_JAVA_LIBRARY)
@@ -46,7 +46,7 @@
 ifneq ($(TARGET_BUILD_APPS),)
     LOCAL_SDK_VERSION := 11
 else
-    LOCAL_JAVA_LIBRARIES := core
+    LOCAL_JAVA_LIBRARIES := core-libart
     LOCAL_NO_STANDARD_LIBRARIES := true
 endif
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
@@ -58,30 +58,27 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_system_src_files)
 LOCAL_JAVACFLAGS := -encoding UTF-8
-LOCAL_JAVA_LIBRARIES := conscrypt core
+LOCAL_JAVA_LIBRARIES := core-libart conscrypt
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-tests
+LOCAL_MODULE := okhttp-tests-nojarjar
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_test_src_files)
 LOCAL_JAVACFLAGS := -encoding UTF-8
-LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := okhttp-nojarjar junit4-target mockwebserver bouncycastle-nojarjar
+LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar
 LOCAL_NO_STANDARD_LIBRARIES := true
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
-ifeq ($(WITH_HOST_DALVIK),true)
-    include $(CLEAR_VARS)
-    LOCAL_MODULE := okhttp-hostdex
-    LOCAL_MODULE_TAGS := optional
-    LOCAL_SRC_FILES := $(okhttp_system_src_files)
-    LOCAL_JAVACFLAGS := -encoding UTF-8
-    LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-    LOCAL_JAVA_LIBRARIES := conscrypt-hostdex
-    LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-    include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
-endif
+include $(CLEAR_VARS)
+LOCAL_MODULE := okhttp-hostdex
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(okhttp_system_src_files)
+LOCAL_JAVACFLAGS := -encoding UTF-8
+LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
+LOCAL_JAVA_LIBRARIES := conscrypt-hostdex
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57820a4..6140fb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,113 @@
 Change Log
 ==========
 
-Version 1.2.1 *(2013-08-23)*
-----------------------------
+## Version 1.5.3
+
+_2014-03-29_
+
+ * Fix bug where the Content-Length header was not always dropped when
+   following a redirect from a POST to a GET.
+ * Implement basic support for `Thread.interrupt()`. OkHttp now checks
+   for an interruption before doing a blocking call. If it is interrupted,
+   it throws an `InterruptedIOException`.
+
+## Version 1.5.2
+
+_2014-03-17_
+
+ * Fix bug where deleting a file that was absent from the `HttpResponseCache`
+   caused an IOException.
+ * Fix bug in HTTP/2 where our HPACK decoder wasn't emitting entries in
+   certain eviction scenarios, leading to dropped response headers.
+
+## Version 1.5.1
+
+_2014-03-11_
+
+ * Fix 1.5.0 regression where connections should not have been recycled.
+ * Fix 1.5.0 regression where transparent Gzip was broken by attempting to
+   recover from another I/O failure.
+ * Fix problems where spdy/3.1 headers may not have been compressed properly.
+ * Fix problems with spdy/3.1 and http/2 where the wrong window size was being
+   used.
+ * Fix 1.5.0 regression where conditional cache responses could corrupt the
+   connection pool.
+
+
+## Version 1.5.0
+
+_2014-03-07_
+
+
+##### OkHttp no longer uses the default SSL context.
+
+Applications that want to use the global SSL context with OkHttp should configure their
+OkHttpClient instances with the following:
+
+```java
+okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
+```
+
+A simpler solution is to avoid the shared default SSL socket factory. Instead, if you
+need to customize SSL, do so for your specific OkHttpClient instance only.
+
+##### Synthetic headers have changed
+
+Previously OkHttp added a synthetic response header, `OkHttp-Selected-Transport`. It
+has been replaced with a new synthetic header, `OkHttp-Selected-Protocol`.
+
+##### Changes
+
+ * New: Support for `HTTP-draft-09/2.0`.
+ * New: Support for `spdy/3.1`. Dropped support for `spdy/3`.
+ * New: Use ALPN on Android platforms that support it (4.4+)
+ * New: CacheControl model and parser.
+ * New: Protocol selection in MockWebServer.
+ * Fix: Route selection shouldn't use TLS modes that we know will fail.
+ * Fix: Cache SPDY responses even if the response body is closed prematurely.
+ * Fix: Use strict timeouts when aborting a download.
+ * Fix: Support Shoutcast HTTP responses like `ICY 200 OK`.
+ * Fix: Don't unzip if there isn't a response body.
+ * Fix: Don't leak gzip streams on redirects.
+ * Fix: Don't do DNS lookups on invalid hosts.
+ * Fix: Exhaust the underlying stream when reading gzip streams.
+ * Fix: Support the `PATCH` method.
+ * Fix: Support request bodies on `DELETE` method.
+ * Fix: Drop the `okhttp-protocols` module.
+ * Internal: Replaced internal byte array buffers with pooled buffers ("OkBuffer").
+
+
+## Version 1.3.0
+
+_2014-01-11_
+
+ * New: Support for "PATCH" HTTP method in client and MockWebServer.
+ * Fix: Drop `Content-Length` header when redirected from POST to GET.
+ * Fix: Correctly read cached header entries with malformed header names.
+ * Fix: Do not directly support any authentication schemes other than "Basic".
+ * Fix: Respect read timeouts on recycled connections.
+ * Fix: Transmit multiple cookie values as a single header with delimiter.
+ * Fix: Ensure `null` is never returned from a connection's `getHeaderFields()`.
+ * Fix: Persist proper `Content-Encoding` header to cache for GZip responses.
+ * Fix: Eliminate rare race condition in SPDY streams that would prevent connection reuse.
+ * Fix: Change HTTP date formats to UTC to conform to RFC2616 section 3.3.
+ * Fix: Support SPDY header blocks with trailing bytes.
+ * Fix: Allow `;` as separator for `Cache-Control` header.
+ * Fix: Correct bug where HTTPS POST requests were always automatically buffered.
+ * Fix: Honor read timeout when parsing SPDY headers.
+
+
+## Version 1.2.1
+
+_2013-08-23_
 
  * Resolve issue with 'jar-with-dependencies' artifact creation.
  * Fix: Support empty SPDY header values.
 
 
-Version 1.2.0 *(2013-08-11)*
-----------------------------
+## Version 1.2.0
+
+_2013-08-11_
 
  *  New APIs on OkHttpClient to set default timeouts for connect and read.
  *  Fix bug when caching SPDY responses.
@@ -34,15 +132,17 @@
  *  Bring MockWebServer into OkHttp and teach it SPDY.
 
 
-Version 1.1.1 *(2013-06-23)*
-----------------------------
+## Version 1.1.1
+
+_2013-06-23_
 
  * Fix: ClassCastException when caching responses that were redirected from
    HTTP to HTTPS.
 
 
-Version 1.1.0 *(2013-06-15)*
-----------------------------
+## Version 1.1.0
+
+_2013-06-15_
 
  * Fix: Connection reuse was broken for most HTTPS connections due to a bug in
    the way the hostname verifier was selected.
@@ -54,22 +154,26 @@
    Use `X-Android-Transports` to write the preferred transports and
    `X-Android-Selected-Transport` to read the negotiated transport.
 
-Version 1.0.2 *(2013-05-11)*
-----------------------------
+
+## Version 1.0.2
+
+_2013-05-11_
 
  * Fix: Remove use of Java 6-only APIs.
  * Fix: Properly handle exceptions from `NetworkInterface` when querying MTU.
  * Fix: Ensure MTU has a reasonable default and upper-bound.
 
 
-Version 1.0.1 *(2013-05-06)*
-----------------------------
+## Version 1.0.1
+
+_2013-05-06_
 
  * Correct casing of SSL in method names (`getSslSocketFactory`/`setSslSocketFactory`).
 
 
-Version 1.0.0 *(2013-05-06)*
-----------------------------
+## Version 1.0.0
+
+_2013-05-06_
 
 Initial release.
 
diff --git a/README.android b/README.android
index 1a99815..05c6114 100644
--- a/README.android
+++ b/README.android
@@ -4,7 +4,10 @@
 
 Local patches
 -------------
-- Changes to libcore/util/Libcore.java to remove OpenJDK / Jetty dependencies.
-- Change SpdyWriter.java to call Libcore#newDeflaterOutputStream instead
-  of using a hidden constructor.
 
+- Addition of classes in android/ :
+  - com.squareup.okhttp.internal.Platform - to replace the Platform class that
+    comes with okhttp. No use of reflection.
+  - com.squareup.okhttp.Http(s)Handler - integration with Android's corelibs.
+  - Removal of reference to a codehause annotation used in
+    okio/src/main/java/okio/DeflaterSink.java
diff --git a/README.md b/README.md
index 7b62596..cd3bd02 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,14 @@
 Building
 --------
 
+OkHttp requires Java 7 to build and run tests. Runtime compatibility with Java 6 is enforced as
+part of the build to ensure compliance with Android and older versions of the JVM.
+
+
+
+Testing
+-------
+
 ### On the Desktop
 
 Run OkHttp tests on the desktop with Maven. Running SPDY tests on the desktop uses
@@ -45,10 +53,11 @@
 mvn clean
 mvn package -DskipTests
 vogar \
-    --classpath ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.47/bcprov-jdk15on-1.47.jar \
-    --classpath ~/.m2/repository/com/google/mockwebserver/mockwebserver/20130122/mockwebserver-20130122.jar \
-    --classpath target/okhttp-0.9-SNAPSHOT.jar \
-    ./src/test/java
+    --classpath ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48.jar \
+    --classpath mockwebserver/target/mockwebserver-2.0.0-SNAPSHOT.jar \
+    --classpath okhttp-protocols/target/okhttp-protocols-2.0.0-SNAPSHOT.jar \
+    --classpath okhttp/target/okhttp-2.0.0-SNAPSHOT.jar \
+    okhttp/src/test
 ```
 
 MockWebServer
@@ -91,7 +100,7 @@
 
 
  [1]: http://square.github.io/okhttp
- [2]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST&c=jar-with-dependencies
+ [2]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST
  [3]: http://wiki.eclipse.org/Jetty/Feature/NPN
  [4]: https://code.google.com/p/vogar/
- [5]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=mockwebserver&v=LATEST&c=jar-with-dependencies
+ [5]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=mockwebserver&v=LATEST
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
new file mode 100644
index 0000000..36c3101
--- /dev/null
+++ b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
@@ -0,0 +1,100 @@
+/*
+ *  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;
+
+import libcore.net.event.NetworkEventDispatcher;
+import libcore.net.event.NetworkEventListener;
+
+/**
+ * A provider of the shared Android {@link ConnectionPool}. This class is aware of network
+ * configuration change events: When the network configuration changes the pool object is discarded
+ * and a later calls to {@link #get()} will return a new pool.
+ */
+public class ConfigAwareConnectionPool {
+
+  private static final long CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
+
+  private static final int CONNECTION_POOL_MAX_IDLE_CONNECTIONS;
+  private static final long CONNECTION_POOL_KEEP_ALIVE_DURATION_MS;
+  static {
+    String keepAliveProperty = System.getProperty("http.keepAlive");
+    String keepAliveDurationProperty = System.getProperty("http.keepAliveDuration");
+    String maxIdleConnectionsProperty = System.getProperty("http.maxConnections");
+    CONNECTION_POOL_KEEP_ALIVE_DURATION_MS = (keepAliveDurationProperty != null
+        ? Long.parseLong(keepAliveDurationProperty)
+        : CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS);
+    if (keepAliveProperty != null && !Boolean.parseBoolean(keepAliveProperty)) {
+      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 0;
+    } else if (maxIdleConnectionsProperty != null) {
+      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = Integer.parseInt(maxIdleConnectionsProperty);
+    } else {
+      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 5;
+    }
+  }
+
+  private static final ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool();
+
+  private final NetworkEventDispatcher networkEventDispatcher;
+
+  /**
+   * {@code true} if the ConnectionPool reset has been registered with the
+   * {@link NetworkEventDispatcher}.
+   */
+  private boolean networkEventListenerRegistered;
+
+  private ConnectionPool connectionPool;
+
+  /** Visible for testing. Use {@link #getInstance()} */
+  protected ConfigAwareConnectionPool(NetworkEventDispatcher networkEventDispatcher) {
+    this.networkEventDispatcher = networkEventDispatcher;
+  }
+
+  private ConfigAwareConnectionPool() {
+    networkEventDispatcher = NetworkEventDispatcher.getInstance();
+  }
+
+  public static ConfigAwareConnectionPool getInstance() {
+    return instance;
+  }
+
+  /**
+   * Returns the current {@link ConnectionPool} to use.
+   */
+  public synchronized ConnectionPool get() {
+    if (connectionPool == null) {
+      // Only register the listener once the first time a ConnectionPool is created.
+      if (!networkEventListenerRegistered) {
+        networkEventDispatcher.addListener(new NetworkEventListener() {
+          @Override
+          public void onNetworkConfigurationChanged() {
+            synchronized (ConfigAwareConnectionPool.this) {
+              // If the network config has changed then existing pooled connections should not be
+              // re-used. By setting connectionPool to null it ensures that the next time
+              // getConnectionPool() is called a new pool will be created.
+              connectionPool = null;
+            }
+          }
+        });
+        networkEventListenerRegistered = true;
+      }
+      connectionPool = new ConnectionPool(
+          CONNECTION_POOL_MAX_IDLE_CONNECTIONS, CONNECTION_POOL_KEEP_ALIVE_DURATION_MS);
+    }
+    return connectionPool;
+  }
+}
diff --git a/android/main/java/com/squareup/okhttp/HttpHandler.java b/android/main/java/com/squareup/okhttp/HttpHandler.java
index c656c04..bcb47b0 100644
--- a/android/main/java/com/squareup/okhttp/HttpHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpHandler.java
@@ -19,11 +19,16 @@
 
 import java.io.IOException;
 import java.net.Proxy;
+import java.net.ResponseCache;
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.URLStreamHandler;
 
 public class HttpHandler extends URLStreamHandler {
+
+    private final ConfigAwareConnectionPool configAwareConnectionPool =
+            ConfigAwareConnectionPool.getInstance();
+
     @Override protected URLConnection openConnection(URL url) throws IOException {
         return newOkHttpClient(null /* proxy */).open(url);
     }
@@ -40,12 +45,29 @@
     }
 
     protected OkHttpClient newOkHttpClient(Proxy proxy) {
+        OkHttpClient okHttpClient = createHttpOkHttpClient(proxy);
+        okHttpClient.setConnectionPool(configAwareConnectionPool.get());
+        return okHttpClient;
+    }
+
+    /**
+     * Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
+     * Android.
+     */
+    public static OkHttpClient createHttpOkHttpClient(Proxy proxy) {
         OkHttpClient client = new OkHttpClient();
         client.setFollowProtocolRedirects(false);
         if (proxy != null) {
             client.setProxy(proxy);
         }
 
+        // Explicitly set the response cache.
+        ResponseCache responseCache = ResponseCache.getDefault();
+        if (responseCache != null) {
+            client.setResponseCache(responseCache);
+        }
+
         return client;
     }
+
 }
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index 421b7ff..e3372f2 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -17,11 +17,7 @@
 
 package com.squareup.okhttp;
 
-import java.io.IOException;
 import java.net.Proxy;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
 import java.util.Arrays;
 import java.util.List;
 
@@ -30,7 +26,9 @@
 import javax.net.ssl.HttpsURLConnection;
 
 public final class HttpsHandler extends HttpHandler {
-    private static final List<String> ENABLED_TRANSPORTS = Arrays.asList("http/1.1");
+    private static final List<Protocol> ENABLED_PROTOCOLS = Arrays.asList(Protocol.HTTP_11);
+    private final ConfigAwareConnectionPool configAwareConnectionPool =
+            ConfigAwareConnectionPool.getInstance();
 
     @Override protected int getDefaultPort() {
         return 443;
@@ -38,8 +36,20 @@
 
     @Override
     protected OkHttpClient newOkHttpClient(Proxy proxy) {
-        OkHttpClient client = super.newOkHttpClient(proxy);
-        client.setTransports(ENABLED_TRANSPORTS);
+        OkHttpClient okHttpClient = createHttpsOkHttpClient(proxy);
+        okHttpClient.setConnectionPool(configAwareConnectionPool.get());
+        return okHttpClient;
+    }
+
+    /**
+     * Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
+     * Android.
+     */
+    public static OkHttpClient createHttpsOkHttpClient(Proxy proxy) {
+        // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
+        OkHttpClient client = HttpHandler.createHttpOkHttpClient(proxy);
+
+        client.setProtocols(ENABLED_PROTOCOLS);
 
         HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
         // Assume that the internal verifier is better than the
@@ -47,6 +57,10 @@
         if (!(verifier instanceof DefaultHostnameVerifier)) {
             client.setHostnameVerifier(verifier);
         }
+        // OkHttp does not automatically honor the system-wide SSLSocketFactory set with
+        // HttpsURLConnection.setDefaultSSLSocketFactory().
+        // See https://github.com/square/okhttp/issues/184 for details.
+        client.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
 
         return client;
     }
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 0fb5f26..7d0e847 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -20,16 +20,19 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.InetSocketAddress;
-import java.net.NetworkInterface;
 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.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 import javax.net.ssl.SSLSocket;
+
 import com.android.org.conscrypt.OpenSSLSocketImpl;
+import com.squareup.okhttp.Protocol;
+import okio.ByteString;
 
 /**
  * Access to proprietary Android APIs. Doesn't use reflection.
@@ -37,12 +40,6 @@
 public final class Platform {
     private static final Platform PLATFORM = new Platform();
 
-    /*
-     * Default for the maximum transmission unit, used only if
-     * there's an error retrieving it via NetworkInterface.
-     */
-    private static final int DEFAULT_MTU = 1400;
-
     public static Platform get() {
         return PLATFORM;
     }
@@ -99,19 +96,31 @@
     /**
      * Returns the negotiated protocol, or null if no protocol was negotiated.
      */
-    public byte[] getNpnSelectedProtocol(SSLSocket socket) {
-        return socket instanceof OpenSSLSocketImpl
-                ? ((OpenSSLSocketImpl) socket).getNpnSelectedProtocol()
-                : null;
+    public ByteString getNpnSelectedProtocol(SSLSocket socket) {
+        if (!(socket instanceof OpenSSLSocketImpl)) {
+            return null;
+        }
+
+        OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
+        // Prefer ALPN's result if it is present.
+        byte[] alpnResult = socketImpl.getAlpnSelectedProtocol();
+        if (alpnResult != null) {
+            return ByteString.of(alpnResult);
+        }
+        byte[] npnResult = socketImpl.getNpnSelectedProtocol();
+        return npnResult == null ? null : ByteString.of(npnResult);
     }
 
     /**
      * Sets client-supported protocols on a socket to send to a server. The
      * protocols are only sent if the socket implementation supports NPN.
      */
-    public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+    public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
         if (socket instanceof OpenSSLSocketImpl) {
-            ((OpenSSLSocketImpl) socket).setNpnProtocols(npnProtocols);
+            OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
+            byte[] protocols = concatLengthPrefixed(npnProtocols);
+            socketImpl.setAlpnProtocols(protocols);
+            socketImpl.setNpnProtocols(protocols);
         }
     }
 
@@ -125,28 +134,6 @@
         return new DeflaterOutputStream(out, deflater, syncFlush);
     }
 
-    /**
-     * Returns the maximum transmission unit of the network interface used by
-     * {@code socket}, or a reasonable default if there's an error retrieving
-     * it from the socket.
-     *
-     * <p>The returned value should only be used as an optimization; such as to
-     * size buffers efficiently.
-     */
-    public int getMtu(Socket socket) {
-        try {
-            NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
-                socket.getLocalAddress());
-            if (networkInterface != null) {
-                return networkInterface.getMTU();
-            }
-
-            return DEFAULT_MTU;
-        } catch (SocketException exception) {
-            return DEFAULT_MTU;
-        }
-    }
-
     public void connectSocket(Socket socket, InetSocketAddress address,
               int connectTimeout) throws IOException {
         socket.connect(address, connectTimeout);
@@ -156,4 +143,26 @@
     public String getPrefix() {
         return "X-Android";
     }
+
+    /**
+     * Concatenation of 8-bit, length prefixed protocol names.
+     *
+     * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
+     */
+    static byte[] concatLengthPrefixed(List<Protocol> protocols) {
+        int size = 0;
+        for (Protocol protocol : protocols) {
+            size += protocol.name.size() + 1; // add a byte for 8-bit length prefix.
+        }
+        byte[] result = new byte[size];
+        int pos = 0;
+        for (Protocol protocol : protocols) {
+            int nameSize = protocol.name.size();
+            result[pos++] = (byte) nameSize;
+            // toByteArray allocates an array, but this is only called on new connections.
+            System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize);
+            pos += nameSize;
+        }
+        return result;
+    }
 }
diff --git a/android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java b/android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java
new file mode 100644
index 0000000..6477e10
--- /dev/null
+++ b/android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java
@@ -0,0 +1,49 @@
+/*
+ *  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;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import libcore.net.event.NetworkEventDispatcher;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Tests for {@link ConfigAwareConnectionPool}.
+ */
+public class ConfigAwareConnectionPoolTest {
+
+  @Test
+  public void getInstance() {
+    assertSame(ConfigAwareConnectionPool.getInstance(), ConfigAwareConnectionPool.getInstance());
+  }
+
+  @Test
+  public void get() throws Exception {
+    NetworkEventDispatcher networkEventDispatcher = new NetworkEventDispatcher() {};
+    ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool(networkEventDispatcher) {};
+    assertSame(instance.get(), instance.get());
+
+    ConnectionPool beforeEventInstance = instance.get();
+    networkEventDispatcher.onNetworkConfigurationChanged();
+
+    assertNotSame(beforeEventInstance, instance.get());
+  }
+}
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..59f571f
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,8 @@
+OkHttp Benchmarks
+=======================================
+
+This module allows you to test the performance of HTTP clients.
+
+### Running
+  1. If you made modifications to `com.squareup.okhttp.benchmarks.Benchmark` run `mvn compile`.
+  2. Run `mvn exec:exec` to launch a new JVM, which will execute the benchmark.
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
new file mode 100644
index 0000000..6425e64
--- /dev/null
+++ b/benchmarks/pom.xml
@@ -0,0 +1,98 @@
+<?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.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>benchmarks</artifactId>
+  <name>Benchmarks</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.caliper</groupId>
+      <artifactId>caliper</artifactId>
+      <version>1.0-beta-1</version>
+    </dependency>
+    <!-- caliper needs to be updated to be compatible with guava 16 -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>14.0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty.npn</groupId>
+      <artifactId>npn-boot</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport</artifactId>
+      <version>4.0.15.Final</version>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-handler</artifactId>
+      <version>4.0.15.Final</version>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-codec-http</artifactId>
+      <version>4.0.15.Final</version>
+    </dependency>
+    <!-- Netty needs this if gzip is enabled. -->
+    <dependency>
+      <groupId>com.jcraft</groupId>
+      <artifactId>jzlib</artifactId>
+      <version>1.1.2</version>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+    <plugin>
+      <groupId>org.codehaus.mojo</groupId>
+      <artifactId>exec-maven-plugin</artifactId>
+      <executions>
+        <execution>
+          <goals>
+            <goal>java</goal>
+          </goals>
+        </execution>
+      </executions>
+      <configuration>
+        <executable>java</executable>
+        <arguments>
+          <argument>-Xms512m</argument>
+          <argument>-Xmx512m</argument>
+          <commandlineArgs>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</commandlineArgs>
+          <argument>-classpath</argument>
+          <classpath/>
+          <argument>com.squareup.okhttp.benchmarks.Benchmark</argument>
+        </arguments>
+      </configuration>
+    </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
new file mode 100644
index 0000000..cb8e719
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.squareup.okhttp.internal.SslContextBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.GZIPInputStream;
+import javax.net.ssl.SSLContext;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.PoolingClientConnectionManager;
+
+/** Benchmark Apache HTTP client. */
+class ApacheHttpClient extends SynchronousHttpClient {
+  private static final boolean VERBOSE = false;
+
+  private HttpClient client;
+
+  @Override public void prepare(Benchmark benchmark) {
+    super.prepare(benchmark);
+    ClientConnectionManager connectionManager = new PoolingClientConnectionManager();
+    if (benchmark.tls) {
+      SSLContext sslContext = SslContextBuilder.localhost();
+      connectionManager.getSchemeRegistry().register(
+          new Scheme("https", 443, new SSLSocketFactory(sslContext)));
+    }
+    client = new DefaultHttpClient(connectionManager);
+  }
+
+  @Override public Runnable request(URL url) {
+    return new ApacheHttpClientRequest(url);
+  }
+
+  class ApacheHttpClientRequest implements Runnable {
+    private final URL url;
+
+    public ApacheHttpClientRequest(URL url) {
+      this.url = url;
+    }
+
+    public void run() {
+      long start = System.nanoTime();
+      try {
+        HttpResponse response = client.execute(new HttpGet(url.toString()));
+        InputStream in = response.getEntity().getContent();
+        Header contentEncoding = response.getFirstHeader("Content-Encoding");
+        if (contentEncoding != null && contentEncoding.getValue().equals("gzip")) {
+          in = new GZIPInputStream(in);
+        }
+
+        long total = readAllAndClose(in);
+        long finish = System.nanoTime();
+
+        if (VERBOSE) {
+          System.out.println(String.format("Transferred % 8d bytes in %4d ms",
+              total, TimeUnit.NANOSECONDS.toMillis(finish - start)));
+        }
+      } catch (IOException e) {
+        System.out.println("Failed: " + e);
+      }
+    }
+  }
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
new file mode 100644
index 0000000..151128d
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.google.caliper.Param;
+import com.google.caliper.model.ArbitraryMeasurement;
+import com.google.caliper.runner.CaliperMain;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.Dispatcher;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.SSLContext;
+
+/**
+ * This benchmark is fake, but may be useful for certain relative comparisons.
+ * It uses a local connection to a MockWebServer to measure how many identical
+ * requests per second can be carried over a fixed number of threads.
+ */
+public class Benchmark extends com.google.caliper.Benchmark {
+  private static final int NUM_REPORTS = 10;
+  private static final boolean VERBOSE = false;
+
+  private final Random random = new Random(0);
+
+  /** Which client to run.*/
+  @Param
+  Client client;
+
+  /** How many concurrent requests to execute. */
+  @Param({ "1", "10" })
+  int concurrencyLevel;
+
+  /** How many requests to enqueue to await threads to execute them. */
+  @Param({ "10" })
+  int targetBacklog;
+
+  /** True to use TLS. */
+  // TODO: compare different ciphers?
+  @Param
+  boolean tls;
+
+  /** True to use gzip content-encoding for the response body. */
+  @Param
+  boolean gzip;
+
+  /** Don't combine chunked with SPDY_3 or HTTP_2; that's not allowed. */
+  @Param
+  boolean chunked;
+
+  /** The size of the HTTP response body, in uncompressed bytes. */
+  @Param({ "128", "1048576" })
+  int bodyByteCount;
+
+  /** How many additional headers were included, beyond the built-in ones. */
+  @Param({ "0", "20" })
+  int headerCount;
+
+  /** Which ALPN/NPN protocols are in use. Only useful with TLS. */
+  List<Protocol> protocols = Arrays.asList(Protocol.HTTP_11);
+
+  public static void main(String[] args) {
+    List<String> allArgs = new ArrayList<String>();
+    allArgs.add("--instrument");
+    allArgs.add("arbitrary");
+    allArgs.addAll(Arrays.asList(args));
+
+    CaliperMain.main(Benchmark.class, allArgs.toArray(new String[allArgs.size()]));
+  }
+
+  @ArbitraryMeasurement(description = "requests per second")
+  public double run() throws Exception {
+    if (VERBOSE) System.out.println(toString());
+    HttpClient httpClient = client.create();
+
+    // Prepare the client & server
+    httpClient.prepare(this);
+    MockWebServer server = startServer();
+    URL url = server.getUrl("/");
+
+    int requestCount = 0;
+    long reportStart = System.nanoTime();
+    long reportPeriod = TimeUnit.SECONDS.toNanos(1);
+    int reports = 0;
+    double best = 0.0;
+
+    // Run until we've printed enough reports.
+    while (reports < NUM_REPORTS) {
+      // Print a report if we haven't recently.
+      long now = System.nanoTime();
+      double reportDuration = now - reportStart;
+      if (reportDuration > reportPeriod) {
+        double requestsPerSecond = requestCount / reportDuration * TimeUnit.SECONDS.toNanos(1);
+        if (VERBOSE) {
+          System.out.println(String.format("Requests per second: %.1f", requestsPerSecond));
+        }
+        best = Math.max(best, requestsPerSecond);
+        requestCount = 0;
+        reportStart = now;
+        reports++;
+      }
+
+      // Fill the job queue with work.
+      while (httpClient.acceptingJobs()) {
+        httpClient.enqueue(url);
+        requestCount++;
+      }
+
+      // The job queue is full. Take a break.
+      sleep(1);
+    }
+
+    return best;
+  }
+
+  @Override public String toString() {
+    List<Object> modifiers = new ArrayList<Object>();
+    if (tls) modifiers.add("tls");
+    if (gzip) modifiers.add("gzip");
+    if (chunked) modifiers.add("chunked");
+    modifiers.addAll(protocols);
+
+    return String.format("%s %s\nbodyByteCount=%s headerCount=%s concurrencyLevel=%s",
+        client, modifiers, bodyByteCount, headerCount, concurrencyLevel);
+  }
+
+  private void sleep(int millis) {
+    try {
+      Thread.sleep(millis);
+    } catch (InterruptedException ignored) {
+    }
+  }
+
+  private MockWebServer startServer() throws IOException {
+    Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING);
+    MockWebServer server = new MockWebServer();
+
+    if (tls) {
+      SSLContext sslContext = SslContextBuilder.localhost();
+      server.useHttps(sslContext.getSocketFactory(), false);
+      server.setNpnEnabled(true);
+      server.setNpnProtocols(protocols);
+    }
+
+    final MockResponse response = newResponse();
+    server.setDispatcher(new Dispatcher() {
+      @Override public MockResponse dispatch(RecordedRequest request) {
+        return response;
+      }
+    });
+
+    server.play();
+    return server;
+  }
+
+  private MockResponse newResponse() throws IOException {
+    byte[] body = new byte[bodyByteCount];
+    random.nextBytes(body);
+
+    MockResponse result = new MockResponse();
+
+    if (gzip) {
+      body = gzip(body);
+      result.addHeader("Content-Encoding: gzip");
+    }
+
+    if (chunked) {
+      result.setChunkedBody(body, 1024);
+    } else {
+      result.setBody(body);
+    }
+
+    for (int i = 0; i < headerCount; i++) {
+      result.addHeader(randomString(12), randomString(20));
+    }
+
+    return result;
+  }
+
+  private String randomString(int length) {
+    String alphabet = "-abcdefghijklmnopqrstuvwxyz";
+    char[] result = new char[length];
+    for (int i = 0; i < length; i++) {
+      result[i] = alphabet.charAt(random.nextInt(alphabet.length()));
+    }
+    return new String(result);
+  }
+
+  /** Returns a gzipped copy of {@code bytes}. */
+  private byte[] gzip(byte[] bytes) throws IOException {
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
+    gzippedOut.write(bytes);
+    gzippedOut.close();
+    return bytesOut.toByteArray();
+  }
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Client.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Client.java
new file mode 100644
index 0000000..bd777aa
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Client.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+enum Client {
+  OkHttp {
+    @Override HttpClient create() {
+      return new OkHttp();
+    }
+  },
+
+  OkHttpAsync {
+    @Override HttpClient create() {
+      return new OkHttpAsync();
+    }
+  },
+
+  Apache {
+    @Override HttpClient create() {
+      return new ApacheHttpClient();
+    }
+  },
+
+  UrlConnection {
+    @Override HttpClient create() {
+      return new UrlConnection();
+    }
+  },
+
+  Netty {
+    @Override HttpClient create() {
+      return new NettyHttpClient();
+    }
+  };
+
+  abstract HttpClient create();
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java
new file mode 100644
index 0000000..136c5d8
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import java.net.URL;
+
+/** An HTTP client to benchmark. */
+interface HttpClient {
+  void prepare(Benchmark benchmark);
+  void enqueue(URL url) throws Exception;
+  boolean acceptingJobs();
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
new file mode 100644
index 0000000..9044d0a
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.PooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpContent;
+import io.netty.handler.codec.http.HttpContentDecompressor;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.LastHttpContent;
+import io.netty.handler.ssl.SslHandler;
+import java.net.URL;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+
+/** Netty isn't an HTTP client, but it's almost one. */
+class NettyHttpClient implements HttpClient {
+  private static final boolean VERBOSE = false;
+
+  // Guarded by this. Real apps need more capable connection management.
+  private final Deque<HttpChannel> freeChannels = new ArrayDeque<HttpChannel>();
+  private final Deque<URL> backlog = new ArrayDeque<URL>();
+
+  private int totalChannels = 0;
+  private int concurrencyLevel;
+  private int targetBacklog;
+  private Bootstrap bootstrap;
+
+  @Override public void prepare(final Benchmark benchmark) {
+    this.concurrencyLevel = benchmark.concurrencyLevel;
+    this.targetBacklog = benchmark.targetBacklog;
+
+    ChannelInitializer<SocketChannel> channelInitializer = new ChannelInitializer<SocketChannel>() {
+      @Override public void initChannel(SocketChannel channel) throws Exception {
+        ChannelPipeline pipeline = channel.pipeline();
+
+        if (benchmark.tls) {
+          SSLContext sslContext = SslContextBuilder.localhost();
+          SSLEngine engine = sslContext.createSSLEngine();
+          engine.setUseClientMode(true);
+          pipeline.addLast("ssl", new SslHandler(engine));
+        }
+
+        pipeline.addLast("codec", new HttpClientCodec());
+        pipeline.addLast("inflater", new HttpContentDecompressor());
+        pipeline.addLast("handler", new HttpChannel(channel));
+      }
+    };
+
+    bootstrap = new Bootstrap();
+    bootstrap.group(new NioEventLoopGroup(concurrencyLevel))
+        .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
+        .channel(NioSocketChannel.class)
+        .handler(channelInitializer);
+  }
+
+  @Override public void enqueue(URL url) throws Exception {
+    HttpChannel httpChannel = null;
+    synchronized (this) {
+      if (!freeChannels.isEmpty()) {
+        httpChannel = freeChannels.pop();
+      } else if (totalChannels < concurrencyLevel) {
+        totalChannels++; // Create a new channel. (outside of the synchronized block).
+      } else {
+        backlog.add(url); // Enqueue this for later, to be picked up when another request completes.
+        return;
+      }
+    }
+    if (httpChannel == null) {
+      Channel channel = bootstrap.connect(url.getHost(), Util.getEffectivePort(url))
+          .sync().channel();
+      httpChannel = (HttpChannel) channel.pipeline().last();
+    }
+    httpChannel.sendRequest(url);
+  }
+
+  @Override public synchronized boolean acceptingJobs() {
+    return backlog.size() < targetBacklog || hasFreeChannels();
+  }
+
+  private boolean hasFreeChannels() {
+    int activeChannels = totalChannels - freeChannels.size();
+    return activeChannels < concurrencyLevel;
+  }
+
+  private void release(HttpChannel httpChannel) {
+    URL url;
+    synchronized (this) {
+      url = backlog.pop();
+      if (url == null) {
+        // There were no URLs in the backlog. Pool this channel for later.
+        freeChannels.push(httpChannel);
+        return;
+      }
+    }
+
+    // We removed a URL from the backlog. Schedule it right away.
+    httpChannel.sendRequest(url);
+  }
+
+  class HttpChannel extends SimpleChannelInboundHandler<HttpObject> {
+    private final SocketChannel channel;
+    byte[] buffer = new byte[1024];
+    int total;
+    long start;
+
+    public HttpChannel(SocketChannel channel) {
+      this.channel = channel;
+    }
+
+    private void sendRequest(URL url) {
+      start = System.nanoTime();
+      total = 0;
+      HttpRequest request = new DefaultFullHttpRequest(
+          HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath());
+      request.headers().set(HttpHeaders.Names.HOST, url.getHost());
+      request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);
+      channel.writeAndFlush(request);
+    }
+
+    @Override protected void channelRead0(
+        ChannelHandlerContext context, HttpObject message) throws Exception {
+      if (message instanceof HttpResponse) {
+        receive((HttpResponse) message);
+      }
+      if (message instanceof HttpContent) {
+        receive((HttpContent) message);
+        if (message instanceof LastHttpContent) {
+          release(this);
+        }
+      }
+    }
+
+    @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+      super.channelInactive(ctx);
+    }
+
+    void receive(HttpResponse response) {
+      // Don't do anything with headers.
+    }
+
+    void receive(HttpContent content) {
+      // Consume the response body.
+      ByteBuf byteBuf = content.content();
+      for (int toRead; (toRead = byteBuf.readableBytes()) > 0; ) {
+        byteBuf.readBytes(buffer, 0, Math.min(buffer.length, toRead));
+        total += toRead;
+      }
+
+      if (VERBOSE && content instanceof LastHttpContent) {
+        long finish = System.nanoTime();
+        System.out.println(String.format("Transferred % 8d bytes in %4d ms",
+            total, TimeUnit.NANOSECONDS.toMillis(finish - start)));
+      }
+    }
+
+    @Override public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
+      System.out.println("Failed: " + cause);
+    }
+  }
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
new file mode 100644
index 0000000..03b9e3c
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+
+class OkHttp extends SynchronousHttpClient {
+  private static final boolean VERBOSE = false;
+
+  private OkHttpClient client;
+
+  @Override public void prepare(Benchmark benchmark) {
+    super.prepare(benchmark);
+    client = new OkHttpClient();
+    client.setProtocols(benchmark.protocols);
+
+    if (benchmark.tls) {
+      SSLContext sslContext = SslContextBuilder.localhost();
+      SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+      HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+        @Override public boolean verify(String s, SSLSession session) {
+          return true;
+        }
+      };
+      client.setSslSocketFactory(socketFactory);
+      client.setHostnameVerifier(hostnameVerifier);
+    }
+  }
+
+  @Override public Runnable request(URL url) {
+    return new OkHttpRequest(url);
+  }
+
+  class OkHttpRequest implements Runnable {
+    private final URL url;
+
+    public OkHttpRequest(URL url) {
+      this.url = url;
+    }
+
+    public void run() {
+      long start = System.nanoTime();
+      try {
+        HttpURLConnection urlConnection = client.open(url);
+        long total = readAllAndClose(urlConnection.getInputStream());
+        long finish = System.nanoTime();
+
+        if (VERBOSE) {
+          System.out.println(String.format("Transferred % 8d bytes in %4d ms",
+              total, TimeUnit.NANOSECONDS.toMillis(finish - start)));
+        }
+      } catch (IOException e) {
+        System.out.println("Failed: " + e);
+      }
+    }
+  }
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
new file mode 100644
index 0000000..b7633b7
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.squareup.okhttp.Dispatcher;
+import com.squareup.okhttp.Failure;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import java.io.IOException;
+import java.net.URL;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+
+class OkHttpAsync implements HttpClient {
+  private static final boolean VERBOSE = false;
+
+  private final AtomicInteger requestsInFlight = new AtomicInteger();
+
+  private OkHttpClient client;
+  private Response.Receiver receiver;
+  private int concurrencyLevel;
+  private int targetBacklog;
+
+  @Override public void prepare(final Benchmark benchmark) {
+    concurrencyLevel = benchmark.concurrencyLevel;
+    targetBacklog = benchmark.targetBacklog;
+
+    client = new OkHttpClient();
+    client.setProtocols(benchmark.protocols);
+    client.setDispatcher(new Dispatcher(new ThreadPoolExecutor(benchmark.concurrencyLevel,
+        benchmark.concurrencyLevel, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>())));
+
+    if (benchmark.tls) {
+      SSLContext sslContext = SslContextBuilder.localhost();
+      SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+      HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+        @Override public boolean verify(String s, SSLSession session) {
+          return true;
+        }
+      };
+      client.setSslSocketFactory(socketFactory);
+      client.setHostnameVerifier(hostnameVerifier);
+    }
+
+    receiver = new Response.Receiver() {
+      @Override public void onFailure(Failure failure) {
+        System.out.println("Failed: " + failure.exception());
+      }
+
+      @Override public boolean onResponse(Response response) throws IOException {
+        Response.Body body = response.body();
+        long total = SynchronousHttpClient.readAllAndClose(body.byteStream());
+        long finish = System.nanoTime();
+        if (VERBOSE) {
+          long start = (Long) response.request().tag();
+          System.out.printf("Transferred % 8d bytes in %4d ms%n",
+              total, TimeUnit.NANOSECONDS.toMillis(finish - start));
+        }
+        requestsInFlight.decrementAndGet();
+        return true;
+      }
+    };
+  }
+
+  @Override public void enqueue(URL url) throws Exception {
+    requestsInFlight.incrementAndGet();
+    client.enqueue(new Request.Builder().tag(System.nanoTime()).url(url).build(), receiver);
+  }
+
+  @Override public synchronized boolean acceptingJobs() {
+    return requestsInFlight.get() < (concurrencyLevel + targetBacklog);
+  }
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java
new file mode 100644
index 0000000..b15eedc
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Any HTTP client with a blocking API. */
+abstract class SynchronousHttpClient implements HttpClient {
+  ThreadPoolExecutor executor;
+  int targetBacklog;
+
+  @Override public void prepare(Benchmark benchmark) {
+    this.targetBacklog = benchmark.targetBacklog;
+    executor = new ThreadPoolExecutor(benchmark.concurrencyLevel, benchmark.concurrencyLevel,
+        1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  }
+
+  @Override public void enqueue(URL url) {
+    executor.execute(request(url));
+  }
+
+  @Override public boolean acceptingJobs() {
+    return executor.getQueue().size() < targetBacklog;
+  }
+
+  static long readAllAndClose(InputStream in) throws IOException {
+    byte[] buffer = new byte[1024];
+    long total = 0;
+    for (int count; (count = in.read(buffer)) != -1; ) {
+      total += count;
+    }
+    in.close();
+    return total;
+  }
+
+  abstract Runnable request(URL url);
+}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
new file mode 100644
index 0000000..79abb69
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.benchmarks;
+
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.GZIPInputStream;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+
+class UrlConnection extends SynchronousHttpClient {
+  private static final boolean VERBOSE = false;
+
+  @Override public void prepare(Benchmark benchmark) {
+    super.prepare(benchmark);
+    if (benchmark.tls) {
+      SSLContext sslContext = SslContextBuilder.localhost();
+      SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+      HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+        @Override public boolean verify(String s, SSLSession session) {
+          return true;
+        }
+      };
+      HttpsURLConnectionImpl.setDefaultHostnameVerifier(hostnameVerifier);
+      HttpsURLConnectionImpl.setDefaultSSLSocketFactory(socketFactory);
+    }
+  }
+
+  @Override public Runnable request(URL url) {
+    return new UrlConnectionRequest(url);
+  }
+
+  static class UrlConnectionRequest implements Runnable {
+    private final URL url;
+
+    public UrlConnectionRequest(URL url) {
+      this.url = url;
+    }
+
+    public void run() {
+      long start = System.nanoTime();
+      try {
+        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+        InputStream in = urlConnection.getInputStream();
+        if ("gzip".equals(urlConnection.getHeaderField("Content-Encoding"))) {
+          in = new GZIPInputStream(in);
+        }
+
+        long total = readAllAndClose(in);
+        long finish = System.nanoTime();
+
+        if (VERBOSE) {
+          System.out.println(String.format("Transferred % 8d bytes in %4d ms",
+              total, TimeUnit.NANOSECONDS.toMillis(finish - start)));
+        }
+      } catch (IOException e) {
+        System.out.println("Failed: " + e);
+      }
+    }
+  }
+}
diff --git a/concurrency.md b/concurrency.md
new file mode 100644
index 0000000..0858133
--- /dev/null
+++ b/concurrency.md
@@ -0,0 +1,63 @@
+# Concurrency in OkHttp
+
+The HttpURLConnection API is a blocking API. You make a blocking write to send a request, and a blocking read to receive the response.
+
+#### Blocking APIs
+
+Blocking APIs are convenient because you get top-to-bottom procedural code without indirection. Network calls work like regular method calls: ask for data and it is returned. If the request fails, you get a stacktrace right were the call was made.
+
+Blocking APIs may be inefficient because you hold a thread idle while waiting on the network. Threads are expensive because they have both a memory overhead and a context-switching overhead.
+
+#### Framed protocols
+
+Framed protocols like spdy/3 and http/2 don't lend themselves to blocking APIs. Each application-layer thread wants to do blocking I/O for a specific stream, but the streams are multiplexed on the socket. You can't just talk to the socket, you need to cooperate with the other application-layer threads that you're sharing it with.
+
+Framing rules make it impractical to implement spdy/3 or http/2 correctly on a single blocking thread. The flow-control features introduce feedback between reads and writes, requiring writes to acknowledge reads and reads to throttle writes.
+
+In OkHttp we expose a blocking API over a framed protocol. This document explains the code and policy that makes that work.
+
+## Threads
+
+#### Application's calling thread
+
+The application-layer must block on writing I/O. We can't return from a write until we've pushed its bytes onto the socket. Otherwise, if the write fails we are unable to deliver its IOException to the application. We would have told the application layer that the write succeeded, but it didn't!
+
+The application-layer can also do blocking reads. If the application asks to read and there's nothing available, we need to hold that thread until either the bytes arrive, the stream is closed, or a timeout elapses. If we get bytes but there's nobody asking for them, we buffer them. We don't consider bytes as delivered for flow control until they're consumed by the application.
+
+Consider an application streaming a video over http/2. Perhaps the user pauses the video and the application stops reading bytes from this stream. The buffer will fill up, and flow control prevents the server from sending more data on this stream. When the user unpauses her video the buffer drains, the read is acknowledged, and the server proceeds to stream data.
+
+#### Shared reader thread
+
+We can't rely on application threads to read data from the socket. Application threads are transient: sometimes they're reading and writing and sometimes they're off doing application-layer things. But the socket is permanent, and it needs constant attention: we dispatch all incoming frames so the connection is good-to-go when the application layer needs it.
+
+So we have a dedicated thread for every socket that just reads frames and dispatches them.
+
+The reader thread must never run application-layer code. Otherwise one slow stream can hold up the entire connection.
+
+Similarly, the reader thread must never block on writing because this can deadlock the connection. Consider a client and server that both violate this rule. If you get unlucky, they could fill up their TCP buffers (so that writes block) and then use their reader threads to write a frame. Nobody is reading on either end, and the buffers are never drained.
+
+#### Do-stuff-later pool
+
+Sometimes there's an action required like calling the application layer or responding to a ping, and the thread discovering the action is not the thread that should do the work. We enqueue a runnable on this executor and it gets handled by one of the executor's threads.
+
+## Locks
+
+We have 3 different things that we synchronize on.
+
+#### SpdyConnection
+
+This lock guards internal state of each connection. This lock is never held for blocking operations. That means that we acquire the lock, read or write a few fields and release the lock. No I/O and no application-layer callbacks.
+
+#### SpdyStream
+
+This lock guards the internal state of each stream. As above, it is never held for blocking operations. When we need to hold an application thread to block a read, we use wait/notify on this lock. This works because the lock is released while `wait()` is waiting.
+
+#### FrameWriter
+
+Socket writes are guarded by the FrameWriter. Only one stream can write at a time so that messages are not interleaved. Writes are either made by application-layer threads or the do-stuff-later pool.
+
+### Holding multiple locks
+
+You're allowed to take the SpdyConnection lock while holding the FrameWriter lock. But not vice-versa. Because taking the FrameWriter lock can block.
+
+This is necessary for bookkeeping when creating new streams. Correct framing requires that stream IDs are sequential on the socket, so we need to bundle assigning the ID with sending the `SYN_STREAM` frame.
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
index 1629f1c..ccb5fdd 100644
--- a/jarjar-rules.txt
+++ b/jarjar-rules.txt
@@ -1 +1,2 @@
 rule com.squareup.** com.android.@1
+rule okio.** com.android.okio.@1
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
new file mode 100644
index 0000000..39257be
--- /dev/null
+++ b/mockwebserver/README.md
@@ -0,0 +1,146 @@
+MockWebServer
+=============
+
+A scriptable web server for testing HTTP clients
+
+
+### Motivation
+
+This library makes it easy to test that your app Does The Right Thing when it
+makes HTTP and HTTPS calls. It lets you specify which responses to return and
+then verify that requests were made as expected.
+
+Because it exercises your full HTTP stack, you can be confident that you're
+testing everything. You can even copy & paste HTTP responses from your real web
+server to create representative test cases. Or test that your code survives in
+awkward-to-reproduce situations like 500 errors or slow-loading responses.
+
+
+### Example
+
+Use MockWebServer the same way that you use mocking frameworks like
+[Mockito](https://code.google.com/p/mockito/):
+
+1. Script the mocks.
+2. Run application code.
+3. Verify that the expected requests were made.
+
+Here's a complete example:
+
+```
+  public void test() throws Exception {
+    // Create a MockWebServer. These are lean enough that you can create a new
+    // instance for every unit test.
+    MockWebServer server = new MockWebServer();
+
+    // Schedule some responses.
+    server.enqueue(new MockResponse().setBody("hello, world!"));
+    server.enqueue(new MockResponse().setBody("sup, bra?"));
+    server.enqueue(new MockResponse().setBody("yo dog"));
+
+    // Start the server.
+    server.play();
+
+    // Ask the server for its URL. You'll need this to make HTTP requests.
+    URL baseUrl = server.getUrl("/v1/chat/");
+
+    // Exercise your application code, which should make those HTTP requests.
+    // Responses are returned in the same order that they are enqueued.
+    Chat chat = new Chat(baseUrl);
+
+    chat.loadMore();
+    assertEquals("hello, world!", chat.messages());
+
+    chat.loadMore();
+    chat.loadMore();
+    assertEquals(""
+        + "hello, world!\n"
+        + "sup, bra?\n"
+        + "yo dog", chat.messages());
+
+    // Optional: confirm that your app made the HTTP requests you were expecting.
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("/v1/chat/messages/", request1.getPath());
+    assertNotNull(request1.getHeader("Authorization"));
+
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("/v1/chat/messages/2", request2.getPath());
+
+    RecordedRequest request3 = server.takeRequest();
+    assertEquals("/v1/chat/messages/3", request3.getPath());
+
+    // Shut down the server. Instances cannot be reused.
+    server.shutdown();
+  }
+```
+
+Your unit tests might move the `server` into a field so you can shut it down
+from your test's `tearDown()`.
+
+### API
+
+#### MockResponse
+
+Mock responses default to an empty response body and a `200` status code.
+You can set a custom body with a string, input stream or byte array. Also
+add headers with a fluent builder API.
+
+```
+    MockResponse response = new MockResponse()
+        .addHeader("Content-Type", "application/json; charset=utf-8")
+        .addHeader("Cache-Control", "no-cache")
+        .setBody("{}");
+```
+
+MockResponse can be used to simulate a slow network. This is useful for
+testing timeouts and interactive testing.
+
+```
+    response.throttleBody(1024, 1, TimeUnit.SECONDS);
+```
+
+
+#### RecordedRequest
+
+Verify requests by their method, path, HTTP version, body, and headers.
+
+```
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine());
+    assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
+    assertEquals("{}", request.getUtf8Body());
+```
+
+#### Dispatcher
+
+By default MockWebServer uses a queue to specify a series of responses. Use a
+Dispatcher to handle requests using another policy. One natural policy is to
+dispatch on the request path.
+
+
+### Download
+
+The best way to get MockWebServer is via Maven:
+
+```
+<dependency>
+  <groupId>com.squareup.okhttp</groupId>
+  <artifactId>mockwebserver</artifactId>
+  <version>(insert latest version)</version>
+  <scope>test</scope>
+</dependency>
+```
+
+### License
+
+    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.
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index f33f156..9c7af9d 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>mockwebserver</artifactId>
@@ -15,7 +15,7 @@
   <dependencies>
     <dependency>
       <groupId>com.squareup.okhttp</groupId>
-      <artifactId>okhttp-protocols</artifactId>
+      <artifactId>okhttp</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
@@ -35,18 +35,23 @@
   </dependencies>
 
   <build>
-    <!-- Don't restrict test code to Java 1.5 APIs. -->
     <plugins>
       <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>animal-sniffer-maven-plugin</artifactId>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
         <configuration>
-          <signature>
-            <groupId>org.codehaus.mojo.signature</groupId>
-            <artifactId>java16</artifactId>
-            <version>1.0</version>
-          </signature>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
         </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
       </plugin>
     </plugins>
   </build>
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 0677263..253fcbd 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -19,6 +19,8 @@
 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;
@@ -48,6 +50,8 @@
   }
 
   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;
@@ -60,6 +64,20 @@
     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.getLocalHost().getHostName()).build();
+      } catch (GeneralSecurityException e) {
+        throw new RuntimeException(e);
+      } catch (UnknownHostException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return localhost;
+  }
+
   public SSLContext build() throws GeneralSecurityException {
     char[] password = "password".toCharArray();
 
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
index 7371f2e..098f3c9 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
@@ -16,24 +16,25 @@
 
 package com.squareup.okhttp.internal.spdy;
 
+import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.util.Arrays;
 import java.util.List;
-import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
+import okio.BufferedSink;
+import okio.Okio;
 import org.eclipse.jetty.npn.NextProtoNego;
 
+import static com.squareup.okhttp.internal.Util.headerEntries;
+
 /** A basic SPDY server that serves the contents of a local directory. */
 public final class SpdyServer implements IncomingStreamHandler {
   private final File baseDirectory;
@@ -70,7 +71,7 @@
         System.out.println("UNSUPPORTED");
       }
       @Override public List<String> protocols() {
-        return Arrays.asList("spdy/3");
+        return Arrays.asList(Protocol.SPDY_3.name.utf8());
       }
       @Override public void protocolSelected(String protocol) {
         System.out.println("PROTOCOL SELECTED: " + protocol);
@@ -80,12 +81,11 @@
   }
 
   @Override public void receive(final SpdyStream stream) throws IOException {
-    List<String> requestHeaders = stream.getRequestHeaders();
+    List<Header> requestHeaders = stream.getRequestHeaders();
     String path = null;
-    for (int i = 0; i < requestHeaders.size(); i += 2) {
-      String s = requestHeaders.get(i);
-      if (":path".equals(s)) {
-        path = requestHeaders.get(i + 1);
+    for (int i = 0; i < requestHeaders.size(); i++) {
+      if (requestHeaders.get(i).name.equals(Header.TARGET_PATH)) {
+        path = requestHeaders.get(i).value.utf8();
         break;
       }
     }
@@ -107,40 +107,42 @@
   }
 
   private void send404(SpdyStream stream, String path) throws IOException {
-    List<String> responseHeaders =
-        Arrays.asList(":status", "404", ":version", "HTTP/1.1", "content-type", "text/plain");
+    List<Header> responseHeaders =
+        headerEntries(":status", "404", ":version", "HTTP/1.1", "content-type", "text/plain");
     stream.reply(responseHeaders, true);
-    OutputStream out = stream.getOutputStream();
-    String text = "Not found: " + path;
-    out.write(text.getBytes("UTF-8"));
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.writeUtf8("Not found: " + path);
     out.close();
   }
 
   private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
-    List<String> responseHeaders =
-        Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type",
+    List<Header> responseHeaders =
+        headerEntries(":status", "200", ":version", "HTTP/1.1", "content-type",
             "text/html; charset=UTF-8");
     stream.reply(responseHeaders, true);
-    OutputStream out = stream.getOutputStream();
-    Writer writer = new OutputStreamWriter(out, "UTF-8");
+    BufferedSink out = Okio.buffer(stream.getSink());
     for (String file : files) {
-      writer.write("<a href='" + file + "'>" + file + "</a><br>");
+      out.writeUtf8("<a href='" + file + "'>" + file + "</a><br>");
     }
-    writer.close();
+    out.close();
   }
 
   private void serveFile(SpdyStream stream, File file) throws IOException {
-    InputStream in = new FileInputStream(file);
     byte[] buffer = new byte[8192];
     stream.reply(
-        Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type", contentType(file)),
+        headerEntries(":status", "200", ":version", "HTTP/1.1", "content-type", contentType(file)),
         true);
-    OutputStream out = stream.getOutputStream();
-    int count;
-    while ((count = in.read(buffer)) != -1) {
-      out.write(buffer, 0, count);
+    InputStream in = new FileInputStream(file);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    try {
+      int count;
+      while ((count = in.read(buffer)) != -1) {
+        out.write(buffer, 0, count);
+      }
+    } finally {
+      Util.closeQuietly(in);
+      Util.closeQuietly(out);
     }
-    out.close();
   }
 
   private String contentType(File file) {
@@ -154,8 +156,7 @@
     }
 
     SpdyServer server = new SpdyServer(new File(args[0]));
-    SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    server.useHttps(sslContext.getSocketFactory());
+    server.useHttps(SslContextBuilder.localhost().getSocketFactory());
     server.run();
   }
 }
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
index ac6bac4..b7de9b6 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
@@ -24,11 +24,18 @@
   public abstract MockResponse dispatch(RecordedRequest request) throws InterruptedException;
 
   /**
-   * Returns the socket policy of the next request.  Default implementation
-   * returns {@link SocketPolicy#KEEP_OPEN}. Mischievous implementations can
-   * return other values to test HTTP edge cases.
+   * Returns an early guess of the next response, used for policy on how an
+   * incoming request should be received. The default implementation returns an
+   * empty response. Mischievous implementations can return other values to test
+   * HTTP edge cases, such as unhappy socket policies or throttled request
+   * bodies.
    */
-  public SocketPolicy peekSocketPolicy() {
-    return SocketPolicy.KEEP_OPEN;
+  public MockResponse peek() {
+    return new MockResponse().setSocketPolicy(SocketPolicy.KEEP_OPEN);
+  }
+
+  /** @deprecated replaced with {@link #peek}. */
+  protected final SocketPolicy peekSocketPolicy() {
+    throw new UnsupportedOperationException("This API is obsolete. Override peek() instead!");
   }
 }
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 b073c11..7d8e066 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -24,6 +24,7 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 /** A scripted response to be replayed by the mock web server. */
 public final class MockResponse implements Cloneable {
@@ -37,9 +38,16 @@
   /** The response body content, or null if {@code body} is set. */
   private InputStream bodyStream;
 
-  private int bytesPerSecond = Integer.MAX_VALUE;
+  private int throttleBytesPerPeriod = Integer.MAX_VALUE;
+  private long throttlePeriod = 1;
+  private TimeUnit throttleUnit = TimeUnit.SECONDS;
+
   private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
 
+  private int bodyDelayTimeMs = 0;
+
+  private List<PushPromise> promises = new ArrayList<PushPromise>();
+
   /** Creates a new mock response with an empty body. */
   public MockResponse() {
     setBody(new byte[0]);
@@ -48,7 +56,8 @@
   @Override public MockResponse clone() {
     try {
       MockResponse result = (MockResponse) super.clone();
-      result.headers = new ArrayList<String>(result.headers);
+      result.headers = new ArrayList<String>(headers);
+      result.promises = new ArrayList<PushPromise>(promises);
       return result;
     } catch (CloneNotSupportedException e) {
       throw new AssertionError();
@@ -204,19 +213,57 @@
     return this;
   }
 
-  public int getBytesPerSecond() {
-    return bytesPerSecond;
+  /**
+   * Throttles the response body writer to sleep for the given period after each
+   * series of {@code bytesPerPeriod} bytes are written. Use this to simulate
+   * network behavior.
+   */
+  public MockResponse throttleBody(int bytesPerPeriod, long period, TimeUnit unit) {
+    this.throttleBytesPerPeriod = bytesPerPeriod;
+    this.throttlePeriod = period;
+    this.throttleUnit = unit;
+    return this;
+  }
+
+  public int getThrottleBytesPerPeriod() {
+    return throttleBytesPerPeriod;
+  }
+
+  public long getThrottlePeriod() {
+    return throttlePeriod;
+  }
+
+  public TimeUnit getThrottleUnit() {
+    return throttleUnit;
   }
 
   /**
-   * Set simulated network speed, in bytes per second. This applies to the
-   * response body only; response headers are not throttled.
+   * Set the delayed time of the response body to {@code delay}. This applies to the
+   * response body only; response headers are not affected.
    */
-  public MockResponse setBytesPerSecond(int bytesPerSecond) {
-    this.bytesPerSecond = bytesPerSecond;
+  public MockResponse setBodyDelayTimeMs(int delay) {
+    bodyDelayTimeMs = delay;
     return this;
   }
 
+  public int getBodyDelayTimeMs() {
+    return bodyDelayTimeMs;
+  }
+
+  /**
+   * When {@link MockWebServer#setNpnProtocols(java.util.List) protocols}
+   * include a SPDY variant, this attaches a pushed stream to this response.
+   */
+  public MockResponse withPush(PushPromise promise) {
+    this.promises.add(promise);
+    return this;
+  }
+
+  /** Returns the streams the server will push with this response. */
+  public List<PushPromise> getPushPromises() {
+    return promises;
+  }
+
   @Override public String toString() {
     return status;
   }
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 d036d53..5a8357b 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -17,8 +17,12 @@
 
 package com.squareup.okhttp.mockwebserver;
 
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.NamedRunnable;
 import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.spdy.ErrorCode;
+import com.squareup.okhttp.internal.spdy.Header;
 import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
 import com.squareup.okhttp.internal.spdy.SpdyStream;
@@ -41,7 +45,6 @@
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -60,6 +63,10 @@
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
+import okio.BufferedSink;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Okio;
 
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
@@ -69,22 +76,6 @@
  * replays them upon request in sequence.
  */
 public final class MockWebServer {
-  private static final byte[] NPN_PROTOCOLS = {
-      // TODO: support HTTP/2.0.
-      // 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0',
-      6, 's', 'p', 'd', 'y', '/', '3',
-      8, 'h', 't', 't', 'p', '/', '1', '.', '1'
-  };
-  private static final byte[] SPDY3 = new byte[] {
-      's', 'p', 'd', 'y', '/', '3'
-  };
-  private static final byte[] HTTP_20_DRAFT_04 = new byte[] {
-      'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0'
-  };
-  private static final byte[] HTTP_11 = new byte[] {
-      'h', 't', 't', 'p', '/', '1', '.', '1'
-  };
-
   private static final X509TrustManager UNTRUSTED_TRUST_MANAGER = new X509TrustManager() {
     @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
         throws CertificateException {
@@ -119,6 +110,7 @@
 
   private int port = -1;
   private boolean npnEnabled = true;
+  private List<Protocol> npnProtocols = Protocol.HTTP2_SPDY3_AND_HTTP;
 
   public int getPort() {
     if (port == -1) throw new IllegalStateException("Cannot retrieve port before calling play()");
@@ -171,7 +163,7 @@
 
   /**
    * Sets whether NPN is used on incoming HTTPS connections to negotiate a
-   * transport like HTTP/1.1 or SPDY/3. Call this method to disable NPN and
+   * protocol like HTTP/1.1 or SPDY/3. Call this method to disable NPN and
    * SPDY.
    */
   public void setNpnEnabled(boolean npnEnabled) {
@@ -179,6 +171,24 @@
   }
 
   /**
+   * Indicates the protocols supported by NPN on incoming HTTPS connections.
+   * This list is ignored when npn is disabled.
+   *
+   * @param protocols the protocols to use, in order of preference. The list
+   *     must contain "http/1.1". It must not contain null.
+   */
+  public void setNpnProtocols(List<Protocol> protocols) {
+    protocols = Util.immutableList(protocols);
+    if (!protocols.contains(Protocol.HTTP_11)) {
+      throw new IllegalArgumentException("protocols doesn't contain http/1.1: " + protocols);
+    }
+    if (protocols.contains(null)) {
+      throw new IllegalArgumentException("protocols must not contain null");
+    }
+    this.npnProtocols = Util.immutableList(protocols);
+  }
+
+  /**
    * Serve requests with HTTPS rather than otherwise.
    * @param tunnelProxy true to expect the HTTP CONNECT method before
    *     negotiating TLS.
@@ -231,13 +241,13 @@
    */
   public void play(int port) throws IOException {
     if (executor != null) throw new IllegalStateException("play() already called");
-    executor = Executors.newCachedThreadPool();
+    executor = Executors.newCachedThreadPool(Util.threadFactory("MockWebServer", false));
     serverSocket = new ServerSocket(port);
     serverSocket.setReuseAddress(true);
 
     this.port = serverSocket.getLocalPort();
-    executor.execute(namedRunnable("MockWebServer-accept-" + port, new Runnable() {
-      public void run() {
+    executor.execute(new NamedRunnable("MockWebServer %s", port) {
+      @Override protected void execute() {
         try {
           acceptConnections();
         } catch (Throwable e) {
@@ -266,7 +276,7 @@
           } catch (SocketException e) {
             return;
           }
-          SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+          SocketPolicy socketPolicy = dispatcher.peek().getSocketPolicy();
           if (socketPolicy == DISCONNECT_AT_START) {
             dispatchBookkeepingRequest(0, socket);
             socket.close();
@@ -276,7 +286,7 @@
           }
         }
       }
-    }));
+    });
   }
 
   public void shutdown() throws IOException {
@@ -286,11 +296,10 @@
   }
 
   private void serveConnection(final Socket raw) {
-    String name = "MockWebServer-" + raw.getRemoteSocketAddress();
-    executor.execute(namedRunnable(name, new Runnable() {
+    executor.execute(new NamedRunnable("MockWebServer %s", raw.getRemoteSocketAddress()) {
       int sequenceNumber = 0;
 
-      public void run() {
+      @Override protected void execute() {
         try {
           processConnection();
         } catch (Exception e) {
@@ -299,13 +308,13 @@
       }
 
       public void processConnection() throws Exception {
-        Transport transport = Transport.HTTP_11;
+        Protocol protocol = Protocol.HTTP_11;
         Socket socket;
         if (sslSocketFactory != null) {
           if (tunnelProxy) {
             createTunnel();
           }
-          SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+          SocketPolicy socketPolicy = dispatcher.peek().getSocketPolicy();
           if (socketPolicy == FAIL_HANDSHAKE) {
             dispatchBookkeepingRequest(sequenceNumber, raw);
             processHandshakeFailure(raw);
@@ -318,42 +327,27 @@
           openClientSockets.put(socket, true);
 
           if (npnEnabled) {
-            Platform.get().setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+            Platform.get().setNpnProtocols(sslSocket, npnProtocols);
           }
 
           sslSocket.startHandshake();
 
           if (npnEnabled) {
-            byte[] selectedProtocol = Platform.get().getNpnSelectedProtocol(sslSocket);
-            if (selectedProtocol == null || Arrays.equals(selectedProtocol, HTTP_11)) {
-              transport = Transport.HTTP_11;
-            } else if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_04)) {
-              transport = Transport.HTTP_20_DRAFT_04;
-            } else if (Arrays.equals(selectedProtocol, SPDY3)) {
-              transport = Transport.SPDY_3;
-            } else {
-              throw new IllegalStateException(
-                  "Unexpected transport: " + new String(selectedProtocol, Util.US_ASCII));
-            }
+            ByteString selectedProtocol = Platform.get().getNpnSelectedProtocol(sslSocket);
+            protocol = Protocol.find(selectedProtocol);
           }
           openClientSockets.remove(raw);
         } else {
           socket = raw;
         }
 
-        if (transport == Transport.SPDY_3 || transport == Transport.HTTP_20_DRAFT_04) {
-          SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, transport);
-          SpdyConnection.Builder builder = new SpdyConnection.Builder(false, socket)
-              .handler(spdySocketHandler);
-          if (transport == Transport.SPDY_3) {
-            builder.spdy3();
-          } else {
-            builder.http20Draft04();
-          }
-          SpdyConnection spdyConnection = builder.build();
+        if (protocol.spdyVariant) {
+          SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, protocol);
+          SpdyConnection spdyConnection = new SpdyConnection.Builder(false, socket)
+              .protocol(protocol)
+              .handler(spdySocketHandler).build();
           openSpdyConnections.put(spdyConnection, Boolean.TRUE);
           openClientSockets.remove(socket);
-          spdyConnection.readConnectionHeader();
           return;
         }
 
@@ -379,7 +373,7 @@
        */
       private void createTunnel() throws IOException, InterruptedException {
         while (true) {
-          SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+          SocketPolicy socketPolicy = dispatcher.peek().getSocketPolicy();
           if (!processOneRequest(raw, raw.getInputStream(), raw.getOutputStream())) {
             throw new IllegalStateException("Tunnel without any CONNECT!");
           }
@@ -407,20 +401,21 @@
         } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) {
           socket.shutdownOutput();
         }
-        logger.info("Received request: " + request + " and responded: " + response);
+        if (logger.isLoggable(Level.INFO)) {
+          logger.info("Received request: " + request + " and responded: " + response);
+        }
         sequenceNumber++;
         return true;
       }
-    }));
+    });
   }
 
   private void processHandshakeFailure(Socket raw) throws Exception {
     SSLContext context = SSLContext.getInstance("TLS");
     context.init(null, new TrustManager[] { UNTRUSTED_TRUST_MANAGER }, new SecureRandom());
     SSLSocketFactory sslSocketFactory = context.getSocketFactory();
-    SSLSocket socket =
-        (SSLSocket) sslSocketFactory.createSocket(raw, raw.getInetAddress().getHostAddress(),
-            raw.getPort(), true);
+    SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(
+        raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true);
     try {
       socket.startHandshake(); // we're testing a handshake failure
       throw new AssertionError();
@@ -479,9 +474,10 @@
     boolean hasBody = false;
     TruncatingOutputStream requestBody = new TruncatingOutputStream();
     List<Integer> chunkSizes = new ArrayList<Integer>();
+    MockResponse throttlePolicy = dispatcher.peek();
     if (contentLength != -1) {
       hasBody = true;
-      transfer(contentLength, in, requestBody);
+      throttledTransfer(throttlePolicy, in, requestBody, contentLength);
     } else if (chunked) {
       hasBody = true;
       while (true) {
@@ -491,7 +487,7 @@
           break;
         }
         chunkSizes.add(chunkSize);
-        transfer(chunkSize, in, requestBody);
+        throttledTransfer(throttlePolicy, in, requestBody, chunkSize);
         readEmptyLine(in);
       }
     }
@@ -499,13 +495,15 @@
     if (request.startsWith("OPTIONS ")
         || request.startsWith("GET ")
         || request.startsWith("HEAD ")
-        || request.startsWith("DELETE ")
         || request.startsWith("TRACE ")
         || request.startsWith("CONNECT ")) {
       if (hasBody) {
         throw new IllegalArgumentException("Request must not have a body: " + request);
       }
-    } else if (!request.startsWith("POST ") && !request.startsWith("PUT ")) {
+    } else if (!request.startsWith("POST ")
+        && !request.startsWith("PUT ")
+        && !request.startsWith("PATCH ")
+        && !request.startsWith("DELETE ")) { // Permitted as spec is ambiguous.
       throw new UnsupportedOperationException("Unexpected method: " + request);
     }
 
@@ -515,7 +513,9 @@
 
   private void writeResponse(OutputStream out, MockResponse response) throws IOException {
     out.write((response.getStatus() + "\r\n").getBytes(Util.US_ASCII));
-    for (String header : response.getHeaders()) {
+    List<String> headers = response.getHeaders();
+    for (int i = 0, size = headers.size(); i < size; i++) {
+      String header = headers.get(i);
       out.write((header + "\r\n").getBytes(Util.US_ASCII));
     }
     out.write(("\r\n").getBytes(Util.US_ASCII));
@@ -523,43 +523,39 @@
 
     InputStream in = response.getBodyStream();
     if (in == null) return;
-    int bytesPerSecond = response.getBytesPerSecond();
-
-    // Stream data in MTU-sized increments, with a minimum of one packet per second.
-    byte[] buffer = bytesPerSecond >= 1452 ? new byte[1452] : new byte[bytesPerSecond];
-    long delayMs = bytesPerSecond == Integer.MAX_VALUE
-        ? 0
-        : (1000 * buffer.length) / bytesPerSecond;
-
-    int read;
-    long sinceDelay = 0;
-    while ((read = in.read(buffer)) != -1) {
-      out.write(buffer, 0, read);
-      out.flush();
-
-      sinceDelay += read;
-      if (sinceDelay >= buffer.length && delayMs > 0) {
-        sinceDelay %= buffer.length;
-        try {
-          Thread.sleep(delayMs);
-        } catch (InterruptedException e) {
-          throw new AssertionError();
-        }
-      }
-    }
+    throttledTransfer(response, in, out, Long.MAX_VALUE);
   }
 
   /**
    * Transfer bytes from {@code in} to {@code out} until either {@code length}
-   * bytes have been transferred or {@code in} is exhausted.
+   * bytes have been transferred or {@code in} is exhausted. The transfer is
+   * throttled according to {@code throttlePolicy}.
    */
-  private void transfer(long length, InputStream in, OutputStream out) throws IOException {
+  private void throttledTransfer(MockResponse throttlePolicy, InputStream in, OutputStream out,
+      long limit) throws IOException {
     byte[] buffer = new byte[1024];
-    while (length > 0) {
-      int count = in.read(buffer, 0, (int) Math.min(buffer.length, length));
-      if (count == -1) return;
-      out.write(buffer, 0, count);
-      length -= count;
+    int bytesPerPeriod = throttlePolicy.getThrottleBytesPerPeriod();
+    long delayMs = throttlePolicy.getThrottleUnit().toMillis(throttlePolicy.getThrottlePeriod());
+
+    while (true) {
+      for (int b = 0; b < bytesPerPeriod; ) {
+        int toRead = (int) Math.min(Math.min(buffer.length, limit), bytesPerPeriod - b);
+        int read = in.read(buffer, 0, toRead);
+        if (read == -1) return;
+
+        out.write(buffer, 0, read);
+        out.flush();
+        b += read;
+        limit -= read;
+
+        if (limit == 0) return;
+      }
+
+      try {
+        if (delayMs != 0) Thread.sleep(delayMs);
+      } catch (InterruptedException e) {
+        throw new AssertionError();
+      }
     }
   }
 
@@ -615,29 +611,15 @@
     }
   }
 
-  private static Runnable namedRunnable(final String name, final Runnable runnable) {
-    return new Runnable() {
-      public void run() {
-        String originalName = Thread.currentThread().getName();
-        Thread.currentThread().setName(name);
-        try {
-          runnable.run();
-        } finally {
-          Thread.currentThread().setName(originalName);
-        }
-      }
-    };
-  }
-
   /** Processes HTTP requests layered over SPDY/3. */
   private class SpdySocketHandler implements IncomingStreamHandler {
     private final Socket socket;
-    private final Transport transport;
+    private final Protocol protocol;
     private final AtomicInteger sequenceNumber = new AtomicInteger();
 
-    private SpdySocketHandler(Socket socket, Transport transport) {
+    private SpdySocketHandler(Socket socket, Protocol protocol) {
       this.socket = socket;
-      this.transport = transport;
+      this.protocol = protocol;
     }
 
     @Override public void receive(SpdyStream stream) throws IOException {
@@ -650,31 +632,33 @@
         throw new AssertionError(e);
       }
       writeResponse(stream, response);
-      logger.info("Received request: " + request + " and responded: " + response
-          + " transport is " + transport);
+      if (logger.isLoggable(Level.INFO)) {
+        logger.info("Received request: " + request + " and responded: " + response
+            + " protocol is " + protocol.name.utf8());
+      }
     }
 
     private RecordedRequest readRequest(SpdyStream stream) throws IOException {
-      List<String> spdyHeaders = stream.getRequestHeaders();
+      List<Header> spdyHeaders = stream.getRequestHeaders();
       List<String> httpHeaders = new ArrayList<String>();
       String method = "<:method omitted>";
       String path = "<:path omitted>";
-      String version = "<:version omitted>";
-      for (Iterator<String> i = spdyHeaders.iterator(); i.hasNext(); ) {
-        String name = i.next();
-        String value = i.next();
-        if (":method".equals(name)) {
+      String version = protocol == Protocol.SPDY_3 ? "<:version omitted>" : "HTTP/1.1";
+      for (int i = 0, size = spdyHeaders.size(); i < size; i++) {
+        ByteString name = spdyHeaders.get(i).name;
+        String value = spdyHeaders.get(i).value.utf8();
+        if (name.equals(Header.TARGET_METHOD)) {
           method = value;
-        } else if (":path".equals(name)) {
+        } else if (name.equals(Header.TARGET_PATH)) {
           path = value;
-        } else if (":version".equals(name)) {
+        } else if (name.equals(Header.VERSION)) {
           version = value;
         } else {
-          httpHeaders.add(name + ": " + value);
+          httpHeaders.add(name.utf8() + ": " + value);
         }
       }
 
-      InputStream bodyIn = stream.getInputStream();
+      InputStream bodyIn = Okio.buffer(stream.getSource()).inputStream();
       ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
       byte[] buffer = new byte[8192];
       int count;
@@ -689,34 +673,91 @@
     }
 
     private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
-      List<String> spdyHeaders = new ArrayList<String>();
+      if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
+        return;
+      }
+      List<Header> spdyHeaders = new ArrayList<Header>();
       String[] statusParts = response.getStatus().split(" ", 2);
       if (statusParts.length != 2) {
         throw new AssertionError("Unexpected status: " + response.getStatus());
       }
-      spdyHeaders.add(":status");
-      spdyHeaders.add(statusParts[1]);
-      // TODO: no ":version" header for HTTP/2.0, only SPDY.
-      spdyHeaders.add(":version");
-      spdyHeaders.add(statusParts[0]);
-      for (String header : response.getHeaders()) {
+      // TODO: constants for well-known header names.
+      spdyHeaders.add(new Header(Header.RESPONSE_STATUS, statusParts[1]));
+      if (protocol == Protocol.SPDY_3) {
+        spdyHeaders.add(new Header(Header.VERSION, statusParts[0]));
+      }
+      List<String> headers = response.getHeaders();
+      for (int i = 0, size = headers.size(); i < size; i++) {
+        String header = headers.get(i);
         String[] headerParts = header.split(":", 2);
         if (headerParts.length != 2) {
           throw new AssertionError("Unexpected header: " + header);
         }
-        spdyHeaders.add(headerParts[0].toLowerCase(Locale.US).trim());
-        spdyHeaders.add(headerParts[1].trim());
+        spdyHeaders.add(new Header(headerParts[0], headerParts[1]));
       }
-      byte[] body = response.getBody();
-      stream.reply(spdyHeaders, body.length > 0);
-      if (body.length > 0) {
-        stream.getOutputStream().write(body);
-        stream.getOutputStream().close();
+      OkBuffer body = new OkBuffer();
+      if (response.getBody() != null) {
+        body.write(response.getBody());
+      }
+      boolean closeStreamAfterHeaders = body.size() > 0 || !response.getPushPromises().isEmpty();
+      stream.reply(spdyHeaders, closeStreamAfterHeaders);
+      pushPromises(stream, response.getPushPromises());
+      if (body.size() > 0) {
+        if (response.getBodyDelayTimeMs() != 0) {
+          try {
+            Thread.sleep(response.getBodyDelayTimeMs());
+          } catch (InterruptedException e) {
+            throw new AssertionError(e);
+          }
+        }
+        BufferedSink sink = Okio.buffer(stream.getSink());
+        if (response.getThrottleBytesPerPeriod() == Integer.MAX_VALUE) {
+          sink.write(body, body.size());
+          sink.flush();
+        } else {
+          while (body.size() > 0) {
+            long toWrite = Math.min(body.size(), response.getThrottleBytesPerPeriod());
+            sink.write(body, toWrite);
+            sink.flush();
+            try {
+              long delayMs = response.getThrottleUnit().toMillis(response.getThrottlePeriod());
+              if (delayMs != 0) Thread.sleep(delayMs);
+            } catch (InterruptedException e) {
+              throw new AssertionError();
+            }
+          }
+        }
+        sink.close();
+      } else if (closeStreamAfterHeaders) {
+        stream.close(ErrorCode.NO_ERROR);
       }
     }
-  }
 
-  enum Transport {
-    HTTP_11, SPDY_3, HTTP_20_DRAFT_04
+    private void pushPromises(SpdyStream stream, List<PushPromise> promises) throws IOException {
+      for (PushPromise pushPromise : promises) {
+        List<Header> pushedHeaders = new ArrayList<Header>();
+        pushedHeaders.add(new Header(stream.getConnection().getProtocol() == Protocol.SPDY_3
+            ? Header.TARGET_HOST
+            : Header.TARGET_AUTHORITY, getUrl(pushPromise.getPath()).getHost()));
+        pushedHeaders.add(new Header(Header.TARGET_METHOD, pushPromise.getMethod()));
+        pushedHeaders.add(new Header(Header.TARGET_PATH, pushPromise.getPath()));
+        for (int i = 0, size = pushPromise.getHeaders().size(); i < size; i++) {
+          String header = pushPromise.getHeaders().get(i);
+          String[] headerParts = header.split(":", 2);
+          if (headerParts.length != 2) {
+            throw new AssertionError("Unexpected header: " + header);
+          }
+          pushedHeaders.add(new Header(headerParts[0], headerParts[1].trim()));
+        }
+        String requestLine = pushPromise.getMethod() + ' ' + pushPromise.getPath() + " HTTP/1.1";
+        List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
+        requestQueue.add(new RecordedRequest(requestLine, pushPromise.getHeaders(), chunkSizes, 0,
+            Util.EMPTY_BYTE_ARRAY, sequenceNumber.getAndIncrement(), socket));
+        byte[] pushedBody = pushPromise.getResponse().getBody();
+        SpdyStream pushedStream =
+            stream.getConnection().pushStream(stream.getId(), pushedHeaders, pushedBody.length > 0);
+        writeResponse(pushedStream, pushPromise.getResponse());
+      }
+    }
   }
 }
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java
new file mode 100644
index 0000000..d9dd019
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.mockwebserver;
+
+import java.util.List;
+
+/** An HTTP request initiated by the server. */
+public final class PushPromise {
+  private final String method;
+  private final String path;
+  private final List<String> headers;
+  private final MockResponse response;
+
+  public PushPromise(String method, String path, List<String> headers, MockResponse response) {
+    this.method = method;
+    this.path = path;
+    this.headers = headers;
+    this.response = response;
+  }
+
+  public String getMethod() {
+    return method;
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  public List<String> getHeaders() {
+    return headers;
+  }
+
+  public MockResponse getResponse() {
+    return response;
+  }
+}
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 0f0cb28..7c1ddd1 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -44,14 +44,11 @@
     return responseQueue.take();
   }
 
-  @Override public SocketPolicy peekSocketPolicy() {
+  @Override public MockResponse peek() {
     MockResponse peek = responseQueue.peek();
-    if (peek == null) {
-      return failFastResponse != null
-          ? failFastResponse.getSocketPolicy()
-          : SocketPolicy.KEEP_OPEN;
-    }
-    return peek.getSocketPolicy();
+    if (peek != null) return peek;
+    if (failFastResponse != null) return failFastResponse;
+    return super.peek();
   }
 
   public void enqueueResponse(MockResponse response) {
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
index aceacd1..58b5d10 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
@@ -80,7 +80,8 @@
    */
   public String getHeader(String name) {
     name += ":";
-    for (String header : headers) {
+    for (int i = 0, size = headers.size(); i < size; i++) {
+      String header = headers.get(i);
       if (name.regionMatches(true, 0, header, 0, name.length())) {
         return header.substring(name.length()).trim();
       }
@@ -92,7 +93,8 @@
   public List<String> getHeaders(String name) {
     List<String> result = new ArrayList<String>();
     name += ":";
-    for (String header : headers) {
+    for (int i = 0, size = headers.size(); i < size; i++) {
+      String header = headers.get(i);
       if (name.regionMatches(true, 0, header, 0, name.length())) {
         result.add(header.substring(name.length()).trim());
       }
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 7912f3a..76701c4 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -57,5 +57,11 @@
    * Shutdown the socket output after sending the response. For testing bad
    * behavior.
    */
-  SHUTDOWN_OUTPUT_AT_END
+  SHUTDOWN_OUTPUT_AT_END,
+
+  /**
+   * Don't response 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/CustomDispatcherTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
index 22e6a95..7b7e112 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
@@ -15,8 +15,6 @@
  */
 package com.squareup.okhttp.mockwebserver;
 
-import junit.framework.TestCase;
-
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URL;
@@ -24,6 +22,7 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
+import junit.framework.TestCase;
 
 public class CustomDispatcherTest extends TestCase {
 
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 98efc44..2b1651f 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -28,8 +28,11 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import junit.framework.TestCase;
 
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
 public final class MockWebServerTest extends TestCase {
 
     private MockWebServer server = new MockWebServer();
@@ -286,4 +289,53 @@
 
         assertEquals(-1, responseBody.read()); // The body is exhausted.
     }
+
+    /**
+     * Throttle the request body by sleeping 500ms after every 3 bytes. With a
+     * 6-byte request, this should yield one sleep for a total delay of 500ms.
+     */
+    public void testThrottleRequest() throws Exception {
+        server.enqueue(new MockResponse()
+            .throttleBody(3, 500, TimeUnit.MILLISECONDS));
+        server.play();
+
+        long startNanos = System.nanoTime();
+        URLConnection connection = server.getUrl("/").openConnection();
+        connection.setDoOutput(true);
+        connection.getOutputStream().write("ABCDEF".getBytes("UTF-8"));
+        InputStream in = connection.getInputStream();
+        assertEquals(-1, in.read());
+        long elapsedNanos = System.nanoTime() - startNanos;
+        long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+        assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
+        assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
+    }
+
+    /**
+     * Throttle the response body by sleeping 500ms after every 3 bytes. With a
+     * 6-byte response, this should yield one sleep for a total delay of 500ms.
+     */
+    public void testThrottleResponse() throws Exception {
+        server.enqueue(new MockResponse()
+            .setBody("ABCDEF")
+            .throttleBody(3, 500, TimeUnit.MILLISECONDS));
+        server.play();
+
+        long startNanos = System.nanoTime();
+        URLConnection connection = server.getUrl("/").openConnection();
+        InputStream in = connection.getInputStream();
+        assertEquals('A', in.read());
+        assertEquals('B', in.read());
+        assertEquals('C', in.read());
+        assertEquals('D', in.read());
+        assertEquals('E', in.read());
+        assertEquals('F', in.read());
+        assertEquals(-1, in.read());
+        long elapsedNanos = System.nanoTime() - startNanos;
+        long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+        assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
+        assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
+    }
 }
diff --git a/okcurl/README.md b/okcurl/README.md
new file mode 100644
index 0000000..cea5be4
--- /dev/null
+++ b/okcurl/README.md
@@ -0,0 +1,7 @@
+OkCurl
+======
+
+_A curl for the next-generation web._
+
+OkCurl is an OkHttp-backed curl clone which allows you to test OkHttp's HTTP engine (including
+SPDY and HTTP/2) against web servers.
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
new file mode 100644
index 0000000..af0ba2e
--- /dev/null
+++ b/okcurl/pom.xml
@@ -0,0 +1,94 @@
+<?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.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>okcurl</artifactId>
+  <name>OkCurl</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty.npn</groupId>
+      <artifactId>npn-boot</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.airlift</groupId>
+      <artifactId>airline</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+
+    <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.curl.Main</mainClass>
+            </manifest>
+          </archive>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.skife.maven</groupId>
+        <artifactId>really-executable-jar-maven-plugin</artifactId>
+        <version>1.1.0</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>really-executable-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <flags>-Xbootclasspath/p:$0</flags>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
new file mode 100644
index 0000000..9a45f20
--- /dev/null
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.curl;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import io.airlift.command.Arguments;
+import io.airlift.command.Command;
+import io.airlift.command.HelpOption;
+import io.airlift.command.Option;
+import io.airlift.command.SingleCommand;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+@Command(name = Main.NAME, description = "A curl for the next-generation web.")
+public class Main extends HelpOption implements Runnable {
+  static final String NAME = "okcurl";
+  static final int DEFAULT_TIMEOUT = -1;
+
+  static Main fromArgs(String... args) {
+    return SingleCommand.singleCommand(Main.class).parse(args);
+  }
+
+  public static void main(String... args) {
+    fromArgs(args).run();
+  }
+
+  private static String versionString() {
+    try {
+      Properties prop = new Properties();
+      InputStream in = Main.class.getResourceAsStream("/okcurl-version.properties");
+      prop.load(in);
+      in.close();
+      return prop.getProperty("version");
+    } catch (IOException e) {
+      throw new AssertionError("Could not load okcurl-version.properties.");
+    }
+  }
+
+  private static String protocols() {
+    return Joiner.on(", ").join(Lists.transform(Arrays.asList(Protocol.values()),
+        new Function<Protocol, String>() {
+          @Override public String apply(Protocol protocol) {
+            return protocol.name.utf8();
+          }
+        }));
+  }
+
+  @Option(name = { "-X", "--request" }, description = "Specify request command to use")
+  public String method;
+
+  @Option(name = { "-d", "--data" }, description = "HTTP POST data")
+  public String data;
+
+  @Option(name = { "-H", "--header" }, description = "Custom header to pass to server")
+  public List<String> headers;
+
+  @Option(name = { "-A", "--user-agent" }, description = "User-Agent to send to server")
+  public String userAgent = NAME + "/" + versionString();
+
+  @Option(name = "--connect-timeout", description = "Maximum time allowed for connection (seconds)")
+  public int connectTimeout = DEFAULT_TIMEOUT;
+
+  @Option(name = "--read-timeout", description = "Maximum time allowed for reading data (seconds)")
+  public int readTimeout = DEFAULT_TIMEOUT;
+
+  @Option(name = { "-L", "--location" }, description = "Follow redirects")
+  public boolean followRedirects;
+
+  @Option(name = { "-k", "--insecure" },
+      description = "Allow connections to SSL sites without certs")
+  public boolean allowInsecure;
+
+  @Option(name = { "-i", "--include" }, description = "Include protocol headers in the output")
+  public boolean showHeaders;
+
+  @Option(name = { "-e", "--referer" }, description = "Referer URL")
+  public String referer;
+
+  @Option(name = { "-V", "--version" }, description = "Show version number and quit")
+  public boolean version;
+
+  @Arguments(title = "url", description = "Remote resource URL")
+  public String url;
+
+  private OkHttpClient client;
+
+  @Override public void run() {
+    if (showHelpIfRequested()) {
+      return;
+    }
+    if (version) {
+      System.out.println(NAME + " " + versionString());
+      System.out.println("Protocols: " + protocols());
+      return;
+    }
+
+    client = createClient();
+    Request request = createRequest();
+    try {
+      Response response = client.execute(request);
+      if (showHeaders) {
+        System.out.println(response.statusLine());
+        Headers headers = response.headers();
+        for (int i = 0, count = headers.size(); i < count; i++) {
+          System.out.println(headers.name(i) + ": " + headers.value(i));
+        }
+        System.out.println();
+      }
+
+      Response.Body body = response.body();
+      byte[] buffer = new byte[1024];
+      while (body.ready()) {
+        int c = body.byteStream().read(buffer);
+        if (c == -1) {
+          return;
+        }
+        System.out.write(buffer, 0, c);
+      }
+      body.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+    } finally {
+      close();
+    }
+  }
+
+  private OkHttpClient createClient() {
+    OkHttpClient client = new OkHttpClient();
+    client.setFollowProtocolRedirects(followRedirects);
+    if (connectTimeout != DEFAULT_TIMEOUT) {
+      client.setConnectTimeout(connectTimeout, SECONDS);
+    }
+    if (readTimeout != DEFAULT_TIMEOUT) {
+      client.setReadTimeout(readTimeout, SECONDS);
+    }
+    if (allowInsecure) {
+      client.setSslSocketFactory(createInsecureSslSocketFactory());
+    }
+    // If we don't set this reference, there's no way to clean shutdown persistent connections.
+    client.setConnectionPool(ConnectionPool.getDefault());
+    return client;
+  }
+
+  private String getRequestMethod() {
+    if (method != null) {
+      return method;
+    }
+    if (data != null) {
+      return "POST";
+    }
+    return "GET";
+  }
+
+  private Request.Body getRequestBody() {
+    if (data == null) {
+      return null;
+    }
+    String bodyData = data;
+
+    String mimeType = "application/x-form-urlencoded";
+    if (headers != null) {
+      for (String header : headers) {
+        String[] parts = header.split(":", -1);
+        if ("Content-Type".equalsIgnoreCase(parts[0])) {
+          mimeType = parts[1].trim();
+          headers.remove(header);
+          break;
+        }
+      }
+    }
+
+    return Request.Body.create(MediaType.parse(mimeType), bodyData);
+  }
+
+  Request createRequest() {
+    Request.Builder request = new Request.Builder();
+
+    request.url(url);
+    request.method(getRequestMethod(), getRequestBody());
+
+    if (headers != null) {
+      for (String header : headers) {
+        String[] parts = header.split(":", -1);
+        request.header(parts[0], parts[1]);
+      }
+    }
+    if (referer != null) {
+      request.header("Referer", referer);
+    }
+    request.header("User-Agent", userAgent);
+
+    return request.build();
+  }
+
+  private void close() {
+    client.getConnectionPool().evictAll(); // Close any persistent connections.
+  }
+
+  private static SSLSocketFactory createInsecureSslSocketFactory() {
+    try {
+      SSLContext context = SSLContext.getInstance("TLS");
+      TrustManager permissive = new X509TrustManager() {
+        @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+            throws CertificateException {
+        }
+
+        @Override public void checkServerTrusted(X509Certificate[] chain, String authType)
+            throws CertificateException {
+        }
+
+        @Override public X509Certificate[] getAcceptedIssuers() {
+          return null;
+        }
+      };
+      context.init(null, new TrustManager[] { permissive }, null);
+      return context.getSocketFactory();
+    } catch (Exception e) {
+      throw new AssertionError(e);
+    }
+  }
+}
diff --git a/okcurl/src/main/resources/okcurl-version.properties b/okcurl/src/main/resources/okcurl-version.properties
new file mode 100644
index 0000000..defbd48
--- /dev/null
+++ b/okcurl/src/main/resources/okcurl-version.properties
@@ -0,0 +1 @@
+version=${project.version}
diff --git a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
new file mode 100644
index 0000000..6a5b972
--- /dev/null
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.curl;
+
+import com.squareup.okhttp.Request;
+import java.io.IOException;
+import okio.OkBuffer;
+import org.junit.Test;
+
+import static com.squareup.okhttp.curl.Main.fromArgs;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class MainTest {
+  @Test public void simple() {
+    Request request = fromArgs("http://example.com").createRequest();
+    assertEquals("GET", request.method());
+    assertEquals("http://example.com", request.urlString());
+    assertNull(request.body());
+  }
+
+  @Test public void put() {
+    Request request = fromArgs("-X", "PUT", "http://example.com").createRequest();
+    assertEquals("PUT", request.method());
+    assertEquals("http://example.com", request.urlString());
+    assertNull(request.body());
+  }
+
+  @Test public void dataPost() {
+    Request request = fromArgs("-d", "foo", "http://example.com").createRequest();
+    Request.Body 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("foo", bodyAsString(body));
+  }
+
+  @Test public void dataPut() {
+    Request request = fromArgs("-d", "foo", "-X", "PUT", "http://example.com").createRequest();
+    Request.Body 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("foo", bodyAsString(body));
+  }
+
+  @Test public void contentTypeHeader() {
+    Request request = fromArgs("-d", "foo", "-H", "Content-Type: application/json",
+        "http://example.com").createRequest();
+    Request.Body body = request.body();
+    assertEquals("POST", request.method());
+    assertEquals("http://example.com", request.urlString());
+    assertEquals("application/json; charset=utf-8", body.contentType().toString());
+    assertEquals("foo", bodyAsString(body));
+  }
+
+  @Test public void referer() {
+    Request request = fromArgs("-e", "foo", "http://example.com").createRequest();
+    assertEquals("GET", request.method());
+    assertEquals("http://example.com", request.urlString());
+    assertEquals("foo", request.header("Referer"));
+    assertNull(request.body());
+  }
+
+  @Test public void userAgent() {
+    Request request = fromArgs("-A", "foo", "http://example.com").createRequest();
+    assertEquals("GET", request.method());
+    assertEquals("http://example.com", request.urlString());
+    assertEquals("foo", request.header("User-Agent"));
+    assertNull(request.body());
+  }
+
+  private static String bodyAsString(Request.Body body) {
+    try {
+      OkBuffer buffer = new OkBuffer();
+      body.writeTo(buffer);
+      return new String(buffer.readByteString(buffer.size()).toByteArray(),
+          body.contentType().charset());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index f8fa6a4..14eb349 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>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-apache</artifactId>
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Base64.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Base64.java
deleted file mode 100644
index 79cd020..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Base64.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- *  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.
- */
-
-/**
- * @author Alexander Y. Kleymenov
- */
-
-package com.squareup.okhttp.internal;
-
-import java.io.UnsupportedEncodingException;
-
-import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
-
-/**
- * <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> encoder/decoder.
- * In violation of the RFC, this encoder doesn't wrap lines at 76 columns.
- */
-public final class Base64 {
-  private Base64() {
-  }
-
-  public static byte[] decode(byte[] in) {
-    return decode(in, in.length);
-  }
-
-  public static byte[] decode(byte[] in, int len) {
-    // approximate output length
-    int length = len / 4 * 3;
-    // return an empty array on empty or short input without padding
-    if (length == 0) {
-      return EMPTY_BYTE_ARRAY;
-    }
-    // temporary array
-    byte[] out = new byte[length];
-    // number of padding characters ('=')
-    int pad = 0;
-    byte chr;
-    // compute the number of the padding characters
-    // and adjust the length of the input
-    for (; ; len--) {
-      chr = in[len - 1];
-      // skip the neutral characters
-      if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
-        continue;
-      }
-      if (chr == '=') {
-        pad++;
-      } else {
-        break;
-      }
-    }
-    // index in the output array
-    int outIndex = 0;
-    // index in the input array
-    int inIndex = 0;
-    // holds the value of the input character
-    int bits = 0;
-    // holds the value of the input quantum
-    int quantum = 0;
-    for (int i = 0; i < len; i++) {
-      chr = in[i];
-      // skip the neutral characters
-      if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
-        continue;
-      }
-      if ((chr >= 'A') && (chr <= 'Z')) {
-        // char ASCII value
-        //  A    65    0
-        //  Z    90    25 (ASCII - 65)
-        bits = chr - 65;
-      } else if ((chr >= 'a') && (chr <= 'z')) {
-        // char ASCII value
-        //  a    97    26
-        //  z    122   51 (ASCII - 71)
-        bits = chr - 71;
-      } else if ((chr >= '0') && (chr <= '9')) {
-        // char ASCII value
-        //  0    48    52
-        //  9    57    61 (ASCII + 4)
-        bits = chr + 4;
-      } else if (chr == '+') {
-        bits = 62;
-      } else if (chr == '/') {
-        bits = 63;
-      } else {
-        return null;
-      }
-      // append the value to the quantum
-      quantum = (quantum << 6) | (byte) bits;
-      if (inIndex % 4 == 3) {
-        // 4 characters were read, so make the output:
-        out[outIndex++] = (byte) (quantum >> 16);
-        out[outIndex++] = (byte) (quantum >> 8);
-        out[outIndex++] = (byte) quantum;
-      }
-      inIndex++;
-    }
-    if (pad > 0) {
-      // adjust the quantum value according to the padding
-      quantum = quantum << (6 * pad);
-      // make output
-      out[outIndex++] = (byte) (quantum >> 16);
-      if (pad == 1) {
-        out[outIndex++] = (byte) (quantum >> 8);
-      }
-    }
-    // create the resulting array
-    byte[] result = new byte[outIndex];
-    System.arraycopy(out, 0, result, 0, outIndex);
-    return result;
-  }
-
-  private static final byte[] MAP = new byte[] {
-      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
-      'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
-      'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
-      '5', '6', '7', '8', '9', '+', '/'
-  };
-
-  public static String encode(byte[] in) {
-    int length = (in.length + 2) * 4 / 3;
-    byte[] out = new byte[length];
-    int index = 0, end = in.length - in.length % 3;
-    for (int i = 0; i < end; i += 3) {
-      out[index++] = MAP[(in[i] & 0xff) >> 2];
-      out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
-      out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
-      out[index++] = MAP[(in[i + 2] & 0x3f)];
-    }
-    switch (in.length % 3) {
-      case 1:
-        out[index++] = MAP[(in[end] & 0xff) >> 2];
-        out[index++] = MAP[(in[end] & 0x03) << 4];
-        out[index++] = '=';
-        out[index++] = '=';
-        break;
-      case 2:
-        out[index++] = MAP[(in[end] & 0xff) >> 2];
-        out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
-        out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
-        out[index++] = '=';
-        break;
-    }
-    try {
-      return new String(out, 0, index, "US-ASCII");
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError(e);
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Util.java
deleted file mode 100644
index 9c5b008..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Util.java
+++ /dev/null
@@ -1,394 +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;
-
-import java.io.Closeable;
-import java.io.EOFException;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.Socket;
-import java.net.ServerSocket;
-import java.net.URI;
-import java.net.URL;
-import java.nio.ByteOrder;
-import java.nio.charset.Charset;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicReference;
-
-/** Junk drawer of utility methods. */
-public final class Util {
-  public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
-  public static final String[] EMPTY_STRING_ARRAY = new String[0];
-
-  /** A cheap and type-safe constant for the ISO-8859-1 Charset. */
-  public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
-
-  /** A cheap and type-safe constant for the US-ASCII Charset. */
-  public static final Charset US_ASCII = Charset.forName("US-ASCII");
-
-  /** A cheap and type-safe constant for the UTF-8 Charset. */
-  public static final Charset UTF_8 = Charset.forName("UTF-8");
-  private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
-
-  private static final char[] DIGITS =
-      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-
-  private Util() {
-  }
-
-  public static int getEffectivePort(URI uri) {
-    return getEffectivePort(uri.getScheme(), uri.getPort());
-  }
-
-  public static int getEffectivePort(URL url) {
-    return getEffectivePort(url.getProtocol(), url.getPort());
-  }
-
-  private static int getEffectivePort(String scheme, int specifiedPort) {
-    return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
-  }
-
-  public static int getDefaultPort(String scheme) {
-    if ("http".equalsIgnoreCase(scheme)) {
-      return 80;
-    } else if ("https".equalsIgnoreCase(scheme)) {
-      return 443;
-    } else {
-      return -1;
-    }
-  }
-
-  public static void checkOffsetAndCount(int arrayLength, int offset, int count) {
-    if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
-      throw new ArrayIndexOutOfBoundsException();
-    }
-  }
-
-  public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
-    if (order == ByteOrder.BIG_ENDIAN) {
-      dst[offset++] = (byte) ((value >> 24) & 0xff);
-      dst[offset++] = (byte) ((value >> 16) & 0xff);
-      dst[offset++] = (byte) ((value >> 8) & 0xff);
-      dst[offset] = (byte) ((value >> 0) & 0xff);
-    } else {
-      dst[offset++] = (byte) ((value >> 0) & 0xff);
-      dst[offset++] = (byte) ((value >> 8) & 0xff);
-      dst[offset++] = (byte) ((value >> 16) & 0xff);
-      dst[offset] = (byte) ((value >> 24) & 0xff);
-    }
-  }
-
-  /** Returns true if two possibly-null objects are equal. */
-  public static boolean equal(Object a, Object b) {
-    return a == b || (a != null && a.equals(b));
-  }
-
-  /**
-   * Closes {@code closeable}, ignoring any checked exceptions. Does nothing
-   * if {@code closeable} is null.
-   */
-  public static void closeQuietly(Closeable closeable) {
-    if (closeable != null) {
-      try {
-        closeable.close();
-      } catch (RuntimeException rethrown) {
-        throw rethrown;
-      } catch (Exception ignored) {
-      }
-    }
-  }
-
-  /**
-   * Closes {@code socket}, ignoring any checked exceptions. Does nothing if
-   * {@code socket} is null.
-   */
-  public static void closeQuietly(Socket socket) {
-    if (socket != null) {
-      try {
-        socket.close();
-      } catch (RuntimeException rethrown) {
-        throw rethrown;
-      } catch (Exception ignored) {
-      }
-    }
-  }
-
-  /**
-   * Closes {@code serverSocket}, ignoring any checked exceptions. Does nothing if
-   * {@code serverSocket} is null.
-   */
-  public static void closeQuietly(ServerSocket serverSocket) {
-    if (serverSocket != null) {
-      try {
-        serverSocket.close();
-      } catch (RuntimeException rethrown) {
-        throw rethrown;
-      } catch (Exception ignored) {
-      }
-    }
-  }
-
-  /**
-   * Closes {@code a} and {@code b}. If either close fails, this completes
-   * the other close and rethrows the first encountered exception.
-   */
-  public static void closeAll(Closeable a, Closeable b) throws IOException {
-    Throwable thrown = null;
-    try {
-      a.close();
-    } catch (Throwable e) {
-      thrown = e;
-    }
-    try {
-      b.close();
-    } catch (Throwable e) {
-      if (thrown == null) thrown = e;
-    }
-    if (thrown == null) return;
-    if (thrown instanceof IOException) throw (IOException) thrown;
-    if (thrown instanceof RuntimeException) throw (RuntimeException) thrown;
-    if (thrown instanceof Error) throw (Error) thrown;
-    throw new AssertionError(thrown);
-  }
-
-  /**
-   * Deletes the contents of {@code dir}. Throws an IOException if any file
-   * could not be deleted, or if {@code dir} is not a readable directory.
-   */
-  public static void deleteContents(File dir) throws IOException {
-    File[] files = dir.listFiles();
-    if (files == null) {
-      throw new IOException("not a readable directory: " + dir);
-    }
-    for (File file : files) {
-      if (file.isDirectory()) {
-        deleteContents(file);
-      }
-      if (!file.delete()) {
-        throw new IOException("failed to delete file: " + file);
-      }
-    }
-  }
-
-  /**
-   * Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int).
-   * InputStream assumes that you implement InputStream.read(int) and provides default
-   * implementations of the others, but often the opposite is more efficient.
-   */
-  public static int readSingleByte(InputStream in) throws IOException {
-    byte[] buffer = new byte[1];
-    int result = in.read(buffer, 0, 1);
-    return (result != -1) ? buffer[0] & 0xff : -1;
-  }
-
-  /**
-   * Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int).
-   * OutputStream assumes that you implement OutputStream.write(int) and provides default
-   * implementations of the others, but often the opposite is more efficient.
-   */
-  public static void writeSingleByte(OutputStream out, int b) throws IOException {
-    byte[] buffer = new byte[1];
-    buffer[0] = (byte) (b & 0xff);
-    out.write(buffer);
-  }
-
-  /**
-   * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
-   */
-  public static void readFully(InputStream in, byte[] dst) throws IOException {
-    readFully(in, dst, 0, dst.length);
-  }
-
-  /**
-   * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
-   * EOFException if insufficient bytes are available.
-   *
-   * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
-   */
-  public static void readFully(InputStream in, byte[] dst, int offset, int byteCount)
-      throws IOException {
-    if (byteCount == 0) {
-      return;
-    }
-    if (in == null) {
-      throw new NullPointerException("in == null");
-    }
-    if (dst == null) {
-      throw new NullPointerException("dst == null");
-    }
-    checkOffsetAndCount(dst.length, offset, byteCount);
-    while (byteCount > 0) {
-      int bytesRead = in.read(dst, offset, byteCount);
-      if (bytesRead < 0) {
-        throw new EOFException();
-      }
-      offset += bytesRead;
-      byteCount -= bytesRead;
-    }
-  }
-
-  /** Returns the remainder of 'reader' as a string, closing it when done. */
-  public static String readFully(Reader reader) throws IOException {
-    try {
-      StringWriter writer = new StringWriter();
-      char[] buffer = new char[1024];
-      int count;
-      while ((count = reader.read(buffer)) != -1) {
-        writer.write(buffer, 0, count);
-      }
-      return writer.toString();
-    } finally {
-      reader.close();
-    }
-  }
-
-  public static void skipAll(InputStream in) throws IOException {
-    do {
-      in.skip(Long.MAX_VALUE);
-    } while (in.read() != -1);
-  }
-
-  /**
-   * Call {@code in.read()} repeatedly until either the stream is exhausted or
-   * {@code byteCount} bytes have been read.
-   *
-   * <p>This method reuses the skip buffer but is careful to never use it at
-   * the same time that another stream is using it. Otherwise streams that use
-   * the caller's buffer for consistency checks like CRC could be clobbered by
-   * other threads. A thread-local buffer is also insufficient because some
-   * streams may call other streams in their skip() method, also clobbering the
-   * buffer.
-   */
-  public static long skipByReading(InputStream in, long byteCount) throws IOException {
-    if (byteCount == 0) return 0L;
-
-    // acquire the shared skip buffer.
-    byte[] buffer = skipBuffer.getAndSet(null);
-    if (buffer == null) {
-      buffer = new byte[4096];
-    }
-
-    long skipped = 0;
-    while (skipped < byteCount) {
-      int toRead = (int) Math.min(byteCount - skipped, buffer.length);
-      int read = in.read(buffer, 0, toRead);
-      if (read == -1) {
-        break;
-      }
-      skipped += read;
-      if (read < toRead) {
-        break;
-      }
-    }
-
-    // release the shared skip buffer.
-    skipBuffer.set(buffer);
-
-    return skipped;
-  }
-
-  /**
-   * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
-   * Returns the total number of bytes transferred.
-   */
-  public static int copy(InputStream in, OutputStream out) throws IOException {
-    int total = 0;
-    byte[] buffer = new byte[8192];
-    int c;
-    while ((c = in.read(buffer)) != -1) {
-      total += c;
-      out.write(buffer, 0, c);
-    }
-    return total;
-  }
-
-  /**
-   * Returns the ASCII characters up to but not including the next "\r\n", or
-   * "\n".
-   *
-   * @throws java.io.EOFException if the stream is exhausted before the next newline
-   * character.
-   */
-  public static String readAsciiLine(InputStream in) throws IOException {
-    // TODO: support UTF-8 here instead
-    StringBuilder result = new StringBuilder(80);
-    while (true) {
-      int c = in.read();
-      if (c == -1) {
-        throw new EOFException();
-      } else if (c == '\n') {
-        break;
-      }
-
-      result.append((char) c);
-    }
-    int length = result.length();
-    if (length > 0 && result.charAt(length - 1) == '\r') {
-      result.setLength(length - 1);
-    }
-    return result.toString();
-  }
-
-  /** Returns a 32 character string containing a hash of {@code s}. */
-  public static String hash(String s) {
-    try {
-      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
-      byte[] md5bytes = messageDigest.digest(s.getBytes("UTF-8"));
-      return bytesToHexString(md5bytes);
-    } catch (NoSuchAlgorithmException e) {
-      throw new AssertionError(e);
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError(e);
-    }
-  }
-
-  private static String bytesToHexString(byte[] bytes) {
-    char[] digits = DIGITS;
-    char[] buf = new char[bytes.length * 2];
-    int c = 0;
-    for (byte b : bytes) {
-      buf[c++] = digits[(b >> 4) & 0xf];
-      buf[c++] = digits[b & 0xf];
-    }
-    return new String(buf);
-  }
-
-  /** Returns an immutable copy of {@code list}. */
-  public static <T> List<T> immutableList(List<T> list) {
-    return Collections.unmodifiableList(new ArrayList<T>(list));
-  }
-
-  public static ThreadFactory daemonThreadFactory(final String name) {
-    return new ThreadFactory() {
-      @Override public Thread newThread(Runnable runnable) {
-        Thread result = new Thread(runnable, name);
-        result.setDaemon(true);
-        return result;
-      }
-    };
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
deleted file mode 100644
index 1371262..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
+++ /dev/null
@@ -1,55 +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.spdy;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-/** Reads transport frames for SPDY/3 or HTTP/2.0. */
-public interface FrameReader extends Closeable {
-  void readConnectionHeader() throws IOException;
-  boolean nextFrame(Handler handler) throws IOException;
-
-  public interface Handler {
-    void data(boolean inFinished, int streamId, InputStream in, int length) throws IOException;
-    /**
-     * Create or update incoming headers, creating the corresponding streams
-     * if necessary. Frames that trigger this are SPDY SYN_STREAM, HEADERS, and
-     * SYN_REPLY, and HTTP/2.0 HEADERS and PUSH_PROMISE.
-     *
-     * @param inFinished true if the sender will not send further frames.
-     * @param outFinished true if the receiver should not send further frames.
-     * @param streamId the stream owning these headers.
-     * @param associatedStreamId the stream that triggered the sender to create
-     *     this stream.
-     * @param priority or -1 for no priority. For SPDY, priorities range from 0
-     *     (highest) thru 7 (lowest). For HTTP/2.0, priorities range from 0
-     *     (highest) thru 2**31-1 (lowest).
-     */
-    void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
-        int priority, List<String> nameValueBlock, HeadersMode headersMode);
-    void rstStream(int streamId, ErrorCode errorCode);
-    void settings(boolean clearPrevious, Settings settings);
-    void noop();
-    void ping(boolean reply, int payload1, int payload2);
-    void goAway(int lastGoodStreamId, ErrorCode errorCode);
-    void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl);
-    void priority(int streamId, int priority);
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
deleted file mode 100644
index 354f43d..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
+++ /dev/null
@@ -1,43 +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.spdy;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.List;
-
-/** Writes transport frames for SPDY/3 or HTTP/2.0. */
-public interface FrameWriter extends Closeable {
-  /** HTTP/2.0 only. */
-  void connectionHeader() throws IOException;
-
-  /** SPDY/3 only. */
-  void flush() throws IOException;
-  void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
-      int priority, int slot, List<String> nameValueBlock) throws IOException;
-  void synReply(boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException;
-  void headers(int streamId, List<String> nameValueBlock) throws IOException;
-  void rstStream(int streamId, ErrorCode errorCode) throws IOException;
-  void data(boolean outFinished, int streamId, byte[] data) throws IOException;
-  void data(boolean outFinished, int streamId, byte[] data, int offset, int byteCount)
-      throws IOException;
-  void settings(Settings settings) throws IOException;
-  void noop() throws IOException;
-  void ping(boolean reply, int payload1, int payload2) throws IOException;
-  void goAway(int lastGoodStreamId, ErrorCode errorCode) throws IOException;
-  void windowUpdate(int streamId, int deltaWindowSize) throws IOException;
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java
deleted file mode 100644
index 1e799b4..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java
+++ /dev/null
@@ -1,360 +0,0 @@
-package com.squareup.okhttp.internal.spdy;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.List;
-
-/**
- * Read and write HPACK v01.
- * http://http2.github.io/compression-spec/compression-spec.html#rfc.status
- */
-final class Hpack {
-  static final int PREFIX_5_BITS = 0x1f;
-  static final int PREFIX_6_BITS = 0x3f;
-  static final int PREFIX_7_BITS = 0x7f;
-  static final int PREFIX_8_BITS = 0xff;
-
-  static final List<String> INITIAL_CLIENT_TO_SERVER_HEADER_TABLE = Arrays.asList(
-      ":scheme", "http",
-      ":scheme", "https",
-      ":host", "",
-      ":path", "/",
-      ":method", "GET",
-      "accept", "",
-      "accept-charset", "",
-      "accept-encoding", "",
-      "accept-language", "",
-      "cookie", "",
-      "if-modified-since", "",
-      "user-agent", "",
-      "referer", "",
-      "authorization", "",
-      "allow", "",
-      "cache-control", "",
-      "connection", "",
-      "content-length", "",
-      "content-type", "",
-      "date", "",
-      "expect", "",
-      "from", "",
-      "if-match", "",
-      "if-none-match", "",
-      "if-range", "",
-      "if-unmodified-since", "",
-      "max-forwards", "",
-      "proxy-authorization", "",
-      "range", "",
-      "via", ""
-  );
-
-  static final List<String> INITIAL_SERVER_TO_CLIENT_HEADER_TABLE = Arrays.asList(
-      ":status", "200",
-      "age", "",
-      "cache-control", "",
-      "content-length", "",
-      "content-type", "",
-      "date", "",
-      "etag", "",
-      "expires", "",
-      "last-modified", "",
-      "server", "",
-      "set-cookie", "",
-      "vary", "",
-      "via", "",
-      "access-control-allow-origin", "",
-      "accept-ranges", "",
-      "allow", "",
-      "connection", "",
-      "content-disposition", "",
-      "content-encoding", "",
-      "content-language", "",
-      "content-location", "",
-      "content-range", "",
-      "link", "",
-      "location", "",
-      "proxy-authenticate", "",
-      "refresh", "",
-      "retry-after", "",
-      "strict-transport-security", "",
-      "transfer-encoding", "",
-      "www-authenticate", ""
-  );
-
-  private Hpack() {
-  }
-
-  static class Reader {
-    private final long maxBufferSize = 4096; // TODO: needs to come from settings.
-    private final DataInputStream in;
-
-    private final BitSet referenceSet = new BitSet();
-    private final List<String> headerTable;
-    private final List<String> emittedHeaders = new ArrayList<String>();
-    private long bufferSize = 4096;
-    private long bytesLeft = 0;
-
-    Reader(DataInputStream in, boolean client) {
-      this.in = in;
-      this.headerTable = new ArrayList<String>(client
-          ? INITIAL_CLIENT_TO_SERVER_HEADER_TABLE
-          : INITIAL_SERVER_TO_CLIENT_HEADER_TABLE);
-    }
-
-    /**
-     * Read {@code byteCount} bytes of headers from the source stream into the
-     * set of emitted headers.
-     */
-    public void readHeaders(int byteCount) throws IOException {
-      bytesLeft += byteCount;
-      // TODO: limit to 'byteCount' bytes?
-
-      while (bytesLeft > 0) {
-        int b = readByte();
-
-        if ((b & 0x80) != 0) {
-          int index = readInt(b, PREFIX_7_BITS);
-          readIndexedHeader(index);
-        } else if (b == 0x60) {
-          readLiteralHeaderWithoutIndexingNewName();
-        } else if ((b & 0xe0) == 0x60) {
-          int index = readInt(b, PREFIX_5_BITS);
-          readLiteralHeaderWithoutIndexingIndexedName(index - 1);
-        } else if (b == 0x40) {
-          readLiteralHeaderWithIncrementalIndexingNewName();
-        } else if ((b & 0xe0) == 0x40) {
-          int index = readInt(b, PREFIX_5_BITS);
-          readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
-        } else if (b == 0) {
-          readLiteralHeaderWithSubstitutionIndexingNewName();
-        } else if ((b & 0xc0) == 0) {
-          int index = readInt(b, PREFIX_6_BITS);
-          readLiteralHeaderWithSubstitutionIndexingIndexedName(index - 1);
-        } else {
-          throw new AssertionError();
-        }
-      }
-    }
-
-    public void emitReferenceSet() {
-      for (int i = referenceSet.nextSetBit(0); i != -1; i = referenceSet.nextSetBit(i + 1)) {
-        emittedHeaders.add(getName(i));
-        emittedHeaders.add(getValue(i));
-      }
-    }
-
-    /**
-     * Returns all headers emitted since they were last cleared, then clears the
-     * emitted headers.
-     */
-    public List<String> getAndReset() {
-      List<String> result = new ArrayList<String>(emittedHeaders);
-      emittedHeaders.clear();
-      return result;
-    }
-
-    private void readIndexedHeader(int index) {
-      if (referenceSet.get(index)) {
-        referenceSet.clear(index);
-      } else {
-        referenceSet.set(index);
-        emittedHeaders.add(getName(index));
-        emittedHeaders.add(getValue(index));
-      }
-    }
-
-    private void readLiteralHeaderWithoutIndexingIndexedName(int index)
-        throws IOException {
-      String name = getName(index);
-      String value = readString();
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-    }
-
-    private void readLiteralHeaderWithoutIndexingNewName()
-        throws IOException {
-      String name = readString();
-      String value = readString();
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-    }
-
-    private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
-        throws IOException {
-      int index = headerTable.size();
-      String name = getName(nameIndex);
-      String value = readString();
-      appendToHeaderTable(name, value);
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-      referenceSet.set(index);
-    }
-
-    private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
-      int index = headerTable.size();
-      String name = readString();
-      String value = readString();
-      appendToHeaderTable(name, value);
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-      referenceSet.set(index);
-    }
-
-    private void readLiteralHeaderWithSubstitutionIndexingIndexedName(int nameIndex)
-        throws IOException {
-      int index = readInt(readByte(), PREFIX_8_BITS);
-      String name = getName(nameIndex);
-      String value = readString();
-      replaceInHeaderTable(index, name, value);
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-      referenceSet.set(index);
-    }
-
-    private void readLiteralHeaderWithSubstitutionIndexingNewName() throws IOException {
-      String name = readString();
-      int index = readInt(readByte(), PREFIX_8_BITS);
-      String value = readString();
-      replaceInHeaderTable(index, name, value);
-      emittedHeaders.add(name);
-      emittedHeaders.add(value);
-      referenceSet.set(index);
-    }
-
-    private String getName(int index) {
-      return headerTable.get(index * 2);
-    }
-
-    private String getValue(int index) {
-      return headerTable.get(index * 2 + 1);
-    }
-
-    private void appendToHeaderTable(String name, String value) {
-      insertIntoHeaderTable(headerTable.size() * 2, name, value);
-    }
-
-    private void replaceInHeaderTable(int index, String name, String value) {
-      remove(index);
-      insertIntoHeaderTable(index, name, value);
-    }
-
-    private void insertIntoHeaderTable(int index, String name, String value) {
-      // TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
-
-      int delta = 32 + name.length() + value.length();
-
-      // Prune headers to the required length.
-      while (bufferSize + delta > maxBufferSize) {
-        remove(0);
-        index--;
-      }
-
-      if (delta > maxBufferSize) {
-        return; // New values won't fit in the buffer; skip 'em.
-      }
-
-      if (index == 0) index = 0;
-
-      headerTable.add(index * 2, name);
-      headerTable.add(index * 2 + 1, value);
-      bufferSize += delta;
-    }
-
-    private void remove(int index) {
-      String name = headerTable.remove(index * 2);
-      String value = headerTable.remove(index * 2); // No +1 because it's shifted by remove() above.
-      // TODO: This needs to be the length in UTF-8 bytes, not the length in chars.
-      bufferSize -= (32 + name.length() + value.length());
-    }
-
-    private int readByte() throws IOException {
-      bytesLeft--;
-      return in.readByte() & 0xff;
-    }
-
-    int readInt(int firstByte, int prefixMask) throws IOException {
-      int prefix = firstByte & prefixMask;
-      if (prefix < prefixMask) {
-        return prefix; // This was a single byte value.
-      }
-
-      // This is a multibyte value. Read 7 bits at a time.
-      int result = prefixMask;
-      int shift = 0;
-      while (true) {
-        int b = readByte();
-        if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
-          result += (b & 0x7f) << shift;
-          shift += 7;
-        } else {
-          result += b << shift; // Last byte.
-          break;
-        }
-      }
-      return result;
-    }
-
-    /**
-     * Reads a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this method
-     * may be used to read strings that are known to be ASCII-only.
-     */
-    public String readString() throws IOException {
-      int firstByte = readByte();
-      int length = readInt(firstByte, PREFIX_8_BITS);
-      byte[] encoded = new byte[length];
-      bytesLeft -= length;
-      in.readFully(encoded);
-      return new String(encoded, "UTF-8");
-    }
-  }
-
-  static class Writer {
-    private final OutputStream out;
-
-    Writer(OutputStream out) {
-      this.out = out;
-    }
-
-    public void writeHeaders(List<String> nameValueBlock) throws IOException {
-      // TODO: implement a compression strategy.
-      for (int i = 0, size = nameValueBlock.size(); i < size; i += 2) {
-        out.write(0x60); // Literal Header without Indexing - New Name.
-        writeString(nameValueBlock.get(i));
-        writeString(nameValueBlock.get(i + 1));
-      }
-    }
-
-    public void writeInt(int value, int prefixMask, int bits) throws IOException {
-      // Write the raw value for a single byte value.
-      if (value < prefixMask) {
-        out.write(bits | value);
-        return;
-      }
-
-      // Write the mask to start a multibyte value.
-      out.write(bits | prefixMask);
-      value -= prefixMask;
-
-      // Write 7 bits at a time 'til we're done.
-      while (value >= 0x80) {
-        int b = value & 0x7f;
-        out.write(b | 0x80);
-        value >>>= 7;
-      }
-      out.write(value);
-    }
-
-    /**
-     * Writes a UTF-8 encoded string. Since ASCII is a subset of UTF-8, this
-     * method can be used to write strings that are known to be ASCII-only.
-     */
-    public void writeString(String headerName) throws IOException {
-      byte[] bytes = headerName.getBytes("UTF-8");
-      writeInt(bytes.length, PREFIX_8_BITS, 0);
-      out.write(bytes);
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java
deleted file mode 100644
index 1d48def..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft04.java
+++ /dev/null
@@ -1,371 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.spdy;
-
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-import java.util.List;
-
-final class Http20Draft04 implements Variant {
-  private static final byte[] CONNECTION_HEADER;
-  static {
-    try {
-      CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes("UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError();
-    }
-  }
-
-  static final int TYPE_DATA = 0x0;
-  static final int TYPE_HEADERS = 0x1;
-  static final int TYPE_PRIORITY = 0x2;
-  static final int TYPE_RST_STREAM = 0x3;
-  static final int TYPE_SETTINGS = 0x4;
-  static final int TYPE_PUSH_PROMISE = 0x5;
-  static final int TYPE_PING = 0x6;
-  static final int TYPE_GOAWAY = 0x7;
-  static final int TYPE_WINDOW_UPDATE = 0x9;
-
-  static final int FLAG_END_STREAM = 0x1;
-  static final int FLAG_END_HEADERS = 0x4;
-  static final int FLAG_PRIORITY = 0x8;
-  static final int FLAG_PONG = 0x1;
-  static final int FLAG_END_FLOW_CONTROL = 0x1;
-
-  @Override public FrameReader newReader(InputStream in, boolean client) {
-    return new Reader(in, client);
-  }
-
-  @Override public FrameWriter newWriter(OutputStream out, boolean client) {
-    return new Writer(out, client);
-  }
-
-  static final class Reader implements FrameReader {
-    private final DataInputStream in;
-    private final boolean client;
-    private final Hpack.Reader hpackReader;
-
-    Reader(InputStream in, boolean client) {
-      this.in = new DataInputStream(in);
-      this.client = client;
-      this.hpackReader = new Hpack.Reader(this.in, client);
-    }
-
-    @Override public void readConnectionHeader() throws IOException {
-      if (client) return; // Nothing to read; servers don't send connection headers!
-      byte[] connectionHeader = new byte[CONNECTION_HEADER.length];
-      in.readFully(connectionHeader);
-      if (!Arrays.equals(connectionHeader, CONNECTION_HEADER)) {
-        throw ioException("Expected a connection header but was "
-            + Arrays.toString(connectionHeader));
-      }
-    }
-
-    @Override public boolean nextFrame(Handler handler) throws IOException {
-      int w1;
-      try {
-        w1 = in.readInt();
-      } catch (IOException e) {
-        return false; // This might be a normal socket close.
-      }
-      int w2 = in.readInt();
-
-      int length = (w1 & 0xffff0000) >> 16;
-      int type = (w1 & 0xff00) >> 8;
-      int flags = w1 & 0xff;
-      // boolean r = (w2 & 0x80000000) != 0; // Reserved.
-      int streamId = (w2 & 0x7fffffff);
-
-      switch (type) {
-        case TYPE_DATA:
-          readData(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_HEADERS:
-          readHeaders(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_PRIORITY:
-          readPriority(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_RST_STREAM:
-          readRstStream(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_SETTINGS:
-          readSettings(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_PUSH_PROMISE:
-          readPushPromise(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_PING:
-          readPing(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_GOAWAY:
-          readGoAway(handler, flags, length, streamId);
-          return true;
-
-        case TYPE_WINDOW_UPDATE:
-          readWindowUpdate(handler, flags, length, streamId);
-          return true;
-      }
-
-      throw new UnsupportedOperationException("TODO");
-    }
-
-    private void readHeaders(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      if (streamId == 0) throw ioException("TYPE_HEADERS streamId == 0");
-
-      while (true) {
-        hpackReader.readHeaders(length);
-
-        if ((flags & FLAG_END_HEADERS) != 0) {
-          hpackReader.emitReferenceSet();
-          List<String> namesAndValues = hpackReader.getAndReset();
-          boolean inFinished = (flags & FLAG_END_STREAM) != 0;
-          int priority = -1; // TODO: priority
-          handler.headers(false, inFinished, streamId, -1, priority, namesAndValues,
-              HeadersMode.HTTP_20_HEADERS);
-          return;
-        }
-
-        // Read another frame of headers.
-        int w1 = in.readInt();
-        int w2 = in.readInt();
-
-        length = (w1 & 0xffff0000) >> 16;
-        int newType = (w1 & 0xff00) >> 8;
-        flags = w1 & 0xff;
-        // boolean r = (w2 & 0x80000000) != 0; // Reserved.
-        int newStreamId = (w2 & 0x7fffffff);
-
-        if (newType != TYPE_HEADERS) throw ioException("TYPE_HEADERS didn't have FLAG_END_HEADERS");
-        if (newStreamId != streamId) throw ioException("TYPE_HEADERS streamId changed");
-      }
-    }
-
-    private void readData(Handler handler, int flags, int length, int streamId) throws IOException {
-      boolean inFinished = (flags & FLAG_END_STREAM) != 0;
-      handler.data(inFinished, streamId, in, length);
-    }
-
-    private void readPriority(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      if (length != 4) throw ioException("TYPE_PRIORITY length: %d != 4", length);
-      if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0");
-      int w1 = in.readInt();
-      // boolean r = (w1 & 0x80000000) != 0; // Reserved.
-      int priority = (w1 & 0x7fffffff);
-      handler.priority(streamId, priority);
-    }
-
-    private void readRstStream(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length);
-      if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0");
-      int errorCodeInt = in.readInt();
-      ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
-      if (errorCode == null) {
-        throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
-      }
-      handler.rstStream(streamId, errorCode);
-    }
-
-    private void readSettings(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length);
-      if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0");
-      Settings settings = new Settings();
-      for (int i = 0; i < length; i += 8) {
-        int w1 = in.readInt();
-        int value = in.readInt();
-        // int r = (w1 & 0xff000000) >>> 24; // Reserved.
-        int id = w1 & 0xffffff;
-        settings.set(id, 0, value);
-      }
-      handler.settings(false, settings);
-    }
-
-    private void readPushPromise(Handler handler, int flags, int length, int streamId) {
-      // TODO:
-    }
-
-    private void readPing(Handler handler, int flags, int length, int streamId) throws IOException {
-      if (length != 8) throw ioException("TYPE_PING length != 8: %s", length);
-      if (streamId != 0) throw ioException("TYPE_PING streamId != 0");
-      int payload1 = in.readInt();
-      int payload2 = in.readInt();
-      boolean reply = (flags & FLAG_PONG) != 0;
-      handler.ping(reply, payload1, payload2);
-    }
-
-    private void readGoAway(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length);
-      int lastStreamId = in.readInt();
-      int errorCodeInt = in.readInt();
-      int opaqueDataLength = length - 8;
-      ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
-      if (errorCode == null) {
-        throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
-      }
-      if (Util.skipByReading(in, opaqueDataLength) != opaqueDataLength) {
-        throw new IOException("TYPE_GOAWAY opaque data was truncated");
-      }
-      handler.goAway(lastStreamId, errorCode);
-    }
-
-    private void readWindowUpdate(Handler handler, int flags, int length, int streamId)
-        throws IOException {
-      int w1 = in.readInt();
-      // boolean r = (w1 & 0x80000000) != 0; // Reserved.
-      int windowSizeIncrement = (w1 & 0x7fffffff);
-      boolean endFlowControl = (flags & FLAG_END_FLOW_CONTROL) != 0;
-      handler.windowUpdate(streamId, windowSizeIncrement, endFlowControl);
-    }
-
-    private static IOException ioException(String message, Object... args) throws IOException {
-      throw new IOException(String.format(message, args));
-    }
-
-    @Override public void close() throws IOException {
-      in.close();
-    }
-  }
-
-  static final class Writer implements FrameWriter {
-    private final DataOutputStream out;
-    private final boolean client;
-    private final ByteArrayOutputStream hpackBuffer;
-    private final Hpack.Writer hpackWriter;
-
-    Writer(OutputStream out, boolean client) {
-      this.out = new DataOutputStream(out);
-      this.client = client;
-      this.hpackBuffer = new ByteArrayOutputStream();
-      this.hpackWriter = new Hpack.Writer(hpackBuffer);
-    }
-
-    @Override public synchronized void flush() throws IOException {
-      out.flush();
-    }
-
-    @Override public synchronized void connectionHeader() throws IOException {
-      if (!client) return; // Nothing to write; servers don't send connection headers!
-      out.write(CONNECTION_HEADER);
-    }
-
-    @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
-        int streamId, int associatedStreamId, int priority, int slot, List<String> nameValueBlock)
-        throws IOException {
-      if (inFinished) throw new UnsupportedOperationException();
-      headers(outFinished, streamId, priority, nameValueBlock);
-    }
-
-    @Override public synchronized void synReply(boolean outFinished, int streamId,
-        List<String> nameValueBlock) throws IOException {
-      headers(outFinished, streamId, -1, nameValueBlock);
-    }
-
-    @Override public synchronized void headers(int streamId, List<String> nameValueBlock)
-        throws IOException {
-      headers(false, streamId, -1, nameValueBlock);
-    }
-
-    private void headers(boolean outFinished, int streamId, int priority,
-        List<String> nameValueBlock) throws IOException {
-      hpackBuffer.reset();
-      hpackWriter.writeHeaders(nameValueBlock);
-      int type = TYPE_HEADERS;
-      int length = hpackBuffer.size();
-      int flags = FLAG_END_HEADERS;
-      if (outFinished) flags |= FLAG_END_STREAM;
-      if (priority != -1) flags |= FLAG_PRIORITY;
-      out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
-      out.writeInt(streamId & 0x7fffffff);
-      if (priority != -1) out.writeInt(priority & 0x7fffffff);
-      hpackBuffer.writeTo(out);
-    }
-
-    @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
-        throws IOException {
-      throw new UnsupportedOperationException("TODO");
-    }
-
-    @Override public void data(boolean outFinished, int streamId, byte[] data) throws IOException {
-      data(outFinished, streamId, data, 0, data.length);
-    }
-
-    @Override public synchronized void data(boolean outFinished, int streamId, byte[] data,
-        int offset, int byteCount) throws IOException {
-      int type = TYPE_DATA;
-      int flags = 0;
-      if (outFinished) flags |= FLAG_END_STREAM;
-      out.writeInt((byteCount & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
-      out.writeInt(streamId & 0x7fffffff);
-      out.write(data, offset, byteCount);
-    }
-
-    @Override public synchronized void settings(Settings settings) throws IOException {
-      int type = TYPE_SETTINGS;
-      int length = settings.size() * 8;
-      int flags = 0;
-      int streamId = 0;
-      out.writeInt((length & 0xffff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
-      out.writeInt(streamId & 0x7fffffff);
-      for (int i = 0; i < Settings.COUNT; i++) {
-        if (!settings.isSet(i)) continue;
-        out.writeInt(i & 0xffffff);
-        out.writeInt(settings.get(i));
-      }
-    }
-
-    @Override public synchronized void noop() throws IOException {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override public synchronized void ping(boolean reply, int payload1, int payload2)
-        throws IOException {
-      // TODO
-    }
-
-    @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode)
-        throws IOException {
-      // TODO
-    }
-
-    @Override public synchronized void windowUpdate(int streamId, int deltaWindowSize)
-        throws IOException {
-      // TODO
-    }
-
-    @Override public void close() throws IOException {
-      out.close();
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
deleted file mode 100644
index b95d013..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package com.squareup.okhttp.internal.spdy;
-
-import com.squareup.okhttp.internal.Util;
-import java.io.Closeable;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
-import java.util.zip.InflaterInputStream;
-
-/**
- * Reads a SPDY/3 Name/Value header block. This class is made complicated by the
- * requirement that we're strict with which bytes we put in the compressed bytes
- * buffer. We need to put all compressed bytes into that buffer -- but no other
- * bytes.
- */
-class NameValueBlockReader implements Closeable {
-  private final DataInputStream nameValueBlockIn;
-  private final FillableInflaterInputStream fillableInflaterInputStream;
-  private int compressedLimit;
-
-  NameValueBlockReader(final InputStream in) {
-    // Limit the inflater input stream to only those bytes in the Name/Value block. We cut the
-    // inflater off at its source because we can't predict the ratio of compressed bytes to
-    // uncompressed bytes.
-    InputStream throttleStream = new InputStream() {
-      @Override public int read() throws IOException {
-        return Util.readSingleByte(this);
-      }
-
-      @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
-        byteCount = Math.min(byteCount, compressedLimit);
-        int consumed = in.read(buffer, offset, byteCount);
-        compressedLimit -= consumed;
-        return consumed;
-      }
-
-      @Override public void close() throws IOException {
-        in.close();
-      }
-    };
-
-    // Subclass inflater to install a dictionary when it's needed.
-    Inflater inflater = new Inflater() {
-      @Override public int inflate(byte[] buffer, int offset, int count)
-          throws DataFormatException {
-        int result = super.inflate(buffer, offset, count);
-        if (result == 0 && needsDictionary()) {
-          setDictionary(Spdy3.DICTIONARY);
-          result = super.inflate(buffer, offset, count);
-        }
-        return result;
-      }
-    };
-
-    fillableInflaterInputStream = new FillableInflaterInputStream(throttleStream, inflater);
-    nameValueBlockIn = new DataInputStream(fillableInflaterInputStream);
-  }
-
-  /** Extend the inflater stream so we can eagerly fill the compressed bytes buffer if necessary. */
-  static class FillableInflaterInputStream extends InflaterInputStream {
-    public FillableInflaterInputStream(InputStream in, Inflater inf) {
-      super(in, inf);
-    }
-
-    @Override public void fill() throws IOException {
-      super.fill(); // This method is protected in the superclass.
-    }
-  }
-
-  public List<String> readNameValueBlock(int length) throws IOException {
-    this.compressedLimit += length;
-    try {
-      int numberOfPairs = nameValueBlockIn.readInt();
-      if (numberOfPairs < 0) {
-        throw new IOException("numberOfPairs < 0: " + numberOfPairs);
-      }
-      if (numberOfPairs > 1024) {
-        throw new IOException("numberOfPairs > 1024: " + numberOfPairs);
-      }
-      List<String> entries = new ArrayList<String>(numberOfPairs * 2);
-      for (int i = 0; i < numberOfPairs; i++) {
-        String name = readString();
-        String values = readString();
-        if (name.length() == 0) throw new IOException("name.length == 0");
-        entries.add(name);
-        entries.add(values);
-      }
-
-      doneReading();
-
-      return entries;
-    } catch (DataFormatException e) {
-      throw new IOException(e.getMessage());
-    }
-  }
-
-  private void doneReading() throws IOException {
-    if (compressedLimit == 0) return;
-
-    // Read any outstanding unread bytes. One side-effect of deflate compression is that sometimes
-    // there are bytes remaining in the stream after we've consumed all of the content.
-    fillableInflaterInputStream.fill();
-
-    if (compressedLimit != 0) {
-      throw new IOException("compressedLimit > 0: " + compressedLimit);
-    }
-  }
-
-  private String readString() throws DataFormatException, IOException {
-    int length = nameValueBlockIn.readInt();
-    byte[] bytes = new byte[length];
-    Util.readFully(nameValueBlockIn, bytes);
-    return new String(bytes, 0, length, "UTF-8");
-  }
-
-  @Override public void close() throws IOException {
-    nameValueBlockIn.close();
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
deleted file mode 100644
index 5d9a49b..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
+++ /dev/null
@@ -1,463 +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.spdy;
-
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.ProtocolException;
-import java.util.List;
-import java.util.zip.Deflater;
-
-final class Spdy3 implements Variant {
-  static final int TYPE_DATA = 0x0;
-  static final int TYPE_SYN_STREAM = 0x1;
-  static final int TYPE_SYN_REPLY = 0x2;
-  static final int TYPE_RST_STREAM = 0x3;
-  static final int TYPE_SETTINGS = 0x4;
-  static final int TYPE_NOOP = 0x5;
-  static final int TYPE_PING = 0x6;
-  static final int TYPE_GOAWAY = 0x7;
-  static final int TYPE_HEADERS = 0x8;
-  static final int TYPE_WINDOW_UPDATE = 0x9;
-  static final int TYPE_CREDENTIAL = 0x10;
-
-  static final int FLAG_FIN = 0x1;
-  static final int FLAG_UNIDIRECTIONAL = 0x2;
-
-  static final int VERSION = 3;
-
-  static final byte[] DICTIONARY;
-  static {
-    try {
-      DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
-          + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
-          + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
-          + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
-          + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
-          + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
-          + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
-          + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
-          + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
-          + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
-          + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
-          + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
-          + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
-          + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
-          + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
-          + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
-          + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
-          + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
-          + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
-          + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
-          + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
-          + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
-          + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
-          + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
-          + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
-          + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
-          + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
-          + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
-          + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
-          + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
-          + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
-          + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
-          + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError();
-    }
-  }
-
-  @Override public FrameReader newReader(InputStream in, boolean client) {
-    return new Reader(in, client);
-  }
-
-  @Override public FrameWriter newWriter(OutputStream out, boolean client) {
-    return new Writer(out, client);
-  }
-
-  /** Read spdy/3 frames. */
-  static final class Reader implements FrameReader {
-    private final DataInputStream in;
-    private final boolean client;
-    private final NameValueBlockReader nameValueBlockReader;
-
-    Reader(InputStream in, boolean client) {
-      this.in = new DataInputStream(in);
-      this.nameValueBlockReader = new NameValueBlockReader(in);
-      this.client = client;
-    }
-
-    @Override public void readConnectionHeader() {
-    }
-
-    /**
-     * Send the next frame to {@code handler}. Returns true unless there are no
-     * more frames on the stream.
-     */
-    @Override public boolean nextFrame(Handler handler) throws IOException {
-      int w1;
-      try {
-        w1 = in.readInt();
-      } catch (IOException e) {
-        return false; // This might be a normal socket close.
-      }
-      int w2 = in.readInt();
-
-      boolean control = (w1 & 0x80000000) != 0;
-      int flags = (w2 & 0xff000000) >>> 24;
-      int length = (w2 & 0xffffff);
-
-      if (control) {
-        int version = (w1 & 0x7fff0000) >>> 16;
-        int type = (w1 & 0xffff);
-
-        if (version != 3) {
-          throw new ProtocolException("version != 3: " + version);
-        }
-
-        switch (type) {
-          case TYPE_SYN_STREAM:
-            readSynStream(handler, flags, length);
-            return true;
-
-          case TYPE_SYN_REPLY:
-            readSynReply(handler, flags, length);
-            return true;
-
-          case TYPE_RST_STREAM:
-            readRstStream(handler, flags, length);
-            return true;
-
-          case TYPE_SETTINGS:
-            readSettings(handler, flags, length);
-            return true;
-
-          case TYPE_NOOP:
-            if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
-            handler.noop();
-            return true;
-
-          case TYPE_PING:
-            readPing(handler, flags, length);
-            return true;
-
-          case TYPE_GOAWAY:
-            readGoAway(handler, flags, length);
-            return true;
-
-          case TYPE_HEADERS:
-            readHeaders(handler, flags, length);
-            return true;
-
-          case TYPE_WINDOW_UPDATE:
-            readWindowUpdate(handler, flags, length);
-            return true;
-
-          case TYPE_CREDENTIAL:
-            Util.skipByReading(in, length);
-            throw new UnsupportedOperationException("TODO"); // TODO: implement
-
-          default:
-            throw new IOException("Unexpected frame");
-        }
-      } else {
-        int streamId = w1 & 0x7fffffff;
-        boolean inFinished = (flags & FLAG_FIN) != 0;
-        handler.data(inFinished, streamId, in, length);
-        return true;
-      }
-    }
-
-    private void readSynStream(Handler handler, int flags, int length) throws IOException {
-      int w1 = in.readInt();
-      int w2 = in.readInt();
-      int s3 = in.readShort();
-      int streamId = w1 & 0x7fffffff;
-      int associatedStreamId = w2 & 0x7fffffff;
-      int priority = (s3 & 0xe000) >>> 13;
-      int slot = s3 & 0xff;
-      List<String> nameValueBlock = nameValueBlockReader.readNameValueBlock(length - 10);
-
-      boolean inFinished = (flags & FLAG_FIN) != 0;
-      boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0;
-      handler.headers(outFinished, inFinished, streamId, associatedStreamId, priority,
-          nameValueBlock, HeadersMode.SPDY_SYN_STREAM);
-    }
-
-    private void readSynReply(Handler handler, int flags, int length) throws IOException {
-      int w1 = in.readInt();
-      int streamId = w1 & 0x7fffffff;
-      List<String> nameValueBlock = nameValueBlockReader.readNameValueBlock(length - 4);
-      boolean inFinished = (flags & FLAG_FIN) != 0;
-      handler.headers(false, inFinished, streamId, -1, -1, nameValueBlock, HeadersMode.SPDY_REPLY);
-    }
-
-    private void readRstStream(Handler handler, int flags, int length) throws IOException {
-      if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length);
-      int streamId = in.readInt() & 0x7fffffff;
-      int errorCodeInt = in.readInt();
-      ErrorCode errorCode = ErrorCode.fromSpdy3Rst(errorCodeInt);
-      if (errorCode == null) {
-        throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
-      }
-      handler.rstStream(streamId, errorCode);
-    }
-
-    private void readHeaders(Handler handler, int flags, int length) throws IOException {
-      int w1 = in.readInt();
-      int streamId = w1 & 0x7fffffff;
-      List<String> nameValueBlock = nameValueBlockReader.readNameValueBlock(length - 4);
-      handler.headers(false, false, streamId, -1, -1, nameValueBlock, HeadersMode.SPDY_HEADERS);
-    }
-
-    private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
-      if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length);
-      int w1 = in.readInt();
-      int w2 = in.readInt();
-      int streamId = w1 & 0x7fffffff;
-      int deltaWindowSize = w2 & 0x7fffffff;
-      handler.windowUpdate(streamId, deltaWindowSize, false);
-    }
-
-    private void readPing(Handler handler, int flags, int length) throws IOException {
-      if (length != 4) throw ioException("TYPE_PING length: %d != 4", length);
-      int id = in.readInt();
-      boolean reply = client == ((id % 2) == 1);
-      handler.ping(reply, id, 0);
-    }
-
-    private void readGoAway(Handler handler, int flags, int length) throws IOException {
-      if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length);
-      int lastGoodStreamId = in.readInt() & 0x7fffffff;
-      int errorCodeInt = in.readInt();
-      ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt);
-      if (errorCode == null) {
-        throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
-      }
-      handler.goAway(lastGoodStreamId, errorCode);
-    }
-
-    private void readSettings(Handler handler, int flags, int length) throws IOException {
-      int numberOfEntries = in.readInt();
-      if (length != 4 + 8 * numberOfEntries) {
-        throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries);
-      }
-      Settings settings = new Settings();
-      for (int i = 0; i < numberOfEntries; i++) {
-        int w1 = in.readInt();
-        int value = in.readInt();
-        int idFlags = (w1 & 0xff000000) >>> 24;
-        int id = w1 & 0xffffff;
-        settings.set(id, idFlags, value);
-      }
-      boolean clearPrevious = (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0;
-      handler.settings(clearPrevious, settings);
-    }
-
-    private static IOException ioException(String message, Object... args) throws IOException {
-      throw new IOException(String.format(message, args));
-    }
-
-    @Override public void close() throws IOException {
-      Util.closeAll(in, nameValueBlockReader);
-    }
-  }
-
-  /** Write spdy/3 frames. */
-  static final class Writer implements FrameWriter {
-    private final DataOutputStream out;
-    private final ByteArrayOutputStream nameValueBlockBuffer;
-    private final DataOutputStream nameValueBlockOut;
-    private final boolean client;
-
-    Writer(OutputStream out, boolean client) {
-      this.out = new DataOutputStream(out);
-      this.client = client;
-
-      Deflater deflater = new Deflater();
-      deflater.setDictionary(DICTIONARY);
-      nameValueBlockBuffer = new ByteArrayOutputStream();
-      nameValueBlockOut = new DataOutputStream(
-          Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
-    }
-
-    @Override public synchronized void connectionHeader() {
-      // Do nothing: no connection header for SPDY/3.
-    }
-
-    @Override public synchronized void flush() throws IOException {
-      out.flush();
-    }
-
-    @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
-        int streamId, int associatedStreamId, int priority, int slot, List<String> nameValueBlock)
-        throws IOException {
-      writeNameValueBlockToBuffer(nameValueBlock);
-      int length = 10 + nameValueBlockBuffer.size();
-      int type = TYPE_SYN_STREAM;
-      int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0);
-
-      int unused = 0;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(streamId & 0x7fffffff);
-      out.writeInt(associatedStreamId & 0x7fffffff);
-      out.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
-      nameValueBlockBuffer.writeTo(out);
-      out.flush();
-    }
-
-    @Override public synchronized void synReply(
-        boolean outFinished, int streamId, List<String> nameValueBlock) throws IOException {
-      writeNameValueBlockToBuffer(nameValueBlock);
-      int type = TYPE_SYN_REPLY;
-      int flags = (outFinished ? FLAG_FIN : 0);
-      int length = nameValueBlockBuffer.size() + 4;
-
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(streamId & 0x7fffffff);
-      nameValueBlockBuffer.writeTo(out);
-      out.flush();
-    }
-
-    @Override public synchronized void headers(int streamId, List<String> nameValueBlock)
-        throws IOException {
-      writeNameValueBlockToBuffer(nameValueBlock);
-      int flags = 0;
-      int type = TYPE_HEADERS;
-      int length = nameValueBlockBuffer.size() + 4;
-
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(streamId & 0x7fffffff);
-      nameValueBlockBuffer.writeTo(out);
-      out.flush();
-    }
-
-    @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
-        throws IOException {
-      if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
-      int flags = 0;
-      int type = TYPE_RST_STREAM;
-      int length = 8;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(streamId & 0x7fffffff);
-      out.writeInt(errorCode.spdyRstCode);
-      out.flush();
-    }
-
-    @Override public synchronized void data(boolean outFinished, int streamId, byte[] data)
-        throws IOException {
-      data(outFinished, streamId, data, 0, data.length);
-    }
-
-    @Override public synchronized void data(boolean outFinished, int streamId, byte[] data,
-        int offset, int byteCount) throws IOException {
-      int flags = (outFinished ? FLAG_FIN : 0);
-      out.writeInt(streamId & 0x7fffffff);
-      out.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff);
-      out.write(data, offset, byteCount);
-    }
-
-    private void writeNameValueBlockToBuffer(List<String> nameValueBlock) throws IOException {
-      nameValueBlockBuffer.reset();
-      int numberOfPairs = nameValueBlock.size() / 2;
-      nameValueBlockOut.writeInt(numberOfPairs);
-      for (String s : nameValueBlock) {
-        nameValueBlockOut.writeInt(s.length());
-        nameValueBlockOut.write(s.getBytes("UTF-8"));
-      }
-      nameValueBlockOut.flush();
-    }
-
-    @Override public synchronized void settings(Settings settings) throws IOException {
-      int type = TYPE_SETTINGS;
-      int flags = 0;
-      int size = settings.size();
-      int length = 4 + size * 8;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(size);
-      for (int i = 0; i <= Settings.COUNT; i++) {
-        if (!settings.isSet(i)) continue;
-        int settingsFlags = settings.flags(i);
-        out.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff));
-        out.writeInt(settings.get(i));
-      }
-      out.flush();
-    }
-
-    @Override public synchronized void noop() throws IOException {
-      int type = TYPE_NOOP;
-      int length = 0;
-      int flags = 0;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.flush();
-    }
-
-    @Override public synchronized void ping(boolean reply, int payload1, int payload2)
-        throws IOException {
-      boolean payloadIsReply = client != ((payload1 % 2) == 1);
-      if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply");
-      int type = TYPE_PING;
-      int flags = 0;
-      int length = 4;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(payload1);
-      out.flush();
-    }
-
-    @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode)
-        throws IOException {
-      if (errorCode.spdyGoAwayCode == -1) throw new IllegalArgumentException();
-      int type = TYPE_GOAWAY;
-      int flags = 0;
-      int length = 8;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(lastGoodStreamId);
-      out.writeInt(errorCode.spdyGoAwayCode);
-      out.flush();
-    }
-
-    @Override public synchronized void windowUpdate(int streamId, int deltaWindowSize)
-        throws IOException {
-      int type = TYPE_WINDOW_UPDATE;
-      int flags = 0;
-      int length = 8;
-      out.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
-      out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-      out.writeInt(streamId);
-      out.writeInt(deltaWindowSize);
-      out.flush();
-    }
-
-    @Override public void close() throws IOException {
-      Util.closeAll(out, nameValueBlockOut);
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
deleted file mode 100644
index b19bd44..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ /dev/null
@@ -1,599 +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.spdy;
-
-import com.squareup.okhttp.internal.NamedRunnable;
-import com.squareup.okhttp.internal.Util;
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.Socket;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A socket connection to a remote peer. A connection hosts streams which can
- * send and receive data.
- *
- * <p>Many methods in this API are <strong>synchronous:</strong> the call is
- * completed before the method returns. This is typical for Java but atypical
- * for SPDY. This is motivated by exception transparency: an IOException that
- * was triggered by a certain caller can be caught and handled by that caller.
- */
-public final class SpdyConnection implements Closeable {
-
-  // Internal state of this connection is guarded by 'this'. No blocking
-  // operations may be performed while holding this lock!
-  //
-  // Socket writes are guarded by frameWriter.
-  //
-  // Socket reads are unguarded but are only made by the reader thread.
-  //
-  // Certain operations (like SYN_STREAM) need to synchronize on both the
-  // frameWriter (to do blocking I/O) and this (to create streams). Such
-  // operations must synchronize on 'this' last. This ensures that we never
-  // wait for a blocking operation while holding 'this'.
-
-  private static final ExecutorService executor = new ThreadPoolExecutor(0,
-      Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
-      Util.daemonThreadFactory("OkHttp SpdyConnection"));
-
-  /** The protocol variant, like SPDY/3 or HTTP-draft-04/2.0. */
-  final Variant variant;
-
-  /** True if this peer initiated the connection. */
-  final boolean client;
-
-  /**
-   * User code to run in response to an incoming stream. Callbacks must not be
-   * run on the callback executor.
-   */
-  private final IncomingStreamHandler handler;
-  private final FrameReader frameReader;
-  private final FrameWriter frameWriter;
-
-  private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
-  private final String hostName;
-  private int lastGoodStreamId;
-  private int nextStreamId;
-  private boolean shutdown;
-  private long idleStartTimeNs = System.nanoTime();
-
-  /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
-  private Map<Integer, Ping> pings;
-  private int nextPingId;
-
-  /** Lazily-created settings for the peer. */
-  Settings settings;
-
-  private SpdyConnection(Builder builder) {
-    variant = builder.variant;
-    client = builder.client;
-    handler = builder.handler;
-    frameReader = variant.newReader(builder.in, client);
-    frameWriter = variant.newWriter(builder.out, client);
-    nextStreamId = builder.client ? 1 : 2;
-    nextPingId = builder.client ? 1 : 2;
-
-    hostName = builder.hostName;
-
-    new Thread(new Reader(), "Spdy Reader " + hostName).start();
-  }
-
-  /**
-   * Returns the number of {@link SpdyStream#isOpen() open streams} on this
-   * connection.
-   */
-  public synchronized int openStreamCount() {
-    return streams.size();
-  }
-
-  private synchronized SpdyStream getStream(int id) {
-    return streams.get(id);
-  }
-
-  synchronized SpdyStream removeStream(int streamId) {
-    SpdyStream stream = streams.remove(streamId);
-    if (stream != null && streams.isEmpty()) {
-      setIdle(true);
-    }
-    return stream;
-  }
-
-  private synchronized void setIdle(boolean value) {
-    idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE;
-  }
-
-  /** Returns true if this connection is idle. */
-  public synchronized boolean isIdle() {
-    return idleStartTimeNs != Long.MAX_VALUE;
-  }
-
-  /**
-   * Returns the time in ns when this connection became idle or Long.MAX_VALUE
-   * if connection is not idle.
-   */
-  public synchronized long getIdleStartTimeNs() {
-    return idleStartTimeNs;
-  }
-
-  /**
-   * Returns a new locally-initiated stream.
-   *
-   * @param out true to create an output stream that we can use to send data
-   *     to the remote peer. Corresponds to {@code FLAG_FIN}.
-   * @param in true to create an input stream that the remote peer can use to
-   *     send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}.
-   */
-  public SpdyStream newStream(List<String> requestHeaders, boolean out, boolean in)
-      throws IOException {
-    boolean outFinished = !out;
-    boolean inFinished = !in;
-    int associatedStreamId = 0;  // TODO: permit the caller to specify an associated stream?
-    int priority = 0; // TODO: permit the caller to specify a priority?
-    int slot = 0; // TODO: permit the caller to specify a slot?
-    SpdyStream stream;
-    int streamId;
-
-    synchronized (frameWriter) {
-      synchronized (this) {
-        if (shutdown) {
-          throw new IOException("shutdown");
-        }
-        streamId = nextStreamId;
-        nextStreamId += 2;
-        stream = new SpdyStream(
-            streamId, this, outFinished, inFinished, priority, requestHeaders, settings);
-        if (stream.isOpen()) {
-          streams.put(streamId, stream);
-          setIdle(false);
-        }
-      }
-
-      frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, priority, slot,
-          requestHeaders);
-    }
-
-    return stream;
-  }
-
-  void writeSynReply(int streamId, boolean outFinished, List<String> alternating)
-      throws IOException {
-    frameWriter.synReply(outFinished, streamId, alternating);
-  }
-
-  public void writeData(int streamId, boolean outFinished, byte[] buffer, int offset, int byteCount)
-      throws IOException {
-    frameWriter.data(outFinished, streamId, buffer, offset, byteCount);
-  }
-
-  void writeSynResetLater(final int streamId, final ErrorCode errorCode) {
-    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
-      @Override public void execute() {
-        try {
-          writeSynReset(streamId, errorCode);
-        } catch (IOException ignored) {
-        }
-      }
-    });
-  }
-
-  void writeSynReset(int streamId, ErrorCode statusCode) throws IOException {
-    frameWriter.rstStream(streamId, statusCode);
-  }
-
-  void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
-    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s stream %d", hostName, streamId) {
-      @Override public void execute() {
-        try {
-          writeWindowUpdate(streamId, deltaWindowSize);
-        } catch (IOException ignored) {
-        }
-      }
-    });
-  }
-
-  void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
-    frameWriter.windowUpdate(streamId, deltaWindowSize);
-  }
-
-  /**
-   * Sends a ping frame to the peer. Use the returned object to await the
-   * ping's response and observe its round trip time.
-   */
-  public Ping ping() throws IOException {
-    Ping ping = new Ping();
-    int pingId;
-    synchronized (this) {
-      if (shutdown) {
-        throw new IOException("shutdown");
-      }
-      pingId = nextPingId;
-      nextPingId += 2;
-      if (pings == null) pings = new HashMap<Integer, Ping>();
-      pings.put(pingId, ping);
-    }
-    writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping);
-    return ping;
-  }
-
-  private void writePingLater(
-      final boolean reply, final int payload1, final int payload2, final Ping ping) {
-    executor.submit(new NamedRunnable("OkHttp SPDY Writer %s ping %08x%08x",
-        hostName, payload1, payload2) {
-      @Override public void execute() {
-        try {
-          writePing(reply, payload1, payload2, ping);
-        } catch (IOException ignored) {
-        }
-      }
-    });
-  }
-
-  private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException {
-    synchronized (frameWriter) {
-      // Observe the sent time immediately before performing I/O.
-      if (ping != null) ping.send();
-      frameWriter.ping(reply, payload1, payload2);
-    }
-  }
-
-  private synchronized Ping removePing(int id) {
-    return pings != null ? pings.remove(id) : null;
-  }
-
-  /** Sends a noop frame to the peer. */
-  public void noop() throws IOException {
-    frameWriter.noop();
-  }
-
-  public void flush() throws IOException {
-    frameWriter.flush();
-  }
-
-  /**
-   * Degrades this connection such that new streams can neither be created
-   * locally, nor accepted from the remote peer. Existing streams are not
-   * impacted. This is intended to permit an endpoint to gracefully stop
-   * accepting new requests without harming previously established streams.
-   */
-  public void shutdown(ErrorCode statusCode) throws IOException {
-    synchronized (frameWriter) {
-      int lastGoodStreamId;
-      synchronized (this) {
-        if (shutdown) {
-          return;
-        }
-        shutdown = true;
-        lastGoodStreamId = this.lastGoodStreamId;
-      }
-      frameWriter.goAway(lastGoodStreamId, statusCode);
-    }
-  }
-
-  /**
-   * Closes this connection. This cancels all open streams and unanswered
-   * pings. It closes the underlying input and output streams and shuts down
-   * internal executor services.
-   */
-  @Override public void close() throws IOException {
-    close(ErrorCode.NO_ERROR, ErrorCode.CANCEL);
-  }
-
-  private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException {
-    assert (!Thread.holdsLock(this));
-    IOException thrown = null;
-    try {
-      shutdown(connectionCode);
-    } catch (IOException e) {
-      thrown = e;
-    }
-
-    SpdyStream[] streamsToClose = null;
-    Ping[] pingsToCancel = null;
-    synchronized (this) {
-      if (!streams.isEmpty()) {
-        streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
-        streams.clear();
-        setIdle(false);
-      }
-      if (pings != null) {
-        pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
-        pings = null;
-      }
-    }
-
-    if (streamsToClose != null) {
-      for (SpdyStream stream : streamsToClose) {
-        try {
-          stream.close(streamCode);
-        } catch (IOException e) {
-          if (thrown != null) thrown = e;
-        }
-      }
-    }
-
-    if (pingsToCancel != null) {
-      for (Ping ping : pingsToCancel) {
-        ping.cancel();
-      }
-    }
-
-    try {
-      frameReader.close();
-    } catch (IOException e) {
-      thrown = e;
-    }
-    try {
-      frameWriter.close();
-    } catch (IOException e) {
-      if (thrown == null) thrown = e;
-    }
-
-    if (thrown != null) throw thrown;
-  }
-
-  /**
-   * Sends a connection header if the current variant requires it. This should
-   * be called after {@link Builder#build} for all new connections.
-   */
-  public void sendConnectionHeader() throws IOException {
-    frameWriter.connectionHeader();
-    frameWriter.settings(new Settings());
-  }
-
-  /**
-   * Reads a connection header if the current variant requires it. This should
-   * be called after {@link Builder#build} for all new connections.
-   */
-  public void readConnectionHeader() throws IOException {
-    frameReader.readConnectionHeader();
-  }
-
-  public static class Builder {
-    private String hostName;
-    private InputStream in;
-    private OutputStream out;
-    private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
-    private Variant variant = Variant.SPDY3;
-    private boolean client;
-
-    public Builder(boolean client, Socket socket) throws IOException {
-      this("", client, socket.getInputStream(), socket.getOutputStream());
-    }
-
-    public Builder(boolean client, InputStream in, OutputStream out) {
-      this("", client, in, out);
-    }
-
-    /**
-     * @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, client, socket.getInputStream(), socket.getOutputStream());
-    }
-
-    /**
-     * @param client true if this peer initiated the connection; false if this
-     * peer accepted the connection.
-     */
-    public Builder(String hostName, boolean client, InputStream in, OutputStream out) {
-      this.hostName = hostName;
-      this.client = client;
-      this.in = in;
-      this.out = out;
-    }
-
-    public Builder handler(IncomingStreamHandler handler) {
-      this.handler = handler;
-      return this;
-    }
-
-    public Builder spdy3() {
-      this.variant = Variant.SPDY3;
-      return this;
-    }
-
-    public Builder http20Draft04() {
-      this.variant = Variant.HTTP_20_DRAFT_04;
-      return this;
-    }
-
-    public SpdyConnection build() {
-      return new SpdyConnection(this);
-    }
-  }
-
-  private class Reader implements Runnable, FrameReader.Handler {
-    @Override public void run() {
-      ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
-      ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
-      try {
-        while (frameReader.nextFrame(this)) {
-        }
-        connectionErrorCode = ErrorCode.NO_ERROR;
-        streamErrorCode = ErrorCode.CANCEL;
-      } catch (IOException e) {
-        connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
-        streamErrorCode = ErrorCode.PROTOCOL_ERROR;
-      } finally {
-        try {
-          close(connectionErrorCode, streamErrorCode);
-        } catch (IOException ignored) {
-        }
-      }
-    }
-
-    @Override public void data(boolean inFinished, int streamId, InputStream in, int length)
-        throws IOException {
-      SpdyStream dataStream = getStream(streamId);
-      if (dataStream == null) {
-        writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
-        Util.skipByReading(in, length);
-        return;
-      }
-      dataStream.receiveData(in, length);
-      if (inFinished) {
-        dataStream.receiveFin();
-      }
-    }
-
-    @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
-        int associatedStreamId, int priority, List<String> nameValueBlock,
-        HeadersMode headersMode) {
-      SpdyStream stream;
-      synchronized (SpdyConnection.this) {
-        // If we're shutdown, don't bother with this stream.
-        if (shutdown) return;
-
-        stream = getStream(streamId);
-
-        if (stream == null) {
-          // The headers claim to be for an existing stream, but we don't have one.
-          if (headersMode.failIfStreamAbsent()) {
-            writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
-            return;
-          }
-
-          // If the stream ID is less than the last created ID, assume it's already closed.
-          if (streamId <= lastGoodStreamId) return;
-
-          // If the stream ID is in the client's namespace, assume it's already closed.
-          if (streamId % 2 == nextStreamId % 2) return;
-
-          // Create a stream.
-          final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished,
-              inFinished, priority, nameValueBlock, settings);
-          lastGoodStreamId = streamId;
-          streams.put(streamId, newStream);
-          executor.submit(new NamedRunnable("OkHttp Callback %s stream %d", hostName, streamId) {
-            @Override public void execute() {
-              try {
-                handler.receive(newStream);
-              } catch (IOException e) {
-                throw new RuntimeException(e);
-              }
-            }
-          });
-          return;
-        }
-      }
-
-      // The headers claim to be for a new stream, but we already have one.
-      if (headersMode.failIfStreamPresent()) {
-        stream.closeLater(ErrorCode.PROTOCOL_ERROR);
-        removeStream(streamId);
-        return;
-      }
-
-      // Update an existing stream.
-      stream.receiveHeaders(nameValueBlock, headersMode);
-      if (inFinished) stream.receiveFin();
-    }
-
-    @Override public void rstStream(int streamId, ErrorCode errorCode) {
-      SpdyStream rstStream = removeStream(streamId);
-      if (rstStream != null) {
-        rstStream.receiveRstStream(errorCode);
-      }
-    }
-
-    @Override public void settings(boolean clearPrevious, Settings newSettings) {
-      SpdyStream[] streamsToNotify = null;
-      synchronized (SpdyConnection.this) {
-        if (settings == null || clearPrevious) {
-          settings = newSettings;
-        } else {
-          settings.merge(newSettings);
-        }
-        if (!streams.isEmpty()) {
-          streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]);
-        }
-      }
-      if (streamsToNotify != null) {
-        for (SpdyStream stream : streamsToNotify) {
-          // The synchronization here is ugly. We need to synchronize on 'this' to guard
-          // reads to 'settings'. We synchronize on 'stream' to guard the state change.
-          // And we need to acquire the 'stream' lock first, since that may block.
-          // TODO: this can block the reader thread until a write completes. That's bad!
-          synchronized (stream) {
-            synchronized (SpdyConnection.this) {
-              stream.receiveSettings(settings);
-            }
-          }
-        }
-      }
-    }
-
-    @Override public void noop() {
-    }
-
-    @Override public void ping(boolean reply, int payload1, int payload2) {
-      if (reply) {
-        Ping ping = removePing(payload1);
-        if (ping != null) {
-          ping.receive();
-        }
-      } else {
-        // Send a reply to a client ping if this is a server and vice versa.
-        writePingLater(true, payload1, payload2, null);
-      }
-    }
-
-    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode) {
-      synchronized (SpdyConnection.this) {
-        shutdown = true;
-
-        // Fail all streams created after the last good stream ID.
-        for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
-            i.hasNext(); ) {
-          Map.Entry<Integer, SpdyStream> entry = i.next();
-          int streamId = entry.getKey();
-          if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
-            entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM);
-            i.remove();
-          }
-        }
-      }
-    }
-
-    @Override public void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl) {
-      if (streamId == 0) {
-        // TODO: honor whole-stream flow control
-        return;
-      }
-
-      // TODO: honor endFlowControl
-      SpdyStream stream = getStream(streamId);
-      if (stream != null) {
-        stream.receiveWindowUpdate(deltaWindowSize);
-      }
-    }
-
-    @Override public void priority(int streamId, int priority) {
-      // TODO: honor priority.
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
deleted file mode 100644
index e550022..0000000
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ /dev/null
@@ -1,670 +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.spdy;
-
-import com.squareup.okhttp.internal.Util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.OutputStream;
-import java.net.SocketTimeoutException;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
-
-/** A logical bidirectional stream. */
-public final class SpdyStream {
-
-  // Internal state is guarded by this. No long-running or potentially
-  // blocking operations are performed while the lock is held.
-
-  /**
-   * The number of unacknowledged bytes at which the input stream will send
-   * the peer a {@code WINDOW_UPDATE} frame. Must be less than this client's
-   * window size, otherwise the remote peer will stop sending data on this
-   * stream. (Chrome 25 uses 5 MiB.)
-   */
-  public static final int WINDOW_UPDATE_THRESHOLD = Settings.DEFAULT_INITIAL_WINDOW_SIZE / 2;
-
-  private final int id;
-  private final SpdyConnection connection;
-  private final int priority;
-  private long readTimeoutMillis = 0;
-  private int writeWindowSize;
-
-  /** Headers sent by the stream initiator. Immutable and non null. */
-  private final List<String> requestHeaders;
-
-  /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
-  private List<String> responseHeaders;
-
-  private final SpdyDataInputStream in = new SpdyDataInputStream();
-  private final SpdyDataOutputStream out = new SpdyDataOutputStream();
-
-  /**
-   * The reason why this stream was abnormally closed. If there are multiple
-   * reasons to abnormally close this stream (such as both peers closing it
-   * near-simultaneously) then this is the first reason known to this peer.
-   */
-  private ErrorCode errorCode = null;
-
-  SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished,
-      int priority, List<String> requestHeaders, Settings settings) {
-    if (connection == null) throw new NullPointerException("connection == null");
-    if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
-    this.id = id;
-    this.connection = connection;
-    this.in.finished = inFinished;
-    this.out.finished = outFinished;
-    this.priority = priority;
-    this.requestHeaders = requestHeaders;
-
-    setSettings(settings);
-  }
-
-  /**
-   * Returns true if this stream is open. A stream is open until either:
-   * <ul>
-   * <li>A {@code SYN_RESET} frame abnormally terminates the stream.
-   * <li>Both input and output streams have transmitted all data and
-   * headers.
-   * </ul>
-   * Note that the input stream may continue to yield data even after a stream
-   * reports itself as not open. This is because input data is buffered.
-   */
-  public synchronized boolean isOpen() {
-    if (errorCode != null) {
-      return false;
-    }
-    if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) {
-      return false;
-    }
-    return true;
-  }
-
-  /** Returns true if this stream was created by this peer. */
-  public boolean isLocallyInitiated() {
-    boolean streamIsClient = (id % 2 == 1);
-    return connection.client == streamIsClient;
-  }
-
-  public SpdyConnection getConnection() {
-    return connection;
-  }
-
-  public List<String> getRequestHeaders() {
-    return requestHeaders;
-  }
-
-  /**
-   * Returns the stream's response headers, blocking if necessary if they
-   * have not been received yet.
-   */
-  public synchronized List<String> getResponseHeaders() throws IOException {
-    try {
-      while (responseHeaders == null && errorCode == null) {
-        wait();
-      }
-      if (responseHeaders != null) {
-        return responseHeaders;
-      }
-      throw new IOException("stream was reset: " + errorCode);
-    } catch (InterruptedException e) {
-      InterruptedIOException rethrow = new InterruptedIOException();
-      rethrow.initCause(e);
-      throw rethrow;
-    }
-  }
-
-  /**
-   * Returns the reason why this stream was closed, or null if it closed
-   * normally or has not yet been closed.
-   */
-  public synchronized ErrorCode getErrorCode() {
-    return errorCode;
-  }
-
-  /**
-   * Sends a reply to an incoming stream.
-   *
-   * @param out true to create an output stream that we can use to send data
-   * to the remote peer. Corresponds to {@code FLAG_FIN}.
-   */
-  public void reply(List<String> responseHeaders, boolean out) throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
-    boolean outFinished = false;
-    synchronized (this) {
-      if (responseHeaders == null) {
-        throw new NullPointerException("responseHeaders == null");
-      }
-      if (isLocallyInitiated()) {
-        throw new IllegalStateException("cannot reply to a locally initiated stream");
-      }
-      if (this.responseHeaders != null) {
-        throw new IllegalStateException("reply already sent");
-      }
-      this.responseHeaders = responseHeaders;
-      if (!out) {
-        this.out.finished = true;
-        outFinished = true;
-      }
-    }
-    connection.writeSynReply(id, outFinished, responseHeaders);
-  }
-
-  /**
-   * Sets the maximum time to wait on input stream reads before failing with a
-   * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely.
-   */
-  public void setReadTimeout(long readTimeoutMillis) {
-    this.readTimeoutMillis = readTimeoutMillis;
-  }
-
-  public long getReadTimeoutMillis() {
-    return readTimeoutMillis;
-  }
-
-  /** Returns an input stream that can be used to read data from the peer. */
-  public InputStream getInputStream() {
-    return in;
-  }
-
-  /**
-   * Returns an output stream that can be used to write data to the peer.
-   *
-   * @throws IllegalStateException if this stream was initiated by the peer
-   * and a {@link #reply} has not yet been sent.
-   */
-  public OutputStream getOutputStream() {
-    synchronized (this) {
-      if (responseHeaders == null && !isLocallyInitiated()) {
-        throw new IllegalStateException("reply before requesting the output stream");
-      }
-    }
-    return out;
-  }
-
-  /**
-   * Abnormally terminate this stream. This blocks until the {@code RST_STREAM}
-   * frame has been transmitted.
-   */
-  public void close(ErrorCode rstStatusCode) throws IOException {
-    if (!closeInternal(rstStatusCode)) {
-      return; // Already closed.
-    }
-    connection.writeSynReset(id, rstStatusCode);
-  }
-
-  /**
-   * Abnormally terminate this stream. This enqueues a {@code RST_STREAM}
-   * frame and returns immediately.
-   */
-  public void closeLater(ErrorCode errorCode) {
-    if (!closeInternal(errorCode)) {
-      return; // Already closed.
-    }
-    connection.writeSynResetLater(id, errorCode);
-  }
-
-  /** Returns true if this stream was closed. */
-  private boolean closeInternal(ErrorCode errorCode) {
-    assert (!Thread.holdsLock(this));
-    synchronized (this) {
-      if (this.errorCode != null) {
-        return false;
-      }
-      if (in.finished && out.finished) {
-        return false;
-      }
-      this.errorCode = errorCode;
-      notifyAll();
-    }
-    connection.removeStream(id);
-    return true;
-  }
-
-  void receiveHeaders(List<String> headers, HeadersMode headersMode) {
-    assert (!Thread.holdsLock(SpdyStream.this));
-    ErrorCode errorCode = null;
-    boolean open = true;
-    synchronized (this) {
-      if (responseHeaders == null) {
-        if (headersMode.failIfHeadersAbsent()) {
-          errorCode = ErrorCode.PROTOCOL_ERROR;
-        } else {
-          responseHeaders = headers;
-          open = isOpen();
-          notifyAll();
-        }
-      } else {
-        if (headersMode.failIfHeadersPresent()) {
-          errorCode = ErrorCode.STREAM_IN_USE;
-        } else {
-          List<String> newHeaders = new ArrayList<String>();
-          newHeaders.addAll(responseHeaders);
-          newHeaders.addAll(headers);
-          this.responseHeaders = newHeaders;
-        }
-      }
-    }
-    if (errorCode != null) {
-      closeLater(errorCode);
-    } else if (!open) {
-      connection.removeStream(id);
-    }
-  }
-
-  void receiveData(InputStream in, int length) throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
-    this.in.receive(in, length);
-  }
-
-  void receiveFin() {
-    assert (!Thread.holdsLock(SpdyStream.this));
-    boolean open;
-    synchronized (this) {
-      this.in.finished = true;
-      open = isOpen();
-      notifyAll();
-    }
-    if (!open) {
-      connection.removeStream(id);
-    }
-  }
-
-  synchronized void receiveRstStream(ErrorCode errorCode) {
-    if (this.errorCode == null) {
-      this.errorCode = errorCode;
-      notifyAll();
-    }
-  }
-
-  private void setSettings(Settings settings) {
-    // TODO: For HTTP/2.0, also adjust the stream flow control window size
-    // by the difference between the new value and the old value.
-    assert (Thread.holdsLock(connection)); // Because 'settings' is guarded by 'connection'.
-    this.writeWindowSize = settings != null
-        ? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE)
-        : Settings.DEFAULT_INITIAL_WINDOW_SIZE;
-  }
-
-  void receiveSettings(Settings settings) {
-    assert (Thread.holdsLock(this));
-    setSettings(settings);
-    notifyAll();
-  }
-
-  synchronized void receiveWindowUpdate(int deltaWindowSize) {
-    out.unacknowledgedBytes -= deltaWindowSize;
-    notifyAll();
-  }
-
-  int getPriority() {
-    return priority;
-  }
-
-  /**
-   * An input stream that reads the incoming data frames of a stream. Although
-   * this class uses synchronization to safely receive incoming data frames,
-   * it is not intended for use by multiple readers.
-   */
-  private final class SpdyDataInputStream extends InputStream {
-    // Store incoming data bytes in a circular buffer. When the buffer is
-    // empty, pos == -1. Otherwise pos is the first byte to read and limit
-    // is the first byte to write.
-    //
-    // { - - - X X X X - - - }
-    //         ^       ^
-    //        pos    limit
-    //
-    // { X X X - - - - X X X }
-    //         ^       ^
-    //       limit    pos
-
-    private final byte[] buffer = new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE];
-
-    /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */
-    private int pos = -1;
-
-    /** the last byte to be read. Never buffer.length */
-    private int limit;
-
-    /** True if the caller has closed this stream. */
-    private boolean closed;
-
-    /**
-     * True if either side has cleanly shut down this stream. We will
-     * receive no more bytes beyond those already in the buffer.
-     */
-    private boolean finished;
-
-    /**
-     * The total number of bytes consumed by the application (with {@link
-     * #read}), but not yet acknowledged by sending a {@code WINDOW_UPDATE}
-     * frame.
-     */
-    private int unacknowledgedBytes = 0;
-
-    @Override public int available() throws IOException {
-      synchronized (SpdyStream.this) {
-        checkNotClosed();
-        if (pos == -1) {
-          return 0;
-        } else if (limit > pos) {
-          return limit - pos;
-        } else {
-          return limit + (buffer.length - pos);
-        }
-      }
-    }
-
-    @Override public int read() throws IOException {
-      return Util.readSingleByte(this);
-    }
-
-    @Override public int read(byte[] b, int offset, int count) throws IOException {
-      synchronized (SpdyStream.this) {
-        checkOffsetAndCount(b.length, offset, count);
-        waitUntilReadable();
-        checkNotClosed();
-
-        if (pos == -1) {
-          return -1;
-        }
-
-        int copied = 0;
-
-        // drain from [pos..buffer.length)
-        if (limit <= pos) {
-          int bytesToCopy = Math.min(count, buffer.length - pos);
-          System.arraycopy(buffer, pos, b, offset, bytesToCopy);
-          pos += bytesToCopy;
-          copied += bytesToCopy;
-          if (pos == buffer.length) {
-            pos = 0;
-          }
-        }
-
-        // drain from [pos..limit)
-        if (copied < count) {
-          int bytesToCopy = Math.min(limit - pos, count - copied);
-          System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy);
-          pos += bytesToCopy;
-          copied += bytesToCopy;
-        }
-
-        // Flow control: notify the peer that we're ready for more data!
-        unacknowledgedBytes += copied;
-        if (unacknowledgedBytes >= WINDOW_UPDATE_THRESHOLD) {
-          connection.writeWindowUpdateLater(id, unacknowledgedBytes);
-          unacknowledgedBytes = 0;
-        }
-
-        if (pos == limit) {
-          pos = -1;
-          limit = 0;
-        }
-
-        return copied;
-      }
-    }
-
-    /**
-     * Returns once the input stream is either readable or finished. Throws
-     * a {@link SocketTimeoutException} if the read timeout elapses before
-     * that happens.
-     */
-    private void waitUntilReadable() throws IOException {
-      long start = 0;
-      long remaining = 0;
-      if (readTimeoutMillis != 0) {
-        start = (System.nanoTime() / 1000000);
-        remaining = readTimeoutMillis;
-      }
-      try {
-        while (pos == -1 && !finished && !closed && errorCode == null) {
-          if (readTimeoutMillis == 0) {
-            SpdyStream.this.wait();
-          } else if (remaining > 0) {
-            SpdyStream.this.wait(remaining);
-            remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
-          } else {
-            throw new SocketTimeoutException();
-          }
-        }
-      } catch (InterruptedException e) {
-        throw new InterruptedIOException();
-      }
-    }
-
-    void receive(InputStream in, int byteCount) throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-
-      if (byteCount == 0) {
-        return;
-      }
-
-      int pos;
-      int limit;
-      int firstNewByte;
-      boolean finished;
-      boolean flowControlError;
-      synchronized (SpdyStream.this) {
-        finished = this.finished;
-        pos = this.pos;
-        firstNewByte = this.limit;
-        limit = this.limit;
-        flowControlError = byteCount > buffer.length - available();
-      }
-
-      // If the peer sends more data than we can handle, discard it and close the connection.
-      if (flowControlError) {
-        Util.skipByReading(in, byteCount);
-        closeLater(ErrorCode.FLOW_CONTROL_ERROR);
-        return;
-      }
-
-      // Discard data received after the stream is finished. It's probably a benign race.
-      if (finished) {
-        Util.skipByReading(in, byteCount);
-        return;
-      }
-
-      // Fill the buffer without holding any locks. First fill [limit..buffer.length) if that
-      // won't overwrite unread data. Then fill [limit..pos). We can't hold a lock, otherwise
-      // writes will be blocked until reads complete.
-      if (pos < limit) {
-        int firstCopyCount = Math.min(byteCount, buffer.length - limit);
-        Util.readFully(in, buffer, limit, firstCopyCount);
-        limit += firstCopyCount;
-        byteCount -= firstCopyCount;
-        if (limit == buffer.length) {
-          limit = 0;
-        }
-      }
-      if (byteCount > 0) {
-        Util.readFully(in, buffer, limit, byteCount);
-        limit += byteCount;
-      }
-
-      synchronized (SpdyStream.this) {
-        // Update the new limit, and mark the position as readable if necessary.
-        this.limit = limit;
-        if (this.pos == -1) {
-          this.pos = firstNewByte;
-          SpdyStream.this.notifyAll();
-        }
-      }
-    }
-
-    @Override public void close() throws IOException {
-      synchronized (SpdyStream.this) {
-        closed = true;
-        SpdyStream.this.notifyAll();
-      }
-      cancelStreamIfNecessary();
-    }
-
-    private void checkNotClosed() throws IOException {
-      if (closed) {
-        throw new IOException("stream closed");
-      }
-      if (errorCode != null) {
-        throw new IOException("stream was reset: " + errorCode);
-      }
-    }
-  }
-
-  private void cancelStreamIfNecessary() throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
-    boolean open;
-    boolean cancel;
-    synchronized (this) {
-      cancel = !in.finished && in.closed && (out.finished || out.closed);
-      open = isOpen();
-    }
-    if (cancel) {
-      // RST this stream to prevent additional data from being sent. This
-      // is safe because the input stream is closed (we won't use any
-      // further bytes) and the output stream is either finished or closed
-      // (so RSTing both streams doesn't cause harm).
-      SpdyStream.this.close(ErrorCode.CANCEL);
-    } else if (!open) {
-      connection.removeStream(id);
-    }
-  }
-
-  /**
-   * An output stream that writes outgoing data frames of a stream. This class
-   * is not thread safe.
-   */
-  private final class SpdyDataOutputStream extends OutputStream {
-    private final byte[] buffer = new byte[8192];
-    private int pos = 0;
-
-    /** True if the caller has closed this stream. */
-    private boolean closed;
-
-    /**
-     * True if either side has cleanly shut down this stream. We shall send
-     * no more bytes.
-     */
-    private boolean finished;
-
-    /**
-     * The total number of bytes written out to the peer, but not yet
-     * acknowledged with an incoming {@code WINDOW_UPDATE} frame. Writes
-     * block if they cause this to exceed the {@code WINDOW_SIZE}.
-     */
-    private int unacknowledgedBytes = 0;
-
-    @Override public void write(int b) throws IOException {
-      Util.writeSingleByte(this, b);
-    }
-
-    @Override public void write(byte[] bytes, int offset, int count) throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-      checkOffsetAndCount(bytes.length, offset, count);
-      checkNotClosed();
-
-      while (count > 0) {
-        if (pos == buffer.length) {
-          writeFrame(false);
-        }
-        int bytesToCopy = Math.min(count, buffer.length - pos);
-        System.arraycopy(bytes, offset, buffer, pos, bytesToCopy);
-        pos += bytesToCopy;
-        offset += bytesToCopy;
-        count -= bytesToCopy;
-      }
-    }
-
-    @Override public void flush() throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-      checkNotClosed();
-      if (pos > 0) {
-        writeFrame(false);
-        connection.flush();
-      }
-    }
-
-    @Override public void close() throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-      synchronized (SpdyStream.this) {
-        if (closed) {
-          return;
-        }
-        closed = true;
-      }
-      if (!out.finished) {
-        writeFrame(true);
-      }
-      connection.flush();
-      cancelStreamIfNecessary();
-    }
-
-    private void writeFrame(boolean outFinished) throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-
-      int length = pos;
-      synchronized (SpdyStream.this) {
-        waitUntilWritable(length, outFinished);
-        unacknowledgedBytes += length;
-      }
-      connection.writeData(id, outFinished, buffer, 0, pos);
-      pos = 0;
-    }
-
-    /**
-     * Returns once the peer is ready to receive {@code count} bytes.
-     *
-     * @throws IOException if the stream was finished or closed, or the
-     * thread was interrupted.
-     */
-    private void waitUntilWritable(int count, boolean last) throws IOException {
-      try {
-        while (unacknowledgedBytes + count >= writeWindowSize) {
-          SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
-
-          // The stream may have been closed or reset while we were waiting!
-          if (!last && closed) {
-            throw new IOException("stream closed");
-          } else if (finished) {
-            throw new IOException("stream finished");
-          } else if (errorCode != null) {
-            throw new IOException("stream was reset: " + errorCode);
-          }
-        }
-      } catch (InterruptedException e) {
-        throw new InterruptedIOException();
-      }
-    }
-
-    private void checkNotClosed() throws IOException {
-      synchronized (SpdyStream.this) {
-        if (closed) {
-          throw new IOException("stream closed");
-        } else if (finished) {
-          throw new IOException("stream finished");
-        } else if (errorCode != null) {
-          throw new IOException("stream was reset: " + errorCode);
-        }
-      }
-    }
-  }
-}
diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java
deleted file mode 100644
index 0bcadce..0000000
--- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.spdy;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-public class HpackTest {
-  private ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-  private final Hpack.Writer hpackWriter = new Hpack.Writer(new DataOutputStream(bytesOut));
-
-  @Test public void readSingleByteInt() throws IOException {
-    assertEquals(10, new Hpack.Reader(byteStream(), true).readInt(10, 31));
-    assertEquals(10, new Hpack.Reader(byteStream(), true).readInt(0xe0 | 10, 31));
-  }
-
-  @Test public void readMultibyteInt() throws IOException {
-    assertEquals(1337, new Hpack.Reader(byteStream(154, 10), true).readInt(31, 31));
-  }
-
-  @Test public void writeSingleByteInt() throws IOException {
-    hpackWriter.writeInt(10, 31, 0);
-    assertBytes(10);
-    hpackWriter.writeInt(10, 31, 0xe0);
-    assertBytes(0xe0 | 10);
-  }
-
-  @Test public void writeMultibyteInt() throws IOException {
-    hpackWriter.writeInt(1337, 31, 0);
-    assertBytes(31, 154, 10);
-    hpackWriter.writeInt(1337, 31, 0xe0);
-    assertBytes(0xe0 | 31, 154, 10);
-  }
-
-  @Test public void max31BitValue() throws IOException {
-    hpackWriter.writeInt(0x7fffffff, 31, 0);
-    assertBytes(31, 224, 255, 255, 255, 7);
-    assertEquals(0x7fffffff,
-        new Hpack.Reader(byteStream(224, 255, 255, 255, 7), true).readInt(31, 31));
-  }
-
-  @Test public void prefixMask() throws IOException {
-    hpackWriter.writeInt(31, 31, 0);
-    assertBytes(31, 0);
-    assertEquals(31, new Hpack.Reader(byteStream(0), true).readInt(31, 31));
-  }
-
-  @Test public void prefixMaskMinusOne() throws IOException {
-    hpackWriter.writeInt(30, 31, 0);
-    assertBytes(30);
-    assertEquals(31, new Hpack.Reader(byteStream(0), true).readInt(31, 31));
-  }
-
-  @Test public void zero() throws IOException {
-    hpackWriter.writeInt(0, 31, 0);
-    assertBytes(0);
-    assertEquals(0, new Hpack.Reader(byteStream(), true).readInt(0, 31));
-  }
-
-  @Test public void headerName() throws IOException {
-    hpackWriter.writeString("foo");
-    assertBytes(3, 'f', 'o', 'o');
-    assertEquals("foo", new Hpack.Reader(byteStream(3, 'f', 'o', 'o'), true).readString());
-  }
-
-  @Test public void emptyHeaderName() throws IOException {
-    hpackWriter.writeString("");
-    assertBytes(0);
-    assertEquals("", new Hpack.Reader(byteStream(0), true).readString());
-  }
-
-  @Test public void headersRoundTrip() throws IOException {
-    List<String> sentHeaders = Arrays.asList("name", "value");
-    hpackWriter.writeHeaders(sentHeaders);
-    ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray());
-    Hpack.Reader reader = new Hpack.Reader(new DataInputStream(bytesIn), true);
-    reader.readHeaders(bytesOut.size());
-    reader.emitReferenceSet();
-    List<String> receivedHeaders = reader.getAndReset();
-    assertEquals(sentHeaders, receivedHeaders);
-  }
-
-  private DataInputStream byteStream(int... bytes) {
-    byte[] data = intArrayToByteArray(bytes);
-    return new DataInputStream(new ByteArrayInputStream(data));
-  }
-
-  private void assertBytes(int... bytes) {
-    byte[] expected = intArrayToByteArray(bytes);
-    byte[] actual = bytesOut.toByteArray();
-    assertEquals(Arrays.toString(expected), Arrays.toString(actual));
-    bytesOut.reset(); // So the next test starts with a clean slate.
-  }
-
-  private byte[] intArrayToByteArray(int[] bytes) {
-    byte[] data = new byte[bytes.length];
-    for (int i = 0; i < bytes.length; i++) {
-      data[i] = (byte) bytes[i];
-    }
-    return data;
-  }
-}
diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
deleted file mode 100644
index b0f23f4..0000000
--- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
+++ /dev/null
@@ -1,1072 +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.spdy;
-
-import com.squareup.okhttp.internal.Base64;
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import org.junit.After;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.FLOW_CONTROL_ERROR;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.INTERNAL_ERROR;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.INVALID_STREAM;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.REFUSED_STREAM;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.STREAM_IN_USE;
-import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_DATA;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_GOAWAY;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_NOOP;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
-import static com.squareup.okhttp.internal.spdy.SpdyStream.WINDOW_UPDATE_THRESHOLD;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class SpdyConnectionTest {
-  private static final IncomingStreamHandler REJECT_INCOMING_STREAMS = new IncomingStreamHandler() {
-    @Override public void receive(SpdyStream stream) throws IOException {
-      throw new AssertionError();
-    }
-  };
-  private final MockSpdyPeer peer = new MockSpdyPeer(false);
-
-  @After public void tearDown() throws Exception {
-    peer.close();
-  }
-
-  @Test public void clientCreatesStreamAndServerReplies() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.sendFrame().data(true, 1, "robot".getBytes("UTF-8"));
-    peer.acceptFrame(); // DATA
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders());
-    assertStreamData("robot", stream.getInputStream());
-    writeAndClose(stream, "c3po");
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertFalse(synStream.inFinished);
-    assertFalse(synStream.outFinished);
-    assertEquals(1, synStream.streamId);
-    assertEquals(0, synStream.associatedStreamId);
-    assertEquals(Arrays.asList("b", "banana"), synStream.nameValueBlock);
-    MockSpdyPeer.InFrame requestData = peer.takeFrame();
-    assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data));
-  }
-
-  @Test public void headersOnlyStreamIsClosedAfterReplyHeaders() throws Exception {
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("b", "banana"));
-    peer.play();
-
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, false);
-    assertEquals(1, connection.openStreamCount());
-    assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders());
-    assertEquals(0, connection.openStreamCount());
-  }
-
-  @Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // PING
-    peer.sendFrame().synReply(true, 1, Arrays.asList("a", "android"));
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    connection.newStream(Arrays.asList("b", "banana"), false, true);
-    assertEquals(1, connection.openStreamCount());
-    connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-  }
-
-  @Test public void serverCreatesStreamAndClientReplies() throws Exception {
-    // write the mocking script
-    peer.sendFrame().synStream(false, false, 2, 0, 5, 129, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // SYN_REPLY
-    peer.play();
-
-    // play it back
-    final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
-        receiveCount.incrementAndGet();
-        assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders());
-        assertEquals(null, stream.getErrorCode());
-        assertEquals(5, stream.getPriority());
-        stream.reply(Arrays.asList("b", "banana"), true);
-      }
-    };
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame reply = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, reply.type);
-    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
-    assertFalse(reply.inFinished);
-    assertEquals(2, reply.streamId);
-    assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock);
-    assertEquals(1, receiveCount.get());
-  }
-
-  @Test public void replyWithNoData() throws Exception {
-    // write the mocking script
-    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // SYN_REPLY
-    peer.play();
-
-    // play it back
-    final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
-        stream.reply(Arrays.asList("b", "banana"), false);
-        receiveCount.incrementAndGet();
-      }
-    };
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame reply = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, reply.type);
-    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
-    assertTrue(reply.inFinished);
-    assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock);
-    assertEquals(1, receiveCount.get());
-  }
-
-  @Test public void noop() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // NOOP
-    peer.play();
-
-    // play it back
-    SpdyConnection connection =
-        new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS)
-            .build();
-    connection.noop();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_NOOP, ping.type);
-  }
-
-  @Test public void serverPingsClient() throws Exception {
-    // write the mocking script
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void clientPingsServer() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    Ping ping = connection.ping();
-    assertTrue(ping.roundTripTime() > 0);
-    assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
-    assertEquals(TYPE_PING, pingFrame.type);
-    assertEquals(1, pingFrame.streamId);
-  }
-
-  @Test public void unexpectedPingIsNotReturned() throws Exception {
-    // write the mocking script
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 3, 0); // This ping will not be returned.
-    peer.sendFrame().ping(false, 4, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame ping2 = peer.takeFrame();
-    assertEquals(2, ping2.streamId);
-    MockSpdyPeer.InFrame ping4 = peer.takeFrame();
-    assertEquals(4, ping4.streamId);
-  }
-
-  @Test public void serverSendsSettingsToClient() throws Exception {
-    // write the mocking script
-    Settings settings = new Settings();
-    settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10);
-    peer.sendFrame().settings(settings);
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-
-    peer.takeFrame(); // Guarantees that the Settings frame has been processed.
-    synchronized (connection) {
-      assertEquals(10, connection.settings.getMaxConcurrentStreams(-1));
-    }
-  }
-
-  @Test public void multipleSettingsFramesAreMerged() throws Exception {
-    // write the mocking script
-    Settings settings1 = new Settings();
-    settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
-    settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
-    settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300);
-    peer.sendFrame().settings(settings1);
-    Settings settings2 = new Settings();
-    settings2.set(Settings.DOWNLOAD_BANDWIDTH, 0, 400);
-    settings2.set(Settings.DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500);
-    settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
-    peer.sendFrame().settings(settings2);
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame();
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-
-    peer.takeFrame(); // Guarantees that the Settings frame has been processed.
-    synchronized (connection) {
-      assertEquals(100, connection.settings.getUploadBandwidth(-1));
-      assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.UPLOAD_BANDWIDTH));
-      assertEquals(400, connection.settings.getDownloadBandwidth(-1));
-      assertEquals(0, connection.settings.flags(Settings.DOWNLOAD_BANDWIDTH));
-      assertEquals(500, connection.settings.getDownloadRetransRate(-1));
-      assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.DOWNLOAD_RETRANS_RATE));
-      assertEquals(600, connection.settings.getMaxConcurrentStreams(-1));
-      assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.MAX_CONCURRENT_STREAMS));
-    }
-  }
-
-  @Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception {
-    // write the mocking script
-    peer.sendFrame().data(true, 42, "bogus".getBytes("UTF-8"));
-    peer.acceptFrame(); // RST_STREAM
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(42, rstStream.streamId);
-    assertEquals(INVALID_STREAM, rstStream.errorCode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception {
-    // write the mocking script
-    peer.sendFrame().synReply(false, 42, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // RST_STREAM
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(42, rstStream.streamId);
-    assertEquals(INVALID_STREAM, rstStream.errorCode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void clientClosesClientOutputStream() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("b", "banana"));
-    peer.acceptFrame(); // TYPE_DATA
-    peer.acceptFrame(); // TYPE_DATA with FLAG_FIN
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, false);
-    OutputStream out = stream.getOutputStream();
-    out.write("square".getBytes(UTF_8));
-    out.flush();
-    assertEquals(1, connection.openStreamCount());
-    out.close();
-    try {
-      out.write("round".getBytes(UTF_8));
-      fail();
-    } catch (Exception expected) {
-      assertEquals("stream closed", expected.getMessage());
-    }
-    connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertFalse(synStream.inFinished);
-    assertTrue(synStream.outFinished);
-    MockSpdyPeer.InFrame data = peer.takeFrame();
-    assertEquals(TYPE_DATA, data.type);
-    assertFalse(data.inFinished);
-    assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
-    MockSpdyPeer.InFrame fin = peer.takeFrame();
-    assertEquals(TYPE_DATA, fin.type);
-    assertTrue(fin.inFinished);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(1, ping.streamId);
-  }
-
-  @Test public void serverClosesClientOutputStream() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().rstStream(1, CANCEL);
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 1, 0);
-    peer.acceptFrame(); // DATA
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true);
-    OutputStream out = stream.getOutputStream();
-    connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received.
-    try {
-      out.write("square".getBytes(UTF_8));
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: CANCEL", expected.getMessage());
-    }
-    out.close();
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertFalse(synStream.inFinished);
-    assertFalse(synStream.outFinished);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(1, ping.streamId);
-    MockSpdyPeer.InFrame data = peer.takeFrame();
-    assertEquals(TYPE_DATA, data.type);
-    assertEquals(1, data.streamId);
-    assertTrue(data.inFinished);
-    assertFalse(data.outFinished);
-  }
-
-  /**
-   * Test that the client sends a RST_STREAM if doing so won't disrupt the
-   * output stream.
-   */
-  @Test public void clientClosesClientInputStream() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // RST_STREAM
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true);
-    InputStream in = stream.getInputStream();
-    OutputStream out = stream.getOutputStream();
-    in.close();
-    try {
-      in.read();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream closed", expected.getMessage());
-    }
-    try {
-      out.write('a');
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream finished", expected.getMessage());
-    }
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertTrue(synStream.inFinished);
-    assertFalse(synStream.outFinished);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(CANCEL, rstStream.errorCode);
-  }
-
-  /**
-   * Test that the client doesn't send a RST_STREAM if doing so will disrupt
-   * the output stream.
-   */
-  @Test public void clientClosesClientInputStreamIfOutputStreamIsClosed() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // DATA
-    peer.acceptFrame(); // DATA with FLAG_FIN
-    peer.acceptFrame(); // RST_STREAM
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true);
-    InputStream in = stream.getInputStream();
-    OutputStream out = stream.getOutputStream();
-    in.close();
-    try {
-      in.read();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream closed", expected.getMessage());
-    }
-    out.write("square".getBytes(UTF_8));
-    out.flush();
-    out.close();
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertFalse(synStream.inFinished);
-    assertFalse(synStream.outFinished);
-    MockSpdyPeer.InFrame data = peer.takeFrame();
-    assertEquals(TYPE_DATA, data.type);
-    assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
-    MockSpdyPeer.InFrame fin = peer.takeFrame();
-    assertEquals(TYPE_DATA, fin.type);
-    assertTrue(fin.inFinished);
-    assertFalse(fin.outFinished);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(CANCEL, rstStream.errorCode);
-  }
-
-  @Test public void serverClosesClientInputStream() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("b", "banana"));
-    peer.sendFrame().data(true, 1, "square".getBytes(UTF_8));
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-        .handler(REJECT_INCOMING_STREAMS)
-        .build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true);
-    InputStream in = stream.getInputStream();
-    assertStreamData("square", in);
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    assertTrue(synStream.inFinished);
-    assertFalse(synStream.outFinished);
-  }
-
-  @Test public void remoteDoubleSynReply() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // PING
-    peer.sendFrame().synReply(false, 1, Arrays.asList("b", "banana"));
-    peer.sendFrame().ping(true, 1, 0);
-    peer.acceptFrame(); // RST_STREAM
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("c", "cola"), true, true);
-    assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders());
-    connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received.
-    try {
-      stream.getInputStream().read();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: STREAM_IN_USE", expected.getMessage());
-    }
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(1, rstStream.streamId);
-    assertEquals(STREAM_IN_USE, rstStream.errorCode);
-  }
-
-  @Test public void remoteDoubleSynStream() throws Exception {
-    // write the mocking script
-    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // SYN_REPLY
-    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, Arrays.asList("b", "banana"));
-    peer.acceptFrame(); // RST_STREAM
-    peer.play();
-
-    // play it back
-    final AtomicInteger receiveCount = new AtomicInteger();
-    IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
-        receiveCount.incrementAndGet();
-        assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders());
-        assertEquals(null, stream.getErrorCode());
-        stream.reply(Arrays.asList("c", "cola"), true);
-      }
-    };
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame reply = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, reply.type);
-    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(2, rstStream.streamId);
-    assertEquals(PROTOCOL_ERROR, rstStream.errorCode);
-    assertEquals(1, receiveCount.intValue());
-  }
-
-  @Test public void remoteSendsDataAfterInFinished() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.sendFrame().data(true, 1, "robot".getBytes("UTF-8"));
-    peer.sendFrame().data(true, 1, "c3po".getBytes("UTF-8")); // Ignored.
-    peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders());
-    assertStreamData("robot", stream.getInputStream());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void remoteSendsTooMuchData() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("b", "banana"));
-    peer.sendFrame().data(false, 1, new byte[64 * 1024 + 1]);
-    peer.acceptFrame(); // RST_STREAM
-    peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true);
-    assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(1, rstStream.streamId);
-    assertEquals(FLOW_CONTROL_ERROR, rstStream.errorCode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().rstStream(1, REFUSED_STREAM);
-    peer.sendFrame().ping(false, 2, 0);
-    peer.acceptFrame(); // PING
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true);
-    try {
-      stream.getResponseHeaders();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
-    }
-    assertEquals(0, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    assertEquals(2, ping.streamId);
-  }
-
-  @Test public void receiveGoAway() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM 1
-    peer.acceptFrame(); // SYN_STREAM 3
-    peer.sendFrame().goAway(1, PROTOCOL_ERROR);
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 1, 0);
-    peer.acceptFrame(); // DATA STREAM 1
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream1 = connection.newStream(Arrays.asList("a", "android"), true, true);
-    SpdyStream stream2 = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    connection.ping().roundTripTime(); // Ensure that the GO_AWAY has been received.
-    stream1.getOutputStream().write("abc".getBytes(UTF_8));
-    try {
-      stream2.getOutputStream().write("abc".getBytes(UTF_8));
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
-    }
-    stream1.getOutputStream().write("def".getBytes(UTF_8));
-    stream1.getOutputStream().close();
-    try {
-      connection.newStream(Arrays.asList("c", "cola"), true, true);
-      fail();
-    } catch (IOException expected) {
-      assertEquals("shutdown", expected.getMessage());
-    }
-    assertEquals(1, connection.openStreamCount());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream1.type);
-    MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream2.type);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    MockSpdyPeer.InFrame data1 = peer.takeFrame();
-    assertEquals(TYPE_DATA, data1.type);
-    assertEquals(1, data1.streamId);
-    assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
-  }
-
-  @Test public void sendGoAway() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM 1
-    peer.acceptFrame(); // GOAWAY
-    peer.acceptFrame(); // PING
-    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored!
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    connection.newStream(Arrays.asList("a", "android"), true, true);
-    Ping ping = connection.ping();
-    connection.shutdown(PROTOCOL_ERROR);
-    assertEquals(1, connection.openStreamCount());
-    ping.roundTripTime(); // Prevent the peer from exiting prematurely.
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream1.type);
-    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
-    assertEquals(TYPE_PING, pingFrame.type);
-    MockSpdyPeer.InFrame goaway = peer.takeFrame();
-    assertEquals(TYPE_GOAWAY, goaway.type);
-    assertEquals(0, goaway.streamId);
-    assertEquals(PROTOCOL_ERROR, goaway.errorCode);
-  }
-
-  @Test public void noPingsAfterShutdown() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // GOAWAY
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    connection.shutdown(INTERNAL_ERROR);
-    try {
-      connection.ping();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("shutdown", expected.getMessage());
-    }
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame goaway = peer.takeFrame();
-    assertEquals(TYPE_GOAWAY, goaway.type);
-    assertEquals(INTERNAL_ERROR, goaway.errorCode);
-  }
-
-  @Test public void close() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // GOAWAY
-    peer.acceptFrame(); // RST_STREAM
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true);
-    assertEquals(1, connection.openStreamCount());
-    connection.close();
-    assertEquals(0, connection.openStreamCount());
-    try {
-      connection.newStream(Arrays.asList("b", "banana"), true, true);
-      fail();
-    } catch (IOException expected) {
-      assertEquals("shutdown", expected.getMessage());
-    }
-    try {
-      stream.getOutputStream().write(0);
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: CANCEL", expected.getMessage());
-    }
-    try {
-      stream.getInputStream().read();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: CANCEL", expected.getMessage());
-    }
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame goaway = peer.takeFrame();
-    assertEquals(TYPE_GOAWAY, goaway.type);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(1, rstStream.streamId);
-  }
-
-  @Test public void closeCancelsPings() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // PING
-    peer.acceptFrame(); // GOAWAY
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    Ping ping = connection.ping();
-    connection.close();
-    assertEquals(-1, ping.roundTripTime());
-  }
-
-  @Test public void readTimeoutExpires() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.acceptFrame(); // PING
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    stream.setReadTimeout(1000);
-    InputStream in = stream.getInputStream();
-    long startNanos = System.nanoTime();
-    try {
-      in.read();
-      fail();
-    } catch (IOException expected) {
-    }
-    long elapsedNanos = System.nanoTime() - startNanos;
-    assertEquals(1000d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
-    assertEquals(1, connection.openStreamCount());
-    connection.ping().roundTripTime(); // Prevent the peer from exiting prematurely.
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-  }
-
-  @Test public void headers() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // PING
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.sendFrame().headers(1, Arrays.asList("c", "c3po"));
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
-    assertEquals(Arrays.asList("a", "android", "c", "c3po"), stream.getResponseHeaders());
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-  }
-
-  @Test public void headersBeforeReply() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.acceptFrame(); // PING
-    peer.sendFrame().headers(1, Arrays.asList("c", "c3po"));
-    peer.acceptFrame(); // RST_STREAM
-    peer.sendFrame().ping(true, 1, 0);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
-    try {
-      stream.getResponseHeaders();
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
-    }
-
-    // verify the peer received what was expected
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
-    MockSpdyPeer.InFrame ping = peer.takeFrame();
-    assertEquals(TYPE_PING, ping.type);
-    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-    assertEquals(TYPE_RST_STREAM, rstStream.type);
-    assertEquals(PROTOCOL_ERROR, rstStream.errorCode);
-  }
-
-  @Test public void readSendsWindowUpdate() throws Exception {
-    // Write the mocking script.
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    for (int i = 0; i < 3; i++) {
-      peer.sendFrame().data(false, 1, new byte[WINDOW_UPDATE_THRESHOLD]);
-      peer.acceptFrame(); // WINDOW UPDATE
-    }
-    peer.sendFrame().data(true, 1, new byte[0]);
-    peer.play();
-
-    // Play it back.
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders());
-    InputStream in = stream.getInputStream();
-    int total = 0;
-    byte[] buffer = new byte[1024];
-    int count;
-    while ((count = in.read(buffer)) != -1) {
-      total += count;
-      if (total == 3 * WINDOW_UPDATE_THRESHOLD) break;
-    }
-    assertEquals(-1, in.read());
-
-    // Verify the peer received what was expected.
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    for (int i = 0; i < 3; i++) {
-      MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
-      assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
-      assertEquals(1, windowUpdate.streamId);
-      assertEquals(WINDOW_UPDATE_THRESHOLD, windowUpdate.deltaWindowSize);
-    }
-  }
-
-  @Test public void writeAwaitsWindowUpdate() throws Exception {
-    // Write the mocking script. This accepts more data frames than necessary!
-    peer.acceptFrame(); // SYN_STREAM
-    for (int i = 0; i < Settings.DEFAULT_INITIAL_WINDOW_SIZE / 1024; i++) {
-      peer.acceptFrame(); // DATA
-    }
-    peer.play();
-
-    // Play it back.
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    OutputStream out = stream.getOutputStream();
-    out.write(new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE]);
-    interruptAfterDelay(500);
-    try {
-      out.write('a');
-      out.flush();
-      fail();
-    } catch (InterruptedIOException expected) {
-    }
-
-    // Verify the peer received what was expected.
-    MockSpdyPeer.InFrame synStream = peer.takeFrame();
-    assertEquals(TYPE_HEADERS, synStream.type);
-    MockSpdyPeer.InFrame data = peer.takeFrame();
-    assertEquals(TYPE_DATA, data.type);
-  }
-
-  @Test public void testTruncatedDataFrame() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    peer.sendFrame().synReply(false, 1, Arrays.asList("a", "android"));
-    peer.sendTruncatedFrame(8 + 100).data(false, 1, new byte[1024]);
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders());
-    InputStream in = stream.getInputStream();
-    try {
-      Util.readFully(in, new byte[101]);
-      fail();
-    } catch (IOException expected) {
-      assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
-    }
-  }
-
-  /** https://github.com/square/okhttp/issues/333 */
-  @Test public void nameValueBlockHasTrailingCompressedBytes() throws Exception {
-    // write the mocking script
-    peer.acceptFrame(); // SYN_STREAM
-    // This specially-formatted frame has trailing deflated bytes after the name value block.
-    String frame = "gAMAAgAAAgkAAAABeLvjxqfCYgAAAAD//2IAAAAA//9iAAAAAP//YgQAAAD//2IAAAAA//9iAAAAAP/"
-        + "/YgAAAAD//2IEAAAA//9KBAAAAP//YgAAAAD//2IAAAAA//9iAAAAAP//sgEAAAD//2IAAAAA\n//9iBAAAAP//Y"
-        + "gIAAAD//2IGAAAA//9iAQAAAP//YgUAAAD//2IDAAAA//9iBwAAAP//4gAAAAD//+IEAAAA///iAgAAAP//4gYAA"
-        + "AD//+IBAAAA///iBQAAAP//4gMAAAD//+IHAAAA//8SAAAAAP//EgQAAAD//xICAAAA//8SBgAAAP//EgEAAAD//"
-        + "xIFAAAA//8SAwAAAP//EgcAAAD//5IAAAAA//+SBAAAAP//kgIAAAD//5IGAAAA//+SAQAAAP//kgUAAAD//5IDA"
-        + "AAA//+SBwAAAP//UgAAAAD//1IEAAAA//9SAgAAAP//UgYAAAD//1IBAAAA//9SBQAAAP//UgMAAAD//1IHAAAA/"
-        + "//SAAAAAP//0gQAAAD//9ICAAAA///SBgAAAP//0gEAAAD//9IFAAAA///SAwAAAP//0gcAAAD//zIAAAAA//8yB"
-        + "AAAAP//MgIAAAD//zIGAAAA//8yAQAAAP//MgUAAAD//zIDAAAA//8yBwAAAP//sgAAAAD//7IEAAAA//+yAgAAA"
-        + "P//sgYAAAD//w==";
-    peer.sendFrame(Base64.decode(frame.getBytes(UTF_8)));
-    peer.sendFrame().data(true, 1, "robot".getBytes("UTF-8"));
-    peer.acceptFrame(); // DATA
-    peer.play();
-
-    // play it back
-    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-    SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true);
-    assertEquals("a", stream.getResponseHeaders().get(0));
-    assertEquals(60, stream.getResponseHeaders().get(1).length());
-    assertStreamData("robot", stream.getInputStream());
-  }
-
-  private void writeAndClose(SpdyStream stream, String data) throws IOException {
-    OutputStream out = stream.getOutputStream();
-    out.write(data.getBytes("UTF-8"));
-    out.close();
-  }
-
-  private void assertStreamData(String expected, InputStream inputStream) throws IOException {
-    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-    byte[] buffer = new byte[1024];
-    for (int count; (count = inputStream.read(buffer)) != -1; ) {
-      bytesOut.write(buffer, 0, count);
-    }
-    String actual = bytesOut.toString("UTF-8");
-    assertEquals(expected, actual);
-  }
-
-  /** Interrupts the current thread after {@code delayMillis}. */
-  private void interruptAfterDelay(final long delayMillis) {
-    final Thread toInterrupt = Thread.currentThread();
-    new Thread("interrupting cow") {
-      @Override public void run() {
-        try {
-          Thread.sleep(delayMillis);
-          toInterrupt.interrupt();
-        } catch (InterruptedException e) {
-          throw new AssertionError();
-        }
-      }
-    }.start();
-  }
-}
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
new file mode 100644
index 0000000..7c1573d
--- /dev/null
+++ b/okhttp-tests/pom.xml
@@ -0,0 +1,44 @@
+<?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.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>okhttp-tests</artifactId>
+  <name>OkHttp Tests</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.squareup.okio</groupId>
+      <artifactId>okio</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty.npn</groupId>
+      <artifactId>npn-boot</artifactId>
+      <scope>provided</scope>
+    </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>
+  </dependencies>
+</project>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
new file mode 100644
index 0000000..bd9ed97
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.Dispatcher;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+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.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public final class AsyncApiTest {
+  private MockWebServer server = new MockWebServer();
+  private OkHttpClient client = new OkHttpClient();
+  private RecordingReceiver receiver = new RecordingReceiver();
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  private HttpResponseCache cache;
+
+  @Before public void setUp() throws Exception {
+    String tmp = System.getProperty("java.io.tmpdir");
+    File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
+    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
+  }
+
+  @After public void tearDown() throws Exception {
+    server.shutdown();
+    cache.delete();
+  }
+
+  @Test public void get() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .header("User-Agent", "AsyncApiTest")
+        .build();
+    client.enqueue(request, receiver);
+
+    receiver.await(request.url())
+        .assertCode(200)
+        .assertContainsHeaders("Content-Type: text/plain")
+        .assertBody("abc");
+
+    assertTrue(server.takeRequest().getHeaders().contains("User-Agent: AsyncApiTest"));
+  }
+
+  @Test public void connectionPooling() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.enqueue(new MockResponse().setBody("def"));
+    server.enqueue(new MockResponse().setBody("ghi"));
+    server.play();
+
+    client.enqueue(new Request.Builder().url(server.getUrl("/a")).build(), receiver);
+    receiver.await(server.getUrl("/a")).assertBody("abc");
+
+    client.enqueue(new Request.Builder().url(server.getUrl("/b")).build(), receiver);
+    receiver.await(server.getUrl("/b")).assertBody("def");
+
+    client.enqueue(new Request.Builder().url(server.getUrl("/c")).build(), receiver);
+    receiver.await(server.getUrl("/c")).assertBody("ghi");
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    assertEquals(2, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void tls() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request, receiver);
+
+    receiver.await(request.url()).assertHandshake();
+  }
+
+  @Test public void recoverFromTlsHandshakeFailure() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request, receiver);
+
+    receiver.await(request.url()).assertBody("abc");
+  }
+
+  @Test public void post() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
+        .build();
+    client.enqueue(request, receiver);
+
+    receiver.await(request.url())
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("def", recordedRequest.getUtf8Body());
+    assertEquals("3", recordedRequest.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void conditionalCacheHit() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    Request request1 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request1, receiver);
+    receiver.await(request1.url()).assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    Request request2 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request2, receiver);
+    receiver.await(request2.url()).assertCode(200).assertBody("A");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
+  @Test public void conditionalCacheMiss() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    Request request1 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request1, receiver);
+    receiver.await(request1.url()).assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    Request request2 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request2, receiver);
+    receiver.await(request2.url()).assertCode(200).assertBody("B");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
+  @Test public void redirect() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .addHeader("Test", "Redirect from /a to /b")
+        .setBody("/a has moved!"));
+    server.enqueue(new MockResponse()
+        .setResponseCode(302)
+        .addHeader("Location: /c")
+        .addHeader("Test", "Redirect from /b to /c")
+        .setBody("/b has moved!"));
+    server.enqueue(new MockResponse().setBody("C"));
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    client.enqueue(request, receiver);
+
+    receiver.await(server.getUrl("/c"))
+        .assertCode(200)
+        .assertBody("C")
+        .redirectedBy()
+        .assertCode(302)
+        .assertContainsHeaders("Test: Redirect from /b to /c")
+        .redirectedBy()
+        .assertCode(301)
+        .assertContainsHeaders("Test: Redirect from /a to /b");
+
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+    assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
+    assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
+  }
+
+  @Test public void redirectWithRedirectsDisabled() throws Exception {
+    client.setFollowProtocolRedirects(false);
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .addHeader("Test", "Redirect from /a to /b")
+        .setBody("/a has moved!"));
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    client.enqueue(request, receiver);
+
+    receiver.await(server.getUrl("/a"))
+        .assertCode(301)
+        .assertBody("/a has moved!")
+        .assertContainsHeaders("Location: /b");
+  }
+
+  @Test public void follow20Redirects() throws Exception {
+    for (int i = 0; i < 20; i++) {
+      server.enqueue(new MockResponse()
+          .setResponseCode(301)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.enqueue(new MockResponse().setBody("Success!"));
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/0")).build();
+    client.enqueue(request, receiver);
+    receiver.await(server.getUrl("/20"))
+        .assertCode(200)
+        .assertBody("Success!");
+  }
+
+  @Test public void doesNotFollow21Redirects() throws Exception {
+    for (int i = 0; i < 21; i++) {
+      server.enqueue(new MockResponse()
+          .setResponseCode(301)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/0")).build();
+    client.enqueue(request, receiver);
+    receiver.await(server.getUrl("/20")).assertFailure("Too many redirects: 21");
+  }
+
+  @Test public void canceledBeforeResponseReadIsNeverDelivered() throws Exception {
+    client.getDispatcher().setMaxRequests(1); // Force requests to be executed serially.
+    server.setDispatcher(new Dispatcher() {
+      char nextResponse = 'A';
+      @Override public MockResponse dispatch(RecordedRequest request) {
+        client.cancel("request A");
+        return new MockResponse().setBody(Character.toString(nextResponse++));
+      }
+    });
+    server.play();
+
+    // Canceling a request after the server has received a request but before
+    // it has delivered the response. That request will never be received to the
+    // client.
+    Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+    client.enqueue(requestA, receiver);
+    assertEquals("/a", server.takeRequest().getPath());
+
+    // We then make a second request (not canceled) to make sure the receiver
+    // has nothing left to wait for.
+    Request requestB = new Request.Builder().url(server.getUrl("/b")).tag("request B").build();
+    client.enqueue(requestB, receiver);
+    assertEquals("/b", server.takeRequest().getPath());
+    receiver.await(requestB.url()).assertBody("B");
+
+    // At this point we know the receiver is ready: if it hasn't received 'A'
+    // yet it never will.
+    receiver.assertNoResponse(requestA.url());
+  }
+
+  @Test public void canceledAfterResponseIsDeliveredDoesNothing() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    final CountDownLatch latch = new CountDownLatch(1);
+    final AtomicReference<String> bodyRef = new AtomicReference<String>();
+
+    Request request = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+    client.enqueue(request, new Response.Receiver() {
+      @Override public void onFailure(Failure failure) {
+        throw new AssertionError();
+      }
+
+      @Override public boolean onResponse(Response response) throws IOException {
+        client.cancel("request A");
+        bodyRef.set(response.body().string());
+        latch.countDown();
+        return true;
+      }
+    });
+
+    latch.await();
+    assertEquals("A", bodyRef.get());
+  }
+
+  @Test public void connectionReuseWhenResponseBodyConsumed() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.enqueue(new MockResponse().setBody("def"));
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    client.enqueue(request, new Response.Receiver() {
+      @Override public void onFailure(Failure failure) {
+        throw new AssertionError();
+      }
+      @Override public boolean onResponse(Response response) throws IOException {
+        InputStream bytes = response.body().byteStream();
+        assertEquals('a', bytes.read());
+        assertEquals('b', bytes.read());
+        assertEquals('c', bytes.read());
+
+        // This request will share a connection with 'A' cause it's all done.
+        client.enqueue(new Request.Builder().url(server.getUrl("/b")).build(), receiver);
+        return true;
+      }
+    });
+
+    receiver.await(server.getUrl("/b")).assertCode(200).assertBody("def");
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+    assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reuse!
+  }
+
+  @Test public void postBodyRetransmittedOnRedirect() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(302)
+        .addHeader("Location: /b")
+        .setBody("Moved to /b !"));
+    server.enqueue(new MockResponse()
+        .setBody("This is b."));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .post(Request.Body.create(MediaType.parse("text/plain"), "body!"))
+        .build();
+    client.enqueue(request, receiver);
+
+    receiver.await(server.getUrl("/b"))
+        .assertCode(200)
+        .assertBody("This is b.");
+
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("body!", request1.getUtf8Body());
+    assertEquals("5", request1.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", request1.getHeader("Content-Type"));
+    assertEquals(0, request1.getSequenceNumber());
+
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("body!", request2.getUtf8Body());
+    assertEquals("5", request2.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", request2.getHeader("Content-Type"));
+    assertEquals(1, request2.getSequenceNumber());
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
similarity index 75%
rename from okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index e243857..3ab47b4 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -24,82 +24,89 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
-import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
 import java.util.Arrays;
+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 {
   private static final int KEEP_ALIVE_DURATION_MS = 5000;
-  private static final SSLContext sslContext;
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
 
-  static {
-    try {
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    } catch (GeneralSecurityException e) {
-      throw new RuntimeException(e);
-    } catch (UnknownHostException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  private final MockWebServer spdyServer = new MockWebServer();
+  private MockWebServer spdyServer;
   private InetSocketAddress spdySocketAddress;
   private Address spdyAddress;
 
-  private final MockWebServer httpServer = new MockWebServer();
+  private MockWebServer httpServer;
   private Address httpAddress;
   private InetSocketAddress httpSocketAddress;
 
+  private ConnectionPool pool;
   private Connection httpA;
   private Connection httpB;
   private Connection httpC;
   private Connection httpD;
   private Connection httpE;
   private Connection spdyA;
-  private Connection spdyB;
+
+  private Object owner;
 
   @Before public void setUp() throws Exception {
+    setUp(2);
+  }
+
+  private void setUp(int poolSize) throws Exception {
+    SocketFactory socketFactory = SocketFactory.getDefault();
+
+    spdyServer = new MockWebServer();
+    httpServer = new MockWebServer();
     spdyServer.useHttps(sslContext.getSocketFactory(), false);
 
     httpServer.play();
-    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null,
-        HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
+    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
+        null, HttpAuthenticator.SYSTEM_DEFAULT, null, Protocol.SPDY3_AND_HTTP11);
     httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
         httpServer.getPort());
 
     spdyServer.play();
-    spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(),
+    spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
         sslContext.getSocketFactory(), new RecordingHostnameVerifier(),
-        HttpAuthenticator.SYSTEM_DEFAULT, null, Arrays.asList("spdy/3", "http/1.1"));
+        HttpAuthenticator.SYSTEM_DEFAULT, null,Protocol.SPDY3_AND_HTTP11);
     spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
         spdyServer.getPort());
 
     Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
     Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
-    httpA = new Connection(httpRoute);
-    httpA.connect(100, 100, null);
-    httpB = new Connection(httpRoute);
-    httpB.connect(100, 100, null);
-    httpC = new Connection(httpRoute);
-    httpC.connect(100, 100, null);
-    httpD = new Connection(httpRoute);
-    httpD.connect(100, 100, null);
-    httpE = new Connection(httpRoute);
-    httpE.connect(100, 100, null);
-    spdyA = new Connection(spdyRoute);
-    spdyA.connect(100, 100, null);
-    spdyB = new Connection(spdyRoute);
-    spdyB.connect(100, 100, null);
+    pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
+    httpA = new Connection(pool, httpRoute);
+    httpA.connect(200, 200, null);
+    httpB = new Connection(pool, httpRoute);
+    httpB.connect(200, 200, null);
+    httpC = new Connection(pool, httpRoute);
+    httpC.connect(200, 200, null);
+    httpD = new Connection(pool, httpRoute);
+    httpD.connect(200, 200, null);
+    httpE = new Connection(pool, httpRoute);
+    httpE.connect(200, 200, null);
+    spdyA = new Connection(pool, spdyRoute);
+    spdyA.connect(20000, 20000, null);
+
+    owner = new Object();
+    httpA.setOwner(owner);
+    httpB.setOwner(owner);
+    httpC.setOwner(owner);
+    httpD.setOwner(owner);
+    httpE.setOwner(owner);
   }
 
   @After public void tearDown() throws Exception {
@@ -112,23 +119,32 @@
     Util.closeQuietly(httpD);
     Util.closeQuietly(httpE);
     Util.closeQuietly(spdyA);
-    Util.closeQuietly(spdyB);
   }
 
-  @Test public void poolSingleHttpConnection() throws IOException {
-    ConnectionPool pool = new ConnectionPool(1, KEEP_ALIVE_DURATION_MS);
+  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(new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
-    connection.connect(100, 100, null);
+    connection = new Connection(
+        pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
+    connection.connect(200, 200, null);
+    connection.setOwner(owner);
     assertEquals(0, pool.getConnectionCount());
+
     pool.recycle(connection);
+    assertNull(connection.getOwner());
     assertEquals(1, pool.getConnectionCount());
     assertEquals(1, pool.getHttpConnectionCount());
     assertEquals(0, pool.getSpdyConnectionCount());
 
     Connection recycledConnection = pool.get(httpAddress);
+    assertNull(connection.getOwner());
     assertEquals(connection, recycledConnection);
     assertTrue(recycledConnection.isAlive());
 
@@ -137,7 +153,6 @@
   }
 
   @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     pool.recycle(httpB);
     pool.recycle(httpC);
@@ -145,21 +160,18 @@
   }
 
   @Test public void getSpdyConnection() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     assertSame(spdyA, pool.get(spdyAddress));
     assertPooled(pool, spdyA);
   }
 
   @Test public void getHttpConnection() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     assertSame(httpA, pool.get(httpAddress));
     assertPooled(pool);
   }
 
   @Test public void idleConnectionNotReturned() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
     assertNull(pool.get(httpAddress));
@@ -167,7 +179,6 @@
   }
 
   @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     pool.recycle(httpB);
     pool.recycle(httpC);
@@ -176,7 +187,6 @@
   }
 
   @Test public void expiredConnectionsAreEvicted() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     pool.recycle(httpB);
     Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
@@ -185,7 +195,6 @@
   }
 
   @Test public void nonAliveConnectionNotReturned() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     httpA.close();
     assertNull(pool.get(httpAddress));
@@ -193,15 +202,13 @@
   }
 
   @Test public void differentAddressConnectionNotReturned() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     assertNull(pool.get(spdyAddress));
     assertPooled(pool, httpA);
   }
 
   @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     pool.recycle(httpA);
     assertPooled(pool, httpA, spdyA);
     assertSame(spdyA, pool.get(spdyAddress));
@@ -209,34 +216,33 @@
   }
 
   @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     pool.recycle(httpB);
     assertSame(httpA, pool.get(httpAddress));
   }
 
   @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     httpA.getSocket().shutdownInput();
     pool.recycle(httpA); // Should close httpA.
     assertTrue(httpA.getSocket().isClosed());
   }
 
-  @Test public void shareHttpConnectionDoesNothing() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-    pool.maybeShare(httpA);
+  @Test public void shareHttpConnectionFails() throws Exception {
+    try {
+      pool.share(httpA);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
     assertPooled(pool);
   }
 
   @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(spdyA);
     assertPooled(pool);
   }
 
   @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
     assertNull(pool.get(httpAddress));
     assertPooled(pool, spdyA); // Connection should still be in the pool.
@@ -246,7 +252,6 @@
   }
 
   @Test public void validateIdleHttpConnectionTimeout() throws Exception {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
     pool.recycle(httpA);
     Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
     assertNull(pool.get(spdyAddress));
@@ -257,8 +262,6 @@
   }
 
   @Test public void maxConnections() throws IOException, InterruptedException {
-    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-
     // Pool should be empty.
     assertEquals(0, pool.getConnectionCount());
 
@@ -282,7 +285,7 @@
     assertEquals(0, pool.getSpdyConnectionCount());
 
     // spdy A should be added and http B should be removed.
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     Thread.sleep(50);
     assertEquals(2, pool.getConnectionCount());
     assertEquals(1, pool.getHttpConnectionCount());
@@ -290,6 +293,7 @@
 
     // http C should be removed from the pool.
     Connection recycledHttpConnection = pool.get(httpAddress);
+    recycledHttpConnection.setOwner(owner);
     assertNotNull(recycledHttpConnection);
     assertTrue(recycledHttpConnection.isAlive());
     assertEquals(1, pool.getConnectionCount());
@@ -311,13 +315,6 @@
     assertEquals(1, pool.getHttpConnectionCount());
     assertEquals(1, pool.getSpdyConnectionCount());
 
-    // Nothing should change.
-    pool.maybeShare(spdyB);
-    Thread.sleep(50);
-    assertEquals(2, pool.getConnectionCount());
-    assertEquals(1, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getSpdyConnectionCount());
-
     // An http connection should be removed from the pool.
     recycledHttpConnection = pool.get(httpAddress);
     assertNotNull(recycledHttpConnection);
@@ -326,13 +323,6 @@
     assertEquals(0, pool.getHttpConnectionCount());
     assertEquals(1, pool.getSpdyConnectionCount());
 
-    // Shouldn't change numbers because spdyConnections A and B user the same server address.
-    pool.maybeShare(spdyB);
-    Thread.sleep(50);
-    assertEquals(1, pool.getConnectionCount());
-    assertEquals(0, pool.getHttpConnectionCount());
-    assertEquals(1, pool.getSpdyConnectionCount());
-
     // spdy A will be returned and kept in the pool. Pool shouldn't change.
     sharedSpdyConnection = pool.get(spdyAddress);
     assertEquals(spdyA, sharedSpdyConnection);
@@ -362,7 +352,7 @@
     // Add 3 connections to the pool.
     pool.recycle(httpA);
     pool.recycle(httpB);
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     assertEquals(3, pool.getConnectionCount());
     assertEquals(2, pool.getHttpConnectionCount());
     assertEquals(1, pool.getSpdyConnectionCount());
@@ -390,12 +380,12 @@
     assertEquals(0, pool.getSpdyConnectionCount());
   }
 
-  @Test public void evictAllConnections() {
-    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+  @Test public void evictAllConnections() throws Exception {
+    resetWithPoolSize(10);
     pool.recycle(httpA);
     Util.closeQuietly(httpA); // Include a closed connection in the pool.
     pool.recycle(httpB);
-    pool.maybeShare(spdyA);
+    pool.share(spdyA);
     int connectionCount = pool.getConnectionCount();
     assertTrue(connectionCount == 2 || connectionCount == 3);
 
@@ -403,6 +393,26 @@
     assertEquals(0, pool.getConnectionCount());
   }
 
+  @Test public void closeIfOwnedBy() throws Exception {
+    httpA.closeIfOwnedBy(owner);
+    assertFalse(httpA.isAlive());
+    assertFalse(httpA.clearOwner());
+  }
+
+  @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
+    httpA.closeIfOwnedBy(new Object());
+    assertTrue(httpA.isAlive());
+    assertTrue(httpA.clearOwner());
+  }
+
+  @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
+    try {
+      spdyA.closeIfOwnedBy(owner);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
   private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
     assertEquals(Arrays.asList(connections), pool.getConnections());
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java
new file mode 100644
index 0000000..a42362f
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java
@@ -0,0 +1,189 @@
+package com.squareup.okhttp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class DispatcherTest {
+  RecordingExecutor executor = new RecordingExecutor();
+  RecordingReceiver receiver = new RecordingReceiver();
+  Dispatcher dispatcher = new Dispatcher(executor);
+  OkHttpClient client = new OkHttpClient().setDispatcher(dispatcher);
+
+  @Before public void setUp() throws Exception {
+    dispatcher.setMaxRequests(20);
+    dispatcher.setMaxRequestsPerHost(10);
+  }
+
+  @Test public void maxRequestsZero() throws Exception {
+    try {
+      dispatcher.setMaxRequests(0);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void maxPerHostZero() throws Exception {
+    try {
+      dispatcher.setMaxRequestsPerHost(0);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void enqueuedJobsRunImmediately() throws Exception {
+    client.enqueue(newRequest("http://a/1"), receiver);
+    executor.assertJobs("http://a/1");
+  }
+
+  @Test public void maxRequestsEnforced() throws Exception {
+    dispatcher.setMaxRequests(3);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    client.enqueue(newRequest("http://b/1"), receiver);
+    client.enqueue(newRequest("http://b/2"), receiver);
+    executor.assertJobs("http://a/1", "http://a/2", "http://b/1");
+  }
+
+  @Test public void maxPerHostEnforced() throws Exception {
+    dispatcher.setMaxRequestsPerHost(2);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    client.enqueue(newRequest("http://a/3"), receiver);
+    executor.assertJobs("http://a/1", "http://a/2");
+  }
+
+  @Test public void increasingMaxRequestsPromotesJobsImmediately() throws Exception {
+    dispatcher.setMaxRequests(2);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://b/1"), receiver);
+    client.enqueue(newRequest("http://c/1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    client.enqueue(newRequest("http://b/2"), receiver);
+    dispatcher.setMaxRequests(4);
+    executor.assertJobs("http://a/1", "http://b/1", "http://c/1", "http://a/2");
+  }
+
+  @Test public void increasingMaxPerHostPromotesJobsImmediately() throws Exception {
+    dispatcher.setMaxRequestsPerHost(2);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    client.enqueue(newRequest("http://a/3"), receiver);
+    client.enqueue(newRequest("http://a/4"), receiver);
+    client.enqueue(newRequest("http://a/5"), receiver);
+    dispatcher.setMaxRequestsPerHost(4);
+    executor.assertJobs("http://a/1", "http://a/2", "http://a/3", "http://a/4");
+  }
+
+  @Test public void oldJobFinishesNewJobCanRunDifferentHost() throws Exception {
+    dispatcher.setMaxRequests(1);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://b/1"), receiver);
+    executor.finishJob("http://a/1");
+    executor.assertJobs("http://b/1");
+  }
+
+  @Test public void oldJobFinishesNewJobWithSameHostStarts() throws Exception {
+    dispatcher.setMaxRequests(2);
+    dispatcher.setMaxRequestsPerHost(1);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://b/1"), receiver);
+    client.enqueue(newRequest("http://b/2"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    executor.finishJob("http://a/1");
+    executor.assertJobs("http://b/1", "http://a/2");
+  }
+
+  @Test public void oldJobFinishesNewJobCantRunDueToHostLimit() throws Exception {
+    dispatcher.setMaxRequestsPerHost(1);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://b/1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    executor.finishJob("http://b/1");
+    executor.assertJobs("http://a/1");
+  }
+
+  @Test public void cancelingReadyJobPreventsItFromStarting() throws Exception {
+    dispatcher.setMaxRequestsPerHost(1);
+    client.enqueue(newRequest("http://a/1"), receiver);
+    client.enqueue(newRequest("http://a/2", "tag1"), receiver);
+    dispatcher.cancel("tag1");
+    executor.finishJob("http://a/1");
+    executor.assertJobs();
+  }
+
+  @Test public void cancelingRunningJobTakesNoEffectUntilJobFinishes() throws Exception {
+    dispatcher.setMaxRequests(1);
+    client.enqueue(newRequest("http://a/1", "tag1"), receiver);
+    client.enqueue(newRequest("http://a/2"), receiver);
+    dispatcher.cancel("tag1");
+    executor.assertJobs("http://a/1");
+    executor.finishJob("http://a/1");
+    executor.assertJobs("http://a/2");
+  }
+
+  class RecordingExecutor extends AbstractExecutorService {
+    private List<Job> jobs = new ArrayList<Job>();
+
+    @Override public void execute(Runnable command) {
+      jobs.add((Job) command);
+    }
+
+    public void assertJobs(String... expectedUrls) {
+      List<String> actualUrls = new ArrayList<String>();
+      for (Job job : jobs) {
+        actualUrls.add(job.request().urlString());
+      }
+      assertEquals(Arrays.asList(expectedUrls), actualUrls);
+    }
+
+    public void finishJob(String url) {
+      for (Iterator<Job> i = jobs.iterator(); i.hasNext(); ) {
+        Job job = i.next();
+        if (job.request().urlString().equals(url)) {
+          i.remove();
+          dispatcher.finished(job);
+          return;
+        }
+      }
+      throw new AssertionError("No such job: " + url);
+    }
+
+    @Override public void shutdown() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public List<Runnable> shutdownNow() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public boolean isShutdown() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public boolean isTerminated() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public boolean awaitTermination(long timeout, TimeUnit unit)
+        throws InterruptedException {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  private Request newRequest(String url) {
+    return new Request.Builder().url(url).build();
+  }
+
+  private Request newRequest(String url, String tag) {
+    return new Request.Builder().url(url).tag(tag).build();
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/MediaTypeTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/MediaTypeTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java
diff --git a/okhttp/src/test/java/com/squareup/okhttp/RecordedResponse.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
similarity index 61%
rename from okhttp/src/test/java/com/squareup/okhttp/RecordedResponse.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
index f30ae98..5b57baa 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/RecordedResponse.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
@@ -20,6 +20,8 @@
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 /**
@@ -45,8 +47,9 @@
 
   public RecordedResponse assertContainsHeaders(String... expectedHeaders) {
     List<String> actualHeaders = new ArrayList<String>();
-    for (int i = 0; i < response.headerCount(); i++) {
-      actualHeaders.add(response.headerName(i) + ": " + response.headerValue(i));
+    Headers headers = response.headers();
+    for (int i = 0; i < headers.size(); i++) {
+      actualHeaders.add(headers.name(i) + ": " + headers.value(i));
     }
     if (!actualHeaders.containsAll(Arrays.asList(expectedHeaders))) {
       fail("Expected: " + actualHeaders + "\nto contain: " + Arrays.toString(expectedHeaders));
@@ -58,4 +61,30 @@
     assertEquals(expectedBody, body);
     return this;
   }
+
+  public RecordedResponse assertHandshake() {
+    Handshake handshake = response.handshake();
+    assertNotNull(handshake.cipherSuite());
+    assertNotNull(handshake.peerPrincipal());
+    assertEquals(1, handshake.peerCertificates().size());
+    assertNull(handshake.localPrincipal());
+    assertEquals(0, handshake.localCertificates().size());
+    return this;
+  }
+
+  /**
+   * Asserts that the current response was redirected and returns a new recorded
+   * response for the original request.
+   */
+  public RecordedResponse redirectedBy() {
+    Response redirectedBy = response.priorResponse();
+    assertNotNull(redirectedBy);
+    assertNull(redirectedBy.body());
+    return new RecordedResponse(redirectedBy.request(), redirectedBy, null, null);
+  }
+
+  public void assertFailure(String message) {
+    assertNotNull(failure);
+    assertEquals(message, failure.exception().getMessage());
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
new file mode 100644
index 0000000..9bc8475
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records received HTTP responses so they can be later retrieved by tests.
+ */
+public class RecordingReceiver implements Response.Receiver {
+  public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+
+  private final Map<Response, ByteArrayOutputStream> inFlightResponses
+      = new LinkedHashMap<Response, ByteArrayOutputStream>();
+  private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
+
+  @Override public synchronized void onFailure(Failure failure) {
+    responses.add(new RecordedResponse(failure.request(), null, null, failure));
+    notifyAll();
+  }
+
+  @Override public synchronized boolean onResponse(Response response) throws IOException {
+    ByteArrayOutputStream out = inFlightResponses.get(response);
+    if (out == null) {
+      out = new ByteArrayOutputStream();
+      inFlightResponses.put(response, out);
+    }
+
+    byte[] buffer = new byte[1024];
+    Response.Body body = response.body();
+
+    while (body.ready()) {
+      int c = body.byteStream().read(buffer);
+
+      if (c == -1) {
+        inFlightResponses.remove(response);
+        responses.add(new RecordedResponse(
+            response.request(), response, out.toString("UTF-8"), null));
+        notifyAll();
+        return true;
+      }
+
+      out.write(buffer, 0, c);
+    }
+
+    return false;
+  }
+
+  /**
+   * Returns the recorded response triggered by {@code request}. Throws if the
+   * response isn't enqueued before the timeout.
+   */
+  public synchronized RecordedResponse await(URL url) throws Exception {
+    long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
+    while (true) {
+      for (Iterator<RecordedResponse> i = responses.iterator(); i.hasNext(); ) {
+        RecordedResponse recordedResponse = i.next();
+        if (recordedResponse.request.url().equals(url)) {
+          i.remove();
+          return recordedResponse;
+        }
+      }
+
+      long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
+      if (nowMillis >= timeoutMillis) break;
+      wait(timeoutMillis - nowMillis);
+    }
+
+    throw new AssertionError("Timed out waiting for response to " + url);
+  }
+
+  public synchronized void assertNoResponse(URL url) throws Exception {
+    for (RecordedResponse recordedResponse : responses) {
+      if (recordedResponse.request.url().equals(url)) {
+        throw new AssertionError("Expected no response for " + url);
+      }
+    }
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
similarity index 87%
rename from okhttp/src/test/java/com/squareup/okhttp/RequestTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
index ed43e19..08f304e 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/RequestTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -16,10 +16,10 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
+import okio.OkBuffer;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -74,17 +74,8 @@
   }
 
   private String bodyToHex(Request.Body body) throws IOException {
-    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-    body.writeTo(bytes);
-    return bytesToHex(bytes.toByteArray());
-  }
-
-  private String bytesToHex(byte[] bytes) {
-    StringBuilder hex = new StringBuilder();
-    for (byte b : bytes) {
-      if ((b & 0xff) < 0x10) hex.append('0');
-      hex.append(Integer.toHexString(b & 0xff));
-    }
-    return hex.toString();
+    OkBuffer buffer = new OkBuffer();
+    body.writeTo(buffer);
+    return buffer.readByteString(buffer.size()).hex();
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
new file mode 100644
index 0000000..1153299
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.UUID;
+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.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class SyncApiTest {
+  private MockWebServer server = new MockWebServer();
+  private OkHttpClient client = new OkHttpClient();
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  private HttpResponseCache cache;
+
+  @Before public void setUp() throws Exception {
+    String tmp = System.getProperty("java.io.tmpdir");
+    File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
+    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
+  }
+
+  @After public void tearDown() throws Exception {
+    server.shutdown();
+    cache.delete();
+  }
+
+  @Test public void get() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .header("User-Agent", "SyncApiTest")
+        .build();
+
+    onSuccess(request)
+        .assertCode(200)
+        .assertContainsHeaders("Content-Type: text/plain")
+        .assertBody("abc");
+
+    assertTrue(server.takeRequest().getHeaders().contains("User-Agent: SyncApiTest"));
+  }
+
+  @Test public void connectionPooling() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.enqueue(new MockResponse().setBody("def"));
+    server.enqueue(new MockResponse().setBody("ghi"));
+    server.play();
+
+    onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
+        .assertBody("abc");
+
+    onSuccess(new Request.Builder().url(server.getUrl("/b")).build())
+        .assertBody("def");
+
+    onSuccess(new Request.Builder().url(server.getUrl("/c")).build())
+        .assertBody("ghi");
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    assertEquals(2, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void tls() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .addHeader("Content-Type: text/plain"));
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertHandshake();
+  }
+
+  @Test public void recoverFromTlsHandshakeFailure() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertBody("abc");
+  }
+
+  @Test public void post() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
+        .build();
+
+    onSuccess(request)
+        .assertCode(200)
+        .assertBody("abc");
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertEquals("def", recordedRequest.getUtf8Body());
+    assertEquals("3", recordedRequest.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+  }
+
+  @Test public void conditionalCacheHit() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("A");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
+  @Test public void conditionalCacheMiss() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("B");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
+  @Test public void redirect() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .addHeader("Test", "Redirect from /a to /b")
+        .setBody("/a has moved!"));
+    server.enqueue(new MockResponse()
+        .setResponseCode(302)
+        .addHeader("Location: /c")
+        .addHeader("Test", "Redirect from /b to /c")
+        .setBody("/b has moved!"));
+    server.enqueue(new MockResponse().setBody("C"));
+    server.play();
+
+    onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
+        .assertCode(200)
+        .assertBody("C")
+        .redirectedBy()
+        .assertCode(302)
+        .assertContainsHeaders("Test: Redirect from /b to /c")
+        .redirectedBy()
+        .assertCode(301)
+        .assertContainsHeaders("Test: Redirect from /a to /b");
+
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+    assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
+    assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
+  }
+
+  @Test public void redirectWithRedirectsDisabled() throws Exception {
+    client.setFollowProtocolRedirects(false);
+    server.enqueue(new MockResponse()
+        .setResponseCode(301)
+        .addHeader("Location: /b")
+        .addHeader("Test", "Redirect from /a to /b")
+        .setBody("/a has moved!"));
+    server.play();
+
+    onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
+        .assertCode(301)
+        .assertBody("/a has moved!")
+        .assertContainsHeaders("Location: /b");
+  }
+
+  @Test public void follow20Redirects() throws Exception {
+    for (int i = 0; i < 20; i++) {
+      server.enqueue(new MockResponse()
+          .setResponseCode(301)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.enqueue(new MockResponse().setBody("Success!"));
+    server.play();
+
+    onSuccess(new Request.Builder().url(server.getUrl("/0")).build())
+        .assertCode(200)
+        .assertBody("Success!");
+  }
+
+  @Test public void doesNotFollow21Redirects() throws Exception {
+    for (int i = 0; i < 21; i++) {
+      server.enqueue(new MockResponse()
+          .setResponseCode(301)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.play();
+
+    try {
+      client.execute(new Request.Builder().url(server.getUrl("/0")).build());
+      fail();
+    } catch (IOException e) {
+      assertEquals("Too many redirects: 21", e.getMessage());
+    }
+  }
+
+  @Test public void postBodyRetransmittedOnRedirect() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(302)
+        .addHeader("Location: /b")
+        .setBody("Moved to /b !"));
+    server.enqueue(new MockResponse()
+        .setBody("This is b."));
+    server.play();
+
+    Request request = new Request.Builder()
+        .url(server.getUrl("/"))
+        .post(Request.Body.create(MediaType.parse("text/plain"), "body!"))
+        .build();
+
+    onSuccess(request)
+        .assertCode(200)
+        .assertBody("This is b.");
+
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("body!", request1.getUtf8Body());
+    assertEquals("5", request1.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", request1.getHeader("Content-Type"));
+    assertEquals(0, request1.getSequenceNumber());
+
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("body!", request2.getUtf8Body());
+    assertEquals("5", request2.getHeader("Content-Length"));
+    assertEquals("text/plain; charset=utf-8", request2.getHeader("Content-Type"));
+    assertEquals(1, request2.getSequenceNumber());
+  }
+
+  private RecordedResponse onSuccess(Request request) throws IOException {
+    Response response = client.execute(request);
+    return new RecordedResponse(request, response, response.body().string(), null);
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java
new file mode 100644
index 0000000..7f80c3b
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.math.BigInteger;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class BitArrayTest {
+
+  /** Lazy grow into a variable capacity bit set. */
+  @Test public void hpackUseCase() {
+    BitArray b = new BitArray.FixedCapacity();
+    for (int i = 0; i < 64; i++) {
+      b.set(i);
+    }
+    assertTrue(b.get(0));
+    assertTrue(b.get(1));
+    assertTrue(b.get(63));
+    try {
+      b.get(64);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    b = ((BitArray.FixedCapacity) b).toVariableCapacity();
+    assertTrue(b.get(0));
+    assertTrue(b.get(1));
+    assertTrue(b.get(63));
+    assertFalse(b.get(64));
+    b.set(64);
+    assertTrue(b.get(64));
+  }
+
+  @Test public void setExpandsData_FixedCapacity() {
+    BitArray.FixedCapacity b = new BitArray.FixedCapacity();
+    b.set(63);
+    assertEquals(b.data, BigInteger.ZERO.setBit(63).longValue());
+  }
+
+  @Test public void toggleBit_FixedCapacity() {
+    BitArray.FixedCapacity b = new BitArray.FixedCapacity();
+    b.set(63);
+    b.toggle(63);
+    assertEquals(b.data, 0l);
+    b.toggle(1);
+    assertEquals(b.data, 2l);
+  }
+
+  @Test public void shiftLeft_FixedCapacity() {
+    BitArray.FixedCapacity b = new BitArray.FixedCapacity();
+    b.set(0);
+    b.shiftLeft(1);
+    assertEquals(b.data, 2l);
+  }
+
+  @Test public void multipleShifts_FixedCapacity() {
+    BitArray.FixedCapacity b = new BitArray.FixedCapacity();
+    b.set(10);
+    b.shiftLeft(2);
+    b.shiftLeft(2);
+    assertEquals(b.data, BigInteger.ZERO.setBit(10).shiftLeft(2).shiftLeft(2).longValue());
+  }
+
+  @Test public void clearBits_FixedCapacity() {
+    BitArray.FixedCapacity b = new BitArray.FixedCapacity();
+    b.set(1);
+    b.set(3);
+    b.set(5);
+    b.clear();
+    assertEquals(b.data, 0l);
+  }
+
+  @Test public void setExpandsData_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(64);
+    assertEquals(asList(64), b.toIntegerList());
+  }
+
+  @Test public void toggleBit_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(100);
+    b.toggle(100);
+    assertTrue(b.toIntegerList().isEmpty());
+    b.toggle(1);
+    assertEquals(asList(1), b.toIntegerList());
+  }
+
+  @Test public void shiftLeftExpandsData_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(0);
+    b.shiftLeft(64);
+    assertEquals(asList(64), b.toIntegerList());
+  }
+
+  @Test public void shiftLeftFromZero_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(0);
+    b.shiftLeft(1);
+    assertEquals(asList(1), b.toIntegerList());
+  }
+
+  @Test public void shiftLeftAcrossOffset_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(63);
+    assertEquals(1, b.data.length);
+    b.shiftLeft(1);
+    assertEquals(asList(64), b.toIntegerList());
+    assertEquals(2, b.data.length);
+  }
+
+  @Test public void multipleShiftsLeftAcrossOffset_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(1000);
+    b.shiftLeft(67);
+    assertEquals(asList(1067), b.toIntegerList());
+    b.shiftLeft(69);
+    assertEquals(asList(1136), b.toIntegerList());
+  }
+
+  @Test public void clearBits_VariableCapacity() {
+    BitArray.VariableCapacity b = new BitArray.VariableCapacity();
+    b.set(10);
+    b.set(100);
+    b.set(1000);
+    b.clear();
+    assertTrue(b.toIntegerList().isEmpty());
+  }
+
+  @Test public void bigIntegerSanityCheck_VariableCapacity() {
+    BitArray a = new BitArray.VariableCapacity();
+    BigInteger b = BigInteger.ZERO;
+
+    a.set(64);
+    b = b.setBit(64);
+    assertEquals(bigIntegerToString(b), a.toString());
+
+    a.set(1000000);
+    b = b.setBit(1000000);
+    assertEquals(bigIntegerToString(b), a.toString());
+
+    a.shiftLeft(100);
+    b = b.shiftLeft(100);
+    assertEquals(bigIntegerToString(b), a.toString());
+
+    a.set(0xF00D);
+    b = b.setBit(0xF00D);
+    a.set(0xBEEF);
+    b = b.setBit(0xBEEF);
+    a.set(0xDEAD);
+    b = b.setBit(0xDEAD);
+    assertEquals(bigIntegerToString(b), a.toString());
+
+    a.shiftLeft(0xB0B);
+    b = b.shiftLeft(0xB0B);
+    assertEquals(bigIntegerToString(b), a.toString());
+
+    a.toggle(64280);
+    b = b.clearBit(64280);
+    assertEquals(bigIntegerToString(b), a.toString());
+  }
+
+  private static String bigIntegerToString(BigInteger b) {
+    StringBuilder builder = new StringBuilder("{");
+    for (int i = 0, count = b.bitLength(); i < count; i++) {
+      if (b.testBit(i)) {
+        builder.append(i).append(',');
+      }
+    }
+    builder.setCharAt(builder.length() - 1, '}');
+    return builder.toString();
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
similarity index 63%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
index 636acbd..5d3020f 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
@@ -23,28 +23,43 @@
 import java.util.List;
 
 public final class RecordingOkAuthenticator implements OkAuthenticator {
-  public final List<String> calls = new ArrayList<String>();
+  public final List<URL> urls = new ArrayList<URL>();
+  public final List<List<Challenge>> challengesList = new ArrayList<List<Challenge>>();
+  public final List<Proxy> proxies = new ArrayList<Proxy>();
   public final Credential credential;
 
   public RecordingOkAuthenticator(Credential credential) {
     this.credential = credential;
   }
 
+  public URL onlyUrl() {
+    if (urls.size() != 1) throw new IllegalStateException();
+    return urls.get(0);
+  }
+
+  public List<Challenge> onlyChallenge() {
+    if (challengesList.size() != 1) throw new IllegalStateException();
+    return challengesList.get(0);
+  }
+
+  public Proxy onlyProxy() {
+    if (proxies.size() != 1) throw new IllegalStateException();
+    return proxies.get(0);
+  }
+
   @Override public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges)
       throws IOException {
-    calls.add("authenticate"
-        + " proxy=" + proxy.type()
-        + " url=" + url
-        + " challenges=" + challenges);
+    urls.add(url);
+    challengesList.add(challenges);
+    proxies.add(proxy);
     return credential;
   }
 
   @Override public Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges)
       throws IOException {
-    calls.add("authenticateProxy"
-        + " proxy=" + proxy.type()
-        + " url=" + url
-        + " challenges=" + challenges);
+    urls.add(url);
+    challengesList.add(challenges);
+    proxies.add(proxy);
     return credential;
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
new file mode 100644
index 0000000..a44e683
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2010 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.OkHttpClient;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/** Android's CookiesTest. */
+public class CookiesTest {
+
+  private OkHttpClient client;
+
+  @Before
+  public void setUp() throws Exception {
+    client = new OkHttpClient();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    CookieHandler.setDefault(null);
+  }
+
+  @Test
+  public void testNetscapeResponse() throws Exception {
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    CookieHandler.setDefault(cookieManager);
+    MockWebServer server = new MockWebServer();
+    server.play();
+
+    server.enqueue(new MockResponse().addHeader("Set-Cookie: a=android; "
+        + "expires=Fri, 31-Dec-9999 23:59:59 GMT; "
+        + "path=/path; "
+        + "domain=" + server.getCookieDomain() + "; "
+        + "secure"));
+    get(server, "/path/foo");
+
+    List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
+    assertEquals(1, cookies.size());
+    HttpCookie cookie = cookies.get(0);
+    assertEquals("a", cookie.getName());
+    assertEquals("android", cookie.getValue());
+    assertEquals(null, cookie.getComment());
+    assertEquals(null, cookie.getCommentURL());
+    assertEquals(false, cookie.getDiscard());
+    assertEquals(server.getCookieDomain(), cookie.getDomain());
+    assertTrue(cookie.getMaxAge() > 100000000000L);
+    assertEquals("/path", cookie.getPath());
+    assertEquals(true, cookie.getSecure());
+    assertEquals(0, cookie.getVersion());
+  }
+
+  @Test public void testRfc2109Response() throws Exception {
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    CookieHandler.setDefault(cookieManager);
+    MockWebServer server = new MockWebServer();
+    server.play();
+
+    server.enqueue(new MockResponse().addHeader("Set-Cookie: a=android; "
+        + "Comment=this cookie is delicious; "
+        + "Domain=" + server.getCookieDomain() + "; "
+        + "Max-Age=60; "
+        + "Path=/path; "
+        + "Secure; "
+        + "Version=1"));
+    get(server, "/path/foo");
+
+    List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
+    assertEquals(1, cookies.size());
+    HttpCookie cookie = cookies.get(0);
+    assertEquals("a", cookie.getName());
+    assertEquals("android", cookie.getValue());
+    assertEquals("this cookie is delicious", cookie.getComment());
+    assertEquals(null, cookie.getCommentURL());
+    assertEquals(false, cookie.getDiscard());
+    assertEquals(server.getCookieDomain(), cookie.getDomain());
+    assertEquals(60, cookie.getMaxAge());
+    assertEquals("/path", cookie.getPath());
+    assertEquals(true, cookie.getSecure());
+    assertEquals(1, cookie.getVersion());
+  }
+
+  @Test public void testRfc2965Response() throws Exception {
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    CookieHandler.setDefault(cookieManager);
+    MockWebServer server = new MockWebServer();
+    server.play();
+
+    server.enqueue(new MockResponse().addHeader("Set-Cookie2: a=android; "
+        + "Comment=this cookie is delicious; "
+        + "CommentURL=http://google.com/; "
+        + "Discard; "
+        + "Domain=" + server.getCookieDomain() + "; "
+        + "Max-Age=60; "
+        + "Path=/path; "
+        + "Port=\"80,443," + server.getPort() + "\"; "
+        + "Secure; "
+        + "Version=1"));
+    get(server, "/path/foo");
+
+    List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
+    assertEquals(1, cookies.size());
+    HttpCookie cookie = cookies.get(0);
+    assertEquals("a", cookie.getName());
+    assertEquals("android", cookie.getValue());
+    assertEquals("this cookie is delicious", cookie.getComment());
+    assertEquals("http://google.com/", cookie.getCommentURL());
+    assertEquals(true, cookie.getDiscard());
+    assertEquals(server.getCookieDomain(), cookie.getDomain());
+    assertEquals(60, cookie.getMaxAge());
+    assertEquals("/path", cookie.getPath());
+    assertEquals("80,443," + server.getPort(), cookie.getPortlist());
+    assertEquals(true, cookie.getSecure());
+    assertEquals(1, cookie.getVersion());
+  }
+
+  @Test public void testQuotedAttributeValues() throws Exception {
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    CookieHandler.setDefault(cookieManager);
+    MockWebServer server = new MockWebServer();
+    server.play();
+
+    server.enqueue(new MockResponse().addHeader("Set-Cookie2: a=\"android\"; "
+        + "Comment=\"this cookie is delicious\"; "
+        + "CommentURL=\"http://google.com/\"; "
+        + "Discard; "
+        + "Domain=\"" + server.getCookieDomain() + "\"; "
+        + "Max-Age=\"60\"; "
+        + "Path=\"/path\"; "
+        + "Port=\"80,443," + server.getPort() + "\"; "
+        + "Secure; "
+        + "Version=\"1\""));
+    get(server, "/path/foo");
+
+    List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
+    assertEquals(1, cookies.size());
+    HttpCookie cookie = cookies.get(0);
+    assertEquals("a", cookie.getName());
+    assertEquals("android", cookie.getValue());
+    assertEquals("this cookie is delicious", cookie.getComment());
+    assertEquals("http://google.com/", cookie.getCommentURL());
+    assertEquals(true, cookie.getDiscard());
+    assertEquals(server.getCookieDomain(), cookie.getDomain());
+    assertEquals(60, cookie.getMaxAge());
+    assertEquals("/path", cookie.getPath());
+    assertEquals("80,443," + server.getPort(), cookie.getPortlist());
+    assertEquals(true, cookie.getSecure());
+    assertEquals(1, cookie.getVersion());
+  }
+
+  @Test public void testSendingCookiesFromStore() throws Exception {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse());
+    server.play();
+
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    HttpCookie cookieA = new HttpCookie("a", "android");
+    cookieA.setDomain(server.getCookieDomain());
+    cookieA.setPath("/");
+    cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookieA);
+    HttpCookie cookieB = new HttpCookie("b", "banana");
+    cookieB.setDomain(server.getCookieDomain());
+    cookieB.setPath("/");
+    cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookieB);
+    CookieHandler.setDefault(cookieManager);
+
+    get(server, "/");
+    RecordedRequest request = server.takeRequest();
+
+    List<String> receivedHeaders = request.getHeaders();
+    assertContains(receivedHeaders, "Cookie: $Version=\"1\"; "
+        + "a=\"android\";$Path=\"/\";$Domain=\"" + server.getCookieDomain() + "\"; "
+        + "b=\"banana\";$Path=\"/\";$Domain=\"" + server.getCookieDomain() + "\"");
+  }
+
+  @Test public void testRedirectsDoNotIncludeTooManyCookies() throws Exception {
+    MockWebServer redirectTarget = new MockWebServer();
+    redirectTarget.enqueue(new MockResponse().setBody("A"));
+    redirectTarget.play();
+
+    MockWebServer redirectSource = new MockWebServer();
+    redirectSource.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: " + redirectTarget.getUrl("/")));
+    redirectSource.play();
+
+    CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+    HttpCookie cookie = new HttpCookie("c", "cookie");
+    cookie.setDomain(redirectSource.getCookieDomain());
+    cookie.setPath("/");
+    String portList = Integer.toString(redirectSource.getPort());
+    cookie.setPortlist(portList);
+    cookieManager.getCookieStore().add(redirectSource.getUrl("/").toURI(), cookie);
+    CookieHandler.setDefault(cookieManager);
+
+    get(redirectSource, "/");
+    RecordedRequest request = redirectSource.takeRequest();
+
+    assertContains(request.getHeaders(), "Cookie: $Version=\"1\"; "
+        + "c=\"cookie\";$Path=\"/\";$Domain=\"" + redirectSource.getCookieDomain()
+        + "\";$Port=\"" + portList + "\"");
+
+    for (String header : redirectTarget.takeRequest().getHeaders()) {
+      if (header.startsWith("Cookie")) {
+        fail(header);
+      }
+    }
+  }
+
+  /**
+   * Test which headers show up where. The cookie manager should be notified
+   * of both user-specified and derived headers like {@code Host}. Headers
+   * named {@code Cookie} or {@code Cookie2} that are returned by the cookie
+   * manager should show up in the request and in {@code
+   * getRequestProperties}.
+   */
+  @Test public void testHeadersSentToCookieHandler() throws IOException, InterruptedException {
+    final Map<String, List<String>> cookieHandlerHeaders = new HashMap<String, List<String>>();
+    CookieHandler.setDefault(new CookieManager() {
+      @Override
+      public Map<String, List<String>> get(URI uri,
+          Map<String, List<String>> requestHeaders) throws IOException {
+        cookieHandlerHeaders.putAll(requestHeaders);
+        Map<String, List<String>> result = new HashMap<String, List<String>>();
+        result.put("Cookie", Collections.singletonList("Bar=bar"));
+        result.put("Cookie2", Collections.singletonList("Baz=baz"));
+        result.put("Quux", Collections.singletonList("quux"));
+        return result;
+      }
+    });
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse());
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals(Collections.<String, List<String>>emptyMap(),
+        connection.getRequestProperties());
+
+    connection.setRequestProperty("Foo", "foo");
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(5);
+    connection.getOutputStream().close();
+    connection.getInputStream().close();
+
+    RecordedRequest request = server.takeRequest();
+
+    assertContainsAll(cookieHandlerHeaders.keySet(), "Foo");
+    assertContainsAll(cookieHandlerHeaders.keySet(),
+        "Content-type", "User-Agent", "Connection", "Host");
+    assertFalse(cookieHandlerHeaders.containsKey("Cookie"));
+
+    /*
+     * The API specifies that calling getRequestProperties() on a connected instance should fail
+     * with an IllegalStateException, but the RI violates the spec and returns a valid map.
+     * http://www.mail-archive.com/net-dev@openjdk.java.net/msg01768.html
+     */
+    try {
+      assertContainsAll(connection.getRequestProperties().keySet(), "Foo");
+      assertContainsAll(connection.getRequestProperties().keySet(),
+          "Content-type", "Content-Length", "User-Agent", "Connection", "Host");
+      assertContainsAll(connection.getRequestProperties().keySet(), "Cookie", "Cookie2");
+      assertFalse(connection.getRequestProperties().containsKey("Quux"));
+    } catch (IllegalStateException expected) {
+    }
+
+    assertContainsAll(request.getHeaders(), "Foo: foo", "Cookie: Bar=bar", "Cookie2: Baz=baz");
+    assertFalse(request.getHeaders().contains("Quux: quux"));
+  }
+
+  @Test public void testCookiesSentIgnoresCase() throws Exception {
+    CookieHandler.setDefault(new CookieManager() {
+      @Override public Map<String, List<String>> get(URI uri,
+          Map<String, List<String>> requestHeaders) throws IOException {
+        Map<String, List<String>> result = new HashMap<String, List<String>>();
+        result.put("COOKIE", Collections.singletonList("Bar=bar"));
+        result.put("cooKIE2", Collections.singletonList("Baz=baz"));
+        return result;
+      }
+    });
+    MockWebServer server = new MockWebServer();
+    server. enqueue(new MockResponse());
+    server.play();
+
+    get(server, "/");
+
+    RecordedRequest request = server.takeRequest();
+    assertContainsAll(request.getHeaders(), "COOKIE: Bar=bar", "cooKIE2: Baz=baz");
+    assertFalse(request.getHeaders().contains("Quux: quux"));
+  }
+
+  private void assertContains(Collection<String> collection, String element) {
+    for (String c : collection) {
+      if (c != null && c.equalsIgnoreCase(element)) {
+        return;
+      }
+    }
+    fail("No " + element + " in " + collection);
+  }
+
+  private void assertContainsAll(Collection<String> collection, String... toFind) {
+    for (String s : toFind) {
+      assertContains(collection, s);
+    }
+  }
+
+  private Map<String,List<String>> get(MockWebServer server, String path) throws Exception {
+    URLConnection connection = client.open(server.getUrl(path));
+    Map<String, List<String>> headers = connection.getHeaderFields();
+    connection.getInputStream().close();
+    return headers;
+  }
+
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
similarity index 71%
copy from okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
copy to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
index 11d7239..1c5198c 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
@@ -17,17 +17,22 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.net.URL;
+import java.util.List;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSession;
 
-public final class ExternalSpdyExample {
+import static com.squareup.okhttp.internal.http.OkHeaders.SELECTED_PROTOCOL;
+
+public final class ExternalHttp2Example {
   public static void main(String[] args) throws Exception {
-    URL url = new URL("https://www.google.ca/");
-    HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient().open(url);
+    URL url = new URL("https://http2.iijplus.jp/push/test1");
+    HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient()
+        .setProtocols(Protocol.HTTP2_AND_HTTP_11).open(url);
 
     connection.setHostnameVerifier(new HostnameVerifier() {
       @Override public boolean verify(String s, SSLSession sslSession) {
@@ -38,6 +43,11 @@
 
     int responseCode = connection.getResponseCode();
     System.out.println(responseCode);
+    List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
+    // If null, probably you didn't add jetty's npn jar to your boot classpath!
+    if (protocolValues != null && !protocolValues.isEmpty()) {
+      System.out.println("PROTOCOL " + protocolValues.get(0));
+    }
 
     BufferedReader reader =
         new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
similarity index 76%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
index 11d7239..dab90c1 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
@@ -17,17 +17,22 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.net.URL;
+import java.util.List;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSession;
 
+import static com.squareup.okhttp.internal.http.OkHeaders.SELECTED_PROTOCOL;
+
 public final class ExternalSpdyExample {
   public static void main(String[] args) throws Exception {
     URL url = new URL("https://www.google.ca/");
-    HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient().open(url);
+    HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient()
+        .setProtocols(Protocol.SPDY3_AND_HTTP11).open(url);
 
     connection.setHostnameVerifier(new HostnameVerifier() {
       @Override public boolean verify(String s, SSLSession sslSession) {
@@ -38,6 +43,11 @@
 
     int responseCode = connection.getResponseCode();
     System.out.println(responseCode);
+    List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
+    // If null, probably you didn't add jetty's npn jar to your boot classpath!
+    if (protocolValues != null && !protocolValues.isEmpty()) {
+      System.out.println("PROTOCOL " + protocolValues.get(0));
+    }
 
     BufferedReader reader =
         new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
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
new file mode 100644
index 0000000..80db747
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.Headers;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.spdy.Header;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.headerEntries;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public final class HeadersTest {
+  @Test public void parseNameValueBlock() throws IOException {
+    List<Header> headerBlock = headerEntries(
+        "cache-control", "no-cache, no-store",
+        "set-cookie", "Cookie1\u0000Cookie2",
+        ":status", "200 OK",
+        ":version", "HTTP/1.1");
+    Request request = new Request.Builder().url("http://square.com/").build();
+    Response response =
+        SpdyTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+    Headers headers = response.headers();
+    assertEquals(4, headers.size());
+    assertEquals("HTTP/1.1 200 OK", response.statusLine());
+    assertEquals("no-cache, no-store", headers.get("cache-control"));
+    assertEquals("Cookie2", headers.get("set-cookie"));
+    assertEquals(Protocol.SPDY_3.name.utf8(), headers.get(OkHeaders.SELECTED_PROTOCOL));
+    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
+    assertEquals(Protocol.SPDY_3.name.utf8(), headers.value(0));
+    assertEquals("cache-control", headers.name(1));
+    assertEquals("no-cache, no-store", 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));
+    assertNull(headers.get(":status"));
+    assertNull(headers.get(":version"));
+  }
+
+  @Test public void readNameValueBlockDropsForbiddenHeadersSpdy3() throws IOException {
+    List<Header> headerBlock = headerEntries(
+        ":status", "200 OK",
+        ":version", "HTTP/1.1",
+        "connection", "close");
+    Request request = new Request.Builder().url("http://square.com/").build();
+    Response response =
+        SpdyTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+    Headers headers = response.headers();
+    assertEquals(1, headers.size());
+    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
+    assertEquals(Protocol.SPDY_3.name.utf8(), headers.value(0));
+  }
+
+  @Test public void readNameValueBlockDropsForbiddenHeadersHttp2() throws IOException {
+    List<Header> headerBlock = headerEntries(
+        ":status", "200 OK",
+        ":version", "HTTP/1.1",
+        "connection", "close");
+    Request request = new Request.Builder().url("http://square.com/").build();
+    Response response = SpdyTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
+        .request(request).build();
+    Headers headers = response.headers();
+    assertEquals(1, headers.size());
+    assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
+    assertEquals(Protocol.HTTP_2.name.utf8(), headers.value(0));
+  }
+
+  @Test public void toNameValueBlock() {
+    Request request = new Request.Builder()
+        .url("http://square.com/")
+        .header("cache-control", "no-cache, no-store")
+        .addHeader("set-cookie", "Cookie1")
+        .addHeader("set-cookie", "Cookie2")
+        .header(":status", "200 OK")
+        .build();
+    List<Header> headerBlock =
+        SpdyTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
+    List<Header> expected = headerEntries(
+        ":method", "GET",
+        ":path", "/",
+        ":version", "HTTP/1.1",
+        ":host", "square.com",
+        ":scheme", "http",
+        "cache-control", "no-cache, no-store",
+        "set-cookie", "Cookie1\u0000Cookie2",
+        ":status", "200 OK");
+    assertEquals(expected, headerBlock);
+  }
+
+  @Test public void toNameValueBlockDropsForbiddenHeadersSpdy3() {
+    Request request = new Request.Builder()
+        .url("http://square.com/")
+        .header("Connection", "close")
+        .header("Transfer-Encoding", "chunked")
+        .build();
+    List<Header> expected = headerEntries(
+        ":method", "GET",
+        ":path", "/",
+        ":version", "HTTP/1.1",
+        ":host", "square.com",
+        ":scheme", "http");
+    assertEquals(expected, SpdyTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
+  }
+
+  @Test public void toNameValueBlockDropsForbiddenHeadersHttp2() {
+    Request request = new Request.Builder()
+        .url("http://square.com/")
+        .header("Connection", "upgrade")
+        .header("Upgrade", "websocket")
+        .build();
+    List<Header> expected = headerEntries(
+        ":method", "GET",
+        ":path", "/",
+        ":authority", "square.com",
+        ":scheme", "http");
+    assertEquals(expected,
+        SpdyTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java
new file mode 100644
index 0000000..851a9c1
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.PushPromise;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.util.Arrays;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class HttpOverHttp20Draft09Test extends HttpOverSpdyTest {
+
+  public HttpOverHttp20Draft09Test() {
+    super(Protocol.HTTP_2);
+    this.hostHeader = ":authority";
+  }
+
+  @Test public void serverSendsPushPromise_GET() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet")
+        .withPush(new PushPromise("GET", "/foo/bar", Arrays.asList("foo: bar"),
+            new MockResponse().setBody("bar").setStatus("HTTP/1.1 200 Sweet")));
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+    assertEquals(200, connection.getResponseCode());
+    assertEquals("Sweet", connection.getResponseMessage());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    assertContains(request.getHeaders(), ":scheme: https");
+    assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+
+    RecordedRequest pushedRequest = server.takeRequest();
+    assertEquals("GET /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
+    assertEquals(Arrays.asList("foo: bar"), pushedRequest.getHeaders());
+  }
+
+  @Test public void serverSendsPushPromise_HEAD() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet")
+        .withPush(new PushPromise("HEAD", "/foo/bar", Arrays.asList("foo: bar"),
+            new MockResponse().setStatus("HTTP/1.1 204 Sweet")));
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+    assertEquals(200, connection.getResponseCode());
+    assertEquals("Sweet", connection.getResponseMessage());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    assertContains(request.getHeaders(), ":scheme: https");
+    assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+
+    RecordedRequest pushedRequest = server.takeRequest();
+    assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
+    assertEquals(Arrays.asList("foo: bar"), pushedRequest.getHeaders());
+  }
+}
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/http/HttpOverSpdy3Test.java
new file mode 100644
index 0000000..4020bf4
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Protocol;
+
+public class HttpOverSpdy3Test extends HttpOverSpdyTest {
+
+  public HttpOverSpdy3Test() {
+    super(Protocol.SPDY_3);
+  }
+}
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/http/HttpOverSpdyTest.java
new file mode 100644
index 0000000..c725a75
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.HttpResponseCache;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.RecordingAuthenticator;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/** Test how SPDY interacts with HTTP features. */
+public abstract class HttpOverSpdyTest {
+
+  /** Protocol to test, for example {@link com.squareup.okhttp.Protocol#SPDY_3} */
+  private final Protocol protocol;
+  protected String hostHeader = ":host";
+
+  protected HttpOverSpdyTest(Protocol protocol){
+    this.protocol = protocol;
+  }
+
+  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
+    public boolean verify(String hostname, SSLSession session) {
+      return true;
+    }
+  };
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  protected final MockWebServer server = new MockWebServer();
+  protected final String hostName = server.getHostName();
+  protected final OkHttpClient client = new OkHttpClient();
+  protected HttpURLConnection connection;
+  protected HttpResponseCache cache;
+
+  @Before public void setUp() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_11));
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    String systemTmpDir = System.getProperty("java.io.tmpdir");
+    File cacheDir = new File(systemTmpDir, "HttpCache-" + protocol + "-" + UUID.randomUUID());
+    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
+  }
+
+  @After public void tearDown() throws Exception {
+    Authenticator.setDefault(null);
+    server.shutdown();
+  }
+
+  @Test public void get() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet");
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+    assertEquals(200, connection.getResponseCode());
+    assertEquals("Sweet", connection.getResponseMessage());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    assertContains(request.getHeaders(), ":scheme: https");
+    assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+  }
+
+  @Test public void emptyResponse() throws IOException {
+    server.enqueue(new MockResponse());
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    assertEquals(-1, connection.getInputStream().read());
+  }
+
+  byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8);
+
+  /** An output stream can be written to more than once, so we can't guess content length. */
+  @Test public void noDefaultContentLengthOnPost() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE");
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(postBytes);
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
+    assertArrayEquals(postBytes, request.getBody());
+    assertNull(request.getHeader("Content-Length"));
+  }
+
+  @Test public void userSuppliedContentLengthHeader() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE");
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length));
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(postBytes);
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
+    assertArrayEquals(postBytes, request.getBody());
+    assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
+  }
+
+  @Test public void closeAfterFlush() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE");
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length));
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(postBytes); // push bytes into SpdyDataOutputStream.buffer
+    connection.getOutputStream().flush(); // SpdyConnection.writeData subject to write window
+    connection.getOutputStream().close(); // SpdyConnection.writeData empty frame
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
+    assertArrayEquals(postBytes, request.getBody());
+    assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
+  }
+
+  @Test public void setFixedLengthStreamingModeSetsContentLength() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE");
+    server.enqueue(response);
+    server.play();
+
+    connection = client.open(server.getUrl("/foo"));
+    connection.setFixedLengthStreamingMode(postBytes.length);
+    connection.setDoOutput(true);
+    connection.getOutputStream().write(postBytes);
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
+    assertArrayEquals(postBytes, request.getBody());
+    assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
+  }
+
+  @Test public void spdyConnectionReuse() throws Exception {
+    server.enqueue(new MockResponse().setBody("ABCDEF"));
+    server.enqueue(new MockResponse().setBody("GHIJKL"));
+    server.play();
+
+    HttpURLConnection connection1 = client.open(server.getUrl("/r1"));
+    HttpURLConnection connection2 = client.open(server.getUrl("/r2"));
+    assertEquals("ABC", readAscii(connection1.getInputStream(), 3));
+    assertEquals("GHI", readAscii(connection2.getInputStream(), 3));
+    assertEquals("DEF", readAscii(connection1.getInputStream(), 3));
+    assertEquals("JKL", readAscii(connection2.getInputStream(), 3));
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test @Ignore public void synchronousSpdyRequest() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    ExecutorService executor = Executors.newCachedThreadPool();
+    CountDownLatch countDownLatch = new CountDownLatch(2);
+    executor.execute(new SpdyRequest("/r1", countDownLatch));
+    executor.execute(new SpdyRequest("/r2", countDownLatch));
+    countDownLatch.await();
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void gzippedResponseBody() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Content-Encoding: gzip")
+        .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8))));
+    server.play();
+    assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE);
+  }
+
+  @Test public void authenticate() throws Exception {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED)
+        .addHeader("www-authenticate: Basic realm=\"protected area\"")
+        .setBody("Please authenticate."));
+    server.enqueue(new MockResponse().setBody("Successful auth!"));
+    server.play();
+
+    Authenticator.setDefault(new RecordingAuthenticator());
+    connection = client.open(server.getUrl("/"));
+    assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest denied = server.takeRequest();
+    assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*");
+    RecordedRequest accepted = server.takeRequest();
+    assertEquals("GET / HTTP/1.1", accepted.getRequestLine());
+    assertContains(accepted.getHeaders(),
+        "authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+  }
+
+  @Test public void redirect() throws Exception {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: /foo")
+        .setBody("This page has moved!"));
+    server.enqueue(new MockResponse().setBody("This is the new location!"));
+    server.play();
+
+    connection = client.open(server.getUrl("/"));
+    assertContent("This is the new location!", connection, Integer.MAX_VALUE);
+
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("/", request1.getPath());
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("/foo", request2.getPath());
+  }
+
+  @Test public void readAfterLastByte() throws Exception {
+    server.enqueue(new MockResponse().setBody("ABC"));
+    server.play();
+
+    connection = client.open(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    assertEquals("ABC", readAscii(in, 3));
+    assertEquals(-1, in.read());
+    assertEquals(-1, in.read());
+  }
+
+  @Ignore // See https://github.com/square/okhttp/issues/578
+  @Test(timeout = 3000) public void readResponseHeaderTimeout() throws Exception {
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    connection = client.open(server.getUrl("/"));
+    connection.setReadTimeout(1000);
+    assertContent("A", connection, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Test to ensure we don't  throw a read timeout on responses that are
+   * progressing.  For this case, we take a 4KiB body and throttle it to
+   * 1KiB/second.  We set the read timeout to two seconds.  If our
+   * implementation is acting correctly, it will not throw, as it is
+   * progressing.
+   */
+  @Test public void readTimeoutMoreGranularThanBodySize() throws Exception {
+    char[] body = new char[4096]; // 4KiB to read
+    Arrays.fill(body, 'y');
+    server.enqueue(new MockResponse()
+        .setBody(new String(body))
+        .throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second
+    server.play();
+
+    connection = client.open(server.getUrl("/"));
+    connection.setReadTimeout(2000); // 2 seconds to read something.
+    assertContent(new String(body), connection, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Test to ensure we throw a read timeout on responses that are progressing
+   * too slowly.  For this case, we take a 2KiB body and throttle it to
+   * 1KiB/second.  We set the read timeout to half a second.  If our
+   * implementation is acting correctly, it will throw, as a byte doesn't
+   * arrive in time.
+   */
+  @Test public void readTimeoutOnSlowConnection() throws Exception {
+    char[] body = new char[2048]; // 2KiB to read
+    Arrays.fill(body, 'y');
+    server.enqueue(new MockResponse()
+        .setBody(new String(body))
+        .throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second
+    server.play();
+
+    connection = client.open(server.getUrl("/"));
+    connection.setReadTimeout(500); // half a second to read something
+    connection.connect();
+    try {
+      readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+      fail("Should have timed out!");
+    } catch (IOException e){
+      assertEquals("Read timed out", e.getMessage());
+    }
+  }
+
+  @Test public void spdyConnectionTimeout() throws Exception {
+    MockResponse response = new MockResponse().setBody("A");
+    response.setBodyDelayTimeMs(1000);
+    server.enqueue(response);
+    server.play();
+
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    connection1.setReadTimeout(2000);
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    connection2.setReadTimeout(200);
+    connection1.connect();
+    connection2.connect();
+    assertContent("A", connection1, Integer.MAX_VALUE);
+  }
+
+  @Test public void responsesAreCached() throws IOException {
+    client.setOkResponseCache(cache);
+
+    server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A"));
+    server.play();
+
+    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
+    assertEquals(1, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(0, cache.getHitCount());
+    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
+    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
+    assertEquals(3, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(2, cache.getHitCount());
+  }
+
+  @Test public void conditionalCache() throws IOException {
+    client.setOkResponseCache(cache);
+
+    server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A"));
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
+    assertEquals(1, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(0, cache.getHitCount());
+    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
+    assertEquals(2, cache.getRequestCount());
+    assertEquals(2, cache.getNetworkCount());
+    assertEquals(1, cache.getHitCount());
+  }
+
+  @Test public void responseCachedWithoutConsumingFullBody() throws IOException {
+    client.setOkResponseCache(cache);
+
+    server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("ABCD"));
+    server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("EFGH"));
+    server.play();
+
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    InputStream in1 = connection1.getInputStream();
+    assertEquals("AB", readAscii(in1, 2));
+    in1.close();
+
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    InputStream in2 = connection2.getInputStream();
+    assertEquals("ABCD", readAscii(in2, Integer.MAX_VALUE));
+    in2.close();
+  }
+
+  @Test public void acceptAndTransmitCookies() throws Exception {
+    CookieManager cookieManager = new CookieManager();
+    client.setCookieHandler(cookieManager);
+    server.enqueue(
+        new MockResponse().addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain())
+            .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertContent("A", client.open(url), Integer.MAX_VALUE);
+    Map<String, List<String>> requestHeaders = Collections.emptyMap();
+    assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")),
+        cookieManager.get(url.toURI(), requestHeaders));
+
+    assertContent("B", client.open(url), Integer.MAX_VALUE);
+    RecordedRequest requestA = server.takeRequest();
+    assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*");
+    RecordedRequest requestB = server.takeRequest();
+    assertContains(requestB.getHeaders(), "cookie: c=oreo");
+  }
+
+  <T> void assertContains(Collection<T> collection, T value) {
+    assertTrue(collection.toString(), collection.contains(value));
+  }
+
+  void assertContent(String expected, HttpURLConnection connection, int limit)
+      throws IOException {
+    connection.connect();
+    assertEquals(expected, readAscii(connection.getInputStream(), limit));
+  }
+
+  private void assertContainsNoneMatching(List<String> headers, String pattern) {
+    for (String header : headers) {
+      if (header.matches(pattern)) {
+        fail("Header " + header + " matches " + pattern);
+      }
+    }
+  }
+
+  private String readAscii(InputStream in, int count) throws IOException {
+    StringBuilder result = new StringBuilder();
+    for (int i = 0; i < count; i++) {
+      int value = in.read();
+      if (value == -1) {
+        in.close();
+        break;
+      }
+      result.append((char) value);
+    }
+    return result.toString();
+  }
+
+  public byte[] gzip(byte[] bytes) throws IOException {
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
+    gzippedOut.write(bytes);
+    gzippedOut.close();
+    return bytesOut.toByteArray();
+  }
+
+  class SpdyRequest implements Runnable {
+    String path;
+    CountDownLatch countDownLatch;
+    public SpdyRequest(String path, CountDownLatch countDownLatch) {
+      this.path = path;
+      this.countDownLatch = countDownLatch;
+    }
+
+    @Override public void run() {
+      try {
+        HttpURLConnection conn = client.open(server.getUrl(path));
+        assertEquals("A", readAscii(conn.getInputStream(), 1));
+        countDownLatch.countDown();
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
similarity index 86%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index 89f31da..541351a 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -16,8 +16,12 @@
 
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
 import com.squareup.okhttp.ResponseSource;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
@@ -34,35 +38,29 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.net.CacheRequest;
-import java.net.CacheResponse;
 import java.net.CookieHandler;
 import java.net.CookieManager;
 import java.net.HttpCookie;
 import java.net.HttpURLConnection;
-import java.net.InetAddress;
 import java.net.ResponseCache;
-import java.net.SecureCacheResponse;
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
-import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
 import java.security.Principal;
 import java.security.cert.Certificate;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.TimeZone;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.zip.GZIPOutputStream;
@@ -79,33 +77,29 @@
 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;
 
-/** Android's HttpResponseCacheTest. */
+/**
+ * Android's HttpResponseCacheTest. This tests both the {@link HttpResponseCache} implementation and
+ * the behavior of {@link com.squareup.okhttp.OkResponseCache} classes generally.
+ */
 public final class HttpResponseCacheTest {
   private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
     @Override public boolean verify(String s, SSLSession sslSession) {
       return true;
     }
   };
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+
   private final OkHttpClient client = new OkHttpClient();
   private MockWebServer server = new MockWebServer();
   private MockWebServer server2 = new MockWebServer();
   private HttpResponseCache cache;
   private final CookieManager cookieManager = new CookieManager();
 
-  private static final SSLContext sslContext;
-  static {
-    try {
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    } catch (GeneralSecurityException e) {
-      throw new RuntimeException(e);
-    } catch (UnknownHostException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   @Before public void setUp() throws Exception {
     String tmp = System.getProperty("java.io.tmpdir");
     File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
@@ -127,6 +121,20 @@
     return client.open(url);
   }
 
+  @Test public void responseCacheAccessWithOkHttpMember() throws IOException {
+    ResponseCache.setDefault(null);
+    client.setResponseCache(cache);
+    assertSame(cache, client.getOkResponseCache());
+    assertNull(client.getResponseCache());
+  }
+
+  @Test public void responseCacheAccessWithGlobalDefault() throws IOException {
+    ResponseCache.setDefault(cache);
+    client.setResponseCache(null);
+    assertNull(client.getOkResponseCache());
+    assertNull(client.getResponseCache());
+  }
+
   /**
    * Test that response caching is consistent with the RI and the spec.
    * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
@@ -212,11 +220,10 @@
     // exhaust the content stream
     readAscii(conn);
 
-    CacheResponse cached =
-        cache.get(url.toURI(), "GET", Collections.<String, List<String>>emptyMap());
+    Response cached = cache.get(new Request.Builder().url(url).build());
     if (shouldPut) {
       assertNotNull(Integer.toString(responseCode), cached);
-      cached.getBody().close();
+      cached.body().close();
     } else {
       assertNull(Integer.toString(responseCode), cached);
     }
@@ -231,40 +238,21 @@
     final String body = "ABCDE";
     final AtomicInteger cacheCount = new AtomicInteger();
 
-    server.enqueue(
-        new MockResponse().setStatus("HTTP/1.1 200 Fantastic").addHeader("fgh: ijk").setBody(body));
+    server.enqueue(new MockResponse()
+        .setStatus("HTTP/1.1 200 Fantastic")
+        .addHeader("Content-Type: text/plain")
+        .addHeader("fgh: ijk")
+        .setBody(body));
     server.play();
 
-    ResponseCache.setDefault(new ResponseCache() {
-      @Override public CacheResponse get(URI uri, String requestMethod,
-          Map<String, List<String>> requestHeaders) throws IOException {
-        return null;
-      }
-
-      @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
-        HttpURLConnection httpConnection = (HttpURLConnection) conn;
-        try {
-          httpConnection.getRequestProperties();
-          fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-          httpConnection.addRequestProperty("K", "V");
-          fail();
-        } catch (IllegalStateException expected) {
-        }
-        assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null));
-        assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"),
-            httpConnection.getHeaderFields().get(null));
-        assertEquals(200, httpConnection.getResponseCode());
-        assertEquals("Fantastic", httpConnection.getResponseMessage());
-        assertEquals(body.length(), httpConnection.getContentLength());
-        assertEquals("ijk", httpConnection.getHeaderField("fgh"));
-        try {
-          httpConnection.getInputStream(); // the RI doesn't forbid this, but it should
-          fail();
-        } catch (IOException expected) {
-        }
+    client.setOkResponseCache(new AbstractOkResponseCache() {
+      @Override public CacheRequest put(Response response) throws IOException {
+        assertEquals(server.getUrl("/"), response.request().url());
+        assertEquals(200, response.code());
+        assertNull(response.body());
+        assertEquals("5", response.header("Content-Length"));
+        assertEquals("text/plain", response.header("Content-Type"));
+        assertEquals("ijk", response.header("fgh"));
         cacheCount.incrementAndGet();
         return null;
       }
@@ -276,6 +264,32 @@
     assertEquals(1, cacheCount.get());
   }
 
+  /** Don't explode if the cache returns a null body. http://b/3373699 */
+  @Test public void responseCacheReturnsNullOutputStream() throws Exception {
+    final AtomicBoolean aborted = new AtomicBoolean();
+    client.setOkResponseCache(new AbstractOkResponseCache() {
+      @Override public CacheRequest put(Response response) throws IOException {
+        return new CacheRequest() {
+          @Override public void abort() {
+            aborted.set(true);
+          }
+
+          @Override public OutputStream getBody() throws IOException {
+            return null;
+          }
+        };
+      }
+    });
+
+    server.enqueue(new MockResponse().setBody("abcdef"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("abc", readAscii(connection, 3));
+    connection.getInputStream().close();
+    assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+  }
+
   @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
     testResponseCaching(TransferKind.FIXED_LENGTH);
   }
@@ -334,52 +348,32 @@
         .setBody("ABC"));
     server.play();
 
-    HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
-    connection.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("ABC", readAscii(connection));
+    HttpsURLConnection c1 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    c1.setSSLSocketFactory(sslContext.getSocketFactory());
+    c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(c1));
 
     // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
-    String suite = connection.getCipherSuite();
-    List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
-    List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
-    Principal peerPrincipal = connection.getPeerPrincipal();
-    Principal localPrincipal = connection.getLocalPrincipal();
+    String suite = c1.getCipherSuite();
+    List<Certificate> localCerts = toListOrNull(c1.getLocalCertificates());
+    List<Certificate> serverCerts = toListOrNull(c1.getServerCertificates());
+    Principal peerPrincipal = c1.getPeerPrincipal();
+    Principal localPrincipal = c1.getLocalPrincipal();
 
-    connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
-    connection.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("ABC", readAscii(connection));
+    HttpsURLConnection c2 = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
+    c2.setSSLSocketFactory(sslContext.getSocketFactory());
+    c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(c2));
 
     assertEquals(2, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
     assertEquals(1, cache.getHitCount());
 
-    assertEquals(suite, connection.getCipherSuite());
-    assertEquals(localCerts, toListOrNull(connection.getLocalCertificates()));
-    assertEquals(serverCerts, toListOrNull(connection.getServerCertificates()));
-    assertEquals(peerPrincipal, connection.getPeerPrincipal());
-    assertEquals(localPrincipal, connection.getLocalPrincipal());
-  }
-
-  @Test public void cacheReturnsInsecureResponseForSecureRequest() throws IOException {
-    server.useHttps(sslContext.getSocketFactory(), false);
-    server.enqueue(new MockResponse().setBody("ABC"));
-    server.enqueue(new MockResponse().setBody("DEF"));
-    server.play();
-
-    ResponseCache.setDefault(new InsecureResponseCache());
-
-    HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
-    connection1.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("ABC", readAscii(connection1));
-
-    // Not cached!
-    HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
-    connection2.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("DEF", readAscii(connection2));
+    assertEquals(suite, c2.getCipherSuite());
+    assertEquals(localCerts, toListOrNull(c2.getLocalCertificates()));
+    assertEquals(serverCerts, toListOrNull(c2.getServerCertificates()));
+    assertEquals(peerPrincipal, c2.getPeerPrincipal());
+    assertEquals(localPrincipal, c2.getLocalPrincipal());
   }
 
   @Test public void responseCachingAndRedirects() throws Exception {
@@ -445,13 +439,16 @@
 
     HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection1));
+    assertNotNull(connection1.getCipherSuite());
 
     // Cached!
     HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection2));
+    assertNotNull(connection2.getCipherSuite());
 
     assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
     assertEquals(2, cache.getHitCount());
+    assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite());
   }
 
   /**
@@ -494,15 +491,10 @@
     server.enqueue(new MockResponse().setBody("ABC"));
     server.play();
 
-    final AtomicReference<Map<String, List<String>>> requestHeadersRef =
-        new AtomicReference<Map<String, List<String>>>();
-    ResponseCache.setDefault(new ResponseCache() {
-      @Override public CacheResponse get(URI uri, String requestMethod,
-          Map<String, List<String>> requestHeaders) throws IOException {
-        requestHeadersRef.set(requestHeaders);
-        return null;
-      }
-      @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
+    final AtomicReference<Request> requestRef = new AtomicReference<Request>();
+    client.setOkResponseCache(new AbstractOkResponseCache() {
+      @Override public Response get(Request request) throws IOException {
+        requestRef.set(request);
         return null;
       }
     });
@@ -511,7 +503,7 @@
     URLConnection urlConnection = openConnection(url);
     urlConnection.addRequestProperty("A", "android");
     readAscii(urlConnection);
-    assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
+    assertEquals(Arrays.asList("android"), requestRef.get().headers("A"));
   }
 
   @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
@@ -568,7 +560,7 @@
 
   private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
     // Setting a low transfer speed ensures that stream discarding will time out.
-    MockResponse response = new MockResponse().setBytesPerSecond(6);
+    MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS);
     transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
     server.enqueue(response);
     server.enqueue(new MockResponse().setBody("Request #2"));
@@ -774,7 +766,7 @@
 
     URLConnection request2 = openConnection(url);
     if (expectCached) {
-      assertEquals("1", request1.getHeaderField("X-Response-ID"));
+      assertEquals("1", request2.getHeaderField("X-Response-ID"));
     } else {
       assertEquals("2", request2.getHeaderField("X-Response-ID"));
     }
@@ -976,6 +968,22 @@
     assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/"))));
   }
 
+  @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
+    server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A"));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    ConnectionPool pool = ConnectionPool.getDefault();
+    pool.evictAll();
+    client.setConnectionPool(pool);
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals(1, client.getConnectionPool().getConnectionCount());
+  }
+
   @Test public void expiresDateBeforeModifiedDate() throws Exception {
     assertConditionallyCached(
         new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
@@ -1048,6 +1056,9 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertGatewayTimeout(connection);
+    assertEquals(1, cache.getRequestCount());
+    assertEquals(0, cache.getNetworkCount());
+    assertEquals(0, cache.getHitCount());
   }
 
   @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException {
@@ -1060,6 +1071,9 @@
     URLConnection connection = openConnection(server.getUrl("/"));
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertEquals("A", readAscii(connection));
+    assertEquals(2, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(1, cache.getHitCount());
   }
 
   @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
@@ -1072,6 +1086,9 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertGatewayTimeout(connection);
+    assertEquals(2, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(0, cache.getHitCount());
   }
 
   @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
@@ -1082,6 +1099,9 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertGatewayTimeout(connection);
+    assertEquals(2, cache.getRequestCount());
+    assertEquals(1, cache.getNetworkCount());
+    assertEquals(0, cache.getHitCount());
   }
 
   @Test public void requestCacheControlNoCache() throws Exception {
@@ -1156,17 +1176,57 @@
     return server.takeRequest();
   }
 
+  /**
+   * Confirm that {@link URLConnection#setIfModifiedSince} causes an
+   * If-Modified-Since header with a GMT timestamp.
+   *
+   * https://code.google.com/p/android/issues/detail?id=66135
+   */
   @Test public void setIfModifiedSince() throws Exception {
-    Date since = new Date();
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
 
     URL url = server.getUrl("/");
     URLConnection connection = openConnection(url);
-    connection.setIfModifiedSince(since.getTime());
+    connection.setIfModifiedSince(1393666200000L);
     assertEquals("A", readAscii(connection));
     RecordedRequest request = server.takeRequest();
-    assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since)));
+    String ifModifiedSinceHeader = request.getHeader("If-Modified-Since");
+    assertEquals("Sat, 01 Mar 2014 09:30:00 GMT", ifModifiedSinceHeader);
+  }
+
+  /**
+   * For Last-Modified and Date headers, we should echo the date back in the
+   * exact format we were served.
+   */
+  @Test public void retainServedDateFormat() throws Exception {
+    // Serve a response with a non-standard date format that OkHttp supports.
+    Date lastModifiedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1));
+    Date servedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2));
+    DateFormat dateFormat = new SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US);
+    dateFormat.setTimeZone(TimeZone.getTimeZone("EDT"));
+    String lastModifiedString = dateFormat.format(lastModifiedDate);
+    String servedString = dateFormat.format(servedDate);
+
+    // This response should be conditionally cached.
+    server.enqueue(new MockResponse()
+        .addHeader("Last-Modified: " + lastModifiedString)
+        .addHeader("Expires: " + servedString)
+        .setBody("A"));
+    server.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+
+    // The first request has no conditions.
+    RecordedRequest request1 = server.takeRequest();
+    assertNull(request1.getHeader("If-Modified-Since"));
+
+    // The 2nd request uses the server's date format.
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals(lastModifiedString, request2.getHeader("If-Modified-Since"));
   }
 
   @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
@@ -1684,8 +1744,8 @@
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CACHE.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CACHE + " 200", source);
   }
 
   @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
@@ -1701,8 +1761,8 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("B", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source);
   }
 
   @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
@@ -1716,8 +1776,8 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 304", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source);
   }
 
   @Test public void responseSourceHeaderFetched() throws IOException {
@@ -1727,8 +1787,8 @@
     URLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.NETWORK.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.NETWORK + " 200", source);
   }
 
   @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
@@ -1794,7 +1854,7 @@
     writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
     writeFile(cache.getDirectory(), "journal", journalBody);
     cache = new HttpResponseCache(cache.getDirectory(), Integer.MAX_VALUE);
-    client.setResponseCache(cache);
+    client.setOkResponseCache(cache);
 
     HttpURLConnection connection = client.open(url);
     assertEquals(entryBody, readAscii(connection));
@@ -1802,6 +1862,62 @@
     assertEquals("foo", connection.getHeaderField("etag"));
   }
 
+  // Older versions of OkHttp use ResponseCache.get() and ResponseCache.put(). For compatibility
+  // with Android apps when the Android-bundled and and an older app-bundled OkHttp library are in
+  // use at the same time the HttpResponseCache must behave as it always used to. That's not the
+  // same as a fully API-compliant {@link ResponseCache}: That means that the cache
+  // doesn't throw an exception from get() or put() and also does not cache requests/responses from
+  // anything other than the variant of OkHttp that it comes with. It does still return values from
+  // get() and it is not expected to implement any cache-control logic.
+  @Test public void testHttpResponseCacheBackwardsCompatible() throws Exception {
+    assertSame(cache, ResponseCache.getDefault());
+    assertEquals(0, cache.getRequestCount());
+
+    String body = "Body";
+    server.enqueue(new MockResponse()
+        .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setBody(body));
+    server.play();
+
+    URL url = server.getUrl("/");
+
+    // Here we use a HttpURLConnection from URL to represent a non-OkHttp HttpURLConnection. In
+    // Android this would be com.android.okhttp.internal.http.HttpURLConnectionImpl. In tests this
+    // is some other implementation.
+    HttpURLConnection javaConnection = (HttpURLConnection) url.openConnection();
+    assertFalse("This test relies on url.openConnection() not returning an OkHttp connection",
+        javaConnection instanceof HttpURLConnectionImpl);
+    javaConnection.disconnect();
+
+    // This should simply be discarded. It doesn't matter the connection is not useful.
+    cache.put(url.toURI(), javaConnection);
+
+    // Confirm the initial cache state.
+    assertNull(cache.get(url.toURI(), "GET", new HashMap<String, List<String>>()));
+
+    // Now cache a response
+    HttpURLConnection okHttpConnection = openConnection(url);
+    assertEquals(body, readAscii(okHttpConnection));
+    okHttpConnection.disconnect();
+
+    assertEquals(1, server.getRequestCount());
+    assertEquals(0, cache.getHitCount());
+
+    // OkHttp should now find the result cached.
+    HttpURLConnection okHttpConnection2 = openConnection(url);
+    assertEquals(body, readAscii(okHttpConnection2));
+    okHttpConnection2.disconnect();
+
+    assertEquals(1, server.getRequestCount());
+    assertEquals(1, cache.getHitCount());
+
+    // Confirm the unfortunate get() behavior.
+    assertNotNull(cache.get(url.toURI(), "GET", new HashMap<String, List<String>>()));
+    // Only OkHttp makes the necessary callbacks to increment the cache stats.
+    assertEquals(1, cache.getHitCount());
+  }
+
   private void writeFile(File directory, String file, String content) throws IOException {
     OutputStream out = new FileOutputStream(new File(directory, file));
     out.write(content.getBytes(Util.UTF_8));
@@ -1911,7 +2027,8 @@
   private String readAscii(URLConnection connection, int count) throws IOException {
     HttpURLConnection httpConnection = (HttpURLConnection) connection;
     InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST
-        ? connection.getInputStream() : httpConnection.getErrorStream();
+        ? connection.getInputStream()
+        : httpConnection.getErrorStream();
     StringBuilder result = new StringBuilder();
     for (int i = 0; i < count; i++) {
       int value = in.read();
@@ -1942,6 +2059,8 @@
     }
     assertEquals(504, connection.getResponseCode());
     assertEquals(-1, connection.getErrorStream().read());
+    assertEquals(ResponseSource.NONE + " 504",
+        connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
   }
 
   enum TransferKind {
@@ -1989,25 +2108,26 @@
     return bytesOut.toByteArray();
   }
 
-  private class InsecureResponseCache extends ResponseCache {
-    @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
-      return cache.put(uri, connection);
+  static abstract class AbstractOkResponseCache implements OkResponseCache {
+    @Override public Response get(Request request) throws IOException {
+      return null;
     }
 
-    @Override public CacheResponse get(URI uri, String requestMethod,
-        Map<String, List<String>> requestHeaders) throws IOException {
-      final CacheResponse response = cache.get(uri, requestMethod, requestHeaders);
-      if (response instanceof SecureCacheResponse) {
-        return new CacheResponse() {
-          @Override public InputStream getBody() throws IOException {
-            return response.getBody();
-          }
-          @Override public Map<String, List<String>> getHeaders() throws IOException {
-            return response.getHeaders();
-          }
-        };
-      }
-      return response;
+    @Override public CacheRequest put(Response response) throws IOException {
+      return null;
+    }
+
+    @Override public boolean maybeRemove(Request request) throws IOException {
+      return false;
+    }
+
+    @Override public void update(Response cached, Response network) throws IOException {
+    }
+
+    @Override public void trackConditionalCacheHit() {
+    }
+
+    @Override public void trackResponse(ResponseSource source) {
     }
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java
new file mode 100644
index 0000000..8a6c536
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java
@@ -0,0 +1,805 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+
+import static org.junit.Assert.assertArrayEquals;
+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;
+
+/**
+ * Tests for {@link JavaApiConverter}.
+ */
+public class JavaApiConverterTest {
+
+  // $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \
+  //     -newkey rsa:512 -out cert.pem
+  private static final X509Certificate LOCAL_CERT = certificate(""
+      + "-----BEGIN CERTIFICATE-----\n"
+      + "MIIBWDCCAQKgAwIBAgIJANS1EtICX2AZMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV\n"
+      + "BAMTCWxvY2FsaG9zdDAgFw0xMjAxMDIxOTA4NThaGA8yMTExMTIwOTE5MDg1OFow\n"
+      + "FDESMBAGA1UEAxMJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPpt\n"
+      + "atK8r4/hf4hSIs0os/BSlQLbRBaK9AfBReM4QdAklcQqe6CHsStKfI8pp0zs7Ptg\n"
+      + "PmMdpbttL0O7mUboBC8CAwEAAaM1MDMwMQYDVR0RBCowKIIVbG9jYWxob3N0Lmxv\n"
+      + "Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEFBQADQQD0ntfL\n"
+      + "DCzOCv9Ma6Lv5o5jcYWVxvBSTsnt22hsJpWD1K7iY9lbkLwl0ivn73pG2evsAn9G\n"
+      + "X8YKH52fnHsCrhSD\n"
+      + "-----END CERTIFICATE-----");
+
+  // openssl req -x509 -nodes -days 36500 -subj '/CN=*.0.0.1' -newkey rsa:512 -out cert.pem
+  private static final X509Certificate SERVER_CERT = certificate(""
+      + "-----BEGIN CERTIFICATE-----\n"
+      + "MIIBkjCCATygAwIBAgIJAMdemqOwd/BEMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNV\n"
+      + "BAMUByouMC4wLjEwIBcNMTAxMjIwMTY0NDI1WhgPMjExMDExMjYxNjQ0MjVaMBIx\n"
+      + "EDAOBgNVBAMUByouMC4wLjEwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAqY8c9Qrt\n"
+      + "YPWCvb7lclI+aDHM6fgbJcHsS9Zg8nUOh5dWrS7AgeA25wyaokFl4plBbbHQe2j+\n"
+      + "cCjsRiJIcQo9HwIDAQABo3MwcTAdBgNVHQ4EFgQUJ436TZPJvwCBKklZZqIvt1Yt\n"
+      + "JjEwQgYDVR0jBDswOYAUJ436TZPJvwCBKklZZqIvt1YtJjGhFqQUMBIxEDAOBgNV\n"
+      + "BAMUByouMC4wLjGCCQDHXpqjsHfwRDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB\n"
+      + "BQUAA0EAk9i88xdjWoewqvE+iMC9tD2obMchgFDaHH0ogxxiRaIKeEly3g0uGxIt\n"
+      + "fl2WRY8hb4x+zRrwsFaLEpdEvqcjOQ==\n"
+      + "-----END CERTIFICATE-----");
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
+    public boolean verify(String hostname, SSLSession session) {
+      return true;
+    }
+  };
+
+  private MockWebServer server;
+
+  private OkHttpClient client;
+
+  private HttpURLConnection connection;
+
+  @Before public void setUp() throws Exception {
+    server = new MockWebServer();
+    client = new OkHttpClient();
+  }
+
+  @After public void tearDown() throws Exception {
+    if (connection != null) {
+      connection.disconnect();
+    }
+    server.shutdown();
+  }
+
+  @Test public void createOkResponse_fromOkHttpUrlConnection() throws Exception {
+    testCreateOkResponseInternal(new OkHttpURLConnectionFactory(client), false /* isSecure */);
+  }
+
+  @Test public void createOkResponse_fromJavaHttpUrlConnection() throws Exception {
+    testCreateOkResponseInternal(new JavaHttpURLConnectionFactory(), false /* isSecure */);
+  }
+
+  @Test public void createOkResponse_fromOkHttpsUrlConnection() throws Exception {
+    testCreateOkResponseInternal(new OkHttpURLConnectionFactory(client), true /* isSecure */);
+  }
+
+  @Test public void createOkResponse_fromJavaHttpsUrlConnection() throws Exception {
+    testCreateOkResponseInternal(new JavaHttpURLConnectionFactory(), true /* isSecure */);
+  }
+
+  private void testCreateOkResponseInternal(HttpURLConnectionFactory httpUrlConnectionFactory,
+      boolean isSecure) throws Exception {
+    String statusLine = "HTTP/1.1 200 Fantastic";
+    String body = "Nothing happens";
+    final URL serverUrl;
+    MockResponse mockResponse = new MockResponse()
+        .setStatus(statusLine)
+        .addHeader("xyzzy", "baz")
+        .setBody(body);
+    if (isSecure) {
+      serverUrl = configureHttpsServer(
+          mockResponse);
+
+      assertEquals("https", serverUrl.getProtocol());
+    } else {
+      serverUrl = configureServer(
+          mockResponse);
+      assertEquals("http", serverUrl.getProtocol());
+    }
+
+    connection = httpUrlConnectionFactory.open(serverUrl);
+    if (isSecure) {
+      HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) connection;
+      httpsUrlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
+      httpsUrlConnection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    }
+    connection.setRequestProperty("snake", "bird");
+    connection.connect();
+    Response response = JavaApiConverter.createOkResponse(serverUrl.toURI(), connection);
+
+    // Check the response.request()
+    Request request = response.request();
+    assertEquals(isSecure, request.isHttps());
+    assertEquals(serverUrl.toURI(), request.uri());
+    assertNull(request.body());
+    Headers okRequestHeaders = request.headers();
+    // In Java the request headers are unavailable for a connected HttpURLConnection.
+    assertEquals(0, okRequestHeaders.size());
+    assertEquals("GET", request.method());
+
+    // Check the response
+    assertEquals(statusLine, response.statusLine());
+    Headers okResponseHeaders = response.headers();
+    assertEquals("baz", okResponseHeaders.get("xyzzy"));
+    assertEquals(body, response.body().string());
+    if (isSecure) {
+      Handshake handshake = response.handshake();
+      assertNotNull(handshake);
+      HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection;
+      assertNotNullAndEquals(httpsURLConnection.getCipherSuite(), handshake.cipherSuite());
+      assertEquals(httpsURLConnection.getLocalPrincipal(), handshake.localPrincipal());
+      assertNotNullAndEquals(httpsURLConnection.getPeerPrincipal(), handshake.peerPrincipal());
+      assertNotNull(httpsURLConnection.getServerCertificates());
+      assertEquals(Arrays.asList(httpsURLConnection.getServerCertificates()),
+          handshake.peerCertificates());
+      assertNull(httpsURLConnection.getLocalCertificates());
+    } else {
+      assertNull(response.handshake());
+    }
+  }
+
+  @Test public void createOkResponse_fromCacheResponse() throws Exception {
+    final String statusLine = "HTTP/1.1 200 Fantastic";
+    URI uri = new URI("http://foo/bar");
+    Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+    CacheResponse cacheResponse = new CacheResponse() {
+      @Override
+      public Map<String, List<String>> getHeaders() throws IOException {
+        Map<String, List<String>> headers = new HashMap<String, List<String>>();
+        headers.put(null, Collections.singletonList(statusLine));
+        headers.put("xyzzy", Arrays.asList("bar", "baz"));
+        return headers;
+      }
+
+      @Override
+      public InputStream getBody() throws IOException {
+        return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8));
+      }
+    };
+
+    Response response = JavaApiConverter.createOkResponse(request, cacheResponse);
+    assertSame(request, response.request());
+
+    assertNotNullAndEquals(statusLine, response.statusLine());
+    Headers okResponseHeaders = response.headers();
+    assertEquals("baz", okResponseHeaders.get("xyzzy"));
+    assertEquals("HelloWorld", response.body().string());
+    assertNull(response.handshake());
+  }
+
+  @Test public void createOkResponse_fromSecureCacheResponse() throws Exception {
+    final String statusLine = "HTTP/1.1 200 Fantastic";
+    final Principal localPrincipal = LOCAL_CERT.getSubjectX500Principal();
+    final List<Certificate> localCertificates = Arrays.<Certificate>asList(LOCAL_CERT);
+    final Principal serverPrincipal = SERVER_CERT.getSubjectX500Principal();
+    final List<Certificate> serverCertificates = Arrays.<Certificate>asList(SERVER_CERT);
+    URI uri = new URI("https://foo/bar");
+    Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+    SecureCacheResponse cacheResponse = new SecureCacheResponse() {
+      @Override
+      public Map<String, List<String>> getHeaders() throws IOException {
+        Map<String, List<String>> headers = new HashMap<String, List<String>>();
+        headers.put(null, Collections.singletonList(statusLine));
+        headers.put("xyzzy", Arrays.asList("bar", "baz"));
+        return headers;
+      }
+
+      @Override
+      public InputStream getBody() throws IOException {
+        return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8));
+      }
+
+      @Override
+      public String getCipherSuite() {
+        return "SuperSecure";
+      }
+
+      @Override
+      public List<Certificate> getLocalCertificateChain() {
+        return localCertificates;
+      }
+
+      @Override
+      public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
+        return serverCertificates;
+      }
+
+      @Override
+      public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        return serverPrincipal;
+      }
+
+      @Override
+      public Principal getLocalPrincipal() {
+        return localPrincipal;
+      }
+    };
+
+    Response response = JavaApiConverter.createOkResponse(request, cacheResponse);
+    assertSame(request, response.request());
+
+    assertNotNullAndEquals(statusLine, response.statusLine());
+    Headers okResponseHeaders = response.headers();
+    assertEquals("baz", okResponseHeaders.get("xyzzy"));
+    assertEquals("HelloWorld", response.body().string());
+
+    Handshake handshake = response.handshake();
+    assertNotNull(handshake);
+    assertNotNullAndEquals("SuperSecure", handshake.cipherSuite());
+    assertEquals(localPrincipal, handshake.localPrincipal());
+    assertEquals(serverPrincipal, handshake.peerPrincipal());
+    assertEquals(serverCertificates, handshake.peerCertificates());
+    assertEquals(localCertificates, handshake.localCertificates());
+  }
+
+  @Test public void createOkRequest_nullRequestHeaders() throws Exception {
+    URI uri = new URI("http://foo/bar");
+
+    Map<String,List<String>> javaRequestHeaders = null;
+    Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
+    assertFalse(request.isHttps());
+    assertEquals(uri, request.uri());
+    assertNull(request.body());
+    Headers okRequestHeaders = request.headers();
+    assertEquals(0, okRequestHeaders.size());
+    assertEquals("POST", request.method());
+  }
+
+  @Test public void createOkRequest_nonNullRequestHeaders() throws Exception {
+    URI uri = new URI("https://foo/bar");
+
+    Map<String,List<String>> javaRequestHeaders = new HashMap<String, List<String>>();
+    javaRequestHeaders.put("Foo", Arrays.asList("Bar"));
+    Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
+    assertTrue(request.isHttps());
+    assertEquals(uri, request.uri());
+    assertNull(request.body());
+    Headers okRequestHeaders = request.headers();
+    assertEquals(1, okRequestHeaders.size());
+    assertEquals("Bar", okRequestHeaders.get("Foo"));
+    assertEquals("POST", request.method());
+  }
+
+  // Older versions of OkHttp would store the "request line" as a header with a
+  // null key. To support the Android usecase where an old version of OkHttp uses
+  // a newer, Android-bundled, version of HttpResponseCache the null key must be
+  // explicitly ignored.
+  @Test public void createOkRequest_nullRequestHeaderKey() throws Exception {
+    URI uri = new URI("https://foo/bar");
+
+    Map<String,List<String>> javaRequestHeaders = new HashMap<String, List<String>>();
+    javaRequestHeaders.put(null, Arrays.asList("GET / HTTP 1.1"));
+    javaRequestHeaders.put("Foo", Arrays.asList("Bar"));
+    Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
+    assertTrue(request.isHttps());
+    assertEquals(uri, request.uri());
+    assertNull(request.body());
+    Headers okRequestHeaders = request.headers();
+    assertEquals(1, okRequestHeaders.size());
+    assertEquals("Bar", okRequestHeaders.get("Foo"));
+    assertEquals("POST", request.method());
+  }
+
+  @Test public void createJavaUrlConnection_requestChangesForbidden() throws Exception {
+    Response okResponse = createArbitraryOkResponse();
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+    // Check an arbitrary (not complete) set of methods that can be used to modify the
+    // request.
+    try {
+      httpUrlConnection.setRequestProperty("key", "value");
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.setFixedLengthStreamingMode(1234);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.setRequestMethod("PUT");
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.getHeaderFields().put("key", Collections.singletonList("value"));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.getOutputStream();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test public void createJavaUrlConnection_connectionChangesForbidden() throws Exception {
+    Response okResponse = createArbitraryOkResponse();
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+    try {
+      httpUrlConnection.connect();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.disconnect();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test public void createJavaUrlConnection_responseChangesForbidden() throws Exception {
+    Response okResponse = createArbitraryOkResponse();
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+    // Check an arbitrary (not complete) set of methods that can be used to access the response
+    // body.
+    try {
+      httpUrlConnection.getInputStream();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.getContent();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.setFixedLengthStreamingMode(1234);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.setRequestMethod("PUT");
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpUrlConnection.getHeaderFields().put("key", Collections.singletonList("value"));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test public void createJavaUrlConnection_responseHeadersOk() throws Exception {
+    final String statusLine = "HTTP/1.1 200 Fantastic";
+    Response.Body responseBody =
+        createResponseBody("text/plain", "BodyText".getBytes(StandardCharsets.UTF_8));
+    Response okResponse = new Response.Builder()
+        .request(createArbitraryOkRequest())
+        .statusLine(statusLine)
+        .addHeader("A", "c")
+        .addHeader("B", "d")
+        .addHeader("A", "e")
+        .addHeader("Content-Length", Long.toString(responseBody.contentLength()))
+        .body(responseBody)
+        .build();
+
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+    assertEquals(200, httpUrlConnection.getResponseCode());
+    assertEquals("Fantastic", httpUrlConnection.getResponseMessage());
+    assertEquals(responseBody.contentLength(), httpUrlConnection.getContentLength());
+
+    // Check retrieval by string key.
+    assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
+    assertEquals("e", httpUrlConnection.getHeaderField("A"));
+    // The RI and OkHttp supports case-insensitive matching for this method.
+    assertEquals("e", httpUrlConnection.getHeaderField("a"));
+
+    // Check retrieval using a Map.
+    Map<String, List<String>> responseHeaders = httpUrlConnection.getHeaderFields();
+    assertEquals(Arrays.asList(statusLine), responseHeaders.get(null));
+    assertEquals(newSet("c", "e"), newSet(responseHeaders.get("A")));
+    // OkHttp supports case-insensitive matching here. The RI does not.
+    assertEquals(newSet("c", "e"), newSet(responseHeaders.get("a")));
+
+    // Check the Map iterator contains the expected mappings.
+    assertHeadersContainsMapping(responseHeaders, null, statusLine);
+    assertHeadersContainsMapping(responseHeaders, "A", "c", "e");
+    assertHeadersContainsMapping(responseHeaders, "B", "d");
+
+    // Check immutability of the headers Map.
+    try {
+      responseHeaders.put("N", Arrays.asList("o"));
+      fail("Modified an unmodifiable view.");
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      responseHeaders.get("A").add("f");
+      fail("Modified an unmodifiable view.");
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    // Check retrieval of headers by index.
+    assertEquals(null, httpUrlConnection.getHeaderFieldKey(0));
+    assertEquals(statusLine, httpUrlConnection.getHeaderField(0));
+    // After header zero there may be additional entries provided at the beginning or end by the
+    // implementation. It's probably important that the relative ordering of the headers is
+    // preserved, particularly if there are multiple value for the same key.
+    int i = 1;
+    while (!httpUrlConnection.getHeaderFieldKey(i).equals("A")) {
+      i++;
+    }
+    // Check the ordering of the headers set by app code.
+    assertResponseHeaderAtIndex(httpUrlConnection, i++, "A", "c");
+    assertResponseHeaderAtIndex(httpUrlConnection, i++, "B", "d");
+    assertResponseHeaderAtIndex(httpUrlConnection, i++, "A", "e");
+    // There may be some additional headers provided by the implementation.
+    while (httpUrlConnection.getHeaderField(i) != null) {
+      assertNotNull(httpUrlConnection.getHeaderFieldKey(i));
+      i++;
+    }
+    // Confirm the correct behavior when the index is out-of-range.
+    assertNull(httpUrlConnection.getHeaderFieldKey(i));
+  }
+
+  private static void assertResponseHeaderAtIndex(HttpURLConnection httpUrlConnection,
+      int headerIndex, String expectedKey, String expectedValue) {
+    assertEquals(expectedKey, httpUrlConnection.getHeaderFieldKey(headerIndex));
+    assertEquals(expectedValue, httpUrlConnection.getHeaderField(headerIndex));
+  }
+
+  private void assertHeadersContainsMapping(Map<String, List<String>> headers, String expectedKey,
+      String... expectedValues) {
+    assertTrue(headers.containsKey(expectedKey));
+    assertEquals(newSet(expectedValues), newSet(headers.get(expectedKey)));
+  }
+
+  @Test public void createJavaUrlConnection_accessibleRequestInfo_GET() throws Exception {
+    Request okRequest = createArbitraryOkRequest().newBuilder()
+        .method("GET", null)
+        .build();
+    Response okResponse = createArbitraryOkResponse(okRequest);
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+
+    assertEquals("GET", httpUrlConnection.getRequestMethod());
+    assertTrue(httpUrlConnection.getDoInput());
+    assertFalse(httpUrlConnection.getDoOutput());
+  }
+
+  @Test public void createJavaUrlConnection_accessibleRequestInfo_POST() throws Exception {
+    Request okRequest = createArbitraryOkRequest().newBuilder()
+        .method("POST", createRequestBody("PostBody"))
+        .build();
+    Response okResponse = createArbitraryOkResponse(okRequest);
+    HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
+
+    assertEquals("POST", httpUrlConnection.getRequestMethod());
+    assertTrue(httpUrlConnection.getDoInput());
+    assertTrue(httpUrlConnection.getDoOutput());
+  }
+
+  @Test public void createJavaUrlConnection_https_extraHttpsMethods() throws Exception {
+    Request okRequest = createArbitraryOkRequest().newBuilder()
+        .method("GET", null)
+        .url("https://secure/request")
+        .build();
+    Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
+        Arrays.<Certificate>asList(LOCAL_CERT));
+    Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
+        .handshake(handshake)
+        .build();
+    HttpsURLConnection httpsUrlConnection =
+        (HttpsURLConnection) JavaApiConverter.createJavaUrlConnection(okResponse);
+
+    assertEquals("SecureCipher", httpsUrlConnection.getCipherSuite());
+    assertEquals(SERVER_CERT.getSubjectX500Principal(), httpsUrlConnection.getPeerPrincipal());
+    assertArrayEquals(new Certificate[] { LOCAL_CERT }, httpsUrlConnection.getLocalCertificates());
+    assertArrayEquals(new Certificate[] { SERVER_CERT },
+        httpsUrlConnection.getServerCertificates());
+    assertEquals(LOCAL_CERT.getSubjectX500Principal(), httpsUrlConnection.getLocalPrincipal());
+  }
+
+  @Test public void createJavaUrlConnection_https_forbiddenFields() throws Exception {
+    Request okRequest = createArbitraryOkRequest().newBuilder()
+        .url("https://secure/request")
+        .build();
+    Response okResponse = createArbitraryOkResponse(okRequest);
+    HttpsURLConnection httpsUrlConnection =
+        (HttpsURLConnection) JavaApiConverter.createJavaUrlConnection(okResponse);
+
+    try {
+      httpsUrlConnection.getHostnameVerifier();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+    try {
+      httpsUrlConnection.getSSLSocketFactory();
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test public void createJavaCacheResponse_httpGet() throws Exception {
+    Request okRequest =
+        createArbitraryOkRequest().newBuilder()
+            .url("http://insecure/request")
+            .method("GET", null)
+            .build();
+    String statusLine = "HTTP/1.1 200 Fantastic";
+    Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
+        .statusLine(statusLine)
+        .addHeader("key1", "value1_1")
+        .addHeader("key2", "value2")
+        .addHeader("key1", "value1_2")
+        .body(null)
+        .build();
+    CacheResponse javaCacheResponse = JavaApiConverter.createJavaCacheResponse(okResponse);
+    assertFalse(javaCacheResponse instanceof SecureCacheResponse);
+    Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
+    assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
+    assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
+    assertNull(javaCacheResponse.getBody());
+  }
+
+  @Test public void createJavaCacheResponse_httpPost() throws Exception {
+    Request okRequest =
+        createArbitraryOkRequest().newBuilder()
+            .url("http://insecure/request")
+            .method("POST", createRequestBody("RequestBody") )
+            .build();
+    String statusLine = "HTTP/1.1 200 Fantastic";
+    Response.Body responseBody =
+        createResponseBody("text/plain", "ResponseBody".getBytes(StandardCharsets.UTF_8));
+    Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
+        .statusLine(statusLine)
+        .addHeader("key1", "value1_1")
+        .addHeader("key2", "value2")
+        .addHeader("key1", "value1_2")
+        .body(responseBody)
+        .build();
+    CacheResponse javaCacheResponse = JavaApiConverter.createJavaCacheResponse(okResponse);
+    assertFalse(javaCacheResponse instanceof SecureCacheResponse);
+    Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
+    assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
+    assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
+    assertArrayEquals(responseBody.bytes(), readAll(javaCacheResponse.getBody()));
+  }
+
+  @Test public void createJavaCacheResponse_httpsPost() throws Exception {
+    Request okRequest =
+        createArbitraryOkRequest().newBuilder()
+            .url("https://secure/request")
+            .method("POST", createRequestBody("RequestBody") )
+            .build();
+    String statusLine = "HTTP/1.1 200 Fantastic";
+    Response.Body responseBody =
+        createResponseBody("text/plain", "ResponseBody".getBytes(StandardCharsets.UTF_8));
+    Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
+        Arrays.<Certificate>asList(LOCAL_CERT));
+    Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
+        .statusLine(statusLine)
+        .addHeader("key1", "value1_1")
+        .addHeader("key2", "value2")
+        .addHeader("key1", "value1_2")
+        .body(responseBody)
+        .handshake(handshake)
+        .build();
+    SecureCacheResponse javaCacheResponse =
+        (SecureCacheResponse) JavaApiConverter.createJavaCacheResponse(okResponse);
+    Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
+    assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
+    assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
+    assertArrayEquals(responseBody.bytes(), readAll(javaCacheResponse.getBody()));
+    assertEquals(handshake.cipherSuite(), javaCacheResponse.getCipherSuite());
+    assertEquals(handshake.localCertificates(), javaCacheResponse.getLocalCertificateChain());
+    assertEquals(handshake.peerCertificates(), javaCacheResponse.getServerCertificateChain());
+    assertEquals(handshake.localPrincipal(), javaCacheResponse.getLocalPrincipal());
+    assertEquals(handshake.peerPrincipal(), javaCacheResponse.getPeerPrincipal());
+  }
+
+  @Test public void extractJavaHeaders() throws Exception {
+    Request okRequest = createArbitraryOkRequest().newBuilder()
+        .addHeader("key1", "value1_1")
+        .addHeader("key2", "value2")
+        .addHeader("key1", "value1_2")
+        .build();
+    Map<String, List<String>> javaHeaders = JavaApiConverter.extractJavaHeaders(okRequest);
+
+    assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
+    assertEquals(Arrays.asList("value2"), javaHeaders.get("key2"));
+  }
+
+  @Test public void extractOkHeaders() {
+    Map<String, List<String>> javaResponseHeaders = new HashMap<String, List<String>>();
+    javaResponseHeaders.put(null, Arrays.asList("StatusLine"));
+    javaResponseHeaders.put("key1", Arrays.asList("value1_1", "value1_2"));
+    javaResponseHeaders.put("key2", Arrays.asList("value2"));
+
+    Headers okHeaders = JavaApiConverter.extractOkHeaders(javaResponseHeaders);
+    assertEquals(3, okHeaders.size()); // null entry should be stripped out
+    assertEquals(Arrays.asList("value1_1", "value1_2"), okHeaders.values("key1"));
+    assertEquals(Arrays.asList("value2"), okHeaders.values("key2"));
+  }
+
+  @Test public void extractStatusLine() {
+    Map<String, List<String>> javaResponseHeaders = new HashMap<String, List<String>>();
+    javaResponseHeaders.put(null, Arrays.asList("StatusLine"));
+    javaResponseHeaders.put("key1", Arrays.asList("value1_1", "value1_2"));
+    javaResponseHeaders.put("key2", Arrays.asList("value2"));
+    assertEquals("StatusLine", JavaApiConverter.extractStatusLine(javaResponseHeaders));
+
+    assertNull(JavaApiConverter.extractStatusLine(Collections.<String, List<String>>emptyMap()));
+  }
+
+  private URL configureServer(MockResponse mockResponse) throws Exception {
+    server.enqueue(mockResponse);
+    server.play();
+    return server.getUrl("/");
+  }
+
+  private URL configureHttpsServer(MockResponse mockResponse) throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false /* tunnelProxy */);
+    server.enqueue(mockResponse);
+    server.play();
+    return server.getUrl("/");
+  }
+
+  private static <T> void assertNotNullAndEquals(T expected, T actual) {
+    assertNotNull(actual);
+    assertEquals(expected, actual);
+  }
+
+  private interface HttpURLConnectionFactory {
+    public HttpURLConnection open(URL serverUrl) throws IOException;
+  }
+
+  private static class OkHttpURLConnectionFactory implements HttpURLConnectionFactory {
+    protected final OkHttpClient client;
+
+    private OkHttpURLConnectionFactory(OkHttpClient client) {
+      this.client = client;
+    }
+
+    @Override
+    public HttpURLConnection open(URL serverUrl) {
+      return client.open(serverUrl);
+    }
+  }
+
+  private static class JavaHttpURLConnectionFactory implements HttpURLConnectionFactory {
+    @Override
+    public HttpURLConnection open(URL serverUrl) throws IOException {
+      return (HttpURLConnection) serverUrl.openConnection();
+    }
+  }
+
+  private static X509Certificate certificate(String certificate) {
+    try {
+      return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(
+          new ByteArrayInputStream(certificate.getBytes(Util.UTF_8)));
+    } catch (CertificateException e) {
+      fail();
+      return null;
+    }
+  }
+
+  private static <T> Set<T> newSet(T... elements) {
+    return newSet(Arrays.asList(elements));
+  }
+
+  private static <T> Set<T> newSet(List<T> elements) {
+    return new LinkedHashSet<T>(elements);
+  }
+
+  private static Request createArbitraryOkRequest() {
+    return new Request.Builder()
+        .url("http://arbitrary/url")
+        .method("GET", null)
+        .build();
+  }
+
+  private static Response createArbitraryOkResponse(Request request) {
+    return new Response.Builder()
+        .request(request)
+        .statusLine("HTTP/1.1 200 Arbitrary")
+        .build();
+  }
+
+  private static Response createArbitraryOkResponse() {
+    return createArbitraryOkResponse(createArbitraryOkRequest());
+  }
+
+  private static Request.Body createRequestBody(String bodyText) {
+    return Request.Body.create(MediaType.parse("text/plain"), bodyText);
+  }
+
+  private static Response.Body createResponseBody(final String contentType, final byte[] bytes) {
+    return new Response.Body() {
+
+      @Override
+      public boolean ready() throws IOException {
+        return true;
+      }
+
+      @Override
+      public MediaType contentType() {
+        return MediaType.parse(contentType);
+      }
+
+      @Override
+      public long contentLength() {
+        return bytes.length;
+      }
+
+      @Override
+      public InputStream byteStream() {
+        return new ByteArrayInputStream(bytes);
+      }
+    };
+  }
+
+  private byte[] readAll(InputStream in) throws IOException {
+    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+    int value;
+    while ((value = in.read()) != -1) {
+      buffer.write(value);
+    }
+    in.close();
+    return buffer.toByteArray();
+  }
+
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java
new file mode 100644
index 0000000..8d5e152
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+
+import static org.junit.Assert.assertArrayEquals;
+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.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * A white-box test for {@link ResponseCacheAdapter}. See also:
+ * <ul>
+ *   <li>{@link ResponseCacheTest} for black-box tests that check that {@link ResponseCache}
+ *   classes are called correctly by OkHttp.</li>
+ *   <li>{@link JavaApiConverterTest} for tests that check Java API classes / OkHttp conversion
+ *   logic. </li>
+ * </ul>
+ */
+public class ResponseCacheAdapterTest {
+
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
+    public boolean verify(String hostname, SSLSession session) {
+      return true;
+    }
+  };
+
+  private MockWebServer server;
+
+  private OkHttpClient client;
+
+  private HttpURLConnection connection;
+
+  @Before public void setUp() throws Exception {
+    server = new MockWebServer();
+    client = new OkHttpClient();
+  }
+
+  @After public void tearDown() throws Exception {
+    if (connection != null) {
+      connection.disconnect();
+    }
+    server.shutdown();
+  }
+
+  @Test public void get_httpGet() throws Exception {
+    final URL serverUrl = configureServer(new MockResponse());
+    assertEquals("http", serverUrl.getProtocol());
+
+    ResponseCache responseCache = new NoOpResponseCache() {
+      @Override
+      public CacheResponse get(URI uri, String method, Map<String, List<String>> headers) throws IOException {
+        assertEquals(toUri(serverUrl), uri);
+        assertEquals("GET", method);
+        assertTrue("Arbitrary standard header not present", headers.containsKey("User-Agent"));
+        assertEquals(Collections.singletonList("value1"), headers.get("key1"));
+        return null;
+      }
+    };
+    client.setResponseCache(responseCache);
+
+    connection = client.open(serverUrl);
+    connection.setRequestProperty("key1", "value1");
+
+    executeGet(connection);
+  }
+
+  @Test public void get_httpsGet() throws Exception {
+    final URL serverUrl = configureHttpsServer(new MockResponse());
+    assertEquals("https", serverUrl.getProtocol());
+
+    ResponseCache responseCache = new NoOpResponseCache() {
+      @Override
+      public CacheResponse get(URI uri, String method, Map<String, List<String>> headers)
+          throws IOException {
+        assertEquals("https", uri.getScheme());
+        assertEquals(toUri(serverUrl), uri);
+        assertEquals("GET", method);
+        assertTrue("Arbitrary standard header not present", headers.containsKey("User-Agent"));
+        assertEquals(Collections.singletonList("value1"), headers.get("key1"));
+        return null;
+      }
+    };
+    client.setResponseCache(responseCache);
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+    connection = client.open(serverUrl);
+    connection.setRequestProperty("key1", "value1");
+
+    executeGet(connection);
+  }
+
+  @Test public void put_httpGet() throws Exception {
+    final String statusLine = "HTTP/1.1 200 Fantastic";
+    final URL serverUrl = configureServer(
+        new MockResponse()
+            .setStatus(statusLine)
+            .addHeader("A", "c"));
+
+    ResponseCache responseCache = new NoOpResponseCache() {
+      @Override
+      public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+        assertTrue(urlConnection instanceof HttpURLConnection);
+        assertFalse(urlConnection instanceof HttpsURLConnection);
+
+        assertEquals(0, urlConnection.getContentLength());
+
+        HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+        assertEquals("GET", httpUrlConnection.getRequestMethod());
+        assertTrue(httpUrlConnection.getDoInput());
+        assertFalse(httpUrlConnection.getDoOutput());
+
+        assertEquals("Fantastic", httpUrlConnection.getResponseMessage());
+        assertEquals(toUri(serverUrl), uri);
+        assertEquals(serverUrl, urlConnection.getURL());
+        assertEquals("value", urlConnection.getRequestProperty("key"));
+
+        // Check retrieval by string key.
+        assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
+        assertEquals("c", httpUrlConnection.getHeaderField("A"));
+        // The RI and OkHttp supports case-insensitive matching for this method.
+        assertEquals("c", httpUrlConnection.getHeaderField("a"));
+        return null;
+      }
+    };
+    client.setResponseCache(responseCache);
+
+    connection = client.open(serverUrl);
+    connection.setRequestProperty("key", "value");
+    executeGet(connection);
+  }
+
+  @Test public void put_httpPost() throws Exception {
+    final String statusLine = "HTTP/1.1 200 Fantastic";
+    final URL serverUrl = configureServer(
+        new MockResponse()
+            .setStatus(statusLine)
+            .addHeader("A", "c"));
+
+    ResponseCache responseCache = new NoOpResponseCache() {
+      @Override
+      public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+        assertTrue(urlConnection instanceof HttpURLConnection);
+        assertFalse(urlConnection instanceof HttpsURLConnection);
+
+        assertEquals(0, urlConnection.getContentLength());
+
+        HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+        assertEquals("POST", httpUrlConnection.getRequestMethod());
+        assertTrue(httpUrlConnection.getDoInput());
+        assertTrue(httpUrlConnection.getDoOutput());
+
+        assertEquals("Fantastic", httpUrlConnection.getResponseMessage());
+        assertEquals(toUri(serverUrl), uri);
+        assertEquals(serverUrl, urlConnection.getURL());
+        assertEquals("value", urlConnection.getRequestProperty("key"));
+
+        // Check retrieval by string key.
+        assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
+        assertEquals("c", httpUrlConnection.getHeaderField("A"));
+        // The RI and OkHttp supports case-insensitive matching for this method.
+        assertEquals("c", httpUrlConnection.getHeaderField("a"));
+        return null;
+      }
+    };
+    client.setResponseCache(responseCache);
+
+    connection = client.open(serverUrl);
+
+    executePost(connection);
+  }
+
+  @Test public void put_httpsGet() throws Exception {
+    final URL serverUrl = configureHttpsServer(new MockResponse());
+    assertEquals("https", serverUrl.getProtocol());
+
+    ResponseCache responseCache = new NoOpResponseCache() {
+      @Override
+      public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+        assertTrue(urlConnection instanceof HttpsURLConnection);
+        assertEquals(toUri(serverUrl), uri);
+        assertEquals(serverUrl, urlConnection.getURL());
+
+        HttpsURLConnection cacheHttpsUrlConnection = (HttpsURLConnection) urlConnection;
+        HttpsURLConnection realHttpsUrlConnection = (HttpsURLConnection) connection;
+        assertEquals(realHttpsUrlConnection.getCipherSuite(),
+            cacheHttpsUrlConnection.getCipherSuite());
+        assertEquals(realHttpsUrlConnection.getPeerPrincipal(),
+            cacheHttpsUrlConnection.getPeerPrincipal());
+        assertArrayEquals(realHttpsUrlConnection.getLocalCertificates(),
+            cacheHttpsUrlConnection.getLocalCertificates());
+        assertArrayEquals(realHttpsUrlConnection.getServerCertificates(),
+            cacheHttpsUrlConnection.getServerCertificates());
+        assertEquals(realHttpsUrlConnection.getLocalPrincipal(),
+            cacheHttpsUrlConnection.getLocalPrincipal());
+        return null;
+      }
+    };
+    client.setResponseCache(responseCache);
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+    connection = client.open(serverUrl);
+    executeGet(connection);
+  }
+
+  private void executeGet(HttpURLConnection connection) throws IOException {
+    connection.connect();
+    connection.getHeaderFields();
+    connection.disconnect();
+  }
+
+  private void executePost(HttpURLConnection connection) throws IOException {
+    connection.setDoOutput(true);
+    connection.connect();
+    connection.getOutputStream().write("Hello World".getBytes());
+    connection.disconnect();
+  }
+
+  private URL configureServer(MockResponse mockResponse) throws Exception {
+    server.enqueue(mockResponse);
+    server.play();
+    return server.getUrl("/");
+  }
+
+  private URL configureHttpsServer(MockResponse mockResponse) throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false /* tunnelProxy */);
+    server.enqueue(mockResponse);
+    server.play();
+    return server.getUrl("/");
+  }
+
+  private static class NoOpResponseCache extends ResponseCache {
+
+    @Override
+    public CacheResponse get(URI uri, String s, Map<String, List<String>> stringListMap)
+        throws IOException {
+      return null;
+    }
+
+    @Override
+    public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+      return null;
+    }
+  }
+
+  private static URI toUri(URL serverUrl) {
+    try {
+      return serverUrl.toURI();
+    } catch (URISyntaxException e) {
+      fail(e.getMessage());
+      return null;
+    }
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java
similarity index 68%
copy from okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
copy to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java
index 89f31da..1cbd654 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2014 Square, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,59 +16,51 @@
 
 package com.squareup.okhttp.internal.http;
 
-import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.ResponseSource;
 import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.net.CacheRequest;
 import java.net.CacheResponse;
-import java.net.CookieHandler;
 import java.net.CookieManager;
 import java.net.HttpCookie;
 import java.net.HttpURLConnection;
-import java.net.InetAddress;
 import java.net.ResponseCache;
 import java.net.SecureCacheResponse;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
-import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
 import java.security.Principal;
 import java.security.cert.Certificate;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
-import java.util.UUID;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.zip.GZIPOutputStream;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import org.junit.After;
 import org.junit.Before;
@@ -79,201 +71,62 @@
 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;
 
-/** Android's HttpResponseCacheTest. */
-public final class HttpResponseCacheTest {
+/**
+ * Tests for interaction between OkHttp and the ResponseCache. This test is based on
+ * {@link com.squareup.okhttp.internal.http.HttpResponseCacheTest}. Some tests for the
+ * {@link com.squareup.okhttp.OkResponseCache} found in HttpResponseCacheTest provide
+ * coverage for ResponseCache as well.
+ */
+public final class ResponseCacheTest {
   private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
     @Override public boolean verify(String s, SSLSession sslSession) {
       return true;
     }
   };
-  private final OkHttpClient client = new OkHttpClient();
-  private MockWebServer server = new MockWebServer();
-  private MockWebServer server2 = new MockWebServer();
-  private HttpResponseCache cache;
-  private final CookieManager cookieManager = new CookieManager();
 
-  private static final SSLContext sslContext;
-  static {
-    try {
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    } catch (GeneralSecurityException e) {
-      throw new RuntimeException(e);
-    } catch (UnknownHostException e) {
-      throw new RuntimeException(e);
-    }
-  }
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+
+  private OkHttpClient client;
+  private MockWebServer server;
+  private MockWebServer server2;
+  private ResponseCache cache;
 
   @Before public void setUp() throws Exception {
-    String tmp = System.getProperty("java.io.tmpdir");
-    File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
-    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
-    ResponseCache.setDefault(cache);
-    CookieHandler.setDefault(cookieManager);
+    server =  new MockWebServer();
     server.setNpnEnabled(false);
+    server2 =  new MockWebServer();
+
+    client = new OkHttpClient();
+    cache = new InMemoryResponseCache();
+    ResponseCache.setDefault(cache);
   }
 
   @After public void tearDown() throws Exception {
     server.shutdown();
     server2.shutdown();
-    ResponseCache.setDefault(null);
-    cache.delete();
-    CookieHandler.setDefault(null);
+    CookieManager.setDefault(null);
   }
 
   private HttpURLConnection openConnection(URL url) {
     return client.open(url);
   }
 
-  /**
-   * Test that response caching is consistent with the RI and the spec.
-   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
-   */
-  @Test public void responseCachingByResponseCode() throws Exception {
-    // Test each documented HTTP/1.1 code, plus the first unused value in each range.
-    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
-
-    // We can't test 100 because it's not really a response.
-    // assertCached(false, 100);
-    assertCached(false, 101);
-    assertCached(false, 102);
-    assertCached(true, 200);
-    assertCached(false, 201);
-    assertCached(false, 202);
-    assertCached(true, 203);
-    assertCached(false, 204);
-    assertCached(false, 205);
-    assertCached(false, 206); // we don't cache partial responses
-    assertCached(false, 207);
-    assertCached(true, 300);
-    assertCached(true, 301);
-    for (int i = 302; i <= 308; ++i) {
-      assertCached(false, i);
-    }
-    for (int i = 400; i <= 406; ++i) {
-      assertCached(false, i);
-    }
-    // (See test_responseCaching_407.)
-    assertCached(false, 408);
-    assertCached(false, 409);
-    // (See test_responseCaching_410.)
-    for (int i = 411; i <= 418; ++i) {
-      assertCached(false, i);
-    }
-    for (int i = 500; i <= 506; ++i) {
-      assertCached(false, i);
-    }
+  @Test public void responseCacheAccessWithOkHttpMember() throws IOException {
+    ResponseCache.setDefault(null);
+    client.setResponseCache(cache);
+    assertSame(cache, client.getResponseCache());
+    assertTrue(client.getOkResponseCache() instanceof ResponseCacheAdapter);
   }
 
-  /**
-   * Response code 407 should only come from proxy servers. Android's client
-   * throws if it is sent by an origin server.
-   */
-  @Test public void originServerSends407() throws Exception {
-    server.enqueue(new MockResponse().setResponseCode(407));
-    server.play();
-
-    URL url = server.getUrl("/");
-    HttpURLConnection conn = openConnection(url);
-    try {
-      conn.getResponseCode();
-      fail();
-    } catch (IOException expected) {
-    }
-  }
-
-  @Test public void responseCaching_410() throws Exception {
-    // the HTTP spec permits caching 410s, but the RI doesn't.
-    assertCached(true, 410);
-  }
-
-  private void assertCached(boolean shouldPut, int responseCode) throws Exception {
-    server = new MockWebServer();
-    MockResponse response =
-        new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-            .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-            .setResponseCode(responseCode)
-            .setBody("ABCDE")
-            .addHeader("WWW-Authenticate: challenge");
-    if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) {
-      response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"");
-    } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
-      response.addHeader("WWW-Authenticate: Basic realm=\"protected area\"");
-    }
-    server.enqueue(response);
-    server.play();
-
-    URL url = server.getUrl("/");
-    HttpURLConnection conn = openConnection(url);
-    assertEquals(responseCode, conn.getResponseCode());
-
-    // exhaust the content stream
-    readAscii(conn);
-
-    CacheResponse cached =
-        cache.get(url.toURI(), "GET", Collections.<String, List<String>>emptyMap());
-    if (shouldPut) {
-      assertNotNull(Integer.toString(responseCode), cached);
-      cached.getBody().close();
-    } else {
-      assertNull(Integer.toString(responseCode), cached);
-    }
-    server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
-  }
-
-  /**
-   * Test that we can interrogate the response when the cache is being
-   * populated. http://code.google.com/p/android/issues/detail?id=7787
-   */
-  @Test public void responseCacheCallbackApis() throws Exception {
-    final String body = "ABCDE";
-    final AtomicInteger cacheCount = new AtomicInteger();
-
-    server.enqueue(
-        new MockResponse().setStatus("HTTP/1.1 200 Fantastic").addHeader("fgh: ijk").setBody(body));
-    server.play();
-
-    ResponseCache.setDefault(new ResponseCache() {
-      @Override public CacheResponse get(URI uri, String requestMethod,
-          Map<String, List<String>> requestHeaders) throws IOException {
-        return null;
-      }
-
-      @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
-        HttpURLConnection httpConnection = (HttpURLConnection) conn;
-        try {
-          httpConnection.getRequestProperties();
-          fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-          httpConnection.addRequestProperty("K", "V");
-          fail();
-        } catch (IllegalStateException expected) {
-        }
-        assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null));
-        assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"),
-            httpConnection.getHeaderFields().get(null));
-        assertEquals(200, httpConnection.getResponseCode());
-        assertEquals("Fantastic", httpConnection.getResponseMessage());
-        assertEquals(body.length(), httpConnection.getContentLength());
-        assertEquals("ijk", httpConnection.getHeaderField("fgh"));
-        try {
-          httpConnection.getInputStream(); // the RI doesn't forbid this, but it should
-          fail();
-        } catch (IOException expected) {
-        }
-        cacheCount.incrementAndGet();
-        return null;
-      }
-    });
-
-    URL url = server.getUrl("/");
-    HttpURLConnection connection = openConnection(url);
-    assertEquals(body, readAscii(connection));
-    assertEquals(1, cacheCount.get());
+  @Test public void responseCacheAccessWithGlobalDefault() throws IOException {
+    ResponseCache.setDefault(cache);
+    client.setResponseCache(null);
+    assertNull(client.getOkResponseCache());
+    assertNull(client.getResponseCache());
   }
 
   @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
@@ -309,8 +162,6 @@
     assertEquals("spiders", readAscii(urlConnection, "spiders".length()));
     assertEquals(-1, in.read());
     in.close();
-    assertEquals(1, cache.getWriteSuccessCount());
-    assertEquals(0, cache.getWriteAbortCount());
 
     urlConnection = openConnection(server.getUrl("/")); // cached!
     in = urlConnection.getInputStream();
@@ -321,10 +172,6 @@
 
     assertEquals(-1, in.read());
     in.close();
-    assertEquals(1, cache.getWriteSuccessCount());
-    assertEquals(0, cache.getWriteAbortCount());
-    assertEquals(2, cache.getRequestCount());
-    assertEquals(1, cache.getHitCount());
   }
 
   @Test public void secureResponseCaching() throws IOException {
@@ -334,32 +181,28 @@
         .setBody("ABC"));
     server.play();
 
-    HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
-    connection.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("ABC", readAscii(connection));
+    HttpsURLConnection c1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
+    c1.setSSLSocketFactory(sslContext.getSocketFactory());
+    c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(c1));
 
     // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
-    String suite = connection.getCipherSuite();
-    List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
-    List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
-    Principal peerPrincipal = connection.getPeerPrincipal();
-    Principal localPrincipal = connection.getLocalPrincipal();
+    String suite = c1.getCipherSuite();
+    List<Certificate> localCerts = toListOrNull(c1.getLocalCertificates());
+    List<Certificate> serverCerts = toListOrNull(c1.getServerCertificates());
+    Principal peerPrincipal = c1.getPeerPrincipal();
+    Principal localPrincipal = c1.getLocalPrincipal();
 
-    connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
-    connection.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    assertEquals("ABC", readAscii(connection));
+    HttpsURLConnection c2 = (HttpsURLConnection) openConnection(server.getUrl("/")); // cached!
+    c2.setSSLSocketFactory(sslContext.getSocketFactory());
+    c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(c2));
 
-    assertEquals(2, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(1, cache.getHitCount());
-
-    assertEquals(suite, connection.getCipherSuite());
-    assertEquals(localCerts, toListOrNull(connection.getLocalCertificates()));
-    assertEquals(serverCerts, toListOrNull(connection.getServerCertificates()));
-    assertEquals(peerPrincipal, connection.getPeerPrincipal());
-    assertEquals(localPrincipal, connection.getLocalPrincipal());
+    assertEquals(suite, c2.getCipherSuite());
+    assertEquals(localCerts, toListOrNull(c2.getLocalCertificates()));
+    assertEquals(serverCerts, toListOrNull(c2.getServerCertificates()));
+    assertEquals(peerPrincipal, c2.getPeerPrincipal());
+    assertEquals(localPrincipal, c2.getLocalPrincipal());
   }
 
   @Test public void cacheReturnsInsecureResponseForSecureRequest() throws IOException {
@@ -368,15 +211,15 @@
     server.enqueue(new MockResponse().setBody("DEF"));
     server.play();
 
-    ResponseCache.setDefault(new InsecureResponseCache());
+    client.setResponseCache(new InsecureResponseCache(new InMemoryResponseCache()));
 
-    HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     connection1.setSSLSocketFactory(sslContext.getSocketFactory());
     connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
     assertEquals("ABC", readAscii(connection1));
 
     // Not cached!
-    HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    HttpsURLConnection connection2 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     connection2.setSSLSocketFactory(sslContext.getSocketFactory());
     connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
     assertEquals("DEF", readAscii(connection2));
@@ -398,10 +241,6 @@
 
     connection = openConnection(server.getUrl("/")); // cached!
     assertEquals("ABC", readAscii(connection));
-
-    assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
-    assertEquals(2, cache.getNetworkCount());
-    assertEquals(2, cache.getHitCount());
   }
 
   @Test public void redirectToCachedResult() throws Exception {
@@ -443,15 +282,16 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection1));
+    assertNotNull(connection1.getCipherSuite());
 
     // Cached!
-    HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    HttpsURLConnection connection2 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection2));
+    assertNotNull(connection2.getCipherSuite());
 
-    assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
-    assertEquals(2, cache.getHitCount());
+    assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite());
   }
 
   /**
@@ -479,15 +319,12 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    HttpURLConnection connection1 = openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection1));
 
     // Cached!
-    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    HttpURLConnection connection2 = openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection2));
-
-    assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
-    assertEquals(2, cache.getHitCount());
   }
 
   @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException {
@@ -496,13 +333,16 @@
 
     final AtomicReference<Map<String, List<String>>> requestHeadersRef =
         new AtomicReference<Map<String, List<String>>>();
-    ResponseCache.setDefault(new ResponseCache() {
-      @Override public CacheResponse get(URI uri, String requestMethod,
+    client.setResponseCache(new ResponseCache() {
+      @Override
+      public CacheResponse get(URI uri, String requestMethod,
           Map<String, List<String>> requestHeaders) throws IOException {
         requestHeadersRef.set(requestHeaders);
         return null;
       }
-      @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
+
+      @Override
+      public CacheRequest put(URI uri, URLConnection conn) throws IOException {
         return null;
       }
     });
@@ -542,16 +382,13 @@
       reader.readLine();
       fail("This implementation silently ignored a truncated HTTP body.");
     } catch (IOException expected) {
+        expected.printStackTrace();
     } finally {
       reader.close();
     }
 
-    assertEquals(1, cache.getWriteAbortCount());
-    assertEquals(0, cache.getWriteSuccessCount());
     URLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("Request #2", readAscii(connection));
-    assertEquals(1, cache.getWriteAbortCount());
-    assertEquals(1, cache.getWriteSuccessCount());
   }
 
   @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException {
@@ -568,7 +405,7 @@
 
   private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
     // Setting a low transfer speed ensures that stream discarding will time out.
-    MockResponse response = new MockResponse().setBytesPerSecond(6);
+    MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS);
     transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
     server.enqueue(response);
     server.enqueue(new MockResponse().setBody("Request #2"));
@@ -584,12 +421,8 @@
     } catch (IOException expected) {
     }
 
-    assertEquals(1, cache.getWriteAbortCount());
-    assertEquals(0, cache.getWriteSuccessCount());
     connection = openConnection(server.getUrl("/"));
     assertEquals("Request #2", readAscii(connection));
-    assertEquals(1, cache.getWriteAbortCount());
-    assertEquals(1, cache.getWriteSuccessCount());
   }
 
   @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception {
@@ -727,36 +560,22 @@
         .addHeader("Cache-Control: max-age=60"));
   }
 
-  @Test public void requestMethodOptionsIsNotCached() throws Exception {
-    testRequestMethod("OPTIONS", false);
-  }
-
-  @Test public void requestMethodGetIsCached() throws Exception {
+  /**
+   * Tests that the ResponseCache can cache something. The InMemoryResponseCache only caches GET
+   * requests.
+   */
+  @Test public void responseCacheCanCache() throws Exception {
     testRequestMethod("GET", true);
   }
 
-  @Test public void requestMethodHeadIsNotCached() throws Exception {
-    // We could support this but choose not to for implementation simplicity
+  /**
+   * Confirm the ResponseCache can elect to not cache something. The InMemoryResponseCache only
+   * caches GET requests.
+   */
+  @Test public void responseCacheCanIgnore() throws Exception {
     testRequestMethod("HEAD", false);
   }
 
-  @Test public void requestMethodPostIsNotCached() throws Exception {
-    // We could support this but choose not to for implementation simplicity
-    testRequestMethod("POST", false);
-  }
-
-  @Test public void requestMethodPutIsNotCached() throws Exception {
-    testRequestMethod("PUT", false);
-  }
-
-  @Test public void requestMethodDeleteIsNotCached() throws Exception {
-    testRequestMethod("DELETE", false);
-  }
-
-  @Test public void requestMethodTraceIsNotCached() throws Exception {
-    testRequestMethod("TRACE", false);
-  }
-
   private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception {
     // 1. seed the cache (potentially)
     // 2. expect a cache hit or miss
@@ -774,54 +593,25 @@
 
     URLConnection request2 = openConnection(url);
     if (expectCached) {
-      assertEquals("1", request1.getHeaderField("X-Response-ID"));
+      assertEquals("1", request2.getHeaderField("X-Response-ID"));
     } else {
       assertEquals("2", request2.getHeaderField("X-Response-ID"));
     }
   }
 
-  @Test public void postInvalidatesCache() throws Exception {
-    testMethodInvalidates("POST");
-  }
-
-  @Test public void putInvalidatesCache() throws Exception {
-    testMethodInvalidates("PUT");
-  }
-
-  @Test public void deleteMethodInvalidatesCache() throws Exception {
-    testMethodInvalidates("DELETE");
-  }
-
-  private void testMethodInvalidates(String requestMethod) throws Exception {
-    // 1. seed the cache
-    // 2. invalidate it
-    // 3. expect a cache miss
-    server.enqueue(
-        new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.enqueue(new MockResponse().setBody("C"));
-    server.play();
-
-    URL url = server.getUrl("/");
-
-    assertEquals("A", readAscii(openConnection(url)));
-
-    HttpURLConnection invalidate = openConnection(url);
-    invalidate.setRequestMethod(requestMethod);
-    addRequestBodyIfNecessary(requestMethod, invalidate);
-    assertEquals("B", readAscii(invalidate));
-
-    assertEquals("C", readAscii(openConnection(url)));
-  }
-
+  /**
+   * Equivalent to {@link HttpResponseCacheTest#postInvalidatesCacheWithUncacheableResponse()} but
+   * demonstrating that {@link ResponseCache} provides no mechanism for cache invalidation as the
+   * result of locally-made requests. In reality invalidation could take place from other clients at
+   * any time.
+   */
   @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
     // 1. seed the cache
     // 2. invalidate it with uncacheable response
-    // 3. expect a cache miss
+    // 3. the cache to return the original value
     server.enqueue(
         new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
     server.enqueue(new MockResponse().setBody("B").setResponseCode(500));
-    server.enqueue(new MockResponse().setBody("C"));
     server.play();
 
     URL url = server.getUrl("/");
@@ -833,7 +623,7 @@
     addRequestBodyIfNecessary("POST", invalidate);
     assertEquals("B", readAscii(invalidate));
 
-    assertEquals("C", readAscii(openConnection(url)));
+    assertEquals("A", readAscii(openConnection(url)));
   }
 
   @Test public void etag() throws Exception {
@@ -1289,256 +1079,37 @@
     assertEquals(2, server.takeRequest().getSequenceNumber());
   }
 
-  @Test public void statisticsConditionalCacheMiss() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-        .addHeader("Cache-Control: max-age=0")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.enqueue(new MockResponse().setBody("C"));
-    server.play();
-
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(1, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-    assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals("C", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(3, cache.getRequestCount());
-    assertEquals(3, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-  }
-
-  @Test public void statisticsConditionalCacheHit() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-        .addHeader("Cache-Control: max-age=0")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-    server.play();
-
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(1, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(3, cache.getRequestCount());
-    assertEquals(3, cache.getNetworkCount());
-    assertEquals(2, cache.getHitCount());
-  }
-
-  @Test public void statisticsFullCacheHit() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A"));
-    server.play();
-
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(1, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals(3, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(2, cache.getHitCount());
-  }
-
-  @Test public void varyMatchesChangedRequestHeaderField() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    HttpURLConnection frConnection = openConnection(url);
-    frConnection.addRequestProperty("Accept-Language", "fr-CA");
-    assertEquals("A", readAscii(frConnection));
-
-    HttpURLConnection enConnection = openConnection(url);
-    enConnection.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("B", readAscii(enConnection));
-  }
-
-  @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
+  /**
+   * Confirms the cache implementation may determine the criteria for caching. In real caches
+   * this would be the "Vary" headers.
+   */
+  @Test public void cacheCanUseCriteriaBesidesVariantObeyed() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Cache-Control: max-age=60")
+            .addHeader(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A").setBody("A"));
+    server.enqueue(
+        new MockResponse().addHeader("Cache-Control: max-age=60")
+            .addHeader(InMemoryResponseCache.CACHE_VARIANT_HEADER, "B").setBody("B"));
     server.play();
 
     URL url = server.getUrl("/");
     URLConnection connection1 = openConnection(url);
-    connection1.addRequestProperty("Accept-Language", "fr-CA");
+    connection1.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A");
     assertEquals("A", readAscii(connection1));
     URLConnection connection2 = openConnection(url);
-    connection2.addRequestProperty("Accept-Language", "fr-CA");
+    connection2.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A");
     assertEquals("A", readAscii(connection2));
-  }
+    assertEquals(1, server.getRequestCount());
 
-  @Test public void varyMatchesAbsentRequestHeaderField() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Foo")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
+    URLConnection connection3 = openConnection(url);
+    connection3.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "B");
+    assertEquals("B", readAscii(connection3));
+    assertEquals(2, server.getRequestCount());
 
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-  }
-
-  @Test public void varyMatchesAddedRequestHeaderField() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Foo")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    URLConnection fooConnection = openConnection(server.getUrl("/"));
-    fooConnection.addRequestProperty("Foo", "bar");
-    assertEquals("B", readAscii(fooConnection));
-  }
-
-  @Test public void varyMatchesRemovedRequestHeaderField() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Foo")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URLConnection fooConnection = openConnection(server.getUrl("/"));
-    fooConnection.addRequestProperty("Foo", "bar");
-    assertEquals("A", readAscii(fooConnection));
-    assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
-  }
-
-  @Test public void varyFieldsAreCaseInsensitive() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: ACCEPT-LANGUAGE")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    URLConnection connection1 = openConnection(url);
-    connection1.addRequestProperty("Accept-Language", "fr-CA");
-    assertEquals("A", readAscii(connection1));
-    URLConnection connection2 = openConnection(url);
-    connection2.addRequestProperty("accept-language", "fr-CA");
-    assertEquals("A", readAscii(connection2));
-  }
-
-  @Test public void varyMultipleFieldsWithMatch() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language, Accept-Charset")
-        .addHeader("Vary: Accept-Encoding")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    URLConnection connection1 = openConnection(url);
-    connection1.addRequestProperty("Accept-Language", "fr-CA");
-    connection1.addRequestProperty("Accept-Charset", "UTF-8");
-    connection1.addRequestProperty("Accept-Encoding", "identity");
-    assertEquals("A", readAscii(connection1));
-    URLConnection connection2 = openConnection(url);
-    connection2.addRequestProperty("Accept-Language", "fr-CA");
-    connection2.addRequestProperty("Accept-Charset", "UTF-8");
-    connection2.addRequestProperty("Accept-Encoding", "identity");
-    assertEquals("A", readAscii(connection2));
-  }
-
-  @Test public void varyMultipleFieldsWithNoMatch() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language, Accept-Charset")
-        .addHeader("Vary: Accept-Encoding")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    URLConnection frConnection = openConnection(url);
-    frConnection.addRequestProperty("Accept-Language", "fr-CA");
-    frConnection.addRequestProperty("Accept-Charset", "UTF-8");
-    frConnection.addRequestProperty("Accept-Encoding", "identity");
-    assertEquals("A", readAscii(frConnection));
-    URLConnection enConnection = openConnection(url);
-    enConnection.addRequestProperty("Accept-Language", "en-CA");
-    enConnection.addRequestProperty("Accept-Charset", "UTF-8");
-    enConnection.addRequestProperty("Accept-Encoding", "identity");
-    assertEquals("B", readAscii(enConnection));
-  }
-
-  @Test public void varyMultipleFieldValuesWithMatch() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    URLConnection connection1 = openConnection(url);
-    connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
-    connection1.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("A", readAscii(connection1));
-
-    URLConnection connection2 = openConnection(url);
-    connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
-    connection2.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("A", readAscii(connection2));
-  }
-
-  @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    URLConnection connection1 = openConnection(url);
-    connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
-    connection1.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("A", readAscii(connection1));
-
-    URLConnection connection2 = openConnection(url);
-    connection2.addRequestProperty("Accept-Language", "fr-CA");
-    connection2.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("B", readAscii(connection2));
-  }
-
-  @Test public void varyAsterisk() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: *")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
-  }
-
-  @Test public void varyAndHttps() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
-    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
-        .addHeader("Vary: Accept-Language")
-        .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url);
-    connection1.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    connection1.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("A", readAscii(connection1));
-
-    HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url);
-    connection2.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    connection2.addRequestProperty("Accept-Language", "en-US");
-    assertEquals("A", readAscii(connection2));
+    URLConnection connection4 = openConnection(url);
+    connection4.addRequestProperty(InMemoryResponseCache.CACHE_VARIANT_HEADER, "A");
+    assertEquals("A", readAscii(connection4));
+    assertEquals(2, server.getRequestCount());
   }
 
   @Test public void cachePlusCookies() throws Exception {
@@ -1552,11 +1123,14 @@
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
     server.play();
 
+    CookieManager cookieManager = new CookieManager();
+    CookieManager.setDefault(cookieManager);
+
     URL url = server.getUrl("/");
     assertEquals("A", readAscii(openConnection(url)));
-    assertCookies(url, "a=FIRST");
+    assertCookies(cookieManager, url, "a=FIRST");
     assertEquals("A", readAscii(openConnection(url)));
-    assertCookies(url, "a=SECOND");
+    assertCookies(cookieManager, url, "a=SECOND");
   }
 
   @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception {
@@ -1629,7 +1203,8 @@
     assertEquals("299 test danger", connection2.getHeaderField("Warning"));
   }
 
-  public void assertCookies(URL url, String... expectedCookies) throws Exception {
+  public void assertCookies(CookieManager cookieManager, URL url, String... expectedCookies)
+      throws Exception {
     List<String> actualCookies = new ArrayList<String>();
     for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
       actualCookies.add(cookie.toString());
@@ -1644,33 +1219,48 @@
         .addHeader("Cache-Control: max-age=60"));
   }
 
-  @Test public void conditionalHitUpdatesCache() throws Exception {
+  /**
+   * Equivalent to {@link HttpResponseCacheTest#conditionalHitUpdatesCache()}, except a Java
+   * standard cache has no means to update the headers for an existing entry so the behavior is
+   * different.
+   */
+  @Test public void conditionalHitDoesNotUpdateCache() throws Exception {
+    // A response that is cacheable, but with a short life.
     server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
         .addHeader("Cache-Control: max-age=0")
         .setBody("A"));
+    // A response that refers to the previous response, but is cacheable with a long life.
+    // Contains a header we can recognize as having come from the server.
     server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=30")
         .addHeader("Allow: GET, HEAD")
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-    server.enqueue(new MockResponse().setBody("B"));
+    // A response that is cacheable with a long life.
+    server.enqueue(new MockResponse().setBody("B").addHeader("Cache-Control: max-age=30"));
+    // A response that should never be requested.
+    server.enqueue(new MockResponse().setBody("C"));
     server.play();
 
-    // cache miss; seed the cache
+    // cache miss; seed the cache with an entry that will require a network hit to be sure it is
+    // still valid
     HttpURLConnection connection1 = openConnection(server.getUrl("/a"));
     assertEquals("A", readAscii(connection1));
     assertEquals(null, connection1.getHeaderField("Allow"));
 
-    // conditional cache hit; update the cache
+    // conditional cache hit; The cached data should be returned, but the cache is not updated.
     HttpURLConnection connection2 = openConnection(server.getUrl("/a"));
     assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
     assertEquals("A", readAscii(connection2));
     assertEquals("GET, HEAD", connection2.getHeaderField("Allow"));
 
-    // full cache hit
+    // conditional cache hit; The server responds with new data. The cache is updated.
     HttpURLConnection connection3 = openConnection(server.getUrl("/a"));
-    assertEquals("A", readAscii(connection3));
-    assertEquals("GET, HEAD", connection3.getHeaderField("Allow"));
+    assertEquals("B", readAscii(connection3));
 
-    assertEquals(2, server.getRequestCount());
+    // full cache hit; The data from connection3 has now replaced that from connection 1.
+    HttpURLConnection connection4 = openConnection(server.getUrl("/a"));
+    assertEquals("B", readAscii(connection4));
+
+    assertEquals(3, server.getRequestCount());
   }
 
   @Test public void responseSourceHeaderCached() throws IOException {
@@ -1684,8 +1274,8 @@
     connection.addRequestProperty("Cache-Control", "only-if-cached");
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CACHE.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CACHE + " 200", source);
   }
 
   @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
@@ -1701,8 +1291,8 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("B", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source);
   }
 
   @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
@@ -1716,8 +1306,8 @@
     HttpURLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.CONDITIONAL_CACHE.toString() + " 304", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source);
   }
 
   @Test public void responseSourceHeaderFetched() throws IOException {
@@ -1727,8 +1317,8 @@
     URLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("A", readAscii(connection));
 
-    String source = connection.getHeaderField(ResponseHeaders.RESPONSE_SOURCE);
-    assertEquals(ResponseSource.NETWORK.toString() + " 200", source);
+    String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    assertEquals(ResponseSource.NETWORK + " 200", source);
   }
 
   @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
@@ -1737,78 +1327,11 @@
         .addHeader(": A")
         .setBody("body"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
     assertEquals("A", connection.getHeaderField(""));
   }
 
   /**
-   * Old implementations of OkHttp's response cache wrote header fields like
-   * ":status: 200 OK". This broke our cached response parser because it split
-   * on the first colon. This regression test exists to help us read these old
-   * bad cache entries.
-   *
-   * https://github.com/square/okhttp/issues/227
-   */
-  @Test public void testGoldenCacheResponse() throws Exception {
-    cache.close();
-    server.enqueue(new MockResponse()
-        .clearHeaders()
-        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-    server.play();
-
-    URL url = server.getUrl("/");
-    String urlKey = Util.hash(url.toString());
-    String entryMetadata = ""
-        + "" + url + "\n"
-        + "GET\n"
-        + "0\n"
-        + "HTTP/1.1 200 OK\n"
-        + "7\n"
-        + ":status: 200 OK\n"
-        + ":version: HTTP/1.1\n"
-        + "etag: foo\n"
-        + "content-length: 3\n"
-        + "OkHttp-Received-Millis: " + System.currentTimeMillis() + "\n"
-        + "X-Android-Response-Source: NETWORK 200\n"
-        + "OkHttp-Sent-Millis: " + System.currentTimeMillis() + "\n"
-        + "\n"
-        + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n"
-        + "1\n"
-        + "MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgy"
-        + "OTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEF"
-        + "AAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUc"
-        + "ltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFK"
-        + "v6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP"
-        + "7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dm"
-        + "yFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg==\n"
-        + "-1\n";
-    String entryBody = "abc";
-    String journalBody = ""
-        + "libcore.io.DiskLruCache\n"
-        + "1\n"
-        + "201105\n"
-        + "2\n"
-        + "\n"
-        + "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
-    writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata);
-    writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
-    writeFile(cache.getDirectory(), "journal", journalBody);
-    cache = new HttpResponseCache(cache.getDirectory(), Integer.MAX_VALUE);
-    client.setResponseCache(cache);
-
-    HttpURLConnection connection = client.open(url);
-    assertEquals(entryBody, readAscii(connection));
-    assertEquals("3", connection.getHeaderField("Content-Length"));
-    assertEquals("foo", connection.getHeaderField("etag"));
-  }
-
-  private void writeFile(File directory, String file, String content) throws IOException {
-    OutputStream out = new FileOutputStream(new File(directory, file));
-    out.write(content.getBytes(Util.UTF_8));
-    out.close();
-  }
-
-  /**
    * @param delta the offset from the current date to use. Negative
    * values yield dates in the past; positive values yield dates in the
    * future.
@@ -1942,6 +1465,8 @@
     }
     assertEquals(504, connection.getResponseCode());
     assertEquals(-1, connection.getErrorStream().read());
+    assertEquals(ResponseSource.NONE + " 504",
+        connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
   }
 
   enum TransferKind {
@@ -1989,14 +1514,21 @@
     return bytesOut.toByteArray();
   }
 
-  private class InsecureResponseCache extends ResponseCache {
+  private static class InsecureResponseCache extends ResponseCache {
+
+    private final ResponseCache delegate;
+
+    private InsecureResponseCache(ResponseCache delegate) {
+      this.delegate = delegate;
+    }
+
     @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
-      return cache.put(uri, connection);
+      return delegate.put(uri, connection);
     }
 
     @Override public CacheResponse get(URI uri, String requestMethod,
         Map<String, List<String>> requestHeaders) throws IOException {
-      final CacheResponse response = cache.get(uri, requestMethod, requestHeaders);
+      final CacheResponse response = delegate.get(uri, requestMethod, requestHeaders);
       if (response instanceof SecureCacheResponse) {
         return new CacheResponse() {
           @Override public InputStream getBody() throws IOException {
@@ -2010,4 +1542,206 @@
       return response;
     }
   }
+
+  /**
+   * A trivial and non-thread-safe implementation of ResponseCache that uses an in-memory map to
+   * cache GETs.
+   */
+  private static class InMemoryResponseCache extends ResponseCache {
+
+    /** A request / response header that acts a bit like Vary but without the complexity. */
+    public static final String CACHE_VARIANT_HEADER = "CacheVariant";
+
+    private static class Key {
+      private final URI uri;
+      private final String cacheVariant;
+
+      private Key(URI uri, String cacheVariant) {
+        this.uri = uri;
+        this.cacheVariant = cacheVariant;
+      }
+
+      @Override
+      public boolean equals(Object o) {
+        if (this == o) {
+          return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+          return false;
+        }
+
+        Key key = (Key) o;
+
+        if (cacheVariant != null ? !cacheVariant.equals(key.cacheVariant)
+            : key.cacheVariant != null) {
+          return false;
+        }
+        if (!uri.equals(key.uri)) {
+          return false;
+        }
+
+        return true;
+      }
+
+      @Override
+      public int hashCode() {
+        int result = uri.hashCode();
+        result = 31 * result + (cacheVariant != null ? cacheVariant.hashCode() : 0);
+        return result;
+      }
+    }
+
+    private class Entry {
+
+      private final URI uri;
+      private final String cacheVariant;
+      private final String method;
+      private final Map<String, List<String>> responseHeaders;
+      private final String cipherSuite;
+      private final Certificate[] serverCertificates;
+      private final Certificate[] localCertificates;
+      private byte[] body;
+
+      public Entry(URI uri, URLConnection urlConnection) {
+        this.uri = uri;
+        HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+        method = httpUrlConnection.getRequestMethod();
+        cacheVariant = urlConnection.getHeaderField(CACHE_VARIANT_HEADER);
+        responseHeaders = urlConnection.getHeaderFields();
+        if (urlConnection instanceof HttpsURLConnection) {
+          HttpsURLConnection httpsURLConnection = (HttpsURLConnection) urlConnection;
+          cipherSuite = httpsURLConnection.getCipherSuite();
+          Certificate[] serverCertificates;
+          try {
+            serverCertificates = httpsURLConnection.getServerCertificates();
+          } catch (SSLPeerUnverifiedException e) {
+            serverCertificates = null;
+          }
+          this.serverCertificates = serverCertificates;
+          localCertificates = httpsURLConnection.getLocalCertificates();
+        } else {
+          cipherSuite = null;
+          serverCertificates = null;
+          localCertificates = null;
+        }
+      }
+
+      public CacheResponse asCacheResponse() {
+        if (!method.equals(this.method)) {
+          return null;
+        }
+
+        // Handle SSL
+        if (cipherSuite != null) {
+          return new SecureCacheResponse() {
+            @Override
+            public Map<String, List<String>> getHeaders() throws IOException {
+              return responseHeaders;
+            }
+
+            @Override
+            public InputStream getBody() throws IOException {
+              return new ByteArrayInputStream(body);
+            }
+
+            @Override
+            public String getCipherSuite() {
+              return cipherSuite;
+            }
+
+            @Override
+            public List<Certificate> getLocalCertificateChain() {
+              return localCertificates == null ? null : Arrays.asList(localCertificates);
+            }
+
+            @Override
+            public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
+              if (serverCertificates == null) {
+                throw new SSLPeerUnverifiedException("Test implementation");
+              }
+              return Arrays.asList(serverCertificates);
+            }
+
+            @Override
+            public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+              throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Principal getLocalPrincipal() {
+              throw new UnsupportedOperationException();
+            }
+          };
+        } else {
+          return new CacheResponse() {
+            @Override
+            public Map<String, List<String>> getHeaders() throws IOException {
+              return responseHeaders;
+            }
+
+            @Override
+            public InputStream getBody() throws IOException {
+              return new ByteArrayInputStream(body);
+            }
+          };
+        }
+      }
+
+      public CacheRequest asCacheRequest() {
+        return new CacheRequest() {
+          @Override
+          public OutputStream getBody() throws IOException {
+            return new ByteArrayOutputStream() {
+              @Override
+              public void close() throws IOException {
+                super.close();
+                body = toByteArray();
+                cache.put(Entry.this.key(), Entry.this);
+              }
+            };
+          }
+
+          @Override
+          public void abort() {
+            // No-op: close() puts the item in the cache, abort need not do anything.
+          }
+        };
+      }
+
+      private Key key() {
+        return new Key(uri, cacheVariant);
+      }
+    }
+
+    private Map<Key, Entry> cache = new HashMap<Key, Entry>();
+
+    @Override
+    public CacheResponse get(URI uri, String method, Map<String, List<String>> requestHeaders)
+        throws IOException {
+
+      if (!"GET".equals(method)) {
+        return null;
+      }
+
+      String cacheVariant =
+          requestHeaders.containsKey(CACHE_VARIANT_HEADER)
+              ? requestHeaders.get(CACHE_VARIANT_HEADER).get(0) : null;
+      Key key = new Key(uri, cacheVariant);
+      Entry entry = cache.get(key);
+      if (entry == null) {
+        return null;
+      }
+      return entry.asCacheResponse();
+    }
+
+    @Override
+    public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+      if (!"GET".equals(((HttpURLConnection) urlConnection).getRequestMethod())) {
+        return null;
+      }
+
+      Entry entry = new Entry(uri, urlConnection);
+      return entry.asCacheRequest();
+    }
+  }
 }
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
similarity index 88%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index a92db9e..c8e2647 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -18,9 +18,10 @@
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.HostResolver;
 import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.RouteDatabase;
-import com.squareup.okhttp.internal.Dns;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -34,6 +35,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.NoSuchElementException;
+import javax.net.SocketFactory;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLHandshakeException;
@@ -59,16 +61,16 @@
   private static final String uriHost = "hostA";
   private static final int uriPort = 80;
 
-  private static final SSLContext sslContext;
-  private static final SSLSocketFactory socketFactory;
+  private static final SocketFactory socketFactory;
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  private static final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
   private static final HostnameVerifier hostnameVerifier;
   private static final ConnectionPool pool;
 
   static {
     try {
       uri = new URI("http://" + uriHost + ":" + uriPort + "/path");
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-      socketFactory = sslContext.getSocketFactory();
+      socketFactory = SocketFactory.getDefault();
       pool = ConnectionPool.getDefault();
       hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier();
     } catch (Exception e) {
@@ -77,12 +79,13 @@
   }
 
   private final OkAuthenticator authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
-  private final List<String> transports = Arrays.asList("http/1.1");
+  private final List<Protocol> protocols = Arrays.asList(Protocol.HTTP_11);
   private final FakeDns dns = new FakeDns();
   private final FakeProxySelector proxySelector = new FakeProxySelector();
 
   @Test public void singleRoute() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         new RouteDatabase());
 
@@ -101,7 +104,8 @@
   }
 
   @Test public void singleRouteReturnsFailedRoute() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         new RouteDatabase());
 
@@ -109,7 +113,7 @@
     dns.inetAddresses = makeFakeAddresses(255, 1);
     Connection connection = routeSelector.next("GET");
     RouteDatabase routeDatabase = new RouteDatabase();
-    routeDatabase.failed(connection.getRoute(), new IOException());
+    routeDatabase.failed(connection.getRoute());
     routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
     assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
         false);
@@ -122,7 +126,8 @@
   }
 
   @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, proxyA, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator,
+        proxyA, protocols);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         new RouteDatabase());
 
@@ -139,8 +144,8 @@
   }
 
   @Test public void explicitDirectProxy() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, NO_PROXY,
-        transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator,
+        NO_PROXY, protocols);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         new RouteDatabase());
 
@@ -157,7 +162,8 @@
   }
 
   @Test public void proxySelectorReturnsNull() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
 
     proxySelector.proxies = null;
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
@@ -174,7 +180,8 @@
   }
 
   @Test public void proxySelectorReturnsNoProxies() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         new RouteDatabase());
 
@@ -191,7 +198,8 @@
   }
 
   @Test public void proxySelectorReturnsMultipleProxies() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
 
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
@@ -226,7 +234,8 @@
   }
 
   @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
 
     proxySelector.proxies.add(NO_PROXY);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
@@ -244,7 +253,8 @@
   }
 
   @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
-    Address address = new Address(uriHost, uriPort, null, null, authenticator, null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
+        protocols);
 
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
@@ -283,9 +293,10 @@
     assertFalse(routeSelector.hasNext());
   }
 
+  // https://github.com/square/okhttp/issues/442
   @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
-        Proxy.NO_PROXY, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+        hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
     RouteDatabase routeDatabase = new RouteDatabase();
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         routeDatabase);
@@ -294,11 +305,12 @@
     Connection connection = routeSelector.next("GET");
     routeSelector.connectFailed(connection, new IOException("Non SSL exception"));
     assertTrue(routeDatabase.failedRoutesCount() == 2);
+    assertFalse(routeSelector.hasNext());
   }
 
   @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
-        Proxy.NO_PROXY, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+        hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
     RouteDatabase routeDatabase = new RouteDatabase();
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
         routeDatabase);
@@ -307,11 +319,12 @@
     Connection connection = routeSelector.next("GET");
     routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception"));
     assertTrue(routeDatabase.failedRoutesCount() == 1);
+    assertTrue(routeSelector.hasNext());
   }
 
   @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
-        null, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+        hostnameVerifier, authenticator, null, protocols);
     proxySelector.proxies.add(proxyA);
     proxySelector.proxies.add(proxyB);
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
@@ -357,8 +370,8 @@
   }
 
   @Test public void failedRoutesAreLast() throws Exception {
-    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, authenticator,
-        Proxy.NO_PROXY, transports);
+    Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+        hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
 
     RouteDatabase routeDatabase = new RouteDatabase();
     RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
@@ -374,7 +387,7 @@
     // Check that we do indeed have more than one route.
     assertTrue(regularRoutes.size() > 1);
     // Add first regular route as failed.
-    routeDatabase.failed(regularRoutes.get(0).getRoute(), new SSLHandshakeException("none"));
+    routeDatabase.failed(regularRoutes.get(0).getRoute());
     // Reset selector
     routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
 
@@ -410,7 +423,7 @@
     }
   }
 
-  private static class FakeDns implements Dns {
+  private static class FakeDns implements HostResolver {
     List<String> requestedHosts = new ArrayList<String>();
     InetAddress[] inetAddresses;
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java
new file mode 100644
index 0000000..885570a
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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 java.io.IOException;
+import java.net.ProtocolException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class StatusLineTest {
+  @Test public void parse() throws IOException {
+    String message = "Temporary Redirect";
+    int version = 1;
+    int code = 200;
+    StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " " + message);
+    assertEquals(message, statusLine.message());
+    assertEquals(version, statusLine.httpMinorVersion());
+    assertEquals(code, statusLine.code());
+  }
+
+  @Test public void emptyMessage() throws IOException {
+    int version = 1;
+    int code = 503;
+    StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " ");
+    assertEquals("", statusLine.message());
+    assertEquals(version, statusLine.httpMinorVersion());
+    assertEquals(code, statusLine.code());
+  }
+
+  /**
+   * This is not defined in the protocol but some servers won't add the leading
+   * empty space when the message is empty.
+   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
+   */
+  @Test public void emptyMessageAndNoLeadingSpace() throws IOException {
+    int version = 1;
+    int code = 503;
+    StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code);
+    assertEquals("", statusLine.message());
+    assertEquals(version, statusLine.httpMinorVersion());
+    assertEquals(code, statusLine.code());
+  }
+
+  // https://github.com/square/okhttp/issues/386
+  @Test public void shoutcast() throws IOException {
+    StatusLine statusLine = new StatusLine("ICY 200 OK");
+    assertEquals("OK", statusLine.message());
+    assertEquals(0, statusLine.httpMinorVersion());
+    assertEquals(200, statusLine.code());
+  }
+
+  @Test public void missingProtocol() throws IOException {
+    assertInvalid("");
+    assertInvalid(" ");
+    assertInvalid("200 OK");
+    assertInvalid(" 200 OK");
+  }
+
+  @Test public void protocolVersions() throws IOException {
+    assertInvalid("HTTP/2.0 200 OK");
+    assertInvalid("HTTP/2.1 200 OK");
+    assertInvalid("HTTP/-.1 200 OK");
+    assertInvalid("HTTP/1.- 200 OK");
+    assertInvalid("HTTP/0.1 200 OK");
+    assertInvalid("HTTP/101 200 OK");
+    assertInvalid("HTTP/1.1_200 OK");
+  }
+
+  @Test public void nonThreeDigitCode() throws IOException {
+    assertInvalid("HTTP/1.1  OK");
+    assertInvalid("HTTP/1.1 2 OK");
+    assertInvalid("HTTP/1.1 20 OK");
+    assertInvalid("HTTP/1.1 2000 OK");
+    assertInvalid("HTTP/1.1 two OK");
+    assertInvalid("HTTP/1.1 2");
+    assertInvalid("HTTP/1.1 2000");
+    assertInvalid("HTTP/1.1 two");
+  }
+
+  @Test public void truncated() throws IOException {
+    assertInvalid("");
+    assertInvalid("H");
+    assertInvalid("HTTP/1");
+    assertInvalid("HTTP/1.");
+    assertInvalid("HTTP/1.1");
+    assertInvalid("HTTP/1.1 ");
+    assertInvalid("HTTP/1.1 2");
+    assertInvalid("HTTP/1.1 20");
+  }
+
+  @Test public void wrongMessageDelimiter() throws IOException {
+    assertInvalid("HTTP/1.1 200_");
+  }
+
+  private void assertInvalid(String statusLine) throws IOException {
+    try {
+      new StatusLine(statusLine);
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
new file mode 100644
index 0000000..7e7ce0b
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public final class ThreadInterruptTest {
+  private final MockWebServer server = new MockWebServer();
+  private final OkHttpClient client = new OkHttpClient();
+
+  @Test public void interruptWritingRequestBody() throws Exception {
+    int requestBodySize = 10 * 1024 * 1024; // 10 MiB
+
+    server.enqueue(new MockResponse()
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    interruptLater(500);
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    connection.setFixedLengthStreamingMode(requestBodySize);
+    OutputStream requestBody = connection.getOutputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      for (int i = 0; i < requestBodySize; i += buffer.length) {
+        requestBody.write(buffer);
+        requestBody.flush();
+      }
+      fail("Expected thread to be interrupted");
+    } catch (InterruptedIOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  @Test public void interruptReadingResponseBody() throws Exception {
+    int responseBodySize = 10 * 1024 * 1024; // 10 MiB
+
+    server.enqueue(new MockResponse()
+        .setBody(new byte[responseBodySize])
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    interruptLater(500);
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    InputStream responseBody = connection.getInputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      while (responseBody.read(buffer) != -1) {
+      }
+      fail("Expected thread to be interrupted");
+    } catch (InterruptedIOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  private void interruptLater(final int delayMillis) {
+    final Thread toInterrupt = Thread.currentThread();
+    Thread interruptingCow = new Thread() {
+      @Override public void run() {
+        try {
+          sleep(delayMillis);
+          toInterrupt.interrupt();
+        } catch (InterruptedException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+    interruptingCow.start();
+  }
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
similarity index 75%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 7725f3d..a9f902a 100644
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -16,8 +16,12 @@
 
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.HttpResponseCache;
+import com.squareup.okhttp.OkAuthenticator.Challenge;
+import com.squareup.okhttp.OkAuthenticator.Credential;
 import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.RecordingAuthenticator;
 import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.RecordingOkAuthenticator;
@@ -42,13 +46,13 @@
 import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.ResponseCache;
+import java.net.Socket;
 import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
 import java.net.URI;
 import java.net.URL;
 import java.net.URLConnection;
 import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -64,10 +68,12 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
+import javax.net.SocketFactory;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -76,37 +82,34 @@
 import org.junit.Ignore;
 import org.junit.Test;
 
-import static com.squareup.okhttp.OkAuthenticator.Credential;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+import static com.squareup.okhttp.internal.http.OkHeaders.SELECTED_PROTOCOL;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
 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.assertTrue;
 import static org.junit.Assert.fail;
 
 /** Android's URLConnectionTest. */
 public final class URLConnectionTest {
+  private static final SSLContext sslContext = SslContextBuilder.localhost();
+
   private MockWebServer server = new MockWebServer();
   private MockWebServer server2 = new MockWebServer();
 
   private final OkHttpClient client = new OkHttpClient();
+  private HttpURLConnection connection;
   private HttpResponseCache cache;
   private String hostName;
 
-  private static final SSLContext sslContext;
-  static {
-    try {
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    } catch (GeneralSecurityException e) {
-      throw new RuntimeException(e);
-    } catch (UnknownHostException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
   @Before public void setUp() throws Exception {
     hostName = server.getHostName();
     server.setNpnEnabled(false);
@@ -131,12 +134,12 @@
     server.enqueue(new MockResponse());
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.addRequestProperty("D", "e");
-    urlConnection.addRequestProperty("D", "f");
-    assertEquals("f", urlConnection.getRequestProperty("D"));
-    assertEquals("f", urlConnection.getRequestProperty("d"));
-    Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
+    connection = client.open(server.getUrl("/"));
+    connection.addRequestProperty("D", "e");
+    connection.addRequestProperty("D", "f");
+    assertEquals("f", connection.getRequestProperty("D"));
+    assertEquals("f", connection.getRequestProperty("d"));
+    Map<String, List<String>> requestHeaders = connection.getRequestProperties();
     assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
     assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
     try {
@@ -150,21 +153,21 @@
     } catch (UnsupportedOperationException expected) {
     }
     try {
-      urlConnection.setRequestProperty(null, "j");
+      connection.setRequestProperty(null, "j");
       fail();
     } catch (NullPointerException expected) {
     }
     try {
-      urlConnection.addRequestProperty(null, "k");
+      connection.addRequestProperty(null, "k");
       fail();
     } catch (NullPointerException expected) {
     }
-    urlConnection.setRequestProperty("NullValue", null);
-    assertNull(urlConnection.getRequestProperty("NullValue"));
-    urlConnection.addRequestProperty("AnotherNullValue", null);
-    assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
+    connection.setRequestProperty("NullValue", null);
+    assertNull(connection.getRequestProperty("NullValue"));
+    connection.addRequestProperty("AnotherNullValue", null);
+    assertNull(connection.getRequestProperty("AnotherNullValue"));
 
-    urlConnection.getResponseCode();
+    connection.getResponseCode();
     RecordedRequest request = server.takeRequest();
     assertContains(request.getHeaders(), "D: e");
     assertContains(request.getHeaders(), "D: f");
@@ -174,17 +177,17 @@
     assertContainsNoneMatching(request.getHeaders(), "null:.*");
 
     try {
-      urlConnection.addRequestProperty("N", "o");
+      connection.addRequestProperty("N", "o");
       fail("Set header after connect");
     } catch (IllegalStateException expected) {
     }
     try {
-      urlConnection.setRequestProperty("P", "q");
+      connection.setRequestProperty("P", "q");
       fail("Set header after connect");
     } catch (IllegalStateException expected) {
     }
     try {
-      urlConnection.getRequestProperties();
+      connection.getRequestProperties();
       fail();
     } catch (IllegalStateException expected) {
     }
@@ -192,10 +195,10 @@
 
   @Test public void getRequestPropertyReturnsLastValue() throws Exception {
     server.play();
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.addRequestProperty("A", "value1");
-    urlConnection.addRequestProperty("A", "value2");
-    assertEquals("value2", urlConnection.getRequestProperty("A"));
+    connection = client.open(server.getUrl("/"));
+    connection.addRequestProperty("A", "value1");
+    connection.addRequestProperty("A", "value2");
+    assertEquals("value2", connection.getRequestProperty("A"));
   }
 
   @Test public void responseHeaders() throws IOException, InterruptedException {
@@ -206,11 +209,11 @@
         .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    assertEquals(200, urlConnection.getResponseCode());
-    assertEquals("Fantastic", urlConnection.getResponseMessage());
-    assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null));
-    Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
+    connection = client.open(server.getUrl("/"));
+    assertEquals(200, connection.getResponseCode());
+    assertEquals("Fantastic", connection.getResponseMessage());
+    assertEquals("HTTP/1.0 200 Fantastic", connection.getHeaderField(null));
+    Map<String, List<String>> responseHeaders = connection.getHeaderFields();
     assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
     assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
     assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a")));
@@ -224,21 +227,21 @@
       fail("Modified an unmodifiable view.");
     } catch (UnsupportedOperationException expected) {
     }
-    assertEquals("A", urlConnection.getHeaderFieldKey(0));
-    assertEquals("c", urlConnection.getHeaderField(0));
-    assertEquals("B", urlConnection.getHeaderFieldKey(1));
-    assertEquals("d", urlConnection.getHeaderField(1));
-    assertEquals("A", urlConnection.getHeaderFieldKey(2));
-    assertEquals("e", urlConnection.getHeaderField(2));
+    assertEquals("A", connection.getHeaderFieldKey(0));
+    assertEquals("c", connection.getHeaderField(0));
+    assertEquals("B", connection.getHeaderFieldKey(1));
+    assertEquals("d", connection.getHeaderField(1));
+    assertEquals("A", connection.getHeaderFieldKey(2));
+    assertEquals("e", connection.getHeaderField(2));
   }
 
   @Test public void serverSendsInvalidResponseHeaders() throws Exception {
     server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK"));
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
-      urlConnection.getResponseCode();
+      connection.getResponseCode();
       fail();
     } catch (IOException expected) {
     }
@@ -248,9 +251,9 @@
     server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
-      urlConnection.getResponseCode();
+      connection.getResponseCode();
       fail();
     } catch (IOException expected) {
     }
@@ -260,9 +263,9 @@
     server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK"));
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
-      urlConnection.getResponseCode();
+      connection.getResponseCode();
       fail();
     } catch (IOException expected) {
     }
@@ -272,9 +275,9 @@
     server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK"));
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
-      urlConnection.getResponseCode();
+      connection.getResponseCode();
       fail();
     } catch (IOException expected) {
     }
@@ -285,7 +288,7 @@
     URL url = server.getUrl("/foo");
     server.shutdown();
 
-    HttpURLConnection connection = client.open(url);
+    connection = client.open(url);
     try {
       connection.connect();
       fail();
@@ -316,7 +319,7 @@
     client.setProxySelector(proxySelector);
     server2.shutdown();
 
-    HttpURLConnection connection = client.open(server.getUrl("/def"));
+    connection = client.open(server.getUrl("/def"));
     connection.setDoOutput(true);
     transferKind.setForRequest(connection, 4);
     connection.getOutputStream().write("body".getBytes("UTF-8"));
@@ -328,14 +331,14 @@
   @Test public void getErrorStreamOnSuccessfulRequest() throws Exception {
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertNull(connection.getErrorStream());
   }
 
   @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
     server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE));
   }
 
@@ -404,6 +407,17 @@
     testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
   }
 
+  @Test public void invalidHost() throws Exception {
+    // Note that 1234.1.1.1 is an invalid host in a URI, but URL isn't as strict.
+    URL url = new URL("http://1234.1.1.1/index.html");
+    HttpURLConnection connection = client.open(url);
+    try {
+      connection.connect();
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+  }
+
   private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception {
     server.enqueue(new MockResponse().setBody("This connection won't pool properly")
         .setSocketPolicy(socketPolicy));
@@ -515,7 +529,7 @@
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    connection = client.open(server.getUrl("/foo"));
 
     assertContent("this response comes via HTTPS", connection);
 
@@ -523,6 +537,36 @@
     assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
   }
 
+  @Test public void inspectHandshakeThroughoutRequestLifecycle() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse());
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    HttpsURLConnection httpsConnection = (HttpsURLConnection) client.open(server.getUrl("/foo"));
+
+    // Prior to calling connect(), getting the cipher suite is forbidden.
+    try {
+      httpsConnection.getCipherSuite();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    // Calling connect establishes a handshake...
+    httpsConnection.connect();
+    assertNotNull(httpsConnection.getCipherSuite());
+
+    // ...which remains after we read the response body...
+    assertContent("", httpsConnection);
+    assertNotNull(httpsConnection.getCipherSuite());
+
+    // ...and after we disconnect.
+    httpsConnection.disconnect();
+    assertNotNull(httpsConnection.getCipherSuite());
+  }
+
   @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
     server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
@@ -535,7 +579,7 @@
 
     client.setSslSocketFactory(clientSocketFactory);
     client.setHostnameVerifier(hostnameVerifier);
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertContent("this response comes via HTTPS", connection);
 
     connection = client.open(server.getUrl("/"));
@@ -568,19 +612,87 @@
   }
 
   @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
-    server.useHttps(sslContext.getSocketFactory(), false);
+    // Android-specific changes below related to http://b/17750026
+    // Android's server sockets will fail the handshake if the client sets TLS_FALLBACK_SCSV,
+    // attempts a retry over SSLv3 and the server has newer protocols enabled. It is not
+    // possible to turn TLS_FALLBACK_SCSV behavior off on the server so we fail the first connection
+    // by simulating a handshake failure and we set the server to only accept the SSLv3
+    // protocol (satisfying the TLS_FALLBACK_SCSV check for the second connection). This is as close
+    // as we can get to simulating a server that fails TLSv1.X and which also does not support
+    // TLS_FALLBACK_SCSV.
+    SSLSocketFactory serverSocketFactory =
+        new LimitedProtocolsSocketFactory(sslContext.getSocketFactory(), "SSLv3");
+    server.useHttps(serverSocketFactory, false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
     server.enqueue(new MockResponse().setBody("this response comes via SSL"));
     server.play();
 
-    client.setSslSocketFactory(sslContext.getSocketFactory());
+    RecordingSocketFactory clientSocketFactory =
+        new RecordingSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(clientSocketFactory);
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    connection = client.open(server.getUrl("/foo"));
 
     assertContent("this response comes via SSL", connection);
 
+    assertEquals(2, server.getRequestCount());
     RecordedRequest request = server.takeRequest();
     assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+    assertEquals("SSLv3", request.getSslProtocol());
+    assertEquals(2, clientSocketFactory.getCreatedSockets().size());
+  }
+
+  // Android-specific changes below related to http://b/17750026
+  // After the introduction of the TLS_FALLBACK_SCSV we expect a failure if the initial
+  // handshake fails and the server supports TLS_FALLBACK_SCSV. MockWebServer on Android uses
+  // sockets that enforced TLS_FALLBACK_SCSV checks by default.
+  @Test public void connectViaHttpsWithSSLFallback_scsvFailure() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+    server.play();
+
+    RecordingSocketFactory clientSocketFactory =
+        new RecordingSocketFactory(sslContext.getSocketFactory());
+    client.setSslSocketFactory(clientSocketFactory);
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    try {
+        connection = client.open(server.getUrl("/foo"));
+        connection.getInputStream();
+        fail();
+    } catch (SSLHandshakeException expected) {
+    }
+
+    // The first request is registered by MockWebServer and intentionally failed. The second is
+    // failed by the socket layer.
+    assertEquals(1, server.getRequestCount());
+    assertEquals(2, clientSocketFactory.getCreatedSockets().size());
+  }
+
+  /**
+   * When a pooled connection fails, don't blame the route. Otherwise pooled
+   * connection failures can cause unnecessary SSL fallbacks.
+   *
+   * https://github.com/square/okhttp/issues/515
+   */
+  @Test public void sslFallbackNotUsedWhenRecycledConnectionFails() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()
+        .setBody("abc")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    server.enqueue(new MockResponse().setBody("def"));
+    server.play();
+
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    assertContent("abc", client.open(server.getUrl("/")));
+    assertContent("def", client.open(server.getUrl("/")));
+
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("TLSv1", request1.getSslProtocol()); // OkHttp's current best TLS version.
+
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("TLSv1", request2.getSslProtocol()); // OkHttp's current best TLS version.
   }
 
   /**
@@ -593,7 +705,7 @@
     server.enqueue(new MockResponse()); // unused
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    connection = client.open(server.getUrl("/foo"));
     try {
       connection.getInputStream();
       fail();
@@ -621,8 +733,9 @@
     server.play();
 
     URL url = new URL("http://android.com/foo");
-    HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    connection = proxyConfig.connect(server, client, url);
     assertContent("this response comes via a proxy", connection);
+    assertTrue(connection.usingProxy());
 
     RecordedRequest request = server.takeRequest();
     assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
@@ -638,6 +751,48 @@
     assertContent("abc", client.open(server.getUrl("/")));
   }
 
+  public void testConnectViaSocketFactory(boolean useHttps) throws IOException {
+    SocketFactory uselessSocketFactory = new SocketFactory() {
+      public Socket createSocket() { throw new IllegalArgumentException("useless"); }
+      public Socket createSocket(InetAddress host, int port) { return null; }
+      public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+          int localPort) { return null; }
+      public Socket createSocket(String host, int port) { return null; }
+      public Socket createSocket(String host, int port, InetAddress localHost, int localPort) {
+        return null;
+      }
+    };
+
+    if (useHttps) {
+      server.useHttps(sslContext.getSocketFactory(), false);
+      client.setSslSocketFactory(sslContext.getSocketFactory());
+      client.setHostnameVerifier(new RecordingHostnameVerifier());
+    }
+
+    server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK"));
+    server.play();
+
+    client.setSocketFactory(uselessSocketFactory);
+    connection = client.open(server.getUrl("/"));
+    try {
+      connection.getResponseCode();
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    client.setSocketFactory(SocketFactory.getDefault());
+    connection = client.open(server.getUrl("/"));
+    assertEquals(200, connection.getResponseCode());
+  }
+
+  @Test public void connectHttpViaSocketFactory() throws Exception {
+    testConnectViaSocketFactory(false);
+  }
+
+  @Test public void connectHttpsViaSocketFactory() throws Exception {
+    testConnectViaSocketFactory(true);
+  }
+
   @Test public void contentDisagreesWithChunkedHeader() throws IOException {
     MockResponse mockResponse = new MockResponse();
     mockResponse.setChunkedBody("abc", 3);
@@ -671,7 +826,7 @@
     URL url = server.getUrl("/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    connection = proxyConfig.connect(server, client, url);
 
     assertContent("this response comes via HTTPS", connection);
 
@@ -711,7 +866,7 @@
     URL url = new URL("https://android.com/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(hostnameVerifier);
-    HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    connection = proxyConfig.connect(server, client, url);
 
     assertContent("this response comes via a secure proxy", connection);
 
@@ -747,7 +902,7 @@
 
     URL url = new URL("https://android.com/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    HttpURLConnection connection = client.open(url);
+    connection = client.open(url);
 
     try {
       connection.getResponseCode();
@@ -768,7 +923,7 @@
     String tmp = System.getProperty("java.io.tmpdir");
     File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
     cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
-    client.setResponseCache(cache);
+    client.setOkResponseCache(cache);
   }
 
   /** Test which headers are sent unencrypted to the HTTP proxy. */
@@ -786,7 +941,7 @@
     URL url = new URL("https://android.com/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(hostnameVerifier);
-    HttpURLConnection connection = client.open(url);
+    connection = client.open(url);
     connection.addRequestProperty("Private", "Secret");
     connection.addRequestProperty("Proxy-Authorization", "bar");
     connection.addRequestProperty("User-Agent", "baz");
@@ -818,7 +973,7 @@
     URL url = new URL("https://android.com/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(url);
+    connection = client.open(url);
     assertContent("A", connection);
 
     RecordedRequest connect1 = server.takeRequest();
@@ -848,7 +1003,7 @@
     URL url = new URL("https://android.com/foo");
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(url);
+    connection = client.open(url);
     connection.setRequestProperty("Connection", "close");
 
     assertContent("this response comes via a proxy", connection);
@@ -877,7 +1032,7 @@
     server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     InputStream in = connection.getInputStream();
     assertEquals('A', (char) in.read());
     connection.disconnect();
@@ -892,9 +1047,8 @@
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.disconnect();
-
     assertContent("A", connection);
     assertEquals(200, connection.getResponseCode());
   }
@@ -1106,6 +1260,41 @@
     assertEquals(1, server.takeRequest().getSequenceNumber());
   }
 
+  @Test public void transparentGzipWorksAfterExceptionRecovery() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("a")
+        .setSocketPolicy(SHUTDOWN_INPUT_AT_END));
+    server.enqueue(new MockResponse()
+        .addHeader("Content-Encoding: gzip")
+        .setBody(gzip("b".getBytes(UTF_8))));
+    server.play();
+
+    // Seed the pool with a bad connection.
+    assertContent("a", client.open(server.getUrl("/")));
+
+    // This connection will need to be recovered. When it is, transparent gzip should still work!
+    assertContent("b", client.open(server.getUrl("/")));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // Connection is not pooled.
+  }
+
+  @Test public void endOfStreamResponseIsNotPooled() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("{}")
+        .clearHeaders()
+        .setSocketPolicy(DISCONNECT_AT_END));
+    server.play();
+
+    ConnectionPool pool = ConnectionPool.getDefault();
+    pool.evictAll();
+    client.setConnectionPool(pool);
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertContent("{}", connection);
+    assertEquals(0, client.getConnectionPool().getConnectionCount());
+  }
+
   @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
     testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED);
   }
@@ -1125,40 +1314,62 @@
 
     server.play();
 
-    URLConnection connection1 = client.open(server.getUrl("/"));
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
     InputStream in1 = connection1.getInputStream();
     assertEquals("ABCDE", readAscii(in1, 5));
-    in1.close();
+    connection1.disconnect();
 
     HttpURLConnection connection2 = client.open(server.getUrl("/"));
     InputStream in2 = connection2.getInputStream();
     assertEquals("LMNOP", readAscii(in2, 5));
-    in2.close();
+    connection2.disconnect();
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
     assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection is pooled!
   }
 
-  /**
-   * Obnoxiously test that the chunk sizes transmitted exactly equal the
-   * requested data+chunk header size. Although setChunkedStreamingMode()
-   * isn't specific about whether the size applies to the data or the
-   * complete chunk, the RI interprets it as a complete chunk.
-   */
+  @Test public void streamDiscardingIsTimely() throws Exception {
+    // This response takes at least a full second to serve: 10,000 bytes served 100 bytes at a time.
+    server.enqueue(new MockResponse()
+        .setBody(new byte[10000])
+        .throttleBody(100, 10, MILLISECONDS));
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    long startNanos = System.nanoTime();
+    URLConnection connection1 = client.open(server.getUrl("/"));
+    InputStream in = connection1.getInputStream();
+    in.close();
+    long elapsedNanos = System.nanoTime() - startNanos;
+    long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+    // If we're working correctly, this should be greater than 100ms, but less than double that.
+    // Previously we had a bug where we would download the entire response body as long as no
+    // individual read took longer than 100ms.
+    assertTrue(String.format("Time to close: %sms", elapsedMillis), elapsedMillis < 500);
+
+    // Do another request to confirm that the discarded connection was not pooled.
+    assertContent("A", client.open(server.getUrl("/")));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // Connection is not pooled.
+  }
+
   @Test public void setChunkedStreamingMode() throws IOException, InterruptedException {
     server.enqueue(new MockResponse());
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.setChunkedStreamingMode(8);
-    urlConnection.setDoOutput(true);
-    OutputStream outputStream = urlConnection.getOutputStream();
-    outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
-    assertEquals(200, urlConnection.getResponseCode());
+    String body = "ABCDEFGHIJKLMNOPQ";
+    connection = client.open(server.getUrl("/"));
+    connection.setChunkedStreamingMode(0); // OkHttp does not honor specific chunk sizes.
+    connection.setDoOutput(true);
+    OutputStream outputStream = connection.getOutputStream();
+    outputStream.write(body.getBytes("US-ASCII"));
+    assertEquals(200, connection.getResponseCode());
 
     RecordedRequest request = server.takeRequest();
-    assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
-    assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
+    assertEquals(body, new String(request.getBody(), "US-ASCII"));
+    assertEquals(Arrays.asList(body.length()), request.getChunkSizes());
   }
 
   @Test public void authenticateWithFixedLengthStreaming() throws Exception {
@@ -1177,7 +1388,7 @@
     server.play();
 
     Authenticator.setDefault(new RecordingAuthenticator());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     byte[] requestBody = { 'A', 'B', 'C', 'D' };
     if (streamingMode == StreamingMode.FIXED_LENGTH) {
@@ -1261,7 +1472,6 @@
     server.enqueue(pleaseAuthenticate);
     server.play();
 
-    HttpURLConnection connection;
     if (proxy) {
       client.setProxy(server.toProxyAddress());
       connection = client.open(new URL("http://android.com"));
@@ -1281,10 +1491,11 @@
     assertValidRequestMethod("POST");
     assertValidRequestMethod("PUT");
     assertValidRequestMethod("TRACE");
+    assertValidRequestMethod("PATCH");
   }
 
   private void assertValidRequestMethod(String requestMethod) throws Exception {
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setRequestMethod(requestMethod);
     assertEquals(requestMethod, connection.getRequestMethod());
   }
@@ -1300,7 +1511,7 @@
   }
 
   private void assertInvalidRequestMethod(String requestMethod) throws Exception {
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
       connection.setRequestMethod(requestMethod);
       fail();
@@ -1308,9 +1519,35 @@
     }
   }
 
+  @Test public void shoutcast() throws Exception {
+    server.enqueue(new MockResponse().setStatus("ICY 200 OK")
+        // .addHeader("HTTP/1.0 200 OK")
+        .addHeader("Accept-Ranges: none")
+        .addHeader("Content-Type: audio/mpeg")
+        .addHeader("icy-br:128")
+        .addHeader("ice-audio-info: bitrate=128;samplerate=44100;channels=2")
+        .addHeader("icy-br:128")
+        .addHeader("icy-description:Rock")
+        .addHeader("icy-genre:riders")
+        .addHeader("icy-name:A2RRock")
+        .addHeader("icy-pub:1")
+        .addHeader("icy-url:http://www.A2Rradio.com")
+        .addHeader("Server: Icecast 2.3.3-kh8")
+        .addHeader("Cache-Control: no-cache")
+        .addHeader("Pragma: no-cache")
+        .addHeader("Expires: Mon, 26 Jul 1997 05:00:00 GMT")
+        .addHeader("icy-metaint:16000")
+        .setBody("mp3 data"));
+    server.play();
+    connection = client.open(server.getUrl("/"));
+    assertEquals(200, connection.getResponseCode());
+    assertEquals("OK", connection.getResponseMessage());
+    assertContent("mp3 data", connection);
+  }
+
   @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception {
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
       connection.setFixedLengthStreamingMode(-2);
       fail();
@@ -1320,14 +1557,14 @@
 
   @Test public void canSetNegativeChunkedStreamingMode() throws Exception {
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setChunkedStreamingMode(-2);
   }
 
   @Test public void cannotSetFixedLengthStreamingModeAfterConnect() throws Exception {
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
     try {
       connection.setFixedLengthStreamingMode(1);
@@ -1339,7 +1576,7 @@
   @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception {
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
     try {
       connection.setChunkedStreamingMode(1);
@@ -1350,7 +1587,7 @@
 
   @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setChunkedStreamingMode(1);
     try {
       connection.setFixedLengthStreamingMode(1);
@@ -1361,7 +1598,7 @@
 
   @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception {
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setFixedLengthStreamingMode(1);
     try {
       connection.setChunkedStreamingMode(1);
@@ -1389,7 +1626,7 @@
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     byte[] requestBody = { 'A', 'B', 'C', 'D' };
     if (streamingMode == StreamingMode.FIXED_LENGTH) {
@@ -1429,7 +1666,7 @@
     server.play();
 
     Authenticator.setDefault(new RecordingAuthenticator());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     byte[] requestBody = { 'A', 'B', 'C', 'D' };
     OutputStream outputStream = connection.getOutputStream();
@@ -1464,7 +1701,40 @@
     server.play();
 
     Authenticator.setDefault(new RecordingAuthenticator());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
+    assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    // no authorization header for the first request...
+    RecordedRequest request = server.takeRequest();
+    assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+
+    // ...but the three requests that follow requests include an authorization header
+    for (int i = 0; i < 3; i++) {
+      request = server.takeRequest();
+      assertEquals("GET / HTTP/1.1", request.getRequestLine());
+      assertContains(request.getHeaders(),
+          "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+    }
+  }
+
+  /** https://code.google.com/p/android/issues/detail?id=74026 */
+  @Test public void authenticateWithGetAndTransparentGzip() throws Exception {
+    MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
+        .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+        .setBody("Please authenticate.");
+    // fail auth three times...
+    server.enqueue(pleaseAuthenticate);
+    server.enqueue(pleaseAuthenticate);
+    server.enqueue(pleaseAuthenticate);
+    // ...then succeed the fourth time
+    MockResponse successfulResponse = new MockResponse()
+        .addHeader("Content-Encoding", "gzip")
+        .setBody(gzip("Successful auth!".getBytes("UTF-8")));
+    server.enqueue(successfulResponse);
+    server.play();
+
+    Authenticator.setDefault(new RecordingAuthenticator());
+    connection = client.open(server.getUrl("/"));
     assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
 
     // no authorization header for the first request...
@@ -1489,7 +1759,7 @@
     server.play();
 
     Authenticator.setDefault(new RecordingAuthenticator());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
   }
 
@@ -1536,7 +1806,7 @@
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("This is the new location!",
         readAscii(connection.getInputStream(), Integer.MAX_VALUE));
 
@@ -1557,7 +1827,7 @@
     client.setFollowProtocolRedirects(false);
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
   }
 
@@ -1568,7 +1838,7 @@
     server.play();
 
     client.setFollowProtocolRedirects(false);
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
   }
 
@@ -1609,7 +1879,7 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     client.setFollowProtocolRedirects(true);
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertContent("This is secure HTTPS!", connection);
     assertFalse(connection instanceof HttpsURLConnection);
   }
@@ -1642,7 +1912,7 @@
     server.enqueue(new MockResponse().setBody("This is the first server again!"));
     server.play();
 
-    URLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertContent("This is the 2nd server!", connection);
     assertEquals(server2.getUrl("/"), connection.getURL());
 
@@ -1690,30 +1960,40 @@
 
   @Test public void response300MultipleChoiceWithPost() throws Exception {
     // Chrome doesn't follow the redirect, but Firefox and the RI both do
-    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE);
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE, TransferKind.END_OF_STREAM);
   }
 
   @Test public void response301MovedPermanentlyWithPost() throws Exception {
-    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM);
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM, TransferKind.END_OF_STREAM);
   }
 
   @Test public void response302MovedTemporarilyWithPost() throws Exception {
-    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP);
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.END_OF_STREAM);
   }
 
   @Test public void response303SeeOtherWithPost() throws Exception {
-    testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER);
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER, TransferKind.END_OF_STREAM);
   }
 
-  private void testResponseRedirectedWithPost(int redirectCode) throws Exception {
+  @Test public void postRedirectToGetWithChunkedRequest() throws Exception {
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.CHUNKED);
+  }
+
+  @Test public void postRedirectToGetWithStreamedRequest() throws Exception {
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.FIXED_LENGTH);
+  }
+
+  private void testResponseRedirectedWithPost(int redirectCode, TransferKind transferKind)
+      throws Exception {
     server.enqueue(new MockResponse().setResponseCode(redirectCode)
         .addHeader("Location: /page2")
         .setBody("This page has moved!"));
     server.enqueue(new MockResponse().setBody("Page 2"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/page1"));
+    connection = client.open(server.getUrl("/page1"));
     connection.setDoOutput(true);
+    transferKind.setForRequest(connection, 4);
     byte[] requestBody = { 'A', 'B', 'C', 'D' };
     OutputStream outputStream = connection.getOutputStream();
     outputStream.write(requestBody);
@@ -1736,7 +2016,7 @@
     server.enqueue(new MockResponse().setBody("Page 2"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/page1"));
+    connection = client.open(server.getUrl("/page1"));
     connection.setDoOutput(true);
     connection.addRequestProperty("Content-Length", "4");
     connection.addRequestProperty("Content-Type", "text/plain; charset=utf-8");
@@ -1762,7 +2042,7 @@
         .setBody("This page has moved!"));
     server.enqueue(new MockResponse().setBody("Proxy Response"));
 
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    connection = client.open(server.getUrl("/foo"));
     // Fails on the RI, which gets "Proxy Response"
     assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
 
@@ -1789,7 +2069,7 @@
 
   private void test307Redirect(String method) throws Exception {
     MockResponse response1 = new MockResponse()
-        .setResponseCode(HttpURLConnectionImpl.HTTP_TEMP_REDIRECT)
+        .setResponseCode(HTTP_TEMP_REDIRECT)
         .addHeader("Location: /page2");
     if (!method.equals("HEAD")) {
       response1.setBody("This page has moved!");
@@ -1798,7 +2078,7 @@
     server.enqueue(new MockResponse().setBody("Page 2"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/page1"));
+    connection = client.open(server.getUrl("/page1"));
     connection.setRequestMethod(method);
     byte[] requestBody = { 'A', 'B', 'C', 'D' };
     if (method.equals("POST")) {
@@ -1844,7 +2124,7 @@
     server.enqueue(new MockResponse().setBody("Success!"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/0"));
+    connection = client.open(server.getUrl("/0"));
     assertContent("Success!", connection);
     assertEquals(server.getUrl("/20"), connection.getURL());
   }
@@ -1857,7 +2137,7 @@
     }
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/0"));
+    connection = client.open(server.getUrl("/0"));
     try {
       connection.getInputStream();
       fail();
@@ -1902,9 +2182,9 @@
     server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive
     server.play();
 
-    URLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.setReadTimeout(1000);
-    InputStream in = urlConnection.getInputStream();
+    URLConnection connection = client.open(server.getUrl("/"));
+    connection.setReadTimeout(1000);
+    InputStream in = connection.getInputStream();
     assertEquals('A', in.read());
     assertEquals('B', in.read());
     assertEquals('C', in.read());
@@ -1919,11 +2199,11 @@
     server.enqueue(new MockResponse());
     server.play();
 
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.setRequestProperty("Transfer-encoding", "chunked");
-    urlConnection.setDoOutput(true);
-    urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
-    assertEquals(200, urlConnection.getResponseCode());
+    connection = client.open(server.getUrl("/"));
+    connection.setRequestProperty("Transfer-encoding", "chunked");
+    connection.setDoOutput(true);
+    connection.getOutputStream().write("ABC".getBytes("UTF-8"));
+    assertEquals(200, connection.getResponseCode());
 
     RecordedRequest request = server.takeRequest();
     assertEquals("ABC", new String(request.getBody(), "UTF-8"));
@@ -2007,10 +2287,10 @@
   }
 
   @Test public void singleByteReadIsSigned() throws IOException {
-    server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
+    server.enqueue(new MockResponse().setBody(new byte[] {-2, -1}));
     server.play();
 
-    URLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     InputStream in = connection.getInputStream();
     assertEquals(254, in.read());
     assertEquals(255, in.read());
@@ -2038,7 +2318,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     byte[] upload = "def".getBytes("UTF-8");
 
@@ -2052,7 +2332,7 @@
     out.write(upload);
     assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
 
-    out.flush(); // dubious but permitted
+    out.flush(); // Dubious but permitted.
     try {
       out.write("ghi".getBytes("UTF-8"));
       fail();
@@ -2069,7 +2349,7 @@
     }
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
       connection.getInputStream();
       fail();
@@ -2084,7 +2364,7 @@
   }
 
   @Test public void dnsFailureThrowsIOException() throws IOException {
-    HttpURLConnection connection = client.open(new URL("http://host.unlikelytld"));
+    connection = client.open(new URL("http://host.unlikelytld"));
     try {
       connection.connect();
       fail();
@@ -2093,7 +2373,7 @@
   }
 
   @Test public void malformedUrlThrowsUnknownHostException() throws IOException {
-    HttpURLConnection connection = client.open(new URL("http:///foo.html"));
+    connection = client.open(new URL("http:///foo.html"));
     try {
       connection.connect();
       fail();
@@ -2107,11 +2387,10 @@
     server.play();
 
     // The request should work once and then fail
-    URLConnection connection1 = client.open(server.getUrl(""));
+    HttpURLConnection connection1 = client.open(server.getUrl(""));
     connection1.setReadTimeout(100);
     InputStream input = connection1.getInputStream();
     assertEquals("ABC", readAscii(input, Integer.MAX_VALUE));
-    input.close();
     server.shutdown();
     try {
       HttpURLConnection connection2 = client.open(server.getUrl(""));
@@ -2162,7 +2441,7 @@
         .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     InputStream in = connection.getInputStream();
     assertEquals("ABC", readAscii(in, 3));
     assertEquals(-1, in.read());
@@ -2172,7 +2451,7 @@
   @Test public void getContent() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     InputStream in = (InputStream) connection.getContent();
     assertEquals("A", readAscii(in, Integer.MAX_VALUE));
   }
@@ -2180,7 +2459,7 @@
   @Test public void getContentOfType() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
       connection.getContent(null);
       fail();
@@ -2191,14 +2470,13 @@
       fail();
     } catch (NullPointerException expected) {
     }
-    assertNull(connection.getContent(new Class[] { getClass() }));
-    connection.disconnect();
+    assertNull(connection.getContent(new Class[] {getClass()}));
   }
 
   @Test public void getOutputStreamOnGetFails() throws Exception {
     server.enqueue(new MockResponse());
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
       connection.getOutputStream();
       fail();
@@ -2209,7 +2487,7 @@
   @Test public void getOutputAfterGetInputStreamFails() throws Exception {
     server.enqueue(new MockResponse());
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     try {
       connection.getInputStream();
@@ -2222,7 +2500,7 @@
   @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception {
     server.enqueue(new MockResponse());
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.connect();
     try {
       connection.setDoOutput(true);
@@ -2234,13 +2512,12 @@
       fail();
     } catch (IllegalStateException expected) {
     }
-    connection.disconnect();
   }
 
   @Test public void clientSendsContentLength() throws Exception {
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     OutputStream out = connection.getOutputStream();
     out.write(new byte[] { 'A', 'B', 'C' });
@@ -2253,25 +2530,22 @@
   @Test public void getContentLengthConnects() throws Exception {
     server.enqueue(new MockResponse().setBody("ABC"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals(3, connection.getContentLength());
-    connection.disconnect();
   }
 
   @Test public void getContentTypeConnects() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("text/plain", connection.getContentType());
-    connection.disconnect();
   }
 
   @Test public void getContentEncodingConnects() throws Exception {
     server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC"));
     server.play();
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     assertEquals("identity", connection.getContentEncoding());
-    connection.disconnect();
   }
 
   // http://b/4361656
@@ -2303,7 +2577,7 @@
     transferKind.setBody(response, body, 4);
     server.enqueue(response);
     server.play();
-    URLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     InputStream in = connection.getInputStream();
     for (int i = 0; i < body.length(); i++) {
       assertTrue(in.available() >= 0);
@@ -2342,6 +2616,7 @@
       throws Exception {
     server.enqueue(new MockResponse().setBody("A").setSocketPolicy(SHUTDOWN_INPUT_AT_END));
     server.enqueue(new MockResponse().setBody("B"));
+    server.enqueue(new MockResponse().setBody("C"));
     server.play();
 
     assertContent("A", client.open(server.getUrl("/a")));
@@ -2350,7 +2625,7 @@
     byte[] requestBody = new byte[requestSize];
     new Random(0).nextBytes(requestBody);
 
-    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection = client.open(server.getUrl("/b"));
     connection.setRequestMethod("POST");
     transferKind.setForRequest(connection, requestBody.length);
     for (int i = 0; i < requestBody.length; i += 1024) {
@@ -2370,7 +2645,7 @@
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection = client.open(server.getUrl("/b"));
     connection.setRequestProperty("Content-Length", "4");
     connection.setRequestMethod("POST");
     OutputStream out = connection.getOutputStream();
@@ -2388,7 +2663,7 @@
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/b"));
+    connection = client.open(server.getUrl("/b"));
     connection.setRequestProperty("Content-Length", "3");
     connection.setRequestMethod("POST");
     OutputStream out = connection.getOutputStream();
@@ -2397,6 +2672,7 @@
     out.write('c');
     try {
       out.write('d');
+      out.flush();
       fail();
     } catch (IOException expected) {
     }
@@ -2429,26 +2705,26 @@
   @Test public void emptyRequestHeaderValueIsAllowed() throws Exception {
     server.enqueue(new MockResponse().setBody("body"));
     server.play();
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.addRequestProperty("B", "");
-    assertContent("body", urlConnection);
-    assertEquals("", urlConnection.getRequestProperty("B"));
+    connection = client.open(server.getUrl("/"));
+    connection.addRequestProperty("B", "");
+    assertContent("body", connection);
+    assertEquals("", connection.getRequestProperty("B"));
   }
 
   @Test public void emptyResponseHeaderValueIsAllowed() throws Exception {
     server.enqueue(new MockResponse().addHeader("A:").setBody("body"));
     server.play();
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    assertContent("body", urlConnection);
-    assertEquals("", urlConnection.getHeaderField("A"));
+    connection = client.open(server.getUrl("/"));
+    assertContent("body", connection);
+    assertEquals("", connection.getHeaderField("A"));
   }
 
   @Test public void emptyRequestHeaderNameIsStrict() throws Exception {
     server.enqueue(new MockResponse().setBody("body"));
     server.play();
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     try {
-      urlConnection.setRequestProperty("", "A");
+      connection.setRequestProperty("", "A");
       fail();
     } catch (IllegalArgumentException expected) {
     }
@@ -2457,9 +2733,9 @@
   @Test public void emptyResponseHeaderNameIsLenient() throws Exception {
     server.enqueue(new MockResponse().addHeader(":A").setBody("body"));
     server.play();
-    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-    urlConnection.getResponseCode();
-    assertEquals("A", urlConnection.getHeaderField(""));
+    connection = client.open(server.getUrl("/"));
+    connection.getResponseCode();
+    assertEquals("A", connection.getHeaderField(""));
   }
 
   @Test @Ignore public void deflateCompression() {
@@ -2491,31 +2767,95 @@
     assertContains(server.takeRequest().getHeaders(),
         "Authorization: " + credential.getHeaderValue());
 
-    assertEquals(1, authenticator.calls.size());
-    String call = authenticator.calls.get(0);
-    assertTrue(call, call.contains("proxy=DIRECT"));
-    assertTrue(call, call.contains("url=" + server.getUrl("/private")));
-    assertTrue(call, call.contains("challenges=[Basic realm=\"protected area\"]"));
+    assertEquals(Proxy.NO_PROXY, authenticator.onlyProxy());
+    URL url = authenticator.onlyUrl();
+    assertEquals("/private", url.getPath());
+    assertEquals(Arrays.asList(new Challenge("Basic", "protected area")), authenticator.onlyChallenge());
   }
 
-  @Test public void setTransports() throws Exception {
+  @Test public void npnSetsProtocolHeader_SPDY_3() throws Exception {
+    npnSetsProtocolHeader(Protocol.SPDY_3);
+  }
+
+  @Test public void npnSetsProtocolHeader_HTTP_2() throws Exception {
+    npnSetsProtocolHeader(Protocol.HTTP_2);
+  }
+
+  private void npnSetsProtocolHeader(Protocol protocol) throws IOException {
+    enableNpn(protocol);
     server.enqueue(new MockResponse().setBody("A"));
     server.play();
-    client.setTransports(Arrays.asList("http/1.1"));
+    client.setProtocols(Arrays.asList(Protocol.HTTP_11, protocol));
+    connection = client.open(server.getUrl("/"));
+    List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
+    assertEquals(Arrays.asList(protocol.name.utf8()), protocolValues);
+    assertContent("A", connection);
+  }
+
+  /** For example, empty Protobuf RPC messages end up as a zero-length POST. */
+  @Test public void zeroLengthPost() throws IOException, InterruptedException {
+    zeroLengthPayload("POST");
+  }
+
+  @Test public void zeroLengthPost_SPDY_3() throws Exception {
+    enableNpn(Protocol.SPDY_3);
+    zeroLengthPost();
+  }
+
+  @Test public void zeroLengthPost_HTTP_2() throws Exception {
+    enableNpn(Protocol.HTTP_2);
+    zeroLengthPost();
+  }
+
+  /** For example, creating an Amazon S3 bucket ends up as a zero-length POST. */
+  @Test public void zeroLengthPut() throws IOException, InterruptedException {
+    zeroLengthPayload("PUT");
+  }
+
+  @Test public void zeroLengthPut_SPDY_3() throws Exception {
+    enableNpn(Protocol.SPDY_3);
+    zeroLengthPut();
+  }
+
+  @Test public void zeroLengthPut_HTTP_2() throws Exception {
+    enableNpn(Protocol.HTTP_2);
+    zeroLengthPut();
+  }
+
+  private void zeroLengthPayload(String method)
+      throws IOException, InterruptedException {
+    server.enqueue(new MockResponse());
+    server.play();
+    connection = client.open(server.getUrl("/"));
+    connection.setRequestProperty("Content-Length", "0");
+    connection.setRequestMethod(method);
+    connection.setFixedLengthStreamingMode(0);
+    connection.setDoOutput(true);
+    assertContent("", connection);
+    RecordedRequest zeroLengthPayload = server.takeRequest();
+    assertEquals(method, zeroLengthPayload.getMethod());
+    assertEquals("0", zeroLengthPayload.getHeader("content-length"));
+    assertEquals(0L, zeroLengthPayload.getBodySize());
+  }
+
+  @Test public void setProtocols() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    client.setProtocols(Arrays.asList(Protocol.HTTP_11));
     assertContent("A", client.open(server.getUrl("/")));
   }
 
-  @Test public void setTransportsWithoutHttp11() throws Exception {
+  @Test public void setProtocolsWithoutHttp11() throws Exception {
     try {
-      client.setTransports(Arrays.asList("spdy/3"));
+      client.setProtocols(Arrays.asList(Protocol.SPDY_3));
       fail();
     } catch (IllegalArgumentException expected) {
     }
   }
 
-  @Test public void setTransportsWithNull() throws Exception {
+  @Test public void setProtocolsWithNull() throws Exception {
     try {
-      client.setTransports(Arrays.asList("http/1.1", null));
+      client.setProtocols(Arrays.asList(Protocol.HTTP_11, null));
       fail();
     } catch (IllegalArgumentException expected) {
     }
@@ -2526,7 +2866,7 @@
     server.enqueue(new MockResponse());
     server.play();
 
-    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection = client.open(server.getUrl("/"));
     connection.setDoOutput(true);
     long contentLength = Integer.MAX_VALUE + 1L;
     connection.setFixedLengthStreamingMode(contentLength);
@@ -2543,6 +2883,78 @@
     assertEquals(Long.toString(contentLength), request.getHeader("Content-Length"));
   }
 
+  /**
+   * We had a bug where we attempted to gunzip responses that didn't have a
+   * body. This only came up with 304s since that response code can include
+   * headers (like "Content-Encoding") without any content to go along with it.
+   * https://github.com/square/okhttp/issues/358
+   */
+  @Test public void noTransparentGzipFor304NotModified() throws Exception {
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
+        .addHeader("Content-Encoding: gzip"));
+    server.enqueue(new MockResponse().setBody("b"));
+
+    server.play();
+
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection1.getResponseCode());
+    assertContent("", connection1);
+
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
+    assertContent("b", connection2);
+
+    RecordedRequest requestA = server.takeRequest();
+    assertEquals(0, requestA.getSequenceNumber());
+
+    RecordedRequest requestB = server.takeRequest();
+    assertEquals(1, requestB.getSequenceNumber());
+  }
+
+  /**
+   * We had a bug where we weren't closing Gzip streams on redirects.
+   * https://github.com/square/okhttp/issues/441
+   */
+  @Test public void gzipWithRedirectAndConnectionReuse() throws Exception {
+    server.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: /foo")
+        .addHeader("Content-Encoding: gzip")
+        .setBody(gzip("Moved! Moved! Moved!".getBytes(UTF_8))));
+    server.enqueue(new MockResponse().setBody("This is the new page!"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertContent("This is the new page!", connection);
+
+    RecordedRequest requestA = server.takeRequest();
+    assertEquals(0, requestA.getSequenceNumber());
+
+    RecordedRequest requestB = server.takeRequest();
+    assertEquals(1, requestB.getSequenceNumber());
+  }
+
+  /**
+   * The RFC is unclear in this regard as it only specifies that this should
+   * invalidate the cache entry (if any).
+   */
+  @Test public void bodyPermittedOnDelete() throws Exception {
+    server.enqueue(new MockResponse());
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setRequestMethod("DELETE");
+    connection.setDoOutput(true);
+    connection.getOutputStream().write("BODY".getBytes(UTF_8));
+    assertEquals(200, connection.getResponseCode());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("DELETE", request.getMethod());
+    assertEquals("BODY", new String(request.getBody(), UTF_8));
+  }
+
   /** Returns a gzipped copy of {@code bytes}. */
   public byte[] gzip(byte[] bytes) throws IOException {
     ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
@@ -2556,14 +2968,13 @@
    * Reads at most {@code limit} characters from {@code in} and asserts that
    * content equals {@code expected}.
    */
-  private void assertContent(String expected, URLConnection connection, int limit)
+  private void assertContent(String expected, HttpURLConnection connection, int limit)
       throws IOException {
     connection.connect();
     assertEquals(expected, readAscii(connection.getInputStream(), limit));
-    ((HttpURLConnection) connection).disconnect();
   }
 
-  private void assertContent(String expected, URLConnection connection) throws IOException {
+  private void assertContent(String expected, HttpURLConnection connection) throws IOException {
     assertContent(expected, connection, Integer.MAX_VALUE);
   }
 
@@ -2711,4 +3122,192 @@
     @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
     }
   }
+
+  /**
+   * Tests that use this will fail unless boot classpath is set. Ex. {@code
+   * -Xbootclasspath/p:/tmp/npn-boot-8.1.2.v20120308.jar}
+   */
+  private void enableNpn(Protocol protocol) {
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_11));
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.setNpnEnabled(true);
+    server.setNpnProtocols(client.getProtocols());
+  }
+
+  /**
+   * An {@link SSLSocketFactory} that delegates all method calls.
+   */
+  private static abstract class DelegatingSSLSocketFactory extends SSLSocketFactory {
+
+    private final SSLSocketFactory delegate;
+
+    public DelegatingSSLSocketFactory(SSLSocketFactory delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public String[] getDefaultCipherSuites() {
+      return delegate.getDefaultCipherSuites();
+    }
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+      return delegate.getSupportedCipherSuites();
+    }
+
+    @Override
+    public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
+        throws IOException {
+      return (SSLSocket) delegate.createSocket(s, host, port, autoClose);
+    }
+
+    @Override
+    public SSLSocket createSocket() throws IOException {
+      return (SSLSocket) delegate.createSocket();
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+      return (SSLSocket) delegate.createSocket(host, port);
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port, InetAddress localHost,
+        int localPort) throws IOException, UnknownHostException {
+      return (SSLSocket) delegate.createSocket(host, port, localHost, localPort);
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+      return (SSLSocket) delegate.createSocket(host, port);
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress address, int port,
+        InetAddress localAddress, int localPort) throws IOException {
+      return (SSLSocket) delegate.createSocket(address, port, localAddress, localPort);
+    }
+  }
+
+  /**
+   * An {@link SSLSocketFactory} that creates sockets using a delegate, but overrides the enabled
+   * protocols for any created sockets.
+   */
+  private static class LimitedProtocolsSocketFactory extends DelegatingSSLSocketFactory {
+
+    private final String[] enabledProtocols;
+
+    public LimitedProtocolsSocketFactory(SSLSocketFactory delegate, String... enabledProtocols) {
+      super(delegate);
+      this.enabledProtocols = enabledProtocols;
+    }
+
+    @Override
+    public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
+        throws IOException {
+      SSLSocket socket = super.createSocket(s, host, port, autoClose);
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket() throws IOException {
+      SSLSocket socket = super.createSocket();
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+      SSLSocket socket = super.createSocket(host, port);
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port, InetAddress localHost, int localPort)
+        throws IOException, UnknownHostException {
+      SSLSocket socket = super.createSocket(host, port, localHost, localPort);
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+      SSLSocket socket = super.createSocket(host, port);
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress address, int port, InetAddress localAddress,
+        int localPort) throws IOException {
+      SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
+      socket.setEnabledProtocols(enabledProtocols);
+      return socket;
+    }
+  }
+
+  /**
+   * An SSLSocketFactory that delegates calls and keeps a record of any sockets created.
+   */
+  private static class RecordingSocketFactory extends DelegatingSSLSocketFactory {
+
+    private final List<SSLSocket> createdSockets = new ArrayList<SSLSocket>();
+
+    public RecordingSocketFactory(SSLSocketFactory delegate) {
+      super(delegate);
+    }
+
+    @Override
+    public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
+        throws IOException {
+      SSLSocket socket = super.createSocket(s, host, port, autoClose);
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket() throws IOException {
+      SSLSocket socket = super.createSocket();
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+      SSLSocket socket = super.createSocket(host, port);
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(String host, int port, InetAddress localHost,
+        int localPort) throws IOException, UnknownHostException {
+      SSLSocket socket = super.createSocket(host, port, localHost, localPort);
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+      SSLSocket socket = super.createSocket(host, port);
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    @Override
+    public SSLSocket createSocket(InetAddress address, int port,
+        InetAddress localAddress, int localPort) throws IOException {
+      SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
+      createdSockets.add(socket);
+      return socket;
+    }
+
+    public List<SSLSocket> getCreatedSockets() {
+      return createdSockets;
+    }
+  }
 }
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
new file mode 100644
index 0000000..8f70922
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.List;
+import okio.BufferedSource;
+import okio.ByteString;
+
+import static org.junit.Assert.fail;
+
+class BaseTestHandler implements FrameReader.Handler {
+  @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)
+      throws IOException {
+    fail();
+  }
+
+  @Override
+  public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
+      int priority, List<Header> headerBlock, HeadersMode headersMode) {
+    fail();
+  }
+
+  @Override public void rstStream(int streamId, ErrorCode errorCode) {
+    fail();
+  }
+
+  @Override public void settings(boolean clearPrevious, Settings settings) {
+    fail();
+  }
+
+  @Override public void ackSettings() {
+    fail();
+  }
+
+  @Override public void ping(boolean ack, int payload1, int payload2) {
+    fail();
+  }
+
+  @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+    fail();
+  }
+
+  @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
+    fail();
+  }
+
+  @Override public void priority(int streamId, int priority) {
+    fail();
+  }
+
+  @Override
+  public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) {
+    fail();
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java
new file mode 100644
index 0000000..42ddcab
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java
@@ -0,0 +1,890 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okio.ByteString;
+import okio.OkBuffer;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.headerEntries;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class HpackDraft05Test {
+
+  private final OkBuffer bytesIn = new OkBuffer();
+  private HpackDraft05.Reader hpackReader;
+  private OkBuffer bytesOut = new OkBuffer();
+  private HpackDraft05.Writer hpackWriter;
+
+  @Before public void reset() {
+    hpackReader = newReader(bytesIn);
+    hpackWriter = new HpackDraft05.Writer(bytesOut);
+  }
+
+  /**
+   * Variable-length quantity special cases strings which are longer than 127
+   * bytes.  Values such as cookies can be 4KiB, and should be possible to send.
+   *
+   * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2
+   */
+  @Test public void largeHeaderValue() throws IOException {
+    char[] value = new char[4096];
+    Arrays.fill(value, '!');
+    List<Header> headerBlock = headerEntries("cookie", new String(value));
+
+    hpackWriter.writeHeaders(headerBlock);
+    bytesIn.write(bytesOut, bytesOut.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(0, hpackReader.headerCount);
+
+    assertEquals(headerBlock, hpackReader.getAndReset());
+  }
+
+  /**
+   * HPACK has a max header table size, which can be smaller than the max header message.
+   * Ensure the larger header content is not lost.
+   */
+  @Test public void tooLargeToHPackIsStillEmitted() throws IOException {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-key");
+
+    out.writeByte(0x0d); // Literal value (len = 13)
+    out.writeUtf8("custom-header");
+
+    bytesIn.write(out, out.size());
+    hpackReader.maxHeaderTableByteCount(1);
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(0, hpackReader.headerCount);
+
+    assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndReset());
+  }
+
+  /** Oldest entries are evicted to support newer ones. */
+  @Test public void testEviction() throws IOException {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-foo");
+
+    out.writeByte(0x0d); // Literal value (len = 13)
+    out.writeUtf8("custom-header");
+
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-bar");
+
+    out.writeByte(0x0d); // Literal value (len = 13)
+    out.writeUtf8("custom-header");
+
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-baz");
+
+    out.writeByte(0x0d); // Literal value (len = 13)
+    out.writeUtf8("custom-header");
+
+    bytesIn.write(out, out.size());
+    // Set to only support 110 bytes (enough for 2 headers).
+    hpackReader.maxHeaderTableByteCount(110);
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(2, hpackReader.headerCount);
+
+    Header entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, "custom-bar", "custom-header", 55);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, "custom-baz", "custom-header", 55);
+    assertHeaderReferenced(headerTableLength() - 2);
+
+    // foo isn't here as it is no longer in the table.
+    // TODO: emit before eviction?
+    assertEquals(headerEntries("custom-bar", "custom-header", "custom-baz", "custom-header"),
+        hpackReader.getAndReset());
+
+    // Simulate receiving a small settings frame, that implies eviction.
+    hpackReader.maxHeaderTableByteCount(55);
+    assertEquals(1, hpackReader.headerCount);
+  }
+
+  /** Header table backing array is initially 8 long, let's ensure it grows. */
+  @Test public void dynamicallyGrowsBeyond64Entries() throws IOException {
+    OkBuffer out = new OkBuffer();
+
+    for (int i = 0; i < 256; i++) {
+      out.writeByte(0x00); // Literal indexed
+      out.writeByte(0x0a); // Literal name (len = 10)
+      out.writeUtf8("custom-foo");
+
+      out.writeByte(0x0d); // Literal value (len = 13)
+      out.writeUtf8("custom-header");
+    }
+
+    bytesIn.write(out, out.size());
+    hpackReader.maxHeaderTableByteCount(16384); // Lots of headers need more room!
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(256, hpackReader.headerCount);
+    assertHeaderReferenced(headerTableLength() - 1);
+    assertHeaderReferenced(headerTableLength() - hpackReader.headerCount);
+  }
+
+  @Test public void huffmanDecodingSupported() throws IOException {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x04); // == Literal indexed ==
+                         // Indexed name (idx = 4) -> :path
+    out.writeByte(0x8b); // Literal value Huffman encoded 11 bytes
+                         // decodes to www.example.com which is length 15
+    byte[] huffmanBytes = new byte[] {
+        (byte) 0xdb, (byte) 0x6d, (byte) 0x88, (byte) 0x3e,
+        (byte) 0x68, (byte) 0xd1, (byte) 0xcb, (byte) 0x12,
+        (byte) 0x25, (byte) 0xba, (byte) 0x7f};
+    out.write(huffmanBytes, 0, huffmanBytes.length);
+
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(1, hpackReader.headerCount);
+    assertEquals(52, hpackReader.headerTableByteCount);
+
+    Header entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":path", "www.example.com", 52);
+    assertHeaderReferenced(headerTableLength() - 1);
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.1
+   */
+  @Test public void readLiteralHeaderFieldWithIndexing() throws IOException {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-key");
+
+    out.writeByte(0x0d); // Literal value (len = 13)
+    out.writeUtf8("custom-header");
+
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(1, hpackReader.headerCount);
+    assertEquals(55, hpackReader.headerTableByteCount);
+
+    Header entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, "custom-key", "custom-header", 55);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndReset());
+  }
+
+  /**
+   * Literal Header Field without Indexing - New Name
+   */
+  @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException {
+    List<Header> headerBlock = headerEntries("custom-key", "custom-header");
+
+    OkBuffer expectedBytes = new OkBuffer();
+
+    expectedBytes.writeByte(0x40); // Not indexed
+    expectedBytes.writeByte(0x0a); // Literal name (len = 10)
+    expectedBytes.write("custom-key".getBytes(), 0, 10);
+
+    expectedBytes.writeByte(0x0d); // Literal value (len = 13)
+    expectedBytes.write("custom-header".getBytes(), 0, 13);
+
+    hpackWriter.writeHeaders(headerBlock);
+    assertEquals(expectedBytes, bytesOut);
+
+    bytesIn.write(bytesOut, bytesOut.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(0, hpackReader.headerCount);
+
+    assertEquals(headerBlock, hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.2
+   */
+  @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException {
+    List<Header> headerBlock = headerEntries(":path", "/sample/path");
+
+    OkBuffer expectedBytes = new OkBuffer();
+    expectedBytes.writeByte(0x44); // == Literal not indexed ==
+                                   // Indexed name (idx = 4) -> :path
+    expectedBytes.writeByte(0x0c); // Literal value (len = 12)
+    expectedBytes.write("/sample/path".getBytes(), 0, 12);
+
+    hpackWriter.writeHeaders(headerBlock);
+    assertEquals(expectedBytes, bytesOut);
+
+    bytesIn.write(bytesOut, bytesOut.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(0, hpackReader.headerCount);
+
+    assertEquals(headerBlock, hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.3
+   */
+  @Test public void readIndexedHeaderField() throws IOException {
+    bytesIn.writeByte(0x82); // == Indexed - Add ==
+                             // idx = 2 -> :method: GET
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(1, hpackReader.headerCount);
+    assertEquals(42, hpackReader.headerTableByteCount);
+
+    Header entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.2.1
+   */
+  @Test public void toggleIndex() throws IOException {
+    // Static table entries are copied to the top of the reference set.
+    bytesIn.writeByte(0x82); // == Indexed - Add ==
+                             // idx = 2 -> :method: GET
+    // Specifying an index to an entry in the reference set removes it.
+    bytesIn.writeByte(0x81); // == Indexed - Remove ==
+                             // idx = 1 -> :method: GET
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    assertEquals(1, hpackReader.headerCount);
+    assertEquals(42, hpackReader.headerTableByteCount);
+
+    Header entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderNotReferenced(headerTableLength() - 1);
+
+    assertTrue(hpackReader.getAndReset().isEmpty());
+  }
+
+  /** Ensure a later toggle of the same index emits! */
+  @Test public void toggleIndexOffOn() throws IOException {
+
+    bytesIn.writeByte(0x82); // Copy static header 1 to the header table as index 1.
+    bytesIn.writeByte(0x81); // Remove index 1 from the reference set.
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    assertEquals(1, hpackReader.headerCount);
+    assertTrue(hpackReader.getAndReset().isEmpty());
+
+    bytesIn.writeByte(0x81); // Add index 1 back to the reference set.
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    assertEquals(1, hpackReader.headerCount);
+    assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
+  }
+
+  /** Check later toggle of the same index for large header sets. */
+  @Test public void toggleIndexOffBeyond64Entries() throws IOException {
+    int expectedHeaderCount = 65;
+
+    for (int i = 0; i < expectedHeaderCount; i++) {
+      bytesIn.writeByte(0x82 + i); // Copy static header 1 to the header table as index 1.
+      bytesIn.writeByte(0x81); // Remove index 1 from the reference set.
+    }
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    assertEquals(expectedHeaderCount, hpackReader.headerCount);
+    assertTrue(hpackReader.getAndReset().isEmpty());
+
+    bytesIn.writeByte(0x81); // Add index 1 back to the reference set.
+
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    assertEquals(expectedHeaderCount, hpackReader.headerCount);
+    assertHeaderReferenced(headerTableLength() - expectedHeaderCount);
+    assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.4
+   */
+  @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException {
+    bytesIn.writeByte(0x82); // == Indexed - Add ==
+                             // idx = 2 -> :method: GET
+
+    hpackReader.maxHeaderTableByteCount(0); // SETTINGS_HEADER_TABLE_SIZE == 0
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+
+    // Not buffered in header table.
+    assertEquals(0, hpackReader.headerCount);
+
+    assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.2
+   */
+  @Test public void readRequestExamplesWithoutHuffman() throws IOException {
+    OkBuffer out = firstRequestWithoutHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadFirstRequestWithoutHuffman();
+
+    out = secondRequestWithoutHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadSecondRequestWithoutHuffman();
+
+    out = thirdRequestWithoutHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadThirdRequestWithoutHuffman();
+  }
+
+  private OkBuffer firstRequestWithoutHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x82); // == Indexed - Add ==
+                         // idx = 2 -> :method: GET
+    out.writeByte(0x87); // == Indexed - Add ==
+                         // idx = 7 -> :scheme: http
+    out.writeByte(0x86); // == Indexed - Add ==
+                         // idx = 6 -> :path: /
+    out.writeByte(0x04); // == Literal indexed ==
+                         // Indexed name (idx = 4) -> :authority
+    out.writeByte(0x0f); // Literal value (len = 15)
+    out.writeUtf8("www.example.com");
+
+    return out;
+  }
+
+  private void checkReadFirstRequestWithoutHuffman() {
+    assertEquals(4, hpackReader.headerCount);
+
+    // [  1] (s =  57) :authority: www.example.com
+    Header entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  2] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderReferenced(headerTableLength() - 3);
+
+    // [  3] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderReferenced(headerTableLength() - 2);
+
+    // [  4] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 180
+    assertEquals(180, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":scheme", "http",
+        ":path", "/",
+        ":authority", "www.example.com"), hpackReader.getAndReset());
+  }
+
+  private OkBuffer secondRequestWithoutHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x1b); // == Literal indexed ==
+                         // Indexed name (idx = 27) -> cache-control
+    out.writeByte(0x08); // Literal value (len = 8)
+    out.writeUtf8("no-cache");
+
+    return out;
+  }
+
+  private void checkReadSecondRequestWithoutHuffman() {
+    assertEquals(5, hpackReader.headerCount);
+
+    // [  1] (s =  53) cache-control: no-cache
+    Header entry = hpackReader.headerTable[headerTableLength() - 5];
+    checkEntry(entry, "cache-control", "no-cache", 53);
+    assertHeaderReferenced(headerTableLength() - 5);
+
+    // [  2] (s =  57) :authority: www.example.com
+    entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  3] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderReferenced(headerTableLength() - 3);
+
+    // [  4] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderReferenced(headerTableLength() - 2);
+
+    // [  5] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 233
+    assertEquals(233, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":scheme", "http",
+        ":path", "/",
+        ":authority", "www.example.com",
+        "cache-control", "no-cache"), hpackReader.getAndReset());
+  }
+
+  private OkBuffer thirdRequestWithoutHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x80); // == Empty reference set ==
+    out.writeByte(0x85); // == Indexed - Add ==
+                         // idx = 5 -> :method: GET
+    out.writeByte(0x8c); // == Indexed - Add ==
+                         // idx = 12 -> :scheme: https
+    out.writeByte(0x8b); // == Indexed - Add ==
+                         // idx = 11 -> :path: /index.html
+    out.writeByte(0x84); // == Indexed - Add ==
+                         // idx = 4 -> :authority: www.example.com
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x0a); // Literal name (len = 10)
+    out.writeUtf8("custom-key");
+    out.writeByte(0x0c); // Literal value (len = 12)
+    out.writeUtf8("custom-value");
+
+    return out;
+  }
+
+  private void checkReadThirdRequestWithoutHuffman() {
+    assertEquals(8, hpackReader.headerCount);
+
+    // [  1] (s =  54) custom-key: custom-value
+    Header entry = hpackReader.headerTable[headerTableLength() - 8];
+    checkEntry(entry, "custom-key", "custom-value", 54);
+    assertHeaderReferenced(headerTableLength() - 8);
+
+    // [  2] (s =  48) :path: /index.html
+    entry = hpackReader.headerTable[headerTableLength() - 7];
+    checkEntry(entry, ":path", "/index.html", 48);
+    assertHeaderReferenced(headerTableLength() - 7);
+
+    // [  3] (s =  44) :scheme: https
+    entry = hpackReader.headerTable[headerTableLength() - 6];
+    checkEntry(entry, ":scheme", "https", 44);
+    assertHeaderReferenced(headerTableLength() - 6);
+
+    // [  4] (s =  53) cache-control: no-cache
+    entry = hpackReader.headerTable[headerTableLength() - 5];
+    checkEntry(entry, "cache-control", "no-cache", 53);
+    assertHeaderNotReferenced(headerTableLength() - 5);
+
+    // [  5] (s =  57) :authority: www.example.com
+    entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  6] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderNotReferenced(headerTableLength() - 3);
+
+    // [  7] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderNotReferenced(headerTableLength() - 2);
+
+    // [  8] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 379
+    assertEquals(379, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    // TODO: order is not correct per docs, but then again, the spec doesn't require ordering.
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":authority", "www.example.com",
+        ":scheme", "https",
+        ":path", "/index.html",
+        "custom-key", "custom-value"), hpackReader.getAndReset());
+  }
+
+  /**
+   * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.3
+   */
+  @Test public void readRequestExamplesWithHuffman() throws IOException {
+    OkBuffer out = firstRequestWithHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadFirstRequestWithHuffman();
+
+    out = secondRequestWithHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadSecondRequestWithHuffman();
+
+    out = thirdRequestWithHuffman();
+    bytesIn.write(out, out.size());
+    hpackReader.readHeaders();
+    hpackReader.emitReferenceSet();
+    checkReadThirdRequestWithHuffman();
+  }
+
+  private OkBuffer firstRequestWithHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x82); // == Indexed - Add ==
+                         // idx = 2 -> :method: GET
+    out.writeByte(0x87); // == Indexed - Add ==
+                         // idx = 7 -> :scheme: http
+    out.writeByte(0x86); // == Indexed - Add ==
+                         // idx = 6 -> :path: /
+    out.writeByte(0x04); // == Literal indexed ==
+                         // Indexed name (idx = 4) -> :authority
+    out.writeByte(0x8b); // Literal value Huffman encoded 11 bytes
+                         // decodes to www.example.com which is length 15
+    byte[] huffmanBytes = new byte[] {
+        (byte) 0xdb, (byte) 0x6d, (byte) 0x88, (byte) 0x3e,
+        (byte) 0x68, (byte) 0xd1, (byte) 0xcb, (byte) 0x12,
+        (byte) 0x25, (byte) 0xba, (byte) 0x7f};
+    out.write(huffmanBytes, 0, huffmanBytes.length);
+
+    return out;
+  }
+
+  private void checkReadFirstRequestWithHuffman() {
+    assertEquals(4, hpackReader.headerCount);
+
+    // [  1] (s =  57) :authority: www.example.com
+    Header entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  2] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderReferenced(headerTableLength() - 3);
+
+    // [  3] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderReferenced(headerTableLength() - 2);
+
+    // [  4] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 180
+    assertEquals(180, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":scheme", "http",
+        ":path", "/",
+        ":authority", "www.example.com"), hpackReader.getAndReset());
+  }
+
+  private OkBuffer secondRequestWithHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x1b); // == Literal indexed ==
+                         // Indexed name (idx = 27) -> cache-control
+    out.writeByte(0x86); // Literal value Huffman encoded 6 bytes
+                         // decodes to no-cache which is length 8
+    byte[] huffmanBytes = new byte[] {
+        (byte) 0x63, (byte) 0x65, (byte) 0x4a, (byte) 0x13,
+        (byte) 0x98, (byte) 0xff};
+    out.write(huffmanBytes, 0, huffmanBytes.length);
+
+    return out;
+  }
+
+  private void checkReadSecondRequestWithHuffman() {
+    assertEquals(5, hpackReader.headerCount);
+
+    // [  1] (s =  53) cache-control: no-cache
+    Header entry = hpackReader.headerTable[headerTableLength() - 5];
+    checkEntry(entry, "cache-control", "no-cache", 53);
+    assertHeaderReferenced(headerTableLength() - 5);
+
+    // [  2] (s =  57) :authority: www.example.com
+    entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  3] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderReferenced(headerTableLength() - 3);
+
+    // [  4] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderReferenced(headerTableLength() - 2);
+
+    // [  5] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 233
+    assertEquals(233, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":scheme", "http",
+        ":path", "/",
+        ":authority", "www.example.com",
+        "cache-control", "no-cache"), hpackReader.getAndReset());
+  }
+
+  private OkBuffer thirdRequestWithHuffman() {
+    OkBuffer out = new OkBuffer();
+
+    out.writeByte(0x80); // == Empty reference set ==
+    out.writeByte(0x85); // == Indexed - Add ==
+                         // idx = 5 -> :method: GET
+    out.writeByte(0x8c); // == Indexed - Add ==
+                         // idx = 12 -> :scheme: https
+    out.writeByte(0x8b); // == Indexed - Add ==
+                         // idx = 11 -> :path: /index.html
+    out.writeByte(0x84); // == Indexed - Add ==
+                         // idx = 4 -> :authority: www.example.com
+    out.writeByte(0x00); // Literal indexed
+    out.writeByte(0x88); // Literal name Huffman encoded 8 bytes
+                         // decodes to custom-key which is length 10
+    byte[] huffmanBytes = new byte[] {
+        (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74,
+        (byte) 0x97, (byte) 0x90, (byte) 0xfa, (byte) 0x7f};
+    out.write(huffmanBytes, 0, huffmanBytes.length);
+    out.writeByte(0x89); // Literal value Huffman encoded 6 bytes
+                         // decodes to custom-value which is length 12
+    huffmanBytes = new byte[] {
+        (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74,
+        (byte) 0x97, (byte) 0x9a, (byte) 0x17, (byte) 0xa8,
+        (byte) 0xff};
+    out.write(huffmanBytes, 0, huffmanBytes.length);
+
+    return out;
+  }
+
+  private void checkReadThirdRequestWithHuffman() {
+    assertEquals(8, hpackReader.headerCount);
+
+    // [  1] (s =  54) custom-key: custom-value
+    Header entry = hpackReader.headerTable[headerTableLength() - 8];
+    checkEntry(entry, "custom-key", "custom-value", 54);
+    assertHeaderReferenced(headerTableLength() - 8);
+
+    // [  2] (s =  48) :path: /index.html
+    entry = hpackReader.headerTable[headerTableLength() - 7];
+    checkEntry(entry, ":path", "/index.html", 48);
+    assertHeaderReferenced(headerTableLength() - 7);
+
+    // [  3] (s =  44) :scheme: https
+    entry = hpackReader.headerTable[headerTableLength() - 6];
+    checkEntry(entry, ":scheme", "https", 44);
+    assertHeaderReferenced(headerTableLength() - 6);
+
+    // [  4] (s =  53) cache-control: no-cache
+    entry = hpackReader.headerTable[headerTableLength() - 5];
+    checkEntry(entry, "cache-control", "no-cache", 53);
+    assertHeaderNotReferenced(headerTableLength() - 5);
+
+    // [  5] (s =  57) :authority: www.example.com
+    entry = hpackReader.headerTable[headerTableLength() - 4];
+    checkEntry(entry, ":authority", "www.example.com", 57);
+    assertHeaderReferenced(headerTableLength() - 4);
+
+    // [  6] (s =  38) :path: /
+    entry = hpackReader.headerTable[headerTableLength() - 3];
+    checkEntry(entry, ":path", "/", 38);
+    assertHeaderNotReferenced(headerTableLength() - 3);
+
+    // [  7] (s =  43) :scheme: http
+    entry = hpackReader.headerTable[headerTableLength() - 2];
+    checkEntry(entry, ":scheme", "http", 43);
+    assertHeaderNotReferenced(headerTableLength() - 2);
+
+    // [  8] (s =  42) :method: GET
+    entry = hpackReader.headerTable[headerTableLength() - 1];
+    checkEntry(entry, ":method", "GET", 42);
+    assertHeaderReferenced(headerTableLength() - 1);
+
+    // Table size: 379
+    assertEquals(379, hpackReader.headerTableByteCount);
+
+    // Decoded header set:
+    // TODO: order is not correct per docs, but then again, the spec doesn't require ordering.
+    assertEquals(headerEntries(
+        ":method", "GET",
+        ":authority", "www.example.com",
+        ":scheme", "https",
+        ":path", "/index.html",
+        "custom-key", "custom-value"), hpackReader.getAndReset());
+  }
+
+  @Test public void readSingleByteInt() throws IOException {
+    assertEquals(10, newReader(byteStream()).readInt(10, 31));
+    assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31));
+  }
+
+  @Test public void readMultibyteInt() throws IOException {
+    assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31));
+  }
+
+  @Test public void writeSingleByteInt() throws IOException {
+    hpackWriter.writeInt(10, 31, 0);
+    assertBytes(10);
+    hpackWriter.writeInt(10, 31, 0xe0);
+    assertBytes(0xe0 | 10);
+  }
+
+  @Test public void writeMultibyteInt() throws IOException {
+    hpackWriter.writeInt(1337, 31, 0);
+    assertBytes(31, 154, 10);
+    hpackWriter.writeInt(1337, 31, 0xe0);
+    assertBytes(0xe0 | 31, 154, 10);
+  }
+
+  @Test public void max31BitValue() throws IOException {
+    hpackWriter.writeInt(0x7fffffff, 31, 0);
+    assertBytes(31, 224, 255, 255, 255, 7);
+    assertEquals(0x7fffffff,
+        newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31));
+  }
+
+  @Test public void prefixMask() throws IOException {
+    hpackWriter.writeInt(31, 31, 0);
+    assertBytes(31, 0);
+    assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
+  }
+
+  @Test public void prefixMaskMinusOne() throws IOException {
+    hpackWriter.writeInt(30, 31, 0);
+    assertBytes(30);
+    assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
+  }
+
+  @Test public void zero() throws IOException {
+    hpackWriter.writeInt(0, 31, 0);
+    assertBytes(0);
+    assertEquals(0, newReader(byteStream()).readInt(0, 31));
+  }
+
+  @Test public void headerName() throws IOException {
+    hpackWriter.writeByteString(ByteString.encodeUtf8("foo"));
+    assertBytes(3, 'f', 'o', 'o');
+    assertEquals("foo", newReader(byteStream(3, 'F', 'o', 'o')).readByteString(true).utf8());
+  }
+
+  @Test public void emptyHeaderName() throws IOException {
+    hpackWriter.writeByteString(ByteString.encodeUtf8(""));
+    assertBytes(0);
+    assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString(true));
+    assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString(false));
+  }
+
+  private HpackDraft05.Reader newReader(OkBuffer source) {
+    return new HpackDraft05.Reader(false, 4096, source);
+  }
+
+  private OkBuffer byteStream(int... bytes) {
+    return new OkBuffer().write(intArrayToByteArray(bytes));
+  }
+
+  private void checkEntry(Header entry, String name, String value, int size) {
+    assertEquals(name, entry.name.utf8());
+    assertEquals(value, entry.value.utf8());
+    assertEquals(size, entry.hpackSize);
+  }
+
+  private void assertBytes(int... bytes) {
+    ByteString expected = intArrayToByteArray(bytes);
+    ByteString actual = bytesOut.readByteString(bytesOut.size());
+    assertEquals(expected, actual);
+  }
+
+  private ByteString intArrayToByteArray(int[] bytes) {
+    byte[] data = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      data[i] = (byte) bytes[i];
+    }
+    return ByteString.of(data);
+  }
+
+  private void assertHeaderReferenced(int index) {
+    assertTrue(hpackReader.referencedHeaders.get(index));
+  }
+
+  private void assertHeaderNotReferenced(int index) {
+    assertFalse(hpackReader.referencedHeaders.get(index));
+  }
+
+  private int headerTableLength() {
+    return hpackReader.headerTable.length;
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java
new file mode 100644
index 0000000..248ea09
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.OkBuffer;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.headerEntries;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class Http20Draft09Test {
+  static final int expectedStreamId = 15;
+
+  @Test public void unknownFrameTypeIgnored() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    frame.writeShort(4); // has a 4-byte field
+    frame.writeByte(99); // type 99
+    frame.writeByte(0); // no flags
+    frame.writeInt(expectedStreamId);
+    frame.writeInt(111111111); // custom data
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the unknown frame.
+    fr.nextFrame(new BaseTestHandler());
+  }
+
+  @Test public void onlyOneLiteralHeadersFrame() throws IOException {
+    final List<Header> sentHeaders = headerEntries("name", "value");
+
+    OkBuffer frame = new OkBuffer();
+
+    // Write the headers frame, specifying no more frames are expected.
+    {
+      OkBuffer headerBytes = literalHeaders(sentHeaders);
+      frame.writeShort((int) headerBytes.size());
+      frame.writeByte(Http20Draft09.TYPE_HEADERS);
+      frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM);
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.write(headerBytes, headerBytes.size());
+    }
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the headers frame.
+    fr.nextFrame(new BaseTestHandler() {
+
+      @Override
+      public void headers(boolean outFinished, boolean inFinished, int streamId,
+          int associatedStreamId, int priority, List<Header> headerBlock,
+          HeadersMode headersMode) {
+        assertFalse(outFinished);
+        assertTrue(inFinished);
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(-1, associatedStreamId);
+        assertEquals(-1, priority);
+        assertEquals(sentHeaders, headerBlock);
+        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+      }
+    });
+  }
+
+  @Test public void headersWithPriority() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final List<Header> sentHeaders = headerEntries("name", "value");
+
+    { // Write the headers frame, specifying priority flag and value.
+      OkBuffer headerBytes = literalHeaders(sentHeaders);
+      frame.writeShort((int) (headerBytes.size() + 4));
+      frame.writeByte(Http20Draft09.TYPE_HEADERS);
+      frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_PRIORITY);
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.writeInt(0); // Highest priority is 0.
+      frame.write(headerBytes, headerBytes.size());
+    }
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the headers frame.
+    fr.nextFrame(new BaseTestHandler() {
+
+      @Override
+      public void headers(boolean outFinished, boolean inFinished, int streamId,
+          int associatedStreamId, int priority, List<Header> nameValueBlock,
+          HeadersMode headersMode) {
+        assertFalse(outFinished);
+        assertFalse(inFinished);
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(-1, associatedStreamId);
+        assertEquals(0, priority);
+        assertEquals(sentHeaders, nameValueBlock);
+        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+      }
+    });
+  }
+
+  /** Headers are compressed, then framed. */
+  @Test public void headersFrameThenContinuation() throws IOException {
+
+    OkBuffer frame = new OkBuffer();
+
+    // Decoding the first header will cross frame boundaries.
+    OkBuffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
+    { // Write the first headers frame.
+      frame.writeShort((int) (headerBlock.size() / 2));
+      frame.writeByte(Http20Draft09.TYPE_HEADERS);
+      frame.writeByte(0); // no flags
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.write(headerBlock, headerBlock.size() / 2);
+    }
+
+    { // Write the continuation frame, specifying no more frames are expected.
+      frame.writeShort((int) headerBlock.size());
+      frame.writeByte(Http20Draft09.TYPE_CONTINUATION);
+      frame.writeByte(Http20Draft09.FLAG_END_HEADERS);
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.write(headerBlock, headerBlock.size());
+    }
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Reading the above frames should result in a concatenated headerBlock.
+    fr.nextFrame(new BaseTestHandler() {
+
+      @Override
+      public void headers(boolean outFinished, boolean inFinished, int streamId,
+          int associatedStreamId, int priority, List<Header> headerBlock,
+          HeadersMode headersMode) {
+        assertFalse(outFinished);
+        assertFalse(inFinished);
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(-1, associatedStreamId);
+        assertEquals(-1, priority);
+        assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock);
+        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+      }
+    });
+  }
+
+  @Test public void pushPromise() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final int expectedPromisedStreamId = 11;
+
+    final List<Header> pushPromise = Arrays.asList(
+        new Header(Header.TARGET_METHOD, "GET"),
+        new Header(Header.TARGET_SCHEME, "https"),
+        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+        new Header(Header.TARGET_PATH, "/")
+    );
+
+    { // Write the push promise frame, specifying the associated stream ID.
+      OkBuffer headerBytes = literalHeaders(pushPromise);
+      frame.writeShort((int) (headerBytes.size() + 4));
+      frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE);
+      frame.writeByte(Http20Draft09.FLAG_END_PUSH_PROMISE);
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
+      frame.write(headerBytes, headerBytes.size());
+    }
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the headers frame.
+    fr.nextFrame(new BaseTestHandler() {
+      @Override
+      public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(expectedPromisedStreamId, promisedStreamId);
+        assertEquals(pushPromise, headerBlock);
+      }
+    });
+  }
+
+  /** Headers are compressed, then framed. */
+  @Test public void pushPromiseThenContinuation() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final int expectedPromisedStreamId = 11;
+
+    final List<Header> pushPromise = Arrays.asList(
+        new Header(Header.TARGET_METHOD, "GET"),
+        new Header(Header.TARGET_SCHEME, "https"),
+        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+        new Header(Header.TARGET_PATH, "/")
+    );
+
+    // Decoding the first header will cross frame boundaries.
+    OkBuffer headerBlock = literalHeaders(pushPromise);
+    int firstFrameLength = (int) (headerBlock.size() - 1);
+    { // Write the first headers frame.
+      frame.writeShort(firstFrameLength + 4);
+      frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE);
+      frame.writeByte(0); // no flags
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
+      frame.write(headerBlock, firstFrameLength);
+    }
+
+    { // Write the continuation frame, specifying no more frames are expected.
+      frame.writeShort(1);
+      frame.writeByte(Http20Draft09.TYPE_CONTINUATION);
+      frame.writeByte(Http20Draft09.FLAG_END_HEADERS);
+      frame.writeInt(expectedStreamId & 0x7fffffff);
+      frame.write(headerBlock, 1);
+    }
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Reading the above frames should result in a concatenated headerBlock.
+    fr.nextFrame(new BaseTestHandler() {
+      @Override
+      public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(expectedPromisedStreamId, promisedStreamId);
+        assertEquals(pushPromise, headerBlock);
+      }
+    });
+  }
+
+  @Test public void readRstStreamFrame() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    frame.writeShort(4);
+    frame.writeByte(Http20Draft09.TYPE_RST_STREAM);
+    frame.writeByte(0); // No flags
+    frame.writeInt(expectedStreamId & 0x7fffffff);
+    frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode);
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the reset frame.
+    fr.nextFrame(new BaseTestHandler() {
+      @Override public void rstStream(int streamId, ErrorCode errorCode) {
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode);
+      }
+    });
+  }
+
+  @Test public void readSettingsFrame() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final int reducedTableSizeBytes = 16;
+
+    frame.writeShort(16); // 2 settings * 4 bytes for the code and 4 for the value.
+    frame.writeByte(Http20Draft09.TYPE_SETTINGS);
+    frame.writeByte(0); // No flags
+    frame.writeInt(0 & 0x7fffffff); // Settings are always on the connection stream 0.
+    frame.writeInt(Settings.HEADER_TABLE_SIZE & 0xffffff);
+    frame.writeInt(reducedTableSizeBytes);
+    frame.writeInt(Settings.ENABLE_PUSH & 0xffffff);
+    frame.writeInt(0);
+
+    final Http20Draft09.Reader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    // Consume the settings frame.
+    fr.nextFrame(new BaseTestHandler() {
+      @Override public void settings(boolean clearPrevious, Settings settings) {
+        assertFalse(clearPrevious); // No clearPrevious in http/2.
+        assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize());
+        assertEquals(false, settings.getEnablePush(true));
+      }
+    });
+  }
+
+  @Test public void pingRoundTrip() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final int expectedPayload1 = 7;
+    final int expectedPayload2 = 8;
+
+    // Compose the expected PING frame.
+    frame.writeShort(8); // length
+    frame.writeByte(Http20Draft09.TYPE_PING);
+    frame.writeByte(Http20Draft09.FLAG_ACK);
+    frame.writeInt(0); // connection-level
+    frame.writeInt(expectedPayload1);
+    frame.writeInt(expectedPayload2);
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2));
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    fr.nextFrame(new BaseTestHandler() { // Consume the ping frame.
+      @Override public void ping(boolean ack, int payload1, int payload2) {
+        assertTrue(ack);
+        assertEquals(expectedPayload1, payload1);
+        assertEquals(expectedPayload2, payload2);
+      }
+    });
+  }
+
+  @Test public void maxLengthDataFrame() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final byte[] expectedData = new byte[16383];
+    Arrays.fill(expectedData, (byte) 2);
+
+    // Write the data frame.
+    frame.writeShort(expectedData.length);
+    frame.writeByte(Http20Draft09.TYPE_DATA);
+    frame.writeByte(0); // no flags
+    frame.writeInt(expectedStreamId & 0x7fffffff);
+    frame.write(expectedData);
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, sendDataFrame(new OkBuffer().write(expectedData)));
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    fr.nextFrame(new BaseTestHandler() {
+      @Override public void data(
+          boolean inFinished, int streamId, BufferedSource source, int length) throws IOException {
+        assertFalse(inFinished);
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(16383, length);
+        ByteString data = source.readByteString(length);
+        for (byte b : data.toByteArray()){
+          assertEquals(2, b);
+        }
+      }
+    });
+  }
+
+  @Test public void tooLargeDataFrame() throws IOException {
+    try {
+      sendDataFrame(new OkBuffer().write(new byte[0x1000000]));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("FRAME_SIZE_ERROR length > 16383: 16777216", e.getMessage());
+    }
+  }
+
+  @Test public void windowUpdateRoundTrip() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final long expectedWindowSizeIncrement = 0x7fffffff;
+
+    // Compose the expected window update frame.
+    frame.writeShort(4); // length
+    frame.writeByte(Http20Draft09.TYPE_WINDOW_UPDATE);
+    frame.writeByte(0); // No flags.
+    frame.writeInt(expectedStreamId);
+    frame.writeInt((int) expectedWindowSizeIncrement);
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, windowUpdate(expectedWindowSizeIncrement));
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    fr.nextFrame(new BaseTestHandler() { // Consume the window update frame.
+      @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
+        assertEquals(expectedStreamId, streamId);
+        assertEquals(expectedWindowSizeIncrement, windowSizeIncrement);
+      }
+    });
+  }
+
+  @Test public void badWindowSizeIncrement() throws IOException {
+    try {
+      windowUpdate(0);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0",
+          e.getMessage());
+    }
+    try {
+      windowUpdate(0x80000000L);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648",
+          e.getMessage());
+    }
+  }
+
+  @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
+
+    // Compose the expected GOAWAY frame without debug data.
+    frame.writeShort(8); // Without debug data there's only 2 32-bit fields.
+    frame.writeByte(Http20Draft09.TYPE_GOAWAY);
+    frame.writeByte(0); // no flags.
+    frame.writeInt(0); // connection-scope
+    frame.writeInt(expectedStreamId); // last good stream.
+    frame.writeInt(expectedError.httpCode);
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY));
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    fr.nextFrame(new BaseTestHandler() { // Consume the go away frame.
+      @Override public void goAway(
+          int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+        assertEquals(expectedStreamId, lastGoodStreamId);
+        assertEquals(expectedError, errorCode);
+        assertEquals(0, debugData.size());
+      }
+    });
+  }
+
+  @Test public void goAwayWithDebugDataRoundTrip() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
+    final ByteString expectedData = ByteString.encodeUtf8("abcdefgh");
+
+    // Compose the expected GOAWAY frame without debug data.
+    frame.writeShort(8 + expectedData.size());
+    frame.writeByte(Http20Draft09.TYPE_GOAWAY);
+    frame.writeByte(0); // no flags.
+    frame.writeInt(0); // connection-scope
+    frame.writeInt(0); // never read any stream!
+    frame.writeInt(expectedError.httpCode);
+    frame.write(expectedData.toByteArray());
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray()));
+
+    FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
+
+    fr.nextFrame(new BaseTestHandler() { // Consume the go away frame.
+      @Override public void goAway(
+          int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+        assertEquals(0, lastGoodStreamId);
+        assertEquals(expectedError, errorCode);
+        assertEquals(expectedData, debugData);
+      }
+    });
+  }
+
+  @Test public void frameSizeError() throws IOException {
+    Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true);
+
+    try {
+      writer.frameHeader(16384, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, 0);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("FRAME_SIZE_ERROR length > 16383: 16384", e.getMessage());
+    }
+  }
+
+  @Test public void streamIdHasReservedBit() throws IOException {
+      Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true);
+
+      try {
+      int streamId = 3;
+      streamId |= 1L << 31; // set reserved bit
+      writer.frameHeader(16383, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, streamId);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("reserved bit set: -2147483645", e.getMessage());
+    }
+  }
+
+  private OkBuffer literalHeaders(List<Header> sentHeaders) throws IOException {
+    OkBuffer out = new OkBuffer();
+    new HpackDraft05.Writer(out).writeHeaders(sentHeaders);
+    return out;
+  }
+
+  private OkBuffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException {
+    OkBuffer out = new OkBuffer();
+    new Http20Draft09.Writer(out, true).ping(ack, payload1, payload2);
+    return out;
+  }
+
+  private OkBuffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
+      throws IOException {
+    OkBuffer out = new OkBuffer();
+    new Http20Draft09.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
+    return out;
+  }
+
+  private OkBuffer sendDataFrame(OkBuffer data) throws IOException {
+    OkBuffer out = new OkBuffer();
+    new Http20Draft09.Writer(out, true).dataFrame(expectedStreamId, Http20Draft09.FLAG_NONE, data,
+        (int) data.size());
+    return out;
+  }
+
+  private OkBuffer windowUpdate(long windowSizeIncrement) throws IOException {
+    OkBuffer out = new OkBuffer();
+    new Http20Draft09.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement);
+    return out;
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
new file mode 100644
index 0000000..6206b7e
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 Twitter, 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.spdy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Random;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Original version of this class was lifted from {@code com.twitter.hpack.HuffmanTest}.
+ */
+public class HuffmanTest {
+
+  @Test public void roundTripForRequestAndResponse() throws IOException {
+    String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    for (int i = 0; i < s.length(); i++) {
+      assertRoundTrip(s.substring(0, i).getBytes());
+    }
+
+    Random random = new Random(123456789L);
+    byte[] buf = new byte[4096];
+    random.nextBytes(buf);
+    assertRoundTrip(buf);
+  }
+
+  private void assertRoundTrip(byte[] buf) throws IOException {
+    assertRoundTrip(Huffman.Codec.REQUEST, buf);
+    assertRoundTrip(Huffman.Codec.RESPONSE, buf);
+  }
+
+  private static void assertRoundTrip(Huffman.Codec codec, byte[] buf) throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    DataOutputStream dos = new DataOutputStream(baos);
+
+    codec.encode(buf, dos);
+    assertEquals(baos.size(), codec.encodedLength(buf));
+
+    byte[] decodedBytes = codec.decode(baos.toByteArray());
+    assertTrue(Arrays.equals(buf, decodedBytes));
+  }
+}
diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
similarity index 72%
rename from okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
index 99ddc6d..fd6007d 100644
--- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -17,7 +17,6 @@
 package com.squareup.okhttp.internal.spdy;
 
 import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
@@ -28,34 +27,47 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
-
-import static java.util.concurrent.Executors.defaultThreadFactory;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Okio;
 
 /** Replays prerecorded outgoing frames and records incoming frames. */
 public final class MockSpdyPeer implements Closeable {
   private int frameCount = 0;
-  private final boolean client;
-  private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-  private final FrameWriter frameWriter;
+  private boolean client = false;
+  private Variant variant = new Spdy3();
+  private final OkBuffer bytesOut = new OkBuffer();
+  private FrameWriter frameWriter = variant.newWriter(bytesOut, client);
   private final List<OutFrame> outFrames = new ArrayList<OutFrame>();
   private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
   private int port;
-  private final Executor executor = Executors.newCachedThreadPool(defaultThreadFactory());
+  private final ExecutorService executor = Executors.newSingleThreadExecutor(
+      Util.threadFactory("MockSpdyPeer", false));
   private ServerSocket serverSocket;
   private Socket socket;
 
-  public MockSpdyPeer(boolean client) {
+  public void setVariantAndClient(Variant variant, boolean client) {
+    if (this.variant.getProtocol() == variant.getProtocol() && this.client == client) {
+      return;
+    }
     this.client = client;
-    this.frameWriter = Variant.SPDY3.newWriter(bytesOut, client);
+    this.variant = variant;
+    this.frameWriter = variant.newWriter(bytesOut, client);
   }
 
   public void acceptFrame() {
     frameCount++;
   }
 
+  /** Count of frames sent or received. */
+  public int frameCount() {
+    return frameCount;
+  }
+
   public FrameWriter sendFrame() {
     outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE));
     return frameWriter;
@@ -80,10 +92,6 @@
     return frameWriter;
   }
 
-  public int getPort() {
-    return port;
-  }
-
   public InFrame takeFrame() throws InterruptedException {
     return inFrames.take();
   }
@@ -92,12 +100,13 @@
     if (serverSocket != null) throw new IllegalStateException();
     serverSocket = new ServerSocket(0);
     serverSocket.setReuseAddress(true);
-    this.port = serverSocket.getLocalPort();
+    port = serverSocket.getLocalPort();
     executor.execute(new Runnable() {
       @Override public void run() {
         try {
           readAndWriteFrames();
         } catch (IOException e) {
+          Util.closeQuietly(MockSpdyPeer.this);
           throw new RuntimeException(e);
         }
       }
@@ -109,10 +118,10 @@
     socket = serverSocket.accept();
     OutputStream out = socket.getOutputStream();
     InputStream in = socket.getInputStream();
-    FrameReader reader = Variant.SPDY3.newReader(in, client);
+    FrameReader reader = variant.newReader(Okio.buffer(Okio.source(in)), client);
 
     Iterator<OutFrame> outFramesIterator = outFrames.iterator();
-    byte[] outBytes = bytesOut.toByteArray();
+    byte[] outBytes = bytesOut.readByteString(bytesOut.size()).toByteArray();
     OutFrame nextOutFrame = null;
 
     for (int i = 0; i < frameCount; i++) {
@@ -121,9 +130,9 @@
       }
 
       if (nextOutFrame != null && nextOutFrame.sequence == i) {
-        int start = nextOutFrame.start;
+        long start = nextOutFrame.start;
         int truncateToLength = nextOutFrame.truncateToLength;
-        int end;
+        long end;
         if (outFramesIterator.hasNext()) {
           nextOutFrame = outFramesIterator.next();
           end = nextOutFrame.start;
@@ -132,8 +141,8 @@
         }
 
         // write a frame
-        int length = Math.min(end - start, truncateToLength);
-        out.write(outBytes, start, length);
+        int length = (int) Math.min(end - start, truncateToLength);
+        out.write(outBytes, (int) start, length);
       } else {
         // read a frame
         InFrame inFrame = new InFrame(i, reader);
@@ -148,25 +157,26 @@
     return new Socket("localhost", port);
   }
 
-  @Override public void close() throws IOException {
+  @Override public synchronized void close() throws IOException {
+    executor.shutdown();
     Socket socket = this.socket;
     if (socket != null) {
-      socket.close();
+      Util.closeQuietly(socket);
       this.socket = null;
     }
     ServerSocket serverSocket = this.serverSocket;
     if (serverSocket != null) {
-      serverSocket.close();
+      Util.closeQuietly(serverSocket);
       this.serverSocket = null;
     }
   }
 
   private static class OutFrame {
     private final int sequence;
-    private final int start;
+    private final long start;
     private final int truncateToLength;
 
-    private OutFrame(int sequence, int start, int truncateToLength) {
+    private OutFrame(int sequence, long start, int truncateToLength) {
       this.sequence = sequence;
       this.start = start;
       this.truncateToLength = truncateToLength;
@@ -184,11 +194,14 @@
     public int associatedStreamId;
     public int priority;
     public ErrorCode errorCode;
-    public int deltaWindowSize;
-    public List<String> nameValueBlock;
+    public long windowSizeIncrement;
+    public List<Header> headerBlock;
     public byte[] data;
     public Settings settings;
     public HeadersMode headersMode;
+    public boolean ack;
+    public int payload1;
+    public int payload2;
 
     public InFrame(int sequence, FrameReader reader) {
       this.sequence = sequence;
@@ -202,8 +215,14 @@
       this.settings = settings;
     }
 
+    @Override public void ackSettings() {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = Spdy3.TYPE_SETTINGS;
+      this.ack = true;
+    }
+
     @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
-        int associatedStreamId, int priority, List<String> nameValueBlock,
+        int associatedStreamId, int priority, List<Header> headerBlock,
         HeadersMode headersMode) {
       if (this.type != -1) throw new IllegalStateException();
       this.type = Spdy3.TYPE_HEADERS;
@@ -212,18 +231,17 @@
       this.streamId = streamId;
       this.associatedStreamId = associatedStreamId;
       this.priority = priority;
-      this.nameValueBlock = nameValueBlock;
+      this.headerBlock = headerBlock;
       this.headersMode = headersMode;
     }
 
-    @Override public void data(boolean inFinished, int streamId, InputStream in, int length)
+    @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)
         throws IOException {
       if (this.type != -1) throw new IllegalStateException();
       this.type = Spdy3.TYPE_DATA;
       this.inFinished = inFinished;
       this.streamId = streamId;
-      this.data = new byte[length];
-      Util.readFully(in, this.data);
+      this.data = source.readByteString(length).toByteArray();
     }
 
     @Override public void rstStream(int streamId, ErrorCode errorCode) {
@@ -233,33 +251,39 @@
       this.errorCode = errorCode;
     }
 
-    @Override public void ping(boolean reply, int payload1, int payload2) {
+    @Override public void ping(boolean ack, int payload1, int payload2) {
       if (this.type != -1) throw new IllegalStateException();
       this.type = Spdy3.TYPE_PING;
-      this.streamId = payload1;
+      this.ack = ack;
+      this.payload1 = payload1;
+      this.payload2 = payload2;
     }
 
-    @Override public void noop() {
-      if (this.type != -1) throw new IllegalStateException();
-      this.type = Spdy3.TYPE_NOOP;
-    }
-
-    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode) {
+    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
       if (this.type != -1) throw new IllegalStateException();
       this.type = Spdy3.TYPE_GOAWAY;
       this.streamId = lastGoodStreamId;
       this.errorCode = errorCode;
+      this.data = debugData.toByteArray();
     }
 
-    @Override public void windowUpdate(int streamId, int deltaWindowSize, boolean endFlowControl) {
+    @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
       if (this.type != -1) throw new IllegalStateException();
       this.type = Spdy3.TYPE_WINDOW_UPDATE;
       this.streamId = streamId;
-      this.deltaWindowSize = deltaWindowSize;
+      this.windowSizeIncrement = windowSizeIncrement;
     }
 
     @Override public void priority(int streamId, int priority) {
       throw new UnsupportedOperationException();
     }
+
+    @Override
+    public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) {
+      this.type = Http20Draft09.TYPE_PUSH_PROMISE;
+      this.streamId = streamId;
+      this.associatedStreamId = associatedStreamId;
+      this.headerBlock = headerBlock;
+    }
   }
-}
\ No newline at end of file
+}
diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
similarity index 85%
rename from okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
index a906fc7..294684f 100644
--- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
@@ -17,6 +17,7 @@
 
 import org.junit.Test;
 
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
 import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_BANDWIDTH;
 import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_RETRANS_RATE;
 import static com.squareup.okhttp.internal.spdy.Settings.MAX_CONCURRENT_STREAMS;
@@ -36,20 +37,28 @@
   @Test public void setFields() {
     Settings settings = new Settings();
 
+    // WARNING: clash on flags between spdy/3 and http/2!
     assertEquals(-3, settings.getUploadBandwidth(-3));
-    settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42);
+    assertEquals(-1, settings.getHeaderTableSize());
+    settings.set(UPLOAD_BANDWIDTH, 0, 42);
     assertEquals(42, settings.getUploadBandwidth(-3));
+    settings.set(Settings.HEADER_TABLE_SIZE, 0, 8096);
+    assertEquals(8096, settings.getHeaderTableSize());
 
+    // WARNING: clash on flags between spdy/3 and http/2!
     assertEquals(-3, settings.getDownloadBandwidth(-3));
-    settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53);
+    assertEquals(true, settings.getEnablePush(true));
+    settings.set(DOWNLOAD_BANDWIDTH, 0, 53);
     assertEquals(53, settings.getDownloadBandwidth(-3));
+    settings.set(Settings.ENABLE_PUSH, 0, 0);
+    assertEquals(false, settings.getEnablePush(true));
 
     assertEquals(-3, settings.getRoundTripTime(-3));
     settings.set(Settings.ROUND_TRIP_TIME, 0, 64);
     assertEquals(64, settings.getRoundTripTime(-3));
 
     assertEquals(-3, settings.getMaxConcurrentStreams(-3));
-    settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 75);
+    settings.set(MAX_CONCURRENT_STREAMS, 0, 75);
     assertEquals(75, settings.getMaxConcurrentStreams(-3));
 
     assertEquals(-3, settings.getCurrentCwnd(-3));
@@ -57,12 +66,13 @@
     assertEquals(86, settings.getCurrentCwnd(-3));
 
     assertEquals(-3, settings.getDownloadRetransRate(-3));
-    settings.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 97);
+    settings.set(DOWNLOAD_RETRANS_RATE, 0, 97);
     assertEquals(97, settings.getDownloadRetransRate(-3));
 
-    assertEquals(-3, settings.getInitialWindowSize(-3));
+    assertEquals(DEFAULT_INITIAL_WINDOW_SIZE,
+        settings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
     settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108);
-    assertEquals(108, settings.getInitialWindowSize(-3));
+    assertEquals(108, settings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
 
     assertEquals(-3, settings.getClientCertificateVectorSize(-3));
     settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
new file mode 100644
index 0000000..1904b90
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import okio.ByteString;
+import okio.OkBuffer;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class Spdy3Test {
+  static final int expectedStreamId = 15;
+
+  @Test public void tooLargeDataFrame() throws IOException {
+    try {
+      sendDataFrame(new OkBuffer().write(new byte[0x1000000]));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("FRAME_TOO_LARGE max size is 16Mib: " + 0x1000000L, e.getMessage());
+    }
+  }
+
+  @Test public void badWindowSizeIncrement() throws IOException {
+    try {
+      windowUpdate(0);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("windowSizeIncrement must be between 1 and 0x7fffffff: 0", e.getMessage());
+    }
+    try {
+      windowUpdate(0x80000000L);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("windowSizeIncrement must be between 1 and 0x7fffffff: 2147483648",
+          e.getMessage());
+    }
+  }
+
+  @Test public void goAwayRoundTrip() throws IOException {
+    OkBuffer frame = new OkBuffer();
+
+    final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
+
+    // Compose the expected GOAWAY frame without debug data
+    // |C| Version(15bits) | Type(16bits) |
+    frame.writeInt(0x80000000 | (Spdy3.VERSION & 0x7fff) << 16 | Spdy3.TYPE_GOAWAY & 0xffff);
+    // | Flags (8)  |  Length (24 bits)   |
+    frame.writeInt(8); // no flags and length is 8.
+    frame.writeInt(expectedStreamId); // last good stream.
+    frame.writeInt(expectedError.spdyGoAwayCode);
+
+    // Check writer sends the same bytes.
+    assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY));
+
+    // SPDY/3 does not send debug data, so bytes should be same!
+    assertEquals(frame, sendGoAway(expectedStreamId, expectedError, new byte[8]));
+
+    FrameReader fr = new Spdy3.Reader(frame, false);
+
+    fr.nextFrame(new BaseTestHandler() { // Consume the goAway frame.
+      @Override public void goAway(
+          int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+        assertEquals(expectedStreamId, lastGoodStreamId);
+        assertEquals(expectedError, errorCode);
+        assertEquals(0, debugData.size());
+      }
+    });
+  }
+
+  private void sendDataFrame(OkBuffer source) throws IOException {
+    Spdy3.Writer writer = new Spdy3.Writer(new OkBuffer(), true);
+    writer.sendDataFrame(expectedStreamId, 0, source, (int) source.size());
+  }
+
+  private void windowUpdate(long increment) throws IOException {
+    new Spdy3.Writer(new OkBuffer(), true).windowUpdate(expectedStreamId, increment);
+  }
+
+  private OkBuffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
+      throws IOException {
+    OkBuffer out = new OkBuffer();
+    new Spdy3.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
+    return out;
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
new file mode 100644
index 0000000..2ef127e
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
@@ -0,0 +1,1622 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Okio;
+import okio.Source;
+import org.junit.After;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.headerEntries;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.INTERNAL_ERROR;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.INVALID_STREAM;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.REFUSED_STREAM;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.STREAM_IN_USE;
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_DATA;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_GOAWAY;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_SETTINGS;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class SpdyConnectionTest {
+  private static final Variant SPDY3 = new Spdy3();
+  private static final Variant HTTP_20_DRAFT_09 = new Http20Draft09();
+  private final MockSpdyPeer peer = new MockSpdyPeer();
+
+  @After public void tearDown() throws Exception {
+    peer.close();
+  }
+
+  @Test public void clientCreatesStreamAndServerReplies() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame()
+        .synReply(false, 1, headerEntries("a", "android"));
+    peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
+    peer.acceptFrame(); // DATA
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+    assertStreamData("robot", stream.getSource());
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.writeUtf8("c3po");
+    out.close();
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertFalse(synStream.inFinished);
+    assertFalse(synStream.outFinished);
+    assertEquals(1, synStream.streamId);
+    assertEquals(0, synStream.associatedStreamId);
+    assertEquals(headerEntries("b", "banana"), synStream.headerBlock);
+    MockSpdyPeer.InFrame requestData = peer.takeFrame();
+    assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data));
+  }
+
+  @Test public void headersOnlyStreamIsClosedAfterReplyHeaders() throws Exception {
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+    peer.play();
+
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, false);
+    assertEquals(1, connection.openStreamCount());
+    assertEquals(headerEntries("b", "banana"), stream.getResponseHeaders());
+    assertEquals(0, connection.openStreamCount());
+  }
+
+  @Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synReply(true, 1, headerEntries("a", "android"));
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    connection.newStream(headerEntries("b", "banana"), false, true);
+    assertEquals(1, connection.openStreamCount());
+    connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+  }
+
+  @Test public void serverCreatesStreamAndClientReplies() throws Exception {
+    final List<Header> pushHeaders = headerEntries(
+        ":scheme", "https",
+        ":host", "localhost:8888",
+        ":method", "GET",
+        ":path", "/index.html",
+        ":status", "200",
+        ":version", "HTTP/1.1",
+        "content-type", "text/html");
+    // write the mocking script
+    peer.sendFrame().synStream(false, false, 2, 0, 5, 129, pushHeaders);
+    peer.acceptFrame(); // SYN_REPLY
+    peer.play();
+
+    // play it back
+    final AtomicInteger receiveCount = new AtomicInteger();
+    IncomingStreamHandler handler = new IncomingStreamHandler() {
+      @Override public void receive(SpdyStream stream) throws IOException {
+        receiveCount.incrementAndGet();
+        assertEquals(pushHeaders, stream.getRequestHeaders());
+        assertEquals(null, stream.getErrorCode());
+        assertEquals(5, stream.getPriority());
+        stream.reply(headerEntries("b", "banana"), true);
+      }
+    };
+    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame reply = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, reply.type);
+    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
+    assertFalse(reply.inFinished);
+    assertEquals(2, reply.streamId);
+    assertEquals(headerEntries("b", "banana"), reply.headerBlock);
+    assertEquals(1, receiveCount.get());
+  }
+
+  @Test public void replyWithNoData() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("a", "android"));
+    peer.acceptFrame(); // SYN_REPLY
+    peer.play();
+
+    // play it back
+    final AtomicInteger receiveCount = new AtomicInteger();
+    IncomingStreamHandler handler = new IncomingStreamHandler() {
+      @Override public void receive(SpdyStream stream) throws IOException {
+        stream.reply(headerEntries("b", "banana"), false);
+        receiveCount.incrementAndGet();
+      }
+    };
+
+    connectionBuilder(peer, SPDY3).handler(handler).build();
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame reply = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, reply.type);
+    assertTrue(reply.inFinished);
+    assertEquals(headerEntries("b", "banana"), reply.headerBlock);
+    assertEquals(1, receiveCount.get());
+    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
+  }
+
+  @Test public void serverPingsClient() throws Exception {
+    // write the mocking script
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    connection(peer, SPDY3);
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(0, ping.streamId);
+    assertEquals(2, ping.payload1);
+    assertEquals(0, ping.payload2); // ignored in spdy!
+    assertTrue(ping.ack);
+  }
+
+  @Test public void serverPingsClientHttp2() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    // write the mocking script
+    peer.sendFrame().ping(false, 2, 3);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    connection(peer, HTTP_20_DRAFT_09);
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(0, ping.streamId);
+    assertEquals(2, ping.payload1);
+    assertEquals(3, ping.payload2);
+    assertTrue(ping.ack);
+  }
+
+  @Test public void clientPingsServer() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 5); // payload2 ignored in spdy!
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    Ping ping = connection.ping();
+    assertTrue(ping.roundTripTime() > 0);
+    assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
+    assertEquals(TYPE_PING, pingFrame.type);
+    assertEquals(1, pingFrame.payload1);
+    assertEquals(0, pingFrame.payload2);
+    assertFalse(pingFrame.ack);
+  }
+
+  @Test public void clientPingsServerHttp2() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    // write the mocking script
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 5);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
+    Ping ping = connection.ping();
+    assertTrue(ping.roundTripTime() > 0);
+    assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
+    assertEquals(0, pingFrame.streamId);
+    assertEquals(1, pingFrame.payload1);
+    assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
+    assertFalse(pingFrame.ack);
+  }
+
+  @Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    Settings initial = new Settings();
+    initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 1684);
+    Settings shouldntImpactConnection = new Settings();
+    shouldntImpactConnection.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 3368);
+
+    peer.sendFrame().settings(initial);
+    peer.acceptFrame(); // ACK
+    peer.sendFrame().settings(shouldntImpactConnection);
+    peer.acceptFrame(); // ACK 2
+    peer.acceptFrame(); // HEADERS
+    peer.play();
+
+    SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
+
+    // verify the peer received the ACK
+    MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
+    assertEquals(TYPE_SETTINGS, ackFrame.type);
+    assertEquals(0, ackFrame.streamId);
+    assertTrue(ackFrame.ack);
+    ackFrame = peer.takeFrame();
+    assertEquals(TYPE_SETTINGS, ackFrame.type);
+    assertEquals(0, ackFrame.streamId);
+    assertTrue(ackFrame.ack);
+
+    // This stream was created *after* the connection settings were adjusted.
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+
+    assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
+    assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
+    // New Stream is has the most recent initial window size.
+    assertEquals(3368, stream.bytesLeftInWriteWindow);
+  }
+
+  @Test public void unexpectedPingIsNotReturned() throws Exception {
+    // write the mocking script
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 3, 0); // This ping will not be returned.
+    peer.sendFrame().ping(false, 4, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    connection(peer, SPDY3);
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame ping2 = peer.takeFrame();
+    assertEquals(2, ping2.payload1);
+    MockSpdyPeer.InFrame ping4 = peer.takeFrame();
+    assertEquals(4, ping4.payload1);
+  }
+
+  @Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
+    boolean client = false; // Peer is server, so we are client.
+    Settings settings = new Settings();
+    settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
+
+    SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+
+    // verify the peer's settings were read and applied.
+    synchronized (connection) {
+      assertEquals(0, connection.peerSettings.getHeaderTableSize());
+      Http20Draft09.Reader frameReader = (Http20Draft09.Reader) connection.frameReader;
+      assertEquals(0, frameReader.hpackReader.maxHeaderTableByteCount());
+      // TODO: when supported, check the frameWriter's compression table is unaffected.
+    }
+  }
+
+  @Test public void peerHttp2ClientDisablesPush() throws Exception {
+    boolean client = false; // Peer is client, so we are server.
+    Settings settings = new Settings();
+    settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
+
+    SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+
+    // verify the peer's settings were read and applied.
+    synchronized (connection) {
+      assertFalse(connection.peerSettings.getEnablePush(true));
+    }
+  }
+
+  @Test public void serverSendsSettingsToClient() throws Exception {
+    // write the mocking script
+    Settings settings = new Settings();
+    settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10);
+    peer.sendFrame().settings(settings);
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+
+    peer.takeFrame(); // Guarantees that the peer Settings frame has been processed.
+    synchronized (connection) {
+      assertEquals(10, connection.peerSettings.getMaxConcurrentStreams(-1));
+    }
+  }
+
+  @Test public void multipleSettingsFramesAreMerged() throws Exception {
+    // write the mocking script
+    Settings settings1 = new Settings();
+    settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
+    settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
+    settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300);
+    peer.sendFrame().settings(settings1);
+    Settings settings2 = new Settings();
+    settings2.set(Settings.DOWNLOAD_BANDWIDTH, 0, 400);
+    settings2.set(Settings.DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500);
+    settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
+    peer.sendFrame().settings(settings2);
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame();
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+
+    peer.takeFrame(); // Guarantees that the Settings frame has been processed.
+    synchronized (connection) {
+      assertEquals(100, connection.peerSettings.getUploadBandwidth(-1));
+      assertEquals(PERSIST_VALUE, connection.peerSettings.flags(Settings.UPLOAD_BANDWIDTH));
+      assertEquals(400, connection.peerSettings.getDownloadBandwidth(-1));
+      assertEquals(0, connection.peerSettings.flags(Settings.DOWNLOAD_BANDWIDTH));
+      assertEquals(500, connection.peerSettings.getDownloadRetransRate(-1));
+      assertEquals(PERSIST_VALUE, connection.peerSettings.flags(Settings.DOWNLOAD_RETRANS_RATE));
+      assertEquals(600, connection.peerSettings.getMaxConcurrentStreams(-1));
+      assertEquals(PERSIST_VALUE, connection.peerSettings.flags(Settings.MAX_CONCURRENT_STREAMS));
+    }
+  }
+
+  @Test public void clearSettingsBeforeMerge() throws Exception {
+    // write the mocking script
+    Settings settings1 = new Settings();
+    settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
+    settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
+    settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300);
+    peer.sendFrame().settings(settings1);
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame();
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+
+    peer.takeFrame(); // Guarantees that the Settings frame has been processed.
+
+    // fake a settings frame with clear flag set.
+    Settings settings2 = new Settings();
+    settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
+    connection.readerRunnable.settings(true, settings2);
+
+    synchronized (connection) {
+      assertEquals(-1, connection.peerSettings.getUploadBandwidth(-1));
+      assertEquals(-1, connection.peerSettings.getDownloadBandwidth(-1));
+      assertEquals(-1, connection.peerSettings.getDownloadRetransRate(-1));
+      assertEquals(600, connection.peerSettings.getMaxConcurrentStreams(-1));
+    }
+  }
+
+  @Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception {
+    // write the mocking script
+    peer.sendFrame().data(true, 41, new OkBuffer().writeUtf8("bogus"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    connection(peer, SPDY3);
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(41, rstStream.streamId);
+    assertEquals(INVALID_STREAM, rstStream.errorCode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(2, ping.payload1);
+  }
+
+  @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synReply(false, 41, headerEntries("a", "android"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    connection(peer, SPDY3);
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(41, rstStream.streamId);
+    assertEquals(INVALID_STREAM, rstStream.errorCode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(2, ping.payload1);
+  }
+
+  @Test public void clientClosesClientOutputStream() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+    peer.acceptFrame(); // TYPE_DATA
+    peer.acceptFrame(); // TYPE_DATA with FLAG_FIN
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, false);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.writeUtf8("square");
+    out.flush();
+    assertEquals(1, connection.openStreamCount());
+    out.close();
+    try {
+      out.writeUtf8("round");
+      fail();
+    } catch (Exception expected) {
+      assertEquals("closed", expected.getMessage());
+    }
+    connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertFalse(synStream.inFinished);
+    assertTrue(synStream.outFinished);
+    MockSpdyPeer.InFrame data = peer.takeFrame();
+    assertEquals(TYPE_DATA, data.type);
+    assertFalse(data.inFinished);
+    assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
+    MockSpdyPeer.InFrame fin = peer.takeFrame();
+    assertEquals(TYPE_DATA, fin.type);
+    assertTrue(fin.inFinished);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(1, ping.payload1);
+  }
+
+  @Test public void serverClosesClientOutputStream() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().rstStream(1, CANCEL);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received.
+    try {
+      out.writeUtf8("square");
+      out.flush();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: CANCEL", expected.getMessage());
+    }
+    try {
+      out.close();
+      fail();
+    } catch (IOException expected) {
+      // Close throws because buffered data wasn't flushed.
+    }
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertFalse(synStream.inFinished);
+    assertFalse(synStream.outFinished);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(1, ping.payload1);
+  }
+
+  /**
+   * Test that the client sends a RST_STREAM if doing so won't disrupt the
+   * output stream.
+   */
+  @Test public void clientClosesClientInputStream() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+    Source in = stream.getSource();
+    BufferedSink out = Okio.buffer(stream.getSink());
+    in.close();
+    try {
+      in.read(new OkBuffer(), 1);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream closed", expected.getMessage());
+    }
+    try {
+      out.writeUtf8("a");
+      out.flush();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream finished", expected.getMessage());
+    }
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertTrue(synStream.inFinished);
+    assertFalse(synStream.outFinished);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(CANCEL, rstStream.errorCode);
+  }
+
+  /**
+   * Test that the client doesn't send a RST_STREAM if doing so will disrupt
+   * the output stream.
+   */
+  @Test public void clientClosesClientInputStreamIfOutputStreamIsClosed() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // DATA
+    peer.acceptFrame(); // DATA with FLAG_FIN
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    Source source = stream.getSource();
+    BufferedSink out = Okio.buffer(stream.getSink());
+    source.close();
+    try {
+      source.read(new OkBuffer(), 1);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream closed", expected.getMessage());
+    }
+    out.writeUtf8("square");
+    out.flush();
+    out.close();
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertFalse(synStream.inFinished);
+    assertFalse(synStream.outFinished);
+    MockSpdyPeer.InFrame data = peer.takeFrame();
+    assertEquals(TYPE_DATA, data.type);
+    assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
+    MockSpdyPeer.InFrame fin = peer.takeFrame();
+    assertEquals(TYPE_DATA, fin.type);
+    assertTrue(fin.inFinished);
+    assertFalse(fin.outFinished);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(CANCEL, rstStream.errorCode);
+  }
+
+  @Test public void serverClosesClientInputStream() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+    peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("square"));
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+    Source source = stream.getSource();
+    assertStreamData("square", source);
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    assertTrue(synStream.inFinished);
+    assertFalse(synStream.outFinished);
+  }
+
+  @Test public void remoteDoubleSynReply() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+    peer.sendFrame().ping(true, 1, 0);
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("c", "cola"), true, true);
+    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+    connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received.
+    try {
+      stream.getSource().read(new OkBuffer(), 1);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: STREAM_IN_USE", expected.getMessage());
+    }
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(1, rstStream.streamId);
+    assertEquals(STREAM_IN_USE, rstStream.errorCode);
+  }
+
+  @Test public void remoteDoubleSynStream() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("a", "android"));
+    peer.acceptFrame(); // SYN_REPLY
+    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("b", "banana"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    final AtomicInteger receiveCount = new AtomicInteger();
+    IncomingStreamHandler handler = new IncomingStreamHandler() {
+      @Override public void receive(SpdyStream stream) throws IOException {
+        receiveCount.incrementAndGet();
+        assertEquals(headerEntries("a", "android"), stream.getRequestHeaders());
+        assertEquals(null, stream.getErrorCode());
+        stream.reply(headerEntries("c", "cola"), true);
+      }
+    };
+    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame reply = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, reply.type);
+    assertEquals(HeadersMode.SPDY_REPLY, reply.headersMode);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(2, rstStream.streamId);
+    assertEquals(PROTOCOL_ERROR, rstStream.errorCode);
+    assertEquals(1, receiveCount.intValue());
+  }
+
+  @Test public void remoteSendsDataAfterInFinished() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
+    peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("c3po")); // Ignored.
+    peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+    assertStreamData("robot", stream.getSource());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(2, ping.payload1);
+  }
+
+  @Test public void clientDoesNotLimitFlowControl() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+    peer.sendFrame().data(false, 1, new OkBuffer().write(new byte[64 * 1024 + 1]));
+    peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    assertEquals(headerEntries("b", "banana"), stream.getResponseHeaders());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(2, ping.payload1);
+  }
+
+  @Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().rstStream(1, REFUSED_STREAM);
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame(); // PING
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    try {
+      stream.getResponseHeaders();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
+    }
+    assertEquals(0, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(2, ping.payload1);
+  }
+
+
+  @Test public void receiveGoAway() throws Exception {
+    receiveGoAway(SPDY3);
+  }
+
+  @Test public void receiveGoAwayHttp2() throws Exception {
+    receiveGoAway(HTTP_20_DRAFT_09);
+  }
+
+  private void receiveGoAway(Variant variant) throws Exception {
+    peer.setVariantAndClient(variant, false);
+
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM 1
+    peer.acceptFrame(); // SYN_STREAM 3
+    peer.sendFrame().goAway(1, PROTOCOL_ERROR, Util.EMPTY_BYTE_ARRAY);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 0);
+    peer.acceptFrame(); // DATA STREAM 1
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, variant);
+    SpdyStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
+    SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+    connection.ping().roundTripTime(); // Ensure that the GO_AWAY has been received.
+    BufferedSink sink1 = Okio.buffer(stream1.getSink());
+    BufferedSink sink2 = Okio.buffer(stream2.getSink());
+    sink1.writeUtf8("abc");
+    try {
+      sink2.writeUtf8("abc");
+      sink2.flush();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
+    }
+    sink1.writeUtf8("def");
+    sink1.close();
+    try {
+      connection.newStream(headerEntries("c", "cola"), true, true);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("shutdown", expected.getMessage());
+    }
+    assertEquals(1, connection.openStreamCount());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream1.type);
+    MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream2.type);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    MockSpdyPeer.InFrame data1 = peer.takeFrame();
+    assertEquals(TYPE_DATA, data1.type);
+    assertEquals(1, data1.streamId);
+    assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
+  }
+
+  @Test public void sendGoAway() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM 1
+    peer.acceptFrame(); // GOAWAY
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("b", "b")); // Should be ignored!
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    connection.newStream(headerEntries("a", "android"), true, true);
+    Ping ping = connection.ping();
+    connection.shutdown(PROTOCOL_ERROR);
+    assertEquals(1, connection.openStreamCount());
+    ping.roundTripTime(); // Prevent the peer from exiting prematurely.
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream1.type);
+    MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
+    assertEquals(TYPE_PING, pingFrame.type);
+    MockSpdyPeer.InFrame goaway = peer.takeFrame();
+    assertEquals(TYPE_GOAWAY, goaway.type);
+    assertEquals(0, goaway.streamId);
+    assertEquals(PROTOCOL_ERROR, goaway.errorCode);
+  }
+
+  @Test public void noPingsAfterShutdown() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // GOAWAY
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    connection.shutdown(INTERNAL_ERROR);
+    try {
+      connection.ping();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("shutdown", expected.getMessage());
+    }
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame goaway = peer.takeFrame();
+    assertEquals(TYPE_GOAWAY, goaway.type);
+    assertEquals(INTERNAL_ERROR, goaway.errorCode);
+  }
+
+  @Test public void close() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // GOAWAY
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    assertEquals(1, connection.openStreamCount());
+    connection.close();
+    assertEquals(0, connection.openStreamCount());
+    try {
+      connection.newStream(headerEntries("b", "banana"), true, true);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("shutdown", expected.getMessage());
+    }
+    BufferedSink sink = Okio.buffer(stream.getSink());
+    try {
+      sink.writeByte(0);
+      sink.flush();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: CANCEL", expected.getMessage());
+    }
+    try {
+      stream.getSource().read(new OkBuffer(), 1);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: CANCEL", expected.getMessage());
+    }
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame goaway = peer.takeFrame();
+    assertEquals(TYPE_GOAWAY, goaway.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(1, rstStream.streamId);
+  }
+
+  @Test public void closeCancelsPings() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // PING
+    peer.acceptFrame(); // GOAWAY
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    Ping ping = connection.ping();
+    connection.close();
+    assertEquals(-1, ping.roundTripTime());
+  }
+
+  @Test public void readTimeoutExpires() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    stream.setReadTimeout(1000);
+    Source source = stream.getSource();
+    long startNanos = System.nanoTime();
+    try {
+      source.read(new OkBuffer(), 1);
+      fail();
+    } catch (IOException expected) {
+    }
+    long elapsedNanos = System.nanoTime() - startNanos;
+    assertEquals(1000d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
+    assertEquals(1, connection.openStreamCount());
+    connection.ping().roundTripTime(); // Prevent the peer from exiting prematurely.
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+  }
+
+  @Test public void headers() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.sendFrame().headers(1, headerEntries("c", "c3po"));
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
+    assertEquals(headerEntries("a", "android", "c", "c3po"), stream.getResponseHeaders());
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+  }
+
+  @Test public void headersBeforeReply() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // PING
+    peer.sendFrame().headers(1, headerEntries("c", "c3po"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(true, 1, 0);
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
+    try {
+      stream.getResponseHeaders();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
+    }
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(HeadersMode.SPDY_SYN_STREAM, synStream.headersMode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(PROTOCOL_ERROR, rstStream.errorCode);
+  }
+
+  @Test public void readSendsWindowUpdate() throws Exception {
+    readSendsWindowUpdate(SPDY3);
+  }
+
+  @Test public void readSendsWindowUpdateHttp2() throws Exception {
+    readSendsWindowUpdate(HTTP_20_DRAFT_09);
+  }
+
+  private void readSendsWindowUpdate(Variant variant)
+      throws IOException, InterruptedException {
+    peer.setVariantAndClient(variant, false);
+
+    int windowUpdateThreshold = DEFAULT_INITIAL_WINDOW_SIZE / 2;
+
+    // Write the mocking script.
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    for (int i = 0; i < 3; i++) {
+      // Send frames summing to windowUpdateThreshold.
+      for (int sent = 0, count; sent < windowUpdateThreshold; sent += count) {
+        count = Math.min(variant.maxFrameSize(), windowUpdateThreshold - sent);
+        peer.sendFrame().data(false, 1, data(count));
+      }
+      peer.acceptFrame(); // connection WINDOW UPDATE
+      peer.acceptFrame(); // stream WINDOW UPDATE
+    }
+    peer.sendFrame().data(true, 1, data(0));
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, variant);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
+    assertEquals(0, stream.unacknowledgedBytesRead);
+    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+    Source in = stream.getSource();
+    OkBuffer buffer = new OkBuffer();
+    while (in.read(buffer, 1024) != -1) {
+      if (buffer.size() == 3 * windowUpdateThreshold) break;
+    }
+    assertEquals(-1, in.read(buffer, 1));
+
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    for (int i = 0; i < 3; i++) {
+      List<Integer> windowUpdateStreamIds = new ArrayList(2);
+      for (int j = 0; j < 2; j++) {
+        MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
+        assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
+        windowUpdateStreamIds.add(windowUpdate.streamId);
+        assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
+      }
+      assertTrue(windowUpdateStreamIds.contains(0)); // connection
+      assertTrue(windowUpdateStreamIds.contains(1)); // stream
+    }
+  }
+
+  private OkBuffer data(int byteCount) {
+    return new OkBuffer().write(new byte[byteCount]);
+  }
+
+  @Test public void serverSendsEmptyDataClientDoesntSendWindowUpdate() throws Exception {
+    serverSendsEmptyDataClientDoesntSendWindowUpdate(SPDY3);
+  }
+
+  @Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
+    serverSendsEmptyDataClientDoesntSendWindowUpdate(HTTP_20_DRAFT_09);
+  }
+
+  private void serverSendsEmptyDataClientDoesntSendWindowUpdate(Variant variant)
+      throws IOException, InterruptedException {
+    peer.setVariantAndClient(variant, false);
+
+    // Write the mocking script.
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.sendFrame().data(true, 1, data(0));
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, variant);
+    SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+    assertEquals(-1, client.getSource().read(new OkBuffer(), 1));
+
+    // Verify the peer received what was expected.
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    assertEquals(3, peer.frameCount());
+  }
+
+  @Test public void clientSendsEmptyDataServerDoesntSendWindowUpdate() throws Exception {
+    clientSendsEmptyDataServerDoesntSendWindowUpdate(SPDY3);
+  }
+
+  @Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
+    clientSendsEmptyDataServerDoesntSendWindowUpdate(HTTP_20_DRAFT_09);
+  }
+
+  private void clientSendsEmptyDataServerDoesntSendWindowUpdate(Variant variant)
+      throws IOException, InterruptedException {
+    peer.setVariantAndClient(variant, false);
+
+    // Write the mocking script.
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // DATA
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, variant);
+    SpdyStream client = connection.newStream(headerEntries("b", "banana"), true, true);
+    BufferedSink out = Okio.buffer(client.getSink());
+    out.write(Util.EMPTY_BYTE_ARRAY);
+    out.flush();
+    out.close();
+
+    // Verify the peer received what was expected.
+    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+    assertEquals(TYPE_DATA, peer.takeFrame().type);
+    assertEquals(3, peer.frameCount());
+  }
+
+  @Test public void writeAwaitsWindowUpdate() throws Exception {
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
+
+    // Write the mocking script. This accepts more data frames than necessary!
+    peer.acceptFrame(); // SYN_STREAM
+    for (int i = 0; i < framesThatFillWindow; i++) {
+      peer.acceptFrame(); // DATA
+    }
+    peer.acceptFrame(); // DATA we won't be able to flush until a window update.
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
+    out.flush();
+
+    // Check that we've filled the window for both the stream and also the connection.
+    assertEquals(0, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+
+    out.writeByte('a');
+    assertFlushBlocks(out);
+
+    // receiving a window update on the connection isn't enough.
+    connection.readerRunnable.windowUpdate(0, 1);
+    assertFlushBlocks(out);
+
+    // receiving a window update on the stream will unblock the stream.
+    connection.readerRunnable.windowUpdate(1, 1);
+    out.flush();
+
+    // Verify the peer received what was expected.
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    for (int i = 0; i < framesThatFillWindow; i++) {
+      MockSpdyPeer.InFrame data = peer.takeFrame();
+      assertEquals(TYPE_DATA, data.type);
+    }
+  }
+
+  @Test public void initialSettingsWithWindowSizeAdjustsConnection() throws Exception {
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
+
+    // Write the mocking script. This accepts more data frames than necessary!
+    peer.acceptFrame(); // SYN_STREAM
+    for (int i = 0; i < framesThatFillWindow; i++) {
+      peer.acceptFrame(); // DATA on stream 1
+    }
+    peer.acceptFrame(); // DATA on stream 2
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("a", "apple"), true, true);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
+    out.flush();
+
+    // write 1 more than the window size
+    out.writeByte('a');
+    assertFlushBlocks(out);
+
+    // Check that we've filled the window for both the stream and also the connection.
+    assertEquals(0, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+
+    // Receiving a Settings with a larger window size will unblock the streams.
+    Settings initial = new Settings();
+    initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 1);
+    connection.readerRunnable.settings(false, initial);
+
+    assertEquals(1, connection.bytesLeftInWriteWindow);
+    assertEquals(1, connection.getStream(1).bytesLeftInWriteWindow);
+
+    // The stream should no longer be blocked.
+    out.flush();
+
+    assertEquals(0, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+
+    // Settings after the initial do not affect the connection window size.
+    Settings next = new Settings();
+    next.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 2);
+    connection.readerRunnable.settings(false, next);
+
+    assertEquals(0, connection.bytesLeftInWriteWindow); // connection wasn't affected.
+    assertEquals(1, connection.getStream(1).bytesLeftInWriteWindow);
+  }
+
+  @Test public void testTruncatedDataFrame() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.sendTruncatedFrame(8 + 100).data(false, 1, data(1024));
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+    Source in = stream.getSource();
+    try {
+      Okio.buffer(in).readByteString(101);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
+    }
+  }
+
+  @Test public void blockedStreamDoesntStarveNewStream() throws Exception {
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, SPDY3.maxFrameSize());
+
+    // Write the mocking script. This accepts more data frames than necessary!
+    peer.acceptFrame(); // SYN_STREAM on stream 1
+    for (int i = 0; i < framesThatFillWindow; i++) {
+      peer.acceptFrame(); // DATA on stream 1
+    }
+    peer.acceptFrame(); // SYN_STREAM on stream 2
+    peer.acceptFrame(); // DATA on stream 2
+    peer.play();
+
+    // Play it back.
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream1 = connection.newStream(headerEntries("a", "apple"), true, true);
+    BufferedSink out1 = Okio.buffer(stream1.getSink());
+    out1.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
+    out1.flush();
+
+    // Check that we've filled the window for both the stream and also the connection.
+    assertEquals(0, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+
+    // receiving a window update on the the connection will unblock new streams.
+    connection.readerRunnable.windowUpdate(0, 3);
+
+    assertEquals(3, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+
+    // Another stream should be able to send data even though 1 is blocked.
+    SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+    BufferedSink out2 = Okio.buffer(stream2.getSink());
+    out2.writeUtf8("foo");
+    out2.flush();
+
+    assertEquals(0, connection.bytesLeftInWriteWindow);
+    assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
+    assertEquals(DEFAULT_INITIAL_WINDOW_SIZE - 3, connection.getStream(3).bytesLeftInWriteWindow);
+  }
+
+  @Test public void maxFrameSizeHonored() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    byte[] buff = new byte[HTTP_20_DRAFT_09.maxFrameSize() + 1];
+    Arrays.fill(buff, (byte) '*');
+
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    peer.acceptFrame(); // DATA 1
+    peer.acceptFrame(); // DATA 2
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    BufferedSink out = Okio.buffer(stream.getSink());
+    out.write(buff);
+    out.flush();
+    out.close();
+
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_HEADERS, synStream.type);
+    MockSpdyPeer.InFrame data = peer.takeFrame();
+    assertEquals(HTTP_20_DRAFT_09.maxFrameSize(), data.data.length);
+    data = peer.takeFrame();
+    assertEquals(1, data.data.length);
+  }
+
+  /** https://github.com/square/okhttp/issues/333 */
+  @Test public void headerBlockHasTrailingCompressedBytes512() throws Exception {
+    // This specially-formatted frame has trailing deflated bytes after the name value block.
+    String frame = "gAMAAgAAAgkAAAABeLvjxqfCYgAAAAD//2IAAAAA//9iAAAAAP//YgQAAAD//2IAAAAA//9iAAAAAP/"
+        + "/YgAAAAD//2IEAAAA//9KBAAAAP//YgAAAAD//2IAAAAA//9iAAAAAP//sgEAAAD//2IAAAAA\n//9iBAAAAP//Y"
+        + "gIAAAD//2IGAAAA//9iAQAAAP//YgUAAAD//2IDAAAA//9iBwAAAP//4gAAAAD//+IEAAAA///iAgAAAP//4gYAA"
+        + "AD//+IBAAAA///iBQAAAP//4gMAAAD//+IHAAAA//8SAAAAAP//EgQAAAD//xICAAAA//8SBgAAAP//EgEAAAD//"
+        + "xIFAAAA//8SAwAAAP//EgcAAAD//5IAAAAA//+SBAAAAP//kgIAAAD//5IGAAAA//+SAQAAAP//kgUAAAD//5IDA"
+        + "AAA//+SBwAAAP//UgAAAAD//1IEAAAA//9SAgAAAP//UgYAAAD//1IBAAAA//9SBQAAAP//UgMAAAD//1IHAAAA/"
+        + "//SAAAAAP//0gQAAAD//9ICAAAA///SBgAAAP//0gEAAAD//9IFAAAA///SAwAAAP//0gcAAAD//zIAAAAA//8yB"
+        + "AAAAP//MgIAAAD//zIGAAAA//8yAQAAAP//MgUAAAD//zIDAAAA//8yBwAAAP//sgAAAAD//7IEAAAA//+yAgAAA"
+        + "P//sgYAAAD//w==";
+    headerBlockHasTrailingCompressedBytes(frame, 60);
+  }
+
+  @Test public void headerBlockHasTrailingCompressedBytes2048() throws Exception {
+    // This specially-formatted frame has trailing deflated bytes after the name value block.
+    String frame = "gAMAAgAAB/sAAAABeLvjxqfCAqYjRhAGJmxGxUQAAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAA"
+        + "AAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP/"
+        + "/SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQ"
+        + "AAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD"
+        + "//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0o"
+        + "EAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAA"
+        + "A//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9"
+        + "KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAA"
+        + "AAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP/"
+        + "/SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQ"
+        + "AAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD"
+        + "//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0o"
+        + "EAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAA"
+        + "A//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9"
+        + "KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAA"
+        + "AAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP/"
+        + "/SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQ"
+        + "AAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD"
+        + "//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0o"
+        + "EAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAA"
+        + "A//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9"
+        + "KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAA"
+        + "AAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP/"
+        + "/SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQ"
+        + "AAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD"
+        + "//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0o"
+        + "EAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAA"
+        + "A//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9"
+        + "KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAA"
+        + "AAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP/"
+        + "/SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQAAAD//0oEAAAA//9KBAAAAP//SgQ"
+        + "AAAD//0oEAAAA//8=";
+    headerBlockHasTrailingCompressedBytes(frame, 289);
+  }
+
+  private void headerBlockHasTrailingCompressedBytes(String frame, int length) throws IOException {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame(ByteString.decodeBase64(frame).toByteArray());
+    peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
+    peer.acceptFrame(); // DATA
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    assertEquals("a", stream.getResponseHeaders().get(0).name.utf8());
+    assertEquals(length, stream.getResponseHeaders().get(0).value.size());
+    assertStreamData("robot", stream.getSource());
+  }
+
+  @Test public void pushPromiseStream() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+    final List<Header> expectedRequestHeaders = Arrays.asList(
+        new Header(Header.TARGET_METHOD, "GET"),
+        new Header(Header.TARGET_SCHEME, "https"),
+        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+        new Header(Header.TARGET_PATH, "/cached")
+    );
+    peer.sendFrame().pushPromise(1, 2, expectedRequestHeaders);
+    final List<Header> expectedResponseHeaders = Arrays.asList(
+        new Header(Header.RESPONSE_STATUS, "200")
+    );
+    peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
+    peer.sendFrame().data(true, 1, data(0));
+    peer.play();
+
+    final List events = new ArrayList();
+    PushObserver observer = new PushObserver() {
+
+      @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
+        assertEquals(2, streamId);
+        events.add(requestHeaders);
+        return false;
+      }
+
+      @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
+        assertEquals(2, streamId);
+        assertTrue(last);
+        events.add(responseHeaders);
+        return false;
+      }
+
+      @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
+          boolean last) throws IOException {
+        events.add(new AssertionError("onData"));
+        return false;
+      }
+
+      @Override public void onReset(int streamId, ErrorCode errorCode) {
+        events.add(new AssertionError("onReset"));
+      }
+    };
+
+    // play it back
+    SpdyConnection connection = connectionBuilder(peer, HTTP_20_DRAFT_09)
+        .pushObserver(observer).build();
+    SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+    assertEquals(-1, client.getSource().read(new OkBuffer(), 1));
+
+    // verify the peer received what was expected
+    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+
+    assertEquals(2, events.size());
+    assertEquals(expectedRequestHeaders, events.get(0));
+    assertEquals(expectedResponseHeaders, events.get(1));
+  }
+
+  @Test public void doublePushPromise() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    // write the mocking script
+    peer.sendFrame().pushPromise(1,2, headerEntries("a", "android"));
+    peer.acceptFrame(); // SYN_REPLY
+    peer.sendFrame().pushPromise(1, 2, headerEntries("b", "banana"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connectionBuilder(peer, HTTP_20_DRAFT_09).build();
+    connection.newStream(headerEntries("b", "banana"), false, true);
+
+    // verify the peer received what was expected
+    assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+    assertEquals(PROTOCOL_ERROR, peer.takeFrame().errorCode);
+  }
+
+  @Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
+
+    // write the mocking script
+    peer.sendFrame().pushPromise(1, 2, Arrays.asList(
+        new Header(Header.TARGET_METHOD, "GET"),
+        new Header(Header.TARGET_SCHEME, "https"),
+        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+        new Header(Header.TARGET_PATH, "/cached")
+    ));
+    peer.sendFrame().synReply(true, 2, Arrays.asList(
+        new Header(Header.RESPONSE_STATUS, "200")
+    ));
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
+
+    // play it back
+    connectionBuilder(peer, HTTP_20_DRAFT_09)
+        .pushObserver(PushObserver.CANCEL).build();
+
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(2, rstStream.streamId);
+    assertEquals(CANCEL, rstStream.errorCode);
+  }
+
+  private SpdyConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
+      throws IOException, InterruptedException {
+    peer.setVariantAndClient(HTTP_20_DRAFT_09, client);
+    peer.sendFrame().settings(settings);
+    peer.acceptFrame(); // ACK
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
+
+    // verify the peer received the ACK
+    MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
+    assertEquals(TYPE_SETTINGS, ackFrame.type);
+    assertEquals(0, ackFrame.streamId);
+    assertTrue(ackFrame.ack);
+    return connection;
+  }
+
+  private SpdyConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
+    return connectionBuilder(peer, variant).build();
+  }
+
+  private SpdyConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
+      throws IOException {
+    return new SpdyConnection.Builder(true, peer.openSocket())
+        .pushObserver(IGNORE)
+        .protocol(variant.getProtocol());
+  }
+
+  private void assertStreamData(String expected, Source source) throws IOException {
+    OkBuffer buffer = new OkBuffer();
+    while (source.read(buffer, Long.MAX_VALUE) != -1) {
+    }
+    String actual = buffer.readUtf8(buffer.size());
+    assertEquals(expected, actual);
+  }
+
+  private void assertFlushBlocks(BufferedSink out) throws IOException {
+    interruptAfterDelay(500);
+    try {
+      out.flush();
+      fail();
+    } catch (InterruptedIOException expected) {
+    }
+  }
+
+  /** Interrupts the current thread after {@code delayMillis}. */
+  private void interruptAfterDelay(final long delayMillis) {
+    final Thread toInterrupt = Thread.currentThread();
+    new Thread("interrupting cow") {
+      @Override public void run() {
+        try {
+          Thread.sleep(delayMillis);
+          toInterrupt.interrupt();
+        } catch (InterruptedException e) {
+          throw new AssertionError();
+        }
+      }
+    }.start();
+  }
+
+  static int roundUp(int num, int divisor) {
+    return (num + divisor - 1) / divisor;
+  }
+
+  static final PushObserver IGNORE = new PushObserver() {
+
+    @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
+      return false;
+    }
+
+    @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
+      return false;
+    }
+
+    @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
+        boolean last) throws IOException {
+      source.skip(byteCount);
+      return false;
+    }
+
+    @Override public void onReset(int streamId, ErrorCode errorCode) {
+    }
+  };
+}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/FakeSSLSession.java
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
similarity index 100%
rename from okhttp/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 9bad03d..c7e3ec4 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp</artifactId>
@@ -14,27 +14,10 @@
 
   <dependencies>
     <dependency>
-      <groupId>com.squareup.okhttp</groupId>
-      <artifactId>okhttp-protocols</artifactId>
+      <groupId>com.squareup.okio</groupId>
+      <artifactId>okio</artifactId>
       <version>${project.version}</version>
     </dependency>
-    <dependency>
-      <groupId>com.squareup.okhttp</groupId>
-      <artifactId>mockwebserver</artifactId>
-      <version>${project.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.mortbay.jetty.npn</groupId>
-      <artifactId>npn-boot</artifactId>
-      <scope>provided</scope>
-    </dependency>
-
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <scope>test</scope>
-    </dependency>
   </dependencies>
 
   <build>
@@ -46,24 +29,6 @@
           <excludePackageNames>com.squareup.okhttp.internal:com.squareup.okhttp.internal.*</excludePackageNames>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-assembly-plugin</artifactId>
-        <configuration>
-          <descriptorRefs>
-            <descriptorRef>jar-with-dependencies</descriptorRef>
-          </descriptorRefs>
-        </configuration>
-        <executions>
-          <execution>
-            <phase>package</phase>
-            <goals>
-              <goal>single</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java
index b34bd91..8f80cf4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Address.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Address.java
@@ -19,6 +19,7 @@
 import java.net.Proxy;
 import java.net.UnknownHostException;
 import java.util.List;
+import javax.net.SocketFactory;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLSocketFactory;
 
@@ -38,25 +39,28 @@
   final Proxy proxy;
   final String uriHost;
   final int uriPort;
+  final SocketFactory socketFactory;
   final SSLSocketFactory sslSocketFactory;
   final HostnameVerifier hostnameVerifier;
   final OkAuthenticator authenticator;
-  final List<String> transports;
+  final List<Protocol> protocols;
 
-  public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
-      HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy,
-      List<String> transports) throws UnknownHostException {
+  public Address(String uriHost, int uriPort, SocketFactory socketFactory,
+      SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
+      OkAuthenticator authenticator, Proxy proxy, List<Protocol> protocols)
+      throws UnknownHostException {
     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 (transports == null) throw new IllegalArgumentException("transports == null");
+    if (protocols == null) throw new IllegalArgumentException("protocols == null");
     this.proxy = proxy;
     this.uriHost = uriHost;
     this.uriPort = uriPort;
+    this.socketFactory = socketFactory;
     this.sslSocketFactory = sslSocketFactory;
     this.hostnameVerifier = hostnameVerifier;
     this.authenticator = authenticator;
-    this.transports = Util.immutableList(transports);
+    this.protocols = Util.immutableList(protocols);
   }
 
   /** Returns the hostname of the origin server. */
@@ -72,6 +76,11 @@
     return uriPort;
   }
 
+  /** Returns the socket factory for new connections. */
+  public SocketFactory getSocketFactory() {
+    return socketFactory;
+  }
+
   /**
    * Returns the SSL socket factory, or null if this is not an HTTPS
    * address.
@@ -97,11 +106,12 @@
   }
 
   /**
-   * Returns the client's transports. This method always returns a non-null list
-   * that contains "http/1.1", possibly among other transports.
+   * Returns the protocols the client supports. This method always returns a
+   * non-null list that contains minimally
+   * {@link Protocol#HTTP_11}.
    */
-  public List<String> getTransports() {
-    return transports;
+  public List<Protocol> getProtocols() {
+    return protocols;
   }
 
   /**
@@ -121,7 +131,7 @@
           && equal(this.sslSocketFactory, that.sslSocketFactory)
           && equal(this.hostnameVerifier, that.hostnameVerifier)
           && equal(this.authenticator, that.authenticator)
-          && equal(this.transports, that.transports);
+          && equal(this.protocols, that.protocols);
     }
     return false;
   }
@@ -134,7 +144,7 @@
     result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
     result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0);
     result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
-    result = 31 * result + transports.hashCode();
+    result = 31 * result + protocols.hashCode();
     return result;
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java
new file mode 100644
index 0000000..dc944e4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java
@@ -0,0 +1,176 @@
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.http.HeaderParser;
+
+/**
+ * A Cache-Control header with cache directives from a server or client. These
+ * directives set policy on what responses can be stored, and which requests can
+ * be satisfied by those stored responses.
+ *
+ * <p>See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">RFC
+ * 2616, 14.9</a>.
+ */
+public final class CacheControl {
+  private final boolean noCache;
+  private final boolean noStore;
+  private final int maxAgeSeconds;
+  private final int sMaxAgeSeconds;
+  private final boolean isPublic;
+  private final boolean mustRevalidate;
+  private final int maxStaleSeconds;
+  private final int minFreshSeconds;
+  private final boolean onlyIfCached;
+
+  private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
+      boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds,
+      boolean onlyIfCached) {
+    this.noCache = noCache;
+    this.noStore = noStore;
+    this.maxAgeSeconds = maxAgeSeconds;
+    this.sMaxAgeSeconds = sMaxAgeSeconds;
+    this.isPublic = isPublic;
+    this.mustRevalidate = mustRevalidate;
+    this.maxStaleSeconds = maxStaleSeconds;
+    this.minFreshSeconds = minFreshSeconds;
+    this.onlyIfCached = onlyIfCached;
+  }
+
+  /**
+   * In a response, this field's name "no-cache" is misleading. It doesn't
+   * prevent us from caching the response; it only means we have to validate the
+   * response with the origin server before returning it. We can do this with a
+   * conditional GET.
+   *
+   * <p>In a request, it means do not use a cache to satisfy the request.
+   */
+  public boolean noCache() {
+    return noCache;
+  }
+
+  /** If true, this response should not be cached. */
+  public boolean noStore() {
+    return noStore;
+  }
+
+  /**
+   * The duration past the response's served date that it can be served without
+   * validation.
+   */
+  public int maxAgeSeconds() {
+    return maxAgeSeconds;
+  }
+
+  /**
+   * The "s-maxage" directive is the max age for shared caches. Not to be
+   * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
+   * this directive is not honored by this cache.
+   */
+  public int sMaxAgeSeconds() {
+    return sMaxAgeSeconds;
+  }
+
+  public boolean isPublic() {
+    return isPublic;
+  }
+
+  public boolean mustRevalidate() {
+    return mustRevalidate;
+  }
+
+  public int maxStaleSeconds() {
+    return maxStaleSeconds;
+  }
+
+  public int minFreshSeconds() {
+    return minFreshSeconds;
+  }
+
+  /**
+   * This field's name "only-if-cached" is misleading. It actually means "do
+   * not use the network". It is set by a client who only wants to make a
+   * request if it can be fully satisfied by the cache. Cached responses that
+   * would require validation (ie. conditional gets) are not permitted if this
+   * header is set.
+   */
+  public boolean onlyIfCached() {
+    return onlyIfCached;
+  }
+
+  /**
+   * Returns the cache directives of {@code headers}. This honors both
+   * Cache-Control and Pragma headers if they are present.
+   */
+  public static CacheControl parse(Headers headers) {
+    boolean noCache = false;
+    boolean noStore = false;
+    int maxAgeSeconds = -1;
+    int sMaxAgeSeconds = -1;
+    boolean isPublic = false;
+    boolean mustRevalidate = false;
+    int maxStaleSeconds = -1;
+    int minFreshSeconds = -1;
+    boolean onlyIfCached = false;
+
+    for (int i = 0; i < headers.size(); i++) {
+      if (!headers.name(i).equalsIgnoreCase("Cache-Control")
+          && !headers.name(i).equalsIgnoreCase("Pragma")) {
+        continue;
+      }
+
+      String string = headers.value(i);
+      int pos = 0;
+      while (pos < string.length()) {
+        int tokenStart = pos;
+        pos = HeaderParser.skipUntil(string, pos, "=,;");
+        String directive = string.substring(tokenStart, pos).trim();
+        String parameter;
+
+        if (pos == string.length() || string.charAt(pos) == ',' || string.charAt(pos) == ';') {
+          pos++; // consume ',' or ';' (if necessary)
+          parameter = null;
+        } else {
+          pos++; // consume '='
+          pos = HeaderParser.skipWhitespace(string, pos);
+
+          // quoted string
+          if (pos < string.length() && string.charAt(pos) == '\"') {
+            pos++; // consume '"' open quote
+            int parameterStart = pos;
+            pos = HeaderParser.skipUntil(string, pos, "\"");
+            parameter = string.substring(parameterStart, pos);
+            pos++; // consume '"' close quote (if necessary)
+
+            // unquoted string
+          } else {
+            int parameterStart = pos;
+            pos = HeaderParser.skipUntil(string, pos, ",;");
+            parameter = string.substring(parameterStart, pos).trim();
+          }
+        }
+
+        if ("no-cache".equalsIgnoreCase(directive)) {
+          noCache = true;
+        } else if ("no-store".equalsIgnoreCase(directive)) {
+          noStore = true;
+        } else if ("max-age".equalsIgnoreCase(directive)) {
+          maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("s-maxage".equalsIgnoreCase(directive)) {
+          sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("public".equalsIgnoreCase(directive)) {
+          isPublic = true;
+        } else if ("must-revalidate".equalsIgnoreCase(directive)) {
+          mustRevalidate = true;
+        } else if ("max-stale".equalsIgnoreCase(directive)) {
+          maxStaleSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("min-fresh".equalsIgnoreCase(directive)) {
+          minFreshSeconds = HeaderParser.parseSeconds(parameter);
+        } else if ("only-if-cached".equalsIgnoreCase(directive)) {
+          onlyIfCached = true;
+        }
+      }
+    }
+
+    return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPublic,
+        mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached);
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index a6b798d..94527af 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -18,31 +18,25 @@
 
 import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.http.HttpAuthenticator;
+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.RawHeaders;
 import com.squareup.okhttp.internal.http.SpdyTransport;
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.Proxy;
 import java.net.Socket;
-import java.net.SocketTimeoutException;
-import java.net.URL;
-import java.util.Arrays;
 import javax.net.ssl.SSLSocket;
+import okio.ByteString;
 
 import static java.net.HttpURLConnection.HTTP_OK;
 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
 
 /**
- * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
- * which 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
@@ -55,10 +49,10 @@
  * 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>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
- * for both HTTP and SPDY transports.
+ *   <li>Server Name Indication (SNI) enables one IP address to negotiate secure
+ *       connections for multiple domain names.
+ *   <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
+ *       for both 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
@@ -66,53 +60,99 @@
  * should the attempt fail.
  */
 public final class Connection implements Closeable {
-  private static final byte[] NPN_PROTOCOLS = new byte[] {
-      6, 's', 'p', 'd', 'y', '/', '3',
-      8, 'h', 't', 't', 'p', '/', '1', '.', '1'
-  };
-  private static final byte[] SPDY3 = new byte[] {
-      's', 'p', 'd', 'y', '/', '3'
-  };
-  private static final byte[] HTTP_11 = new byte[] {
-      'h', 't', 't', 'p', '/', '1', '.', '1'
-  };
-
+  private final ConnectionPool pool;
   private final Route route;
 
   private Socket socket;
-  private InputStream in;
-  private OutputStream out;
   private boolean connected = false;
+  private HttpConnection httpConnection;
   private SpdyConnection spdyConnection;
   private int httpMinorVersion = 1; // Assume HTTP/1.1
   private long idleStartTimeNs;
+  private Handshake handshake;
+  private int recycleCount;
 
-  public Connection(Route route) {
+  /**
+   * 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;
   }
 
+  public Object getOwner() {
+    synchronized (pool) {
+      return owner;
+    }
+  }
+
+  public void setOwner(Object owner) {
+    if (isSpdy()) return; // SPDY 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}.
+   */
+  public 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.
+   */
+  public void closeIfOwnedBy(Object owner) throws IOException {
+    if (isSpdy()) 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.
+    socket.close();
+  }
+
   public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
       throws IOException {
-    if (connected) {
-      throw new IllegalStateException("already connected");
+    if (connected) throw new IllegalStateException("already connected");
+
+    if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) {
+      socket = route.address.socketFactory.createSocket();
+    } else {
+      socket = new Socket(route.proxy);
     }
-    connected = true;
-    socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
-    Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
+
     socket.setSoTimeout(readTimeout);
-    in = socket.getInputStream();
-    out = socket.getOutputStream();
+    Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
 
     if (route.address.sslSocketFactory != null) {
       upgradeToTls(tunnelRequest);
+    } else {
+      httpConnection = new HttpConnection(pool, this, socket);
     }
-
-    // Use MTU-sized buffers to send fewer packets.
-    int mtu = Platform.get().getMtu(socket);
-    if (mtu < 1024) mtu = 1024;
-    if (mtu > 8192) mtu = 8192;
-    in = new BufferedInputStream(in, mtu);
-    out = new BufferedOutputStream(out, mtu);
+    connected = true;
   }
 
   /**
@@ -137,9 +177,20 @@
       platform.supportTlsIntolerantServer(sslSocket);
     }
 
-    boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
-    if (useNpn) {
-      platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+    boolean useNpn = false;
+    if (route.modernTls) {
+      boolean http2 = route.address.protocols.contains(Protocol.HTTP_2);
+      boolean spdy3 = route.address.protocols.contains(Protocol.SPDY_3);
+      if (http2 && spdy3) {
+        platform.setNpnProtocols(sslSocket, Protocol.HTTP2_SPDY3_AND_HTTP);
+        useNpn = true;
+      } else if (http2) {
+        platform.setNpnProtocols(sslSocket, Protocol.HTTP2_AND_HTTP_11);
+        useNpn = true;
+      } else if (spdy3) {
+        platform.setNpnProtocols(sslSocket, Protocol.SPDY3_AND_HTTP11);
+        useNpn = true;
+      }
     }
 
     // Force handshake. This can throw!
@@ -150,20 +201,21 @@
       throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
     }
 
-    out = sslSocket.getOutputStream();
-    in = sslSocket.getInputStream();
+    handshake = Handshake.get(sslSocket.getSession());
 
-    byte[] selectedProtocol;
-    if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
-      if (Arrays.equals(selectedProtocol, SPDY3)) {
-        sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
-        spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
-            .build();
-        spdyConnection.sendConnectionHeader();
-      } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
-        throw new IOException(
-            "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
-      }
+    ByteString maybeProtocol;
+    Protocol selectedProtocol = Protocol.HTTP_11;
+    if (useNpn && (maybeProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+      selectedProtocol = Protocol.find(maybeProtocol); // Throws IOE on unknown.
+    }
+
+    if (selectedProtocol.spdyVariant) {
+      sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
+      spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
+          .protocol(selectedProtocol).build();
+      spdyConnection.sendConnectionHeader();
+    } else {
+      httpConnection = new HttpConnection(pool, this, socket);
     }
   }
 
@@ -173,7 +225,7 @@
   }
 
   @Override public void close() throws IOException {
-    socket.close();
+    if (socket != null) socket.close();
   }
 
   /** Returns the route used by this connection. */
@@ -200,37 +252,12 @@
    * #isAlive()}; callers should check {@link #isAlive()} first.
    */
   public boolean isReadable() {
-    if (!(in instanceof BufferedInputStream)) {
-      return true; // Optimistic.
-    }
-    if (isSpdy()) {
-      return true; // Optimistic. We can't test SPDY because its streams are in use.
-    }
-    BufferedInputStream bufferedInputStream = (BufferedInputStream) in;
-    try {
-      int readTimeout = socket.getSoTimeout();
-      try {
-        socket.setSoTimeout(1);
-        bufferedInputStream.mark(1);
-        if (bufferedInputStream.read() == -1) {
-          return false; // Stream is exhausted; socket is closed.
-        }
-        bufferedInputStream.reset();
-        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.
-    }
+    if (httpConnection != null) return httpConnection.isReadable();
+    return true; // SPDY connections, and connections before connect() are both optimistic.
   }
 
   public void resetIdleStartTime() {
-    if (spdyConnection != null) {
-      throw new IllegalStateException("spdyConnection != null");
-    }
+    if (spdyConnection != null) throw new IllegalStateException("spdyConnection != null");
     this.idleStartTimeNs = System.nanoTime();
   }
 
@@ -255,11 +282,15 @@
     return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
   }
 
+  public Handshake getHandshake() {
+    return handshake;
+  }
+
   /** Returns the transport appropriate for this connection. */
   public Object newTransport(HttpEngine httpEngine) throws IOException {
     return (spdyConnection != null)
         ? new SpdyTransport(httpEngine, spdyConnection)
-        : new HttpTransport(httpEngine, out, in);
+        : new HttpTransport(httpEngine, httpConnection);
   }
 
   /**
@@ -270,10 +301,6 @@
     return spdyConnection != null;
   }
 
-  public SpdyConnection getSpdyConnection() {
-    return spdyConnection;
-  }
-
   /**
    * Returns the minor HTTP version that should be used for future requests on
    * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
@@ -301,34 +328,51 @@
     socket.setSoTimeout(newTimeout);
   }
 
+  public void incrementRecycleCount() {
+    recycleCount++;
+  }
+
+  /**
+   * Returns the number of times this connection has been returned to the
+   * connection pool.
+   */
+  public int recycleCount() {
+    return recycleCount;
+  }
+
   /**
    * 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 makeTunnel(TunnelRequest tunnelRequest) throws IOException {
-    RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
+    HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
+    Request request = tunnelRequest.getRequest();
+    String requestLine = tunnelRequest.requestLine();
     while (true) {
-      out.write(requestHeaders.toBytes());
-      RawHeaders responseHeaders = RawHeaders.fromBytes(in);
+      tunnelConnection.writeRequest(request.headers(), requestLine);
+      tunnelConnection.flush();
+      Response response = tunnelConnection.readResponse().request(request).build();
+      tunnelConnection.emptyResponseBody();
 
-      switch (responseHeaders.getResponseCode()) {
+      switch (response.code()) {
         case HTTP_OK:
-          return;
-        case HTTP_PROXY_AUTH:
-          requestHeaders = new RawHeaders(requestHeaders);
-          URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
-          boolean credentialsFound = HttpAuthenticator.processAuthHeader(
-              route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
-              route.proxy, url);
-          if (credentialsFound) {
-            continue;
-          } else {
-            throw new IOException("Failed to authenticate with proxy");
+          // 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!
+          if (tunnelConnection.bufferSize() > 0) {
+            throw new IOException("TLS tunnel buffered too many bytes!");
           }
+          return;
+
+        case HTTP_PROXY_AUTH:
+          request = HttpAuthenticator.processAuthHeader(
+              route.address.authenticator, response, route.proxy);
+          if (request != null) continue;
+          throw new IOException("Failed to authenticate with proxy");
+
         default:
           throw new IOException(
-              "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
+              "Unexpected response code for CONNECT: " + response.code());
       }
     }
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index 42b70b9..fbd8351 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -23,7 +23,6 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -82,9 +81,9 @@
   /** We use a single background thread to cleanup expired connections. */
   private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
       60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
-      Util.daemonThreadFactory("OkHttp ConnectionPool"));
-  private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
-    @Override public Void call() throws Exception {
+      Util.threadFactory("OkHttp ConnectionPool", true));
+  private final Runnable connectionsCleanupRunnable = new Runnable() {
+    @Override public void run() {
       List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
       int idleConnectionCount = 0;
       synchronized (ConnectionPool.this) {
@@ -113,7 +112,6 @@
       for (Connection expiredConnection : expiredConnections) {
         Util.closeQuietly(expiredConnection);
       }
-      return null;
     }
   };
 
@@ -205,7 +203,7 @@
       connections.addFirst(foundConnection); // Add it back after iteration.
     }
 
-    executorService.submit(connectionsCleanupCallable);
+    executorService.execute(connectionsCleanupRunnable);
     return foundConnection;
   }
 
@@ -220,6 +218,10 @@
       return;
     }
 
+    if (!connection.clearOwner()) {
+      return; // This connection isn't eligible for reuse.
+    }
+
     if (!connection.isAlive()) {
       Util.closeQuietly(connection);
       return;
@@ -236,22 +238,20 @@
 
     synchronized (this) {
       connections.addFirst(connection);
+      connection.incrementRecycleCount();
       connection.resetIdleStartTime();
     }
 
-    executorService.submit(connectionsCleanupCallable);
+    executorService.execute(connectionsCleanupRunnable);
   }
 
   /**
    * Shares the SPDY connection with the pool. Callers to this method may
    * continue to use {@code connection}.
    */
-  public void maybeShare(Connection connection) {
-    executorService.submit(connectionsCleanupCallable);
-    if (!connection.isSpdy()) {
-      // Only SPDY connections are sharable.
-      return;
-    }
+  public void share(Connection connection) {
+    if (!connection.isSpdy()) throw new IllegalArgumentException();
+    executorService.execute(connectionsCleanupRunnable);
     if (connection.isAlive()) {
       synchronized (this) {
         connections.addFirst(connection);
@@ -267,8 +267,8 @@
       this.connections.clear();
     }
 
-    for (Connection connection : connections) {
-      Util.closeQuietly(connection);
+    for (int i = 0, size = connections.size(); i < size; i++) {
+      Util.closeQuietly(connections.get(i));
     }
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index 6a6c273..58e06be 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -15,67 +15,147 @@
  */
 package com.squareup.okhttp;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import com.squareup.okhttp.internal.Util;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
-final class Dispatcher {
-  // TODO: thread pool size should be configurable; possibly configurable per host.
-  private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
-      8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
-  private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>();
+/**
+ * Policy on when async requests are executed.
+ *
+ * <p>Each dispatcher uses an {@link ExecutorService} to run jobs internally. If you
+ * supply your own executor, it should be able to run {@link #getMaxRequests the
+ * configured maximum} number of jobs concurrently.
+ */
+public final class Dispatcher {
+  private int maxRequests = 64;
+  private int maxRequestsPerHost = 5;
 
-  public synchronized void enqueue(
-      HttpURLConnection connection, Request request, Response.Receiver responseReceiver) {
-    Job job = new Job(this, connection, request, responseReceiver);
-    List<Job> jobsForTag = enqueuedJobs.get(request.tag());
-    if (jobsForTag == null) {
-      jobsForTag = new ArrayList<Job>(2);
-      enqueuedJobs.put(request.tag(), jobsForTag);
-    }
-    jobsForTag.add(job);
-    executorService.execute(job);
+  /** Executes jobs. Created lazily. */
+  private ExecutorService executorService;
+
+  /** Ready jobs in the order they'll be run. */
+  private final Deque<Job> readyJobs = new ArrayDeque<Job>();
+
+  /** Running jobs. Includes canceled jobs that haven't finished yet. */
+  private final Deque<Job> runningJobs = new ArrayDeque<Job>();
+
+  public Dispatcher(ExecutorService executorService) {
+    this.executorService = executorService;
   }
 
+  public Dispatcher() {
+  }
+
+  public synchronized ExecutorService getExecutorService() {
+    if (executorService == null) {
+      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+          new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
+    }
+    return executorService;
+  }
+
+  /**
+   * Set the maximum number of requests to execute concurrently. Above this
+   * requests queue in memory, waiting for the running jobs to complete.
+   *
+   * <p>If more than {@code maxRequests} requests are in flight when this is
+   * invoked, those requests will remain in flight.
+   */
+  public synchronized void setMaxRequests(int maxRequests) {
+    if (maxRequests < 1) {
+      throw new IllegalArgumentException("max < 1: " + maxRequests);
+    }
+    this.maxRequests = maxRequests;
+    promoteJobs();
+  }
+
+  public synchronized int getMaxRequests() {
+    return maxRequests;
+  }
+
+  /**
+   * Set the maximum number of requests for each host to execute concurrently.
+   * This limits requests by the URL's host name. Note that concurrent requests
+   * to a single IP address may still exceed this limit: multiple hostnames may
+   * share an IP address or be routed through the same HTTP proxy.
+   *
+   * <p>If more than {@code maxRequestsPerHost} requests are in flight when this
+   * is invoked, those requests will remain in flight.
+   */
+  public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
+    if (maxRequestsPerHost < 1) {
+      throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
+    }
+    this.maxRequestsPerHost = maxRequestsPerHost;
+    promoteJobs();
+  }
+
+  public synchronized int getMaxRequestsPerHost() {
+    return maxRequestsPerHost;
+  }
+
+  synchronized void enqueue(OkHttpClient client, Request request, Response.Receiver receiver) {
+    // Copy the client. Otherwise changes (socket factory, redirect policy,
+    // etc.) may incorrectly be reflected in the request when it is executed.
+    client = client.copyWithDefaults();
+    Job job = new Job(this, client, request, receiver);
+
+    if (runningJobs.size() < maxRequests && runningJobsForHost(job) < maxRequestsPerHost) {
+      runningJobs.add(job);
+      getExecutorService().execute(job);
+    } else {
+      readyJobs.add(job);
+    }
+  }
+
+  /**
+   * Cancel all jobs with the tag {@code tag}. If a canceled job is running it
+   * may continue running until it reaches a safe point to finish.
+   */
   public synchronized void cancel(Object tag) {
-    List<Job> jobs = enqueuedJobs.remove(tag);
-    if (jobs == null) return;
-    for (Job job : jobs) {
-      executorService.remove(job);
+    for (Iterator<Job> i = readyJobs.iterator(); i.hasNext(); ) {
+      if (Util.equal(tag, i.next().tag())) i.remove();
+    }
+
+    for (Job job : runningJobs) {
+      if (Util.equal(tag, job.tag())) job.canceled = true;
     }
   }
 
+  /** Used by {@code Job#run} to signal completion. */
   synchronized void finished(Job job) {
-    List<Job> jobs = enqueuedJobs.get(job.request.tag());
-    if (jobs != null) jobs.remove(job);
+    if (!runningJobs.remove(job)) throw new AssertionError("Job wasn't running!");
+    promoteJobs();
   }
 
-  static class RealResponseBody extends Response.Body {
-    private final HttpURLConnection connection;
-    private final InputStream in;
+  private void promoteJobs() {
+    if (runningJobs.size() >= maxRequests) return; // Already running max capacity.
+    if (readyJobs.isEmpty()) return; // No ready jobs to promote.
 
-    RealResponseBody(HttpURLConnection connection, InputStream in) {
-      this.connection = connection;
-      this.in = in;
-    }
+    for (Iterator<Job> i = readyJobs.iterator(); i.hasNext(); ) {
+      Job job = i.next();
 
-    @Override public String contentType() {
-      return connection.getHeaderField("Content-Type");
-    }
+      if (runningJobsForHost(job) < maxRequestsPerHost) {
+        i.remove();
+        runningJobs.add(job);
+        getExecutorService().execute(job);
+      }
 
-    @Override public long contentLength() {
-      return connection.getContentLength(); // TODO: getContentLengthLong
+      if (runningJobs.size() >= maxRequests) return; // Reached max capacity.
     }
+  }
 
-    @Override public InputStream byteStream() throws IOException {
-      return in;
+  /** Returns the number of running jobs that share a host with {@code job}. */
+  private int runningJobsForHost(Job job) {
+    int result = 0;
+    for (Job j : runningJobs) {
+      if (j.host().equals(job.host())) result++;
     }
+    return result;
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Failure.java b/okhttp/src/main/java/com/squareup/okhttp/Failure.java
index a354700..51ee2ea 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Failure.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Failure.java
@@ -17,11 +17,8 @@
 
 /**
  * A failure attempting to retrieve an HTTP response.
- *
- * <h3>Warning: Experimental OkHttp 2.0 API</h3>
- * This class is in beta. APIs are subject to change!
  */
-/* OkHttp 2.0: public */ class Failure {
+public final class Failure {
   private final Request request;
   private final Throwable exception;
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Handshake.java b/okhttp/src/main/java/com/squareup/okhttp/Handshake.java
new file mode 100644
index 0000000..d9f5366
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Handshake.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+
+/**
+ * A record of a TLS handshake. For HTTPS clients, the client is <i>local</i>
+ * and the remote server is its <i>peer</i>.
+ *
+ * <p>This value object describes a completed handshake. Use {@link
+ * javax.net.ssl.SSLSocketFactory} to set policy for new handshakes.
+ */
+public final class Handshake {
+  private final String cipherSuite;
+  private final List<Certificate> peerCertificates;
+  private final List<Certificate> localCertificates;
+
+  private Handshake(
+      String cipherSuite, List<Certificate> peerCertificates, List<Certificate> localCertificates) {
+    this.cipherSuite = cipherSuite;
+    this.peerCertificates = peerCertificates;
+    this.localCertificates = localCertificates;
+  }
+
+  public static Handshake get(SSLSession session) {
+    String cipherSuite = session.getCipherSuite();
+    if (cipherSuite == null) throw new IllegalStateException("cipherSuite == null");
+
+    Certificate[] peerCertificates;
+    try {
+      peerCertificates = session.getPeerCertificates();
+    } catch (SSLPeerUnverifiedException ignored) {
+      peerCertificates = null;
+    }
+    List<Certificate> peerCertificatesList = peerCertificates != null
+        ? Util.immutableList(peerCertificates)
+        : Collections.<Certificate>emptyList();
+
+    Certificate[] localCertificates = session.getLocalCertificates();
+    List<Certificate> localCertificatesList = localCertificates != null
+        ? Util.immutableList(localCertificates)
+        : Collections.<Certificate>emptyList();
+
+    return new Handshake(cipherSuite, peerCertificatesList, localCertificatesList);
+  }
+
+  public static Handshake get(
+      String cipherSuite, List<Certificate> peerCertificates, List<Certificate> localCertificates) {
+    if (cipherSuite == null) throw new IllegalArgumentException("cipherSuite == null");
+    return new Handshake(cipherSuite, Util.immutableList(peerCertificates),
+        Util.immutableList(localCertificates));
+  }
+
+  /** Returns a cipher suite name like "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA". */
+  public String cipherSuite() {
+    return cipherSuite;
+  }
+
+  /** Returns a possibly-empty list of certificates that identify the remote peer. */
+  public List<Certificate> peerCertificates() {
+    return peerCertificates;
+  }
+
+  /** Returns the remote peer's principle, or null if that peer is anonymous. */
+  public Principal peerPrincipal() {
+    return !peerCertificates.isEmpty()
+        ? ((X509Certificate) peerCertificates.get(0)).getSubjectX500Principal()
+        : null;
+  }
+
+  /** Returns a possibly-empty list of certificates that identify this peer. */
+  public List<Certificate> localCertificates() {
+    return localCertificates;
+  }
+
+  /** Returns the local principle, or null if this peer is anonymous. */
+  public Principal localPrincipal() {
+    return !localCertificates.isEmpty()
+        ? ((X509Certificate) localCertificates.get(0)).getSubjectX500Principal()
+        : null;
+  }
+
+  @Override public boolean equals(Object other) {
+    if (!(other instanceof Handshake)) return false;
+    Handshake that = (Handshake) other;
+    return cipherSuite.equals(that.cipherSuite)
+        && peerCertificates.equals(that.peerCertificates)
+        && localCertificates.equals(that.localCertificates);
+  }
+
+  @Override public int hashCode() {
+    int result = 17;
+    result = 31 * result + cipherSuite.hashCode();
+    result = 31 * result + peerCertificates.hashCode();
+    result = 31 * result + localCertificates.hashCode();
+    return result;
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Headers.java b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
new file mode 100644
index 0000000..1221aa4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
@@ -0,0 +1,210 @@
+/*
+ *  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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * The header fields of a single HTTP message. Values are uninterpreted strings;
+ * use {@code Request} and {@code Response} for interpreted headers. This class
+ * maintains the order of the header fields within the HTTP message.
+ *
+ * <p>This class tracks header values line-by-line. A field with multiple comma-
+ * separated values on the same line will be treated as a field with a single
+ * value by this class. It is the caller's responsibility to detect and split
+ * on commas if their field permits multiple values. This simplifies use of
+ * single-valued fields whose values routinely contain commas, such as cookies
+ * or dates.
+ *
+ * <p>This class trims whitespace from values. It never returns values with
+ * leading or trailing whitespace.
+ *
+ * <p>Instances of this class are immutable. Use {@link Builder} to create
+ * instances.
+ */
+public final class Headers {
+  private final String[] namesAndValues;
+
+  private Headers(Builder builder) {
+    this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]);
+  }
+
+  /** Returns the last value corresponding to the specified field, or null. */
+  public String get(String fieldName) {
+    return get(namesAndValues, fieldName);
+  }
+
+  /** Returns the number of field values. */
+  public int size() {
+    return namesAndValues.length / 2;
+  }
+
+  /** Returns the field at {@code position} or null if that is out of range. */
+  public String name(int index) {
+    int fieldNameIndex = index * 2;
+    if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.length) {
+      return null;
+    }
+    return namesAndValues[fieldNameIndex];
+  }
+
+  /** Returns the value at {@code index} or null if that is out of range. */
+  public String value(int index) {
+    int valueIndex = index * 2 + 1;
+    if (valueIndex < 0 || valueIndex >= namesAndValues.length) {
+      return null;
+    }
+    return namesAndValues[valueIndex];
+  }
+
+  /** Returns an immutable case-insensitive set of header names. */
+  public Set<String> names() {
+    TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+    for (int i = 0; i < size(); i++) {
+      result.add(name(i));
+    }
+    return Collections.unmodifiableSet(result);
+  }
+
+  /** Returns an immutable list of the header values for {@code name}. */
+  public List<String> values(String name) {
+    List<String> result = null;
+    for (int i = 0; i < size(); i++) {
+      if (name.equalsIgnoreCase(name(i))) {
+        if (result == null) result = new ArrayList<String>(2);
+        result.add(value(i));
+      }
+    }
+    return result != null
+        ? Collections.unmodifiableList(result)
+        : Collections.<String>emptyList();
+  }
+
+  /** @param fieldNames a case-insensitive set of HTTP header field names. */
+  // TODO: it is very weird to request a case-insensitive set as a parameter.
+  public Headers getAll(Set<String> fieldNames) {
+    Builder result = new Builder();
+    for (int i = 0; i < namesAndValues.length; i += 2) {
+      String fieldName = namesAndValues[i];
+      if (fieldNames.contains(fieldName)) {
+        result.add(fieldName, namesAndValues[i + 1]);
+      }
+    }
+    return result.build();
+  }
+
+  public Builder newBuilder() {
+    Builder result = new Builder();
+    result.namesAndValues.addAll(Arrays.asList(namesAndValues));
+    return result;
+  }
+
+  @Override public String toString() {
+    StringBuilder result = new StringBuilder();
+    for (int i = 0; i < size(); i++) {
+      result.append(name(i)).append(": ").append(value(i)).append("\n");
+    }
+    return result.toString();
+  }
+
+  private static String get(String[] namesAndValues, String fieldName) {
+    for (int i = namesAndValues.length - 2; i >= 0; i -= 2) {
+      if (fieldName.equalsIgnoreCase(namesAndValues[i])) {
+        return namesAndValues[i + 1];
+      }
+    }
+    return null;
+  }
+
+  public static class Builder {
+    private final List<String> namesAndValues = new ArrayList<String>(20);
+
+    /** Add an header line containing a field name, a literal colon, and a value. */
+    public Builder addLine(String line) {
+      int index = line.indexOf(":", 1);
+      if (index != -1) {
+        return addLenient(line.substring(0, index), line.substring(index + 1));
+      } else if (line.startsWith(":")) {
+        // Work around empty header names and header names that start with a
+        // colon (created by old broken SPDY versions of the response cache).
+        return addLenient("", line.substring(1)); // Empty header name.
+      } else {
+        return addLenient("", line); // No header name.
+      }
+    }
+
+    /** Add a field with the specified value. */
+    public Builder add(String fieldName, String value) {
+      if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
+      if (value == null) throw new IllegalArgumentException("value == null");
+      if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+        throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
+      }
+      return addLenient(fieldName, value);
+    }
+
+    /**
+     * Add a field with the specified value without any validation. Only
+     * appropriate for headers from the remote peer.
+     */
+    private Builder addLenient(String fieldName, String value) {
+      namesAndValues.add(fieldName);
+      namesAndValues.add(value.trim());
+      return this;
+    }
+
+    public Builder removeAll(String fieldName) {
+      for (int i = 0; i < namesAndValues.size(); i += 2) {
+        if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+          namesAndValues.remove(i); // field name
+          namesAndValues.remove(i); // value
+        }
+      }
+      return this;
+    }
+
+    /**
+     * Set a field with the specified value. If the field is not found, it is
+     * added. If the field is found, the existing values are replaced.
+     */
+    public Builder set(String fieldName, String value) {
+      removeAll(fieldName);
+      add(fieldName, value);
+      return this;
+    }
+
+    /** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
+    public String get(String fieldName) {
+      for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
+        if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+          return namesAndValues.get(i + 1);
+        }
+      }
+      return null;
+    }
+
+    public Headers build() {
+      return new Headers(this);
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Dns.java b/okhttp/src/main/java/com/squareup/okhttp/HostResolver.java
similarity index 84%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/Dns.java
rename to okhttp/src/main/java/com/squareup/okhttp/HostResolver.java
index 69b2d37..c7a1edb 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Dns.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HostResolver.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal;
+package com.squareup.okhttp;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -22,9 +22,10 @@
  * Domain name service. Prefer this over {@link InetAddress#getAllByName} to
  * make code more testable.
  */
-public interface Dns {
-  Dns DEFAULT = new Dns() {
+public interface HostResolver {
+  HostResolver DEFAULT = new HostResolver() {
     @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+      if (host == null) throw new UnknownHostException("host == null");
       return InetAddress.getAllByName(host);
     }
   };
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
index 8210318..d016877 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
@@ -16,16 +16,12 @@
 
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Base64;
 import com.squareup.okhttp.internal.DiskLruCache;
-import com.squareup.okhttp.internal.StrictLineReader;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpMethod;
 import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
-import com.squareup.okhttp.internal.http.HttpsEngine;
 import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.RawHeaders;
-import com.squareup.okhttp.internal.http.ResponseHeaders;
+import com.squareup.okhttp.internal.http.JavaApiConverter;
 import java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -38,30 +34,39 @@
 import java.io.Writer;
 import java.net.CacheRequest;
 import java.net.CacheResponse;
-import java.net.HttpURLConnection;
 import java.net.ResponseCache;
-import java.net.SecureCacheResponse;
 import java.net.URI;
 import java.net.URLConnection;
-import java.security.Principal;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Okio;
 
-import static com.squareup.okhttp.internal.Util.US_ASCII;
 import static com.squareup.okhttp.internal.Util.UTF_8;
 
 /**
  * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
  * saving time and bandwidth.
  *
+ * <p>This cache extends {@link ResponseCache} but is only intended for use
+ * with OkHttp and is not a general-purpose implementation: The
+ * {@link ResponseCache} API requires that the subclass handles cache-control
+ * logic as well as storage. In OkHttp the {@link HttpResponseCache} only
+ * handles cursory cache-control logic.
+ *
+ * <p>To maintain support for previous releases the {@link HttpResponseCache}
+ * will disregard any {@link #put(java.net.URI, java.net.URLConnection)}
+ * calls with a URLConnection that is not from OkHttp. It will, however,
+ * return cached data for any calls to {@link #get(java.net.URI, String,
+ * java.util.Map)}.
+ *
  * <h3>Cache Optimization</h3>
  * To measure cache effectiveness, this class tracks three statistics:
  * <ul>
@@ -115,7 +120,7 @@
  *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
  * }</pre>
  */
-public final class HttpResponseCache extends ResponseCache {
+public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
   // TODO: add APIs to iterate the cache?
   private static final int VERSION = 201105;
   private static final int ENTRY_METADATA = 0;
@@ -131,51 +136,28 @@
   private int hitCount;
   private int requestCount;
 
-  /**
-   * Although this class only exposes the limited ResponseCache API, it
-   * implements the full OkResponseCache interface. This field is used as a
-   * package private handle to the complete implementation. It delegates to
-   * public and private members of this type.
-   */
-  final OkResponseCache okResponseCache = new OkResponseCache() {
-    @Override public CacheResponse get(URI uri, String requestMethod,
-        Map<String, List<String>> requestHeaders) throws IOException {
-      return HttpResponseCache.this.get(uri, requestMethod, requestHeaders);
-    }
-
-    @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
-      return HttpResponseCache.this.put(uri, connection);
-    }
-
-    @Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
-      HttpResponseCache.this.maybeRemove(requestMethod, uri);
-    }
-
-    @Override public void update(
-        CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException {
-      HttpResponseCache.this.update(conditionalCacheHit, connection);
-    }
-
-    @Override public void trackConditionalCacheHit() {
-      HttpResponseCache.this.trackConditionalCacheHit();
-    }
-
-    @Override public void trackResponse(ResponseSource source) {
-      HttpResponseCache.this.trackResponse(source);
-    }
-  };
-
   public HttpResponseCache(File directory, long maxSize) throws IOException {
     cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
   }
 
-  private String uriToKey(URI uri) {
-    return Util.hash(uri.toString());
+  @Override public CacheResponse get(
+      URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
+      throws IOException {
+
+    Request request = JavaApiConverter.createOkRequest(uri, requestMethod, requestHeaders);
+    Response response = get(request);
+    if (response == null) {
+      return null;
+    }
+    return JavaApiConverter.createJavaCacheResponse(response);
   }
 
-  @Override public CacheResponse get(URI uri, String requestMethod,
-      Map<String, List<String>> requestHeaders) {
-    String key = uriToKey(uri);
+  private static String urlToKey(Request requst) {
+    return Util.hash(requst.urlString());
+  }
+
+  @Override public Response get(Request request) {
+    String key = urlToKey(request);
     DiskLruCache.Snapshot snapshot;
     Entry entry;
     try {
@@ -189,24 +171,32 @@
       return null;
     }
 
-    if (!entry.matches(uri, requestMethod, requestHeaders)) {
-      snapshot.close();
+    Response response = entry.response(request, snapshot);
+
+    if (!entry.matches(request, response)) {
+      Util.closeQuietly(response.body());
       return null;
     }
 
-    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
-        : new EntryCacheResponse(entry, snapshot);
+    return response;
   }
 
   @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
-    if (!(urlConnection instanceof HttpURLConnection)) {
+    if (!isCacheableConnection(urlConnection)) {
       return null;
     }
+    return put(JavaApiConverter.createOkResponse(uri, urlConnection));
+  }
 
-    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
-    String requestMethod = httpConnection.getRequestMethod();
+  private static boolean isCacheableConnection(URLConnection httpConnection) {
+    return (httpConnection instanceof HttpURLConnectionImpl)
+        || (httpConnection instanceof HttpsURLConnectionImpl);
+  }
 
-    if (maybeRemove(requestMethod, uri)) {
+  @Override public CacheRequest put(Response response) throws IOException {
+    String requestMethod = response.request().method();
+
+    if (maybeRemove(response.request())) {
       return null;
     }
     if (!requestMethod.equals("GET")) {
@@ -216,23 +206,14 @@
       return null;
     }
 
-    HttpEngine httpEngine = getHttpEngine(httpConnection);
-    if (httpEngine == null) {
-      // Don't cache unless the HTTP implementation is ours.
-      return null;
-    }
-
-    ResponseHeaders response = httpEngine.getResponseHeaders();
     if (response.hasVaryAll()) {
       return null;
     }
 
-    RawHeaders varyHeaders =
-        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
-    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    Entry entry = new Entry(response);
     DiskLruCache.Editor editor = null;
     try {
-      editor = cache.edit(uriToKey(uri));
+      editor = cache.edit(urlToKey(response.request()));
       if (editor == null) {
         return null;
       }
@@ -244,15 +225,10 @@
     }
   }
 
-  /**
-   * Returns true if the supplied {@code requestMethod} potentially invalidates an entry in the
-   * cache.
-   */
-  private boolean maybeRemove(String requestMethod, URI uri) {
-    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
-        "DELETE")) {
+  @Override public boolean maybeRemove(Request request) {
+    if (HttpMethod.invalidatesCache(request.method())) {
       try {
-        cache.remove(uriToKey(uri));
+        cache.remove(urlToKey(request));
       } catch (IOException ignored) {
         // The cache cannot be written.
       }
@@ -261,20 +237,12 @@
     return false;
   }
 
-  private void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
-      throws IOException {
-    HttpEngine httpEngine = getHttpEngine(httpConnection);
-    URI uri = httpEngine.getUri();
-    ResponseHeaders response = httpEngine.getResponseHeaders();
-    RawHeaders varyHeaders =
-        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
-    Entry entry = new Entry(uri, varyHeaders, httpConnection);
-    DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
-        ? ((EntryCacheResponse) conditionalCacheHit).snapshot
-        : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
+  @Override public void update(Response cached, Response network) {
+    Entry entry = new Entry(network);
+    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
     DiskLruCache.Editor editor = null;
     try {
-      editor = snapshot.edit(); // returns null if snapshot is not current
+      editor = snapshot.edit(); // Returns null if snapshot is not current.
       if (editor != null) {
         entry.writeTo(editor);
         editor.commit();
@@ -294,16 +262,6 @@
     }
   }
 
-  private HttpEngine getHttpEngine(URLConnection httpConnection) {
-    if (httpConnection instanceof HttpURLConnectionImpl) {
-      return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
-    } else if (httpConnection instanceof HttpsURLConnectionImpl) {
-      return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
-    } else {
-      return null;
-    }
-  }
-
   /**
    * Closes the cache and deletes all of its stored values. This will delete
    * all files in the cache directory including files that weren't created by
@@ -345,7 +303,7 @@
     return cache.isClosed();
   }
 
-  private synchronized void trackResponse(ResponseSource source) {
+  @Override public synchronized void trackResponse(ResponseSource source) {
     requestCount++;
 
     switch (source) {
@@ -359,7 +317,7 @@
     }
   }
 
-  private synchronized void trackConditionalCacheHit() {
+  @Override public synchronized void trackConditionalCacheHit() {
     hitCount++;
   }
 
@@ -426,13 +384,12 @@
   }
 
   private static final class Entry {
-    private final String uri;
-    private final RawHeaders varyHeaders;
+    private final String url;
+    private final Headers varyHeaders;
     private final String requestMethod;
-    private final RawHeaders responseHeaders;
-    private final String cipherSuite;
-    private final Certificate[] peerCertificates;
-    private final Certificate[] localCertificates;
+    private final String statusLine;
+    private final Headers responseHeaders;
+    private final Handshake handshake;
 
     /**
      * Reads an entry from an input stream. A typical entry looks like this:
@@ -485,122 +442,91 @@
      */
     public Entry(InputStream in) throws IOException {
       try {
-        StrictLineReader reader = new StrictLineReader(in, US_ASCII);
-        uri = reader.readLine();
-        requestMethod = reader.readLine();
-        varyHeaders = new RawHeaders();
-        int varyRequestHeaderLineCount = reader.readInt();
+        BufferedSource source = Okio.buffer(Okio.source(in));
+        url = source.readUtf8LineStrict();
+        requestMethod = source.readUtf8LineStrict();
+        Headers.Builder varyHeadersBuilder = new Headers.Builder();
+        int varyRequestHeaderLineCount = readInt(source);
         for (int i = 0; i < varyRequestHeaderLineCount; i++) {
-          varyHeaders.addLine(reader.readLine());
+          varyHeadersBuilder.addLine(source.readUtf8LineStrict());
         }
+        varyHeaders = varyHeadersBuilder.build();
 
-        responseHeaders = new RawHeaders();
-        responseHeaders.setStatusLine(reader.readLine());
-        int responseHeaderLineCount = reader.readInt();
+        statusLine = source.readUtf8LineStrict();
+        Headers.Builder responseHeadersBuilder = new Headers.Builder();
+        int responseHeaderLineCount = readInt(source);
         for (int i = 0; i < responseHeaderLineCount; i++) {
-          responseHeaders.addLine(reader.readLine());
+          responseHeadersBuilder.addLine(source.readUtf8LineStrict());
         }
+        responseHeaders = responseHeadersBuilder.build();
 
         if (isHttps()) {
-          String blank = reader.readLine();
+          String blank = source.readUtf8LineStrict();
           if (blank.length() > 0) {
             throw new IOException("expected \"\" but was \"" + blank + "\"");
           }
-          cipherSuite = reader.readLine();
-          peerCertificates = readCertArray(reader);
-          localCertificates = readCertArray(reader);
+          String cipherSuite = source.readUtf8LineStrict();
+          List<Certificate> peerCertificates = readCertificateList(source);
+          List<Certificate> localCertificates = readCertificateList(source);
+          handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates);
         } else {
-          cipherSuite = null;
-          peerCertificates = null;
-          localCertificates = null;
+          handshake = null;
         }
       } finally {
         in.close();
       }
     }
 
-    public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
-        throws IOException {
-      this.uri = uri.toString();
-      this.varyHeaders = varyHeaders;
-      this.requestMethod = httpConnection.getRequestMethod();
-      this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
-
-      SSLSocket sslSocket = getSslSocket(httpConnection);
-      if (sslSocket != null) {
-        cipherSuite = sslSocket.getSession().getCipherSuite();
-        Certificate[] peerCertificatesNonFinal = null;
-        try {
-          peerCertificatesNonFinal = sslSocket.getSession().getPeerCertificates();
-        } catch (SSLPeerUnverifiedException ignored) {
-        }
-        peerCertificates = peerCertificatesNonFinal;
-        localCertificates = sslSocket.getSession().getLocalCertificates();
-      } else {
-        cipherSuite = null;
-        peerCertificates = null;
-        localCertificates = null;
-      }
-    }
-
-    /**
-     * Returns the SSL socket used by {@code httpConnection} for HTTPS, nor null
-     * if the connection isn't using HTTPS. Since we permit redirects across
-     * protocols (HTTP to HTTPS or vice versa), the implementation type of the
-     * connection doesn't necessarily match the implementation type of its HTTP
-     * engine.
-     */
-    private SSLSocket getSslSocket(HttpURLConnection httpConnection) {
-      HttpEngine engine = httpConnection instanceof HttpsURLConnectionImpl
-          ? ((HttpsURLConnectionImpl) httpConnection).getHttpEngine()
-          : ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
-      return engine instanceof HttpsEngine
-          ? ((HttpsEngine) engine).getSslSocket()
-          : null;
+    public Entry(Response response) {
+      this.url = response.request().urlString();
+      this.varyHeaders = response.request().headers().getAll(response.getVaryFields());
+      this.requestMethod = response.request().method();
+      this.statusLine = response.statusLine();
+      this.responseHeaders = response.headers();
+      this.handshake = response.handshake();
     }
 
     public void writeTo(DiskLruCache.Editor editor) throws IOException {
       OutputStream out = editor.newOutputStream(ENTRY_METADATA);
       Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
 
-      writer.write(uri + '\n');
+      writer.write(url + '\n');
       writer.write(requestMethod + '\n');
-      writer.write(Integer.toString(varyHeaders.length()) + '\n');
-      for (int i = 0; i < varyHeaders.length(); i++) {
-        writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
+      writer.write(Integer.toString(varyHeaders.size()) + '\n');
+      for (int i = 0; i < varyHeaders.size(); i++) {
+        writer.write(varyHeaders.name(i) + ": " + varyHeaders.value(i) + '\n');
       }
 
-      writer.write(responseHeaders.getStatusLine() + '\n');
-      writer.write(Integer.toString(responseHeaders.length()) + '\n');
-      for (int i = 0; i < responseHeaders.length(); i++) {
-        writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
+      writer.write(statusLine + '\n');
+      writer.write(Integer.toString(responseHeaders.size()) + '\n');
+      for (int i = 0; i < responseHeaders.size(); i++) {
+        writer.write(responseHeaders.name(i) + ": " + responseHeaders.value(i) + '\n');
       }
 
       if (isHttps()) {
         writer.write('\n');
-        writer.write(cipherSuite + '\n');
-        writeCertArray(writer, peerCertificates);
-        writeCertArray(writer, localCertificates);
+        writer.write(handshake.cipherSuite() + '\n');
+        writeCertArray(writer, handshake.peerCertificates());
+        writeCertArray(writer, handshake.localCertificates());
       }
       writer.close();
     }
 
     private boolean isHttps() {
-      return uri.startsWith("https://");
+      return url.startsWith("https://");
     }
 
-    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
-      int length = reader.readInt();
-      if (length == -1) {
-        return null;
-      }
+    private List<Certificate> readCertificateList(BufferedSource source) throws IOException {
+      int length = readInt(source);
+      if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null.
+
       try {
         CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
-        Certificate[] result = new Certificate[length];
-        for (int i = 0; i < result.length; i++) {
-          String line = reader.readLine();
-          byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
-          result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
+        List<Certificate> result = new ArrayList<Certificate>(length);
+        for (int i = 0; i < length; i++) {
+          String line = source.readUtf8LineStrict();
+          byte[] bytes = ByteString.decodeBase64(line).toByteArray();
+          result.add(certificateFactory.generateCertificate(new ByteArrayInputStream(bytes)));
         }
         return result;
       } catch (CertificateException e) {
@@ -608,16 +534,12 @@
       }
     }
 
-    private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
-      if (certificates == null) {
-        writer.write("-1\n");
-        return;
-      }
+    private void writeCertArray(Writer writer, List<Certificate> certificates) throws IOException {
       try {
-        writer.write(Integer.toString(certificates.length) + '\n');
-        for (Certificate certificate : certificates) {
-          byte[] bytes = certificate.getEncoded();
-          String line = Base64.encode(bytes);
+        writer.write(Integer.toString(certificates.size()) + '\n');
+        for (int i = 0, size = certificates.size(); i < size; i++) {
+          byte[] bytes = certificates.get(i).getEncoded();
+          String line = ByteString.of(bytes).base64();
           writer.write(line + '\n');
         }
       } catch (CertificateEncodingException e) {
@@ -625,98 +547,73 @@
       }
     }
 
-    public boolean matches(URI uri, String requestMethod,
-        Map<String, List<String>> requestHeaders) {
-      return this.uri.equals(uri.toString())
-          && this.requestMethod.equals(requestMethod)
-          && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
-          requestHeaders);
+    public boolean matches(Request request, Response response) {
+      return url.equals(request.urlString())
+          && requestMethod.equals(request.method())
+          && response.varyMatches(varyHeaders, request);
+    }
+
+    public Response response(Request request, DiskLruCache.Snapshot snapshot) {
+      String contentType = responseHeaders.get("Content-Type");
+      String contentLength = responseHeaders.get("Content-Length");
+      return new Response.Builder()
+          .request(request)
+          .statusLine(statusLine)
+          .headers(responseHeaders)
+          .body(new CacheResponseBody(snapshot, contentType, contentLength))
+          .handshake(handshake)
+          .build();
     }
   }
 
-  /**
-   * Returns an input stream that reads the body of a snapshot, closing the
-   * snapshot when the stream is closed.
-   */
-  private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
-    return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
-      @Override public void close() throws IOException {
-        snapshot.close();
-        super.close();
-      }
-    };
+  private static int readInt(BufferedSource source) throws IOException {
+    String line = source.readUtf8LineStrict();
+    try {
+      return Integer.parseInt(line);
+    } catch (NumberFormatException e) {
+      throw new IOException("Expected an integer but was \"" + line + "\"");
+    }
   }
 
-  static class EntryCacheResponse extends CacheResponse {
-    private final Entry entry;
+  private static class CacheResponseBody extends Response.Body {
     private final DiskLruCache.Snapshot snapshot;
-    private final InputStream in;
+    private final InputStream bodyIn;
+    private final String contentType;
+    private final String contentLength;
 
-    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
-      this.entry = entry;
+    public CacheResponseBody(final DiskLruCache.Snapshot snapshot,
+        String contentType, String contentLength) {
       this.snapshot = snapshot;
-      this.in = newBodyInputStream(snapshot);
+      this.contentType = contentType;
+      this.contentLength = contentLength;
+
+      // This input stream closes the snapshot when the stream is closed.
+      this.bodyIn = new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+        @Override public void close() throws IOException {
+          snapshot.close();
+          super.close();
+        }
+      };
     }
 
-    @Override public Map<String, List<String>> getHeaders() {
-      return entry.responseHeaders.toMultimap(true);
+    @Override public boolean ready() throws IOException {
+      return true;
     }
 
-    @Override public InputStream getBody() {
-      return in;
-    }
-  }
-
-  static class EntrySecureCacheResponse extends SecureCacheResponse {
-    private final Entry entry;
-    private final DiskLruCache.Snapshot snapshot;
-    private final InputStream in;
-
-    public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
-      this.entry = entry;
-      this.snapshot = snapshot;
-      this.in = newBodyInputStream(snapshot);
+    @Override public MediaType contentType() {
+      return contentType != null ? MediaType.parse(contentType) : null;
     }
 
-    @Override public Map<String, List<String>> getHeaders() {
-      return entry.responseHeaders.toMultimap(true);
-    }
-
-    @Override public InputStream getBody() {
-      return in;
-    }
-
-    @Override public String getCipherSuite() {
-      return entry.cipherSuite;
-    }
-
-    @Override public List<Certificate> getServerCertificateChain()
-        throws SSLPeerUnverifiedException {
-      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
-        throw new SSLPeerUnverifiedException(null);
+    @Override public long contentLength() {
+      try {
+        return contentLength != null ? Long.parseLong(contentLength) : -1;
+      } catch (NumberFormatException e) {
+        return -1;
       }
-      return Arrays.asList(entry.peerCertificates.clone());
     }
 
-    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
-        throw new SSLPeerUnverifiedException(null);
-      }
-      return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
-    }
-
-    @Override public List<Certificate> getLocalCertificateChain() {
-      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
-        return null;
-      }
-      return Arrays.asList(entry.localCertificates.clone());
-    }
-
-    @Override public Principal getLocalPrincipal() {
-      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
-        return null;
-      }
-      return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
+    @Override public InputStream byteStream() {
+      return bodyIn;
     }
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Job.java b/okhttp/src/main/java/com/squareup/okhttp/Job.java
index 3a45384..721acc8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Job.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Job.java
@@ -15,71 +15,256 @@
  */
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.NamedRunnable;
+import com.squareup.okhttp.internal.http.HttpAuthenticator;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkHeaders;
 import java.io.IOException;
-import java.net.HttpURLConnection;
+import java.io.InputStream;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.URL;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
 
-final class Job implements Runnable {
-  final HttpURLConnection connection;
-  final Request request;
-  final Response.Receiver responseReceiver;
-  final Dispatcher dispatcher;
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER;
+import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
 
-  public Job(Dispatcher dispatcher, HttpURLConnection connection, Request request,
+final class Job extends NamedRunnable {
+  private final Dispatcher dispatcher;
+  private final OkHttpClient client;
+  private final Response.Receiver responseReceiver;
+  private int redirectionCount;
+
+  volatile boolean canceled;
+
+  /** The request; possibly a consequence of redirects or auth headers. */
+  private Request request;
+  HttpEngine engine;
+
+  public Job(Dispatcher dispatcher, OkHttpClient client, Request request,
       Response.Receiver responseReceiver) {
+    super("OkHttp %s", request.urlString());
     this.dispatcher = dispatcher;
-    this.connection = connection;
+    this.client = client;
     this.request = request;
     this.responseReceiver = responseReceiver;
   }
 
-  @Override public void run() {
+  String host() {
+    return request.url().getHost();
+  }
+
+  Request request() {
+    return request;
+  }
+
+  Object tag() {
+    return request.tag();
+  }
+
+  @Override protected void execute() {
     try {
-      sendRequest();
-      Response response = readResponse();
-      responseReceiver.onResponse(response);
+      Response response = getResponse();
+      if (response != null && !canceled) {
+        responseReceiver.onResponse(response);
+      }
     } catch (IOException e) {
       responseReceiver.onFailure(new Failure.Builder()
           .request(request)
           .exception(e)
           .build());
     } finally {
-      connection.disconnect();
+      engine.close(); // Close the connection if it isn't already.
       dispatcher.finished(this);
     }
   }
 
-  private HttpURLConnection sendRequest() throws IOException {
-    for (int i = 0; i < request.headerCount(); i++) {
-      connection.addRequestProperty(request.headerName(i), request.headerValue(i));
-    }
+  /**
+   * Performs the request and returns the response. May return null if this job
+   * was canceled.
+   */
+  Response getResponse() throws IOException {
+    Response redirectedBy = null;
+
+    // Copy body metadata to the appropriate request headers.
     Request.Body body = request.body();
     if (body != null) {
-      connection.setDoOutput(true);
+      MediaType contentType = body.contentType();
+      if (contentType == null) throw new IllegalStateException("contentType == null");
+
+      Request.Builder requestBuilder = request.newBuilder();
+      requestBuilder.header("Content-Type", contentType.toString());
+
       long contentLength = body.contentLength();
-      if (contentLength == -1 || contentLength > Integer.MAX_VALUE) {
-        connection.setChunkedStreamingMode(0);
+      if (contentLength != -1) {
+        requestBuilder.header("Content-Length", Long.toString(contentLength));
+        requestBuilder.removeHeader("Transfer-Encoding");
       } else {
-        // Don't call setFixedLengthStreamingMode(long); that's only available on Java 1.7+.
-        connection.setFixedLengthStreamingMode((int) contentLength);
+        requestBuilder.header("Transfer-Encoding", "chunked");
+        requestBuilder.removeHeader("Content-Length");
       }
-      body.writeTo(connection.getOutputStream());
+
+      request = requestBuilder.build();
     }
-    return connection;
+
+    // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
+    engine = new HttpEngine(client, request, false, null, null, null, null);
+
+    while (true) {
+      if (canceled) return null;
+
+      try {
+        engine.sendRequest();
+
+        if (body != null) {
+          BufferedSink sink = Okio.buffer(engine.getRequestBody());
+          body.writeTo(sink);
+          sink.flush();
+        }
+
+        engine.readResponse();
+      } catch (IOException e) {
+        HttpEngine retryEngine = engine.recover(e);
+        if (retryEngine != null) {
+          engine = retryEngine;
+          continue;
+        }
+
+        // Give up; recovery is not possible.
+        throw e;
+      }
+
+      Response response = engine.getResponse();
+      Request redirect = processResponse(engine, response);
+
+      if (redirect == null) {
+        engine.releaseConnection();
+        return response.newBuilder()
+            .body(new RealResponseBody(response, engine.getResponseBody()))
+            .priorResponse(redirectedBy)
+            .build();
+      }
+
+      if (!sameConnection(request, redirect)) {
+        engine.releaseConnection();
+      }
+
+      Connection connection = engine.close();
+      redirectedBy = response.newBuilder().priorResponse(redirectedBy).build(); // Chained.
+      request = redirect;
+      engine = new HttpEngine(client, request, false, connection, null, null, null);
+    }
   }
 
-  private Response readResponse() throws IOException {
-    int responseCode = connection.getResponseCode();
-    Response.Builder responseBuilder = new Response.Builder(request, responseCode);
+  /**
+   * Figures out the HTTP request to make in response to receiving {@code
+   * response}. This will either add authentication headers or follow
+   * redirects. If a follow-up is either unnecessary or not applicable, this
+   * returns null.
+   */
+  private Request processResponse(HttpEngine engine, Response response) throws IOException {
+    Request request = response.request();
+    Proxy selectedProxy = engine.getRoute() != null
+        ? engine.getRoute().getProxy()
+        : client.getProxy();
+    int responseCode = response.code();
 
-    for (int i = 0; true; i++) {
-      String name = connection.getHeaderFieldKey(i);
-      if (name == null) break;
-      String value = connection.getHeaderField(i);
-      responseBuilder.addHeader(name, value);
+    switch (responseCode) {
+      case HTTP_PROXY_AUTH:
+        if (selectedProxy.type() != Proxy.Type.HTTP) {
+          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
+        }
+        // fall-through
+      case HTTP_UNAUTHORIZED:
+        return HttpAuthenticator.processAuthHeader(
+            client.getAuthenticator(), response, selectedProxy);
+
+      case HTTP_MULT_CHOICE:
+      case HTTP_MOVED_PERM:
+      case HTTP_MOVED_TEMP:
+      case HTTP_SEE_OTHER:
+      case HTTP_TEMP_REDIRECT:
+        if (!client.getFollowProtocolRedirects()) {
+          return null; // This client has is configured to not follow redirects.
+        }
+
+        if (++redirectionCount > HttpURLConnectionImpl.MAX_REDIRECTS) {
+          throw new ProtocolException("Too many redirects: " + redirectionCount);
+        }
+
+        String method = request.method();
+        if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
+          // "If the 307 status code is received in response to a request other than GET or HEAD,
+          // the user agent MUST NOT automatically redirect the request"
+          return null;
+        }
+
+        String location = response.header("Location");
+        if (location == null) {
+          return null;
+        }
+
+        URL url = new URL(request.url(), location);
+        if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
+          return null; // Don't follow redirects to unsupported protocols.
+        }
+
+        return this.request.newBuilder().url(url).build();
+
+      default:
+        return null;
+    }
+  }
+
+  static boolean sameConnection(Request a, Request b) {
+    return a.url().getHost().equals(b.url().getHost())
+        && getEffectivePort(a.url()) == getEffectivePort(b.url())
+        && a.url().getProtocol().equals(b.url().getProtocol());
+  }
+
+  static class RealResponseBody extends Response.Body {
+    private final Response response;
+    private final Source source;
+
+    /** Multiple calls to {@link #byteStream} must return the same instance. */
+    private InputStream in;
+
+    RealResponseBody(Response response, Source source) {
+      this.response = response;
+      this.source = source;
     }
 
-    responseBuilder.body(new Dispatcher.RealResponseBody(connection, connection.getInputStream()));
-    // TODO: set redirectedBy
-    return responseBuilder.build();
+    @Override public boolean ready() throws IOException {
+      return true;
+    }
+
+    @Override public MediaType contentType() {
+      String contentType = response.header("Content-Type");
+      return contentType != null ? MediaType.parse(contentType) : null;
+    }
+
+    @Override public long contentLength() {
+      return OkHeaders.contentLength(response);
+    }
+
+    @Override public Source source() {
+      return source;
+    }
+
+    @Override public InputStream byteStream() {
+      InputStream result = in;
+      return result != null
+          ? result
+          : (in = Okio.buffer(source).inputStream());
+    }
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java b/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java
index a505419..e8ca5ea 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java
@@ -15,12 +15,12 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Base64;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.Proxy;
 import java.net.URL;
 import java.util.List;
+import okio.ByteString;
 
 /**
  * Responds to authentication challenges from the remote web or proxy server by
@@ -97,7 +97,7 @@
       try {
         String usernameAndPassword = userName + ":" + password;
         byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
-        String encoded = Base64.encode(bytes);
+        String encoded = ByteString.of(bytes).base64();
         return new Credential("Basic " + encoded);
       } catch (UnsupportedEncodingException e) {
         throw new AssertionError();
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 945da1b..0bb98c5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -19,8 +19,9 @@
 import com.squareup.okhttp.internal.http.HttpAuthenticator;
 import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
 import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
+import com.squareup.okhttp.internal.http.ResponseCacheAdapter;
 import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
+import java.io.IOException;
 import java.net.CookieHandler;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
@@ -30,29 +31,42 @@
 import java.net.URLConnection;
 import java.net.URLStreamHandler;
 import java.net.URLStreamHandlerFactory;
-import java.util.Arrays;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import javax.net.SocketFactory;
 import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
+import okio.ByteString;
 
-/** Configures and creates HTTP connections. */
-public final class OkHttpClient implements URLStreamHandlerFactory {
-  private static final List<String> DEFAULT_TRANSPORTS
-      = Util.immutableList(Arrays.asList("spdy/3", "http/1.1"));
+/**
+ * Configures and creates HTTP connections. Most applications can use a single
+ * OkHttpClient for all of their HTTP requests - benefiting from a shared
+ * response cache, thread pool, connection re-use, etc.
+ *
+ * Instances of OkHttpClient are intended to be fully configured before they're
+ * shared - once shared they should be treated as immutable and can safely be used
+ * to concurrently open new connections. If required, threads can call
+ * {@link #clone()} to make a shallow copy of the OkHttpClient that can be
+ * safely modified with further configuration changes.
+ */
+public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable {
 
   private final RouteDatabase routeDatabase;
-  private final Dispatcher dispatcher;
+  private Dispatcher dispatcher;
   private Proxy proxy;
-  private List<String> transports;
+  private List<Protocol> protocols;
   private ProxySelector proxySelector;
   private CookieHandler cookieHandler;
-  private ResponseCache responseCache;
+  private OkResponseCache responseCache;
+  private SocketFactory socketFactory;
   private SSLSocketFactory sslSocketFactory;
   private HostnameVerifier hostnameVerifier;
   private OkAuthenticator authenticator;
   private ConnectionPool connectionPool;
+  private HostResolver hostResolver;
   private boolean followProtocolRedirects = true;
   private int connectTimeout;
   private int readTimeout;
@@ -62,11 +76,6 @@
     dispatcher = new Dispatcher();
   }
 
-  private OkHttpClient(OkHttpClient copyFrom) {
-    routeDatabase = copyFrom.routeDatabase;
-    dispatcher = copyFrom.dispatcher;
-  }
-
   /**
    * Sets the default connect timeout for new connections. A value of 0 means no timeout.
    *
@@ -166,34 +175,45 @@
 
   /**
    * Sets the response cache to be used to read and write cached responses.
-   *
-   * <p>If unset, the {@link ResponseCache#getDefault() system-wide default}
-   * response cache will be used.
    */
   public OkHttpClient setResponseCache(ResponseCache responseCache) {
+    return setOkResponseCache(toOkResponseCache(responseCache));
+  }
+
+  public ResponseCache getResponseCache() {
+    return responseCache instanceof ResponseCacheAdapter
+        ? ((ResponseCacheAdapter) responseCache).getDelegate()
+        : null;
+  }
+
+  public OkHttpClient setOkResponseCache(OkResponseCache responseCache) {
     this.responseCache = responseCache;
     return this;
   }
 
-  public ResponseCache getResponseCache() {
+  public OkResponseCache getOkResponseCache() {
     return responseCache;
   }
 
-  public OkResponseCache getOkResponseCache() {
-    if (responseCache instanceof HttpResponseCache) {
-      return ((HttpResponseCache) responseCache).okResponseCache;
-    } else if (responseCache != null) {
-      return new OkResponseCacheAdapter(responseCache);
-    } else {
-      return null;
-    }
+  /**
+   * Sets the socket factory used to create connections.
+   *
+   * <p>If unset, the {@link SocketFactory#getDefault() system-wide default}
+   * socket factory will be used.
+   */
+  public OkHttpClient setSocketFactory(SocketFactory socketFactory) {
+    this.socketFactory = socketFactory;
+    return this;
+  }
+
+  public SocketFactory getSocketFactory() {
+    return socketFactory;
   }
 
   /**
    * Sets the socket factory used to secure HTTPS connections.
    *
-   * <p>If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory()
-   * system-wide default} SSL socket factory will be used.
+   * <p>If unset, a lazily created SSL socket factory will be used.
    */
   public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
     this.sslSocketFactory = sslSocketFactory;
@@ -208,7 +228,8 @@
    * Sets the verifier used to confirm that response certificates apply to
    * requested hostnames for HTTPS connections.
    *
-   * <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
+   * <p>If unset, the
+   * {@link javax.net.ssl.HttpsURLConnection#getDefaultHostnameVerifier()
    * system-wide default} hostname verifier will be used.
    */
   public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
@@ -272,20 +293,54 @@
   }
 
   /**
-   * Configure the transports used by this client to communicate with remote
+   * Sets the dispatcher used to set policy and execute asynchronous requests.
+   * Must not be null.
+   */
+  public OkHttpClient setDispatcher(Dispatcher dispatcher) {
+    if (dispatcher == null) throw new IllegalArgumentException("dispatcher == null");
+    this.dispatcher = dispatcher;
+    return this;
+  }
+
+  public Dispatcher getDispatcher() {
+    return dispatcher;
+  }
+
+  /**
+   * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
+   *     protocols} that can be selected. Please switch to {@link
+   *     #setProtocols(java.util.List)}.
+   */
+  @Deprecated
+  public OkHttpClient setTransports(List<String> transports) {
+    List<Protocol> protocols = new ArrayList<Protocol>(transports.size());
+    for (int i = 0, size = transports.size(); i < size; i++) {
+      try {
+        Protocol protocol = Protocol.find(ByteString.encodeUtf8(transports.get(i)));
+        protocols.add(protocol);
+      } catch (IOException e) {
+        throw new IllegalArgumentException(e);
+      }
+    }
+    return setProtocols(protocols);
+  }
+
+  /**
+   * Configure the protocols used by this client to communicate with remote
    * servers. By default this client will prefer the most efficient transport
-   * available, falling back to more ubiquitous transports. Applications should
+   * available, falling back to more ubiquitous protocols. Applications should
    * only call this method to avoid specific compatibility problems, such as web
    * servers that behave incorrectly when SPDY is enabled.
    *
-   * <p>The following transports are currently supported:
+   * <p>The following protocols are currently supported:
    * <ul>
    *   <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
-   *   <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3">spdy/3</a>
+   *   <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1">spdy/3.1</a>
+   *   <li><a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-09">HTTP-draft-09/2.0</a>
    * </ul>
    *
    * <p><strong>This is an evolving set.</strong> Future releases may drop
-   * support for transitional transports (like spdy/3), in favor of their
+   * support for transitional protocols (like spdy/3.1), in favor of their
    * successors (spdy/4 or http/2.0). The http/1.1 transport will never be
    * dropped.
    *
@@ -295,43 +350,111 @@
    * (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
    * to negotiate a transport.
    *
-   * @param transports the transports to use, in order of preference. The list
+   * @param protocols the protocols to use, in order of preference. The list
    *     must contain "http/1.1". It must not contain null.
    */
-  public OkHttpClient setTransports(List<String> transports) {
-    transports = Util.immutableList(transports);
-    if (!transports.contains("http/1.1")) {
-      throw new IllegalArgumentException("transports doesn't contain http/1.1: " + transports);
+  public OkHttpClient setProtocols(List<Protocol> protocols) {
+    protocols = Util.immutableList(protocols);
+    if (!protocols.contains(Protocol.HTTP_11)) {
+      throw new IllegalArgumentException("protocols doesn't contain http/1.1: " + protocols);
     }
-    if (transports.contains(null)) {
-      throw new IllegalArgumentException("transports must not contain null");
+    if (protocols.contains(null)) {
+      throw new IllegalArgumentException("protocols must not contain null");
     }
-    if (transports.contains("")) {
-      throw new IllegalArgumentException("transports contains an empty string");
-    }
-    this.transports = transports;
+    this.protocols = Util.immutableList(protocols);
     return this;
   }
 
+  /**
+   * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
+   *     protocols} that can be selected. Please switch to {@link
+   *     #getProtocols()}.
+   */
+  @Deprecated
   public List<String> getTransports() {
+    List<String> transports = new ArrayList<String>(protocols.size());
+    for (int i = 0, size = protocols.size(); i < size; i++) {
+      transports.add(protocols.get(i).name.utf8());
+    }
     return transports;
   }
 
-  /**
-   * Schedules {@code request} to be executed.
+  public List<Protocol> getProtocols() {
+    return protocols;
+  }
+
+  /*
+   * Sets the {@code HostResolver} that will be used by this client to resolve
+   * hostnames to IP addresses.
    */
-  /* OkHttp 2.0: public */ void enqueue(Request request, Response.Receiver responseReceiver) {
-    // Create the HttpURLConnection immediately so the enqueued job gets the current settings of
-    // this client. Otherwise changes to this client (socket factory, redirect policy, etc.) may
-    // incorrectly be reflected in the request when it is dispatched later.
-    dispatcher.enqueue(open(request.url()), request, responseReceiver);
+  public OkHttpClient setHostResolver(HostResolver hostResolver) {
+    this.hostResolver = hostResolver;
+    return this;
+  }
+
+  public HostResolver getHostResolver() {
+    return hostResolver;
+  }
+
+  /**
+   * Invokes {@code request} immediately, and blocks until the response can be
+   * processed or is in error.
+   *
+   * <p>The caller may read the response body with the response's
+   * {@link Response#body} method.  To facilitate connection recycling, callers
+   * should always {@link Response.Body#close() close the response body}.
+   *
+   * <p>Note that transport-layer success (receiving a HTTP response code,
+   * headers and body) does not necessarily indicate application-layer
+   * success: {@code response} may still indicate an unhappy HTTP response
+   * code like 404 or 500.
+   *
+   * <h3>Non-blocking responses</h3>
+   *
+   * <p>Receivers do not need to block while waiting for the response body to
+   * download. Instead, they can get called back as data arrives. Use {@link
+   * Response.Body#ready} to check if bytes should be read immediately. While
+   * there is data ready, read it.
+   *
+   * <p>The current implementation of {@link Response.Body#ready} always
+   * returns true when the underlying transport is HTTP/1. This results in
+   * blocking on that transport. For effective non-blocking your server must
+   * support {@link Protocol#SPDY_3} or {@link Protocol#HTTP_2}.
+   *
+   * @throws IOException when the request could not be executed due to a
+   * connectivity problem or timeout. Because networks can fail during an
+   * exchange, it is possible that the remote server accepted the request
+   * before the failure.
+   */
+  public Response execute(Request request) throws IOException {
+    // Copy the client. Otherwise changes (socket factory, redirect policy,
+    // etc.) may incorrectly be reflected in the request when it is executed.
+    OkHttpClient client = copyWithDefaults();
+    Job job = new Job(dispatcher, client, request, null);
+    Response result = job.getResponse(); // Since we don't cancel, this won't be null.
+    job.engine.releaseConnection(); // Transfer ownership of the body to the caller.
+    return result;
+  }
+
+  /**
+   * Schedules {@code request} to be executed at some point in the future. The
+   * {@link #getDispatcher dispatcher} defines when the request will run:
+   * usually immediately unless there are several other requests currently being
+   * executed.
+   *
+   * <p>This client will later call back {@code responseReceiver} with either an
+   * HTTP response or a failure exception. If you {@link #cancel} a request
+   * before it completes the receiver will not be called back.
+   */
+  public void enqueue(Request request, Response.Receiver responseReceiver) {
+    dispatcher.enqueue(this, request, responseReceiver);
   }
 
   /**
    * Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
-   * in flight might not be canceled.
+   * complete cannot be canceled.
    */
-  /* OkHttp 2.0: public */ void cancel(Object tag) {
+  public void cancel(Object tag) {
     dispatcher.cancel(tag);
   }
 
@@ -350,33 +473,84 @@
   }
 
   /**
-   * Returns a shallow copy of this OkHttpClient that uses the system-wide default for
-   * each field that hasn't been explicitly configured.
+   * Returns a shallow copy of this OkHttpClient that uses the system-wide
+   * default for each field that hasn't been explicitly configured.
    */
-  private OkHttpClient copyWithDefaults() {
-    OkHttpClient result = new OkHttpClient(this);
-    result.proxy = proxy;
-    result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
-    result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
-    result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
-    result.sslSocketFactory = sslSocketFactory != null
-        ? sslSocketFactory
-        : HttpsURLConnection.getDefaultSSLSocketFactory();
-    result.hostnameVerifier = hostnameVerifier != null
-        ? hostnameVerifier
-        : OkHostnameVerifier.INSTANCE;
-    result.authenticator = authenticator != null
-        ? authenticator
-        : HttpAuthenticator.SYSTEM_DEFAULT;
-    result.connectionPool = connectionPool != null ? connectionPool : ConnectionPool.getDefault();
-    result.followProtocolRedirects = followProtocolRedirects;
-    result.transports = transports != null ? transports : DEFAULT_TRANSPORTS;
-    result.connectTimeout = connectTimeout;
-    result.readTimeout = readTimeout;
+  OkHttpClient copyWithDefaults() {
+    OkHttpClient result = clone();
+    if (result.proxySelector == null) {
+      result.proxySelector = ProxySelector.getDefault();
+    }
+    if (result.cookieHandler == null) {
+      result.cookieHandler = CookieHandler.getDefault();
+    }
+    if (result.responseCache == null) {
+      result.responseCache = toOkResponseCache(ResponseCache.getDefault());
+    }
+    if (result.socketFactory == null) {
+      result.socketFactory = SocketFactory.getDefault();
+    }
+    if (result.sslSocketFactory == null) {
+      result.sslSocketFactory = getDefaultSSLSocketFactory();
+    }
+    if (result.hostnameVerifier == null) {
+      result.hostnameVerifier = OkHostnameVerifier.INSTANCE;
+    }
+    if (result.authenticator == null) {
+      result.authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
+    }
+    if (result.connectionPool == null) {
+      result.connectionPool = ConnectionPool.getDefault();
+    }
+    if (result.protocols == null) {
+      result.protocols = Protocol.HTTP2_SPDY3_AND_HTTP;
+    }
+    if (result.hostResolver == null) {
+      result.hostResolver = HostResolver.DEFAULT;
+    }
     return result;
   }
 
   /**
+   * Java and Android programs default to using a single global SSL context,
+   * accessible to HTTP clients as {@link SSLSocketFactory#getDefault()}. If we
+   * used the shared SSL context, when OkHttp enables NPN for its SPDY-related
+   * stuff, it would also enable NPN for other usages, which might crash them
+   * because NPN is enabled when it isn't expected to be.
+   * <p>
+   * This code avoids that by defaulting to an OkHttp created SSL context. The
+   * significant drawback of this approach is that apps that customize the
+   * global SSL context will lose these customizations.
+   */
+  private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
+    if (sslSocketFactory == null) {
+      try {
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, null, null);
+        sslSocketFactory = sslContext.getSocketFactory();
+      } catch (GeneralSecurityException e) {
+        throw new AssertionError(); // The system has no TLS. Just give up.
+      }
+    }
+    return sslSocketFactory;
+  }
+
+  /** Returns a shallow copy of this OkHttpClient. */
+  @Override public OkHttpClient clone() {
+    try {
+      return (OkHttpClient) super.clone();
+    } catch (CloneNotSupportedException e) {
+      throw new AssertionError();
+    }
+  }
+
+  private OkResponseCache toOkResponseCache(ResponseCache responseCache) {
+    return responseCache == null || responseCache instanceof OkResponseCache
+        ? (OkResponseCache) responseCache
+        : new ResponseCacheAdapter(responseCache);
+  }
+
+  /**
    * Creates a URLStreamHandler as a {@link URL#setURLStreamHandlerFactory}.
    *
    * <p>This code configures OkHttp to handle all HTTP and HTTPS connections
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java
index ffe6f54..05460f5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java
@@ -17,36 +17,31 @@
 
 import java.io.IOException;
 import java.net.CacheRequest;
-import java.net.CacheResponse;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URLConnection;
-import java.util.List;
-import java.util.Map;
 
 /**
  * An extended response cache API. Unlike {@link java.net.ResponseCache}, this
  * interface supports conditional caching and statistics.
- *
- * <h3>Warning: Experimental OkHttp 2.0 API</h3>
- * This class is in beta. APIs are subject to change!
  */
 public interface OkResponseCache {
-  CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
-      throws IOException;
+  Response get(Request request) throws IOException;
 
-  CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
+  CacheRequest put(Response response) throws IOException;
 
-  /** Remove any cache entries for the supplied {@code uri} if the request method invalidates. */
-  void maybeRemove(String requestMethod, URI uri) throws IOException;
+  /**
+   * Remove any cache entries for the supplied {@code uri}. Returns true if the
+   * supplied {@code requestMethod} potentially invalidates an entry in the
+   * cache.
+   */
+  // TODO: this shouldn't return a boolean.
+  boolean maybeRemove(Request request) throws IOException;
 
   /**
    * Handles a conditional request hit by updating the stored cache response
-   * with the headers from {@code httpConnection}. The cached response body is
-   * not updated. If the stored response has changed since {@code
-   * conditionalCacheHit} was returned, this does nothing.
+   * with the headers from {@code network}. The cached response body is not
+   * updated. If the stored response has changed since {@code cached} was
+   * returned, this does nothing.
    */
-  void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
+  void update(Response cached, Response network) throws IOException;
 
   /** Track an conditional GET that was satisfied by this cache. */
   void trackConditionalCacheHit();
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Protocol.java b/okhttp/src/main/java/com/squareup/okhttp/Protocol.java
new file mode 100644
index 0000000..e2d7ba9
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Protocol.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okio.ByteString;
+
+/**
+ * Contains protocols that OkHttp supports
+ * <a href="http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04">NPN</a> or
+ * <a href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg">ALPN</a> selection.
+ *
+ * <p>
+ * <h3>Protocol vs Scheme</h3>
+ * Despite its name, {@link java.net.URL#getProtocol()} returns the
+ * {@link java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not
+ * the protocol (http/1.1, spdy/3.1, etc.).  OkHttp uses the word protocol to
+ * indicate how HTTP messages are framed.
+ */
+public enum Protocol {
+  HTTP_2("HTTP-draft-09/2.0", true),
+  SPDY_3("spdy/3.1", true),
+  HTTP_11("http/1.1", false);
+
+  public static final List<Protocol> HTTP2_SPDY3_AND_HTTP =
+      Util.immutableList(Arrays.asList(HTTP_2, SPDY_3, HTTP_11));
+  public static final List<Protocol> SPDY3_AND_HTTP11 =
+      Util.immutableList(Arrays.asList(SPDY_3, HTTP_11));
+  public static final List<Protocol> HTTP2_AND_HTTP_11 =
+      Util.immutableList(Arrays.asList(HTTP_2, HTTP_11));
+
+  /** Identifier string used in NPN or ALPN selection. */
+  public final ByteString name;
+
+  /**
+   * When true the protocol is binary framed and derived from SPDY.
+   *
+   * @see com.squareup.okhttp.internal.spdy.Variant
+   */
+  public final boolean spdyVariant;
+
+  Protocol(String name, boolean spdyVariant) {
+    this.name = ByteString.encodeUtf8(name);
+    this.spdyVariant = spdyVariant;
+  }
+
+  /**
+   * Returns the protocol matching {@code input} or {@link #HTTP_11} is on
+   * {@code null}. Throws an {@link IOException} when {@code input} doesn't
+   * match the {@link #name} of a supported protocol.
+   */
+  public static Protocol find(ByteString input) throws IOException {
+    if (input == null) return HTTP_11;
+    for (Protocol protocol : values()) {
+      if (protocol.name.equals(input)) return protocol;
+    }
+    throw new IOException("Unexpected protocol: " + input.utf8());
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index a4e83f4..300dc17 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -15,37 +15,39 @@
  */
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.RawHeaders;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.List;
-import java.util.Set;
+import okio.BufferedSink;
 
 /**
  * An HTTP request. Instances of this class are immutable if their {@link #body}
  * is null or itself immutable.
- *
- * <h3>Warning: Experimental OkHttp 2.0 API</h3>
- * This class is in beta. APIs are subject to change!
  */
-/* OkHttp 2.0: public */ final class Request {
+public final class Request {
   private final URL url;
   private final String method;
-  private final RawHeaders headers;
+  private final Headers headers;
   private final Body body;
   private final Object tag;
 
+  private volatile ParsedHeaders parsedHeaders; // Lazily initialized.
+  private volatile URI uri; // Lazily initialized.
+  private volatile CacheControl cacheControl; // Lazily initialized.
+
   private Request(Builder builder) {
     this.url = builder.url;
     this.method = builder.method;
-    this.headers = new RawHeaders(builder.headers);
+    this.headers = builder.headers.build();
     this.body = builder.body;
     this.tag = builder.tag != null ? builder.tag : this;
   }
@@ -54,6 +56,15 @@
     return url;
   }
 
+  public URI uri() throws IOException {
+    try {
+      URI result = uri;
+      return result != null ? result : (uri = Platform.get().toUriLenient(url));
+    } catch (URISyntaxException e) {
+      throw new IOException(e.getMessage());
+    }
+  }
+
   public String urlString() {
     return url.toString();
   }
@@ -62,6 +73,10 @@
     return method;
   }
 
+  public Headers headers() {
+    return headers;
+  }
+
   public String header(String name) {
     return headers.get(name);
   }
@@ -70,22 +85,6 @@
     return headers.values(name);
   }
 
-  public Set<String> headerNames() {
-    return headers.names();
-  }
-
-  public int headerCount() {
-    return headers.length();
-  }
-
-  public String headerName(int index) {
-    return headers.getFieldName(index);
-  }
-
-  public String headerValue(int index) {
-    return headers.getValue(index);
-  }
-
   public Body body() {
     return body;
   }
@@ -94,22 +93,72 @@
     return tag;
   }
 
-  public abstract static class Body {
-    /**
-     * Returns the Content-Type header for this body, or null if the content
-     * type is unknown.
-     */
-    public MediaType contentType() {
-      return null;
-    }
+  public Builder newBuilder() {
+    return new Builder(this);
+  }
 
-    /** Returns the number of bytes in this body, or -1 if that count is unknown. */
+  public Headers getHeaders() {
+    return headers;
+  }
+
+  public String getUserAgent() {
+    return parsedHeaders().userAgent;
+  }
+
+  public String getProxyAuthorization() {
+    return parsedHeaders().proxyAuthorization;
+  }
+
+  private ParsedHeaders parsedHeaders() {
+    ParsedHeaders result = parsedHeaders;
+    return result != null ? result : (parsedHeaders = new ParsedHeaders(headers));
+  }
+
+  /**
+   * Returns the cache control directives for this response. This is never null,
+   * even if this response contains no {@code Cache-Control} header.
+   */
+  public CacheControl cacheControl() {
+    CacheControl result = cacheControl;
+    return result != null ? result : (cacheControl = CacheControl.parse(headers));
+  }
+
+  public boolean isHttps() {
+    return url().getProtocol().equals("https");
+  }
+
+  /** Parsed request headers, computed on-demand and cached. */
+  private static class ParsedHeaders {
+    private String userAgent;
+    private String proxyAuthorization;
+
+    public ParsedHeaders(Headers headers) {
+      for (int i = 0; i < headers.size(); i++) {
+        String fieldName = headers.name(i);
+        String value = headers.value(i);
+        if ("User-Agent".equalsIgnoreCase(fieldName)) {
+          userAgent = value;
+        } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
+          proxyAuthorization = value;
+        }
+      }
+    }
+  }
+
+  public abstract static class Body {
+    /** Returns the Content-Type header for this body. */
+    public abstract MediaType contentType();
+
+    /**
+     * Returns the number of bytes that will be written to {@code out} in a call
+     * to {@link #writeTo}, or -1 if that count is unknown.
+     */
     public long contentLength() {
       return -1;
     }
 
     /** Writes the content of this request to {@code out}. */
-    public abstract void writeTo(OutputStream out) throws IOException;
+    public abstract void writeTo(BufferedSink sink) throws IOException;
 
     /**
      * Returns a new request body that transmits {@code content}. If {@code
@@ -141,8 +190,8 @@
           return content.length;
         }
 
-        @Override public void writeTo(OutputStream out) throws IOException {
-          out.write(content);
+        @Override public void writeTo(BufferedSink sink) throws IOException {
+          sink.write(content);
         }
       };
     }
@@ -161,7 +210,7 @@
           return file.length();
         }
 
-        @Override public void writeTo(OutputStream out) throws IOException {
+        @Override public void writeTo(BufferedSink sink) throws IOException {
           long length = contentLength();
           if (length == 0) return;
 
@@ -170,7 +219,7 @@
             in = new FileInputStream(file);
             byte[] buffer = new byte[(int) Math.min(8192, length)];
             for (int c; (c = in.read(buffer)) != -1; ) {
-              out.write(buffer, 0, c);
+              sink.write(buffer, 0, c);
             }
           } finally {
             Util.closeQuietly(in);
@@ -182,30 +231,34 @@
 
   public static class Builder {
     private URL url;
-    private String method = "GET";
-    private final RawHeaders headers = new RawHeaders();
+    private String method;
+    private Headers.Builder headers;
     private Body body;
     private Object tag;
 
-    public Builder(String url) {
-      url(url);
+    public Builder() {
+      this.method = "GET";
+      this.headers = new Headers.Builder();
     }
 
-    public Builder(URL url) {
-      url(url);
+    private Builder(Request request) {
+      this.url = request.url;
+      this.method = request.method;
+      this.body = request.body;
+      this.tag = request.tag;
+      this.headers = request.headers.newBuilder();
     }
 
     public Builder url(String url) {
       try {
-        this.url = new URL(url);
-        return this;
+        return url(new URL(url));
       } catch (MalformedURLException e) {
         throw new IllegalArgumentException("Malformed URL: " + url);
       }
     }
 
     public Builder url(URL url) {
-      if (url == null) throw new IllegalStateException("url == null");
+      if (url == null) throw new IllegalArgumentException("url == null");
       this.url = url;
       return this;
     }
@@ -228,6 +281,21 @@
       return this;
     }
 
+    public Builder removeHeader(String name) {
+      headers.removeAll(name);
+      return this;
+    }
+
+    /** Removes all headers on this builder and adds {@code headers}. */
+    public Builder headers(Headers headers) {
+      this.headers = headers.newBuilder();
+      return this;
+    }
+
+    public Builder setUserAgent(String userAgent) {
+      return header("User-Agent", userAgent);
+    }
+
     public Builder get() {
       return method("GET", null);
     }
@@ -264,6 +332,7 @@
     }
 
     public Request build() {
+      if (url == null) throw new IllegalStateException("url == null");
       return new Request(this);
     }
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Response.java b/okhttp/src/main/java/com/squareup/okhttp/Response.java
index 4cef2cd..03c2d1f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Response.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Response.java
@@ -16,36 +16,55 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.RawHeaders;
+import com.squareup.okhttp.internal.http.HttpDate;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.StatusLine;
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Set;
+import java.util.TreeSet;
+import okio.Okio;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.Util.UTF_8;
+import static com.squareup.okhttp.internal.Util.equal;
 
 /**
  * An HTTP response. Instances of this class are not immutable: the response
  * body is a one-shot value that may be consumed only once. All other properties
  * are immutable.
- *
- * <h3>Warning: Experimental OkHttp 2.0 API</h3>
- * This class is in beta. APIs are subject to change!
  */
-/* OkHttp 2.0: public */ final class Response {
+public final class Response {
   private final Request request;
-  private final int code;
-  private final RawHeaders headers;
+  private final StatusLine statusLine;
+  private final Handshake handshake;
+  private final Headers headers;
   private final Body body;
-  private final Response redirectedBy;
+  private Response networkResponse;
+  private Response cacheResponse;
+  private final Response priorResponse;
+
+  private volatile ParsedHeaders parsedHeaders; // Lazily initialized.
+  private volatile CacheControl cacheControl; // Lazily initialized.
 
   private Response(Builder builder) {
     this.request = builder.request;
-    this.code = builder.code;
-    this.headers = new RawHeaders(builder.headers);
+    this.statusLine = builder.statusLine;
+    this.handshake = builder.handshake;
+    this.headers = builder.headers.build();
     this.body = builder.body;
-    this.redirectedBy = builder.redirectedBy;
+    this.networkResponse = builder.networkResponse;
+    this.cacheResponse = builder.cacheResponse;
+    this.priorResponse = builder.priorResponse;
   }
 
   /**
@@ -64,8 +83,32 @@
     return request;
   }
 
+  public String statusLine() {
+    return statusLine.getStatusLine();
+  }
+
   public int code() {
-    return code;
+    return statusLine.code();
+  }
+
+  public String statusMessage() {
+    return statusLine.message();
+  }
+
+  public int httpMinorVersion() {
+    return statusLine.httpMinorVersion();
+  }
+
+  /**
+   * Returns the TLS handshake of the connection that carried this response, or
+   * null if the response was received without TLS.
+   */
+  public Handshake handshake() {
+    return handshake;
+  }
+
+  public List<String> headers(String name) {
+    return headers.values(name);
   }
 
   public String header(String name) {
@@ -77,52 +120,134 @@
     return result != null ? result : defaultValue;
   }
 
-  public List<String> headers(String name) {
-    return headers.values(name);
-  }
-
-  public Set<String> headerNames() {
-    return headers.names();
-  }
-
-  public int headerCount() {
-    return headers.length();
-  }
-
-  public String headerName(int index) {
-    return headers.getFieldName(index);
-  }
-
-  public String headerValue(int index) {
-    return headers.getValue(index);
+  public Headers headers() {
+    return headers;
   }
 
   public Body body() {
     return body;
   }
 
+  public Builder newBuilder() {
+    return new Builder(this);
+  }
+
   /**
    * Returns the response for the HTTP redirect that triggered this response, or
    * null if this response wasn't triggered by an automatic redirect. The body
    * of the returned response should not be read because it has already been
    * consumed by the redirecting client.
    */
-  public Response redirectedBy() {
-    return redirectedBy;
+  public Response priorResponse() {
+    return priorResponse;
   }
 
-  public abstract static class Body {
-    public String contentType() {
-      return null;
+  /**
+   * Returns the raw response received from the network. Will be null if this
+   * response didn't use the network, such as when the response is fully cached.
+   * The body of the returned response should not be read.
+   */
+  public Response networkResponse() {
+    return networkResponse;
+  }
+
+  /**
+   * Returns the raw response received from the cache. Will be null if this
+   * response didn't use the cache. For conditional get requests the cache
+   * response and network response may both be non-null. The body of the
+   * returned response should not be read.
+   */
+  public Response cacheResponse() {
+    return cacheResponse;
+  }
+
+  // TODO: move out of public API
+  public Set<String> getVaryFields() {
+    return parsedHeaders().varyFields;
+  }
+
+  /**
+   * Returns true if a Vary header contains an asterisk. Such responses cannot
+   * be cached.
+   */
+  // TODO: move out of public API
+  public boolean hasVaryAll() {
+    return parsedHeaders().varyFields.contains("*");
+  }
+
+  /**
+   * Returns true if none of the Vary headers on this response have changed
+   * between {@code cachedRequest} and {@code newRequest}.
+   */
+  // TODO: move out of public API
+  public boolean varyMatches(Headers varyHeaders, Request newRequest) {
+    for (String field : parsedHeaders().varyFields) {
+      if (!equal(varyHeaders.values(field), newRequest.headers(field))) return false;
+    }
+    return true;
+  }
+
+  /**
+   * Returns true if this cached response should be used; false if the
+   * network response should be used.
+   */
+  // TODO: move out of public API
+  public boolean validate(Response network) {
+    if (network.code() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+      return true;
     }
 
-    public long contentLength() {
-      return -1;
+    // The HTTP spec says that if the network's response is older than our
+    // cached response, we may return the cache's response. Like Chrome (but
+    // unlike Firefox), this client prefers to return the newer response.
+    ParsedHeaders networkHeaders = network.parsedHeaders();
+    if (parsedHeaders().lastModified != null
+        && networkHeaders.lastModified != null
+        && networkHeaders.lastModified.getTime() < parsedHeaders().lastModified.getTime()) {
+      return true;
     }
 
-    public abstract InputStream byteStream() throws IOException;
+    return false;
+  }
 
-    public byte[] bytes() throws IOException {
+  public abstract static class Body implements Closeable {
+    /** Multiple calls to {@link #charStream()} must return the same instance. */
+    private Reader reader;
+
+    /** Multiple calls to {@link #source()} must return the same instance. */
+    private Source source;
+
+    /**
+     * Returns true if further data from this response body should be read at
+     * this time. For asynchronous protocols like SPDY and HTTP/2, this will
+     * return false once all locally-available body bytes have been read.
+     *
+     * <p>Clients with many concurrent downloads can use this method to reduce
+     * the number of idle threads blocking on reads. See {@link
+     * Receiver#onResponse} for details.
+     */
+    // <h3>Body.ready() vs. InputStream.available()</h3>
+    // TODO: Can we fix response bodies to implement InputStream.available well?
+    // The deflater implementation is broken by default but we could do better.
+    public abstract boolean ready() throws IOException;
+
+    public abstract MediaType contentType();
+
+    /**
+     * Returns the number of bytes in that will returned by {@link #bytes}, or
+     * {@link #byteStream}, or -1 if unknown.
+     */
+    public abstract long contentLength();
+
+    public abstract InputStream byteStream();
+
+    // TODO: Source needs to be an API type for this to be public
+    public Source source() {
+      Source s = source;
+      return s != null ? s : (source = Okio.source(byteStream()));
+    }
+
+    public final byte[] bytes() throws IOException {
       long contentLength = contentLength();
       if (contentLength > Integer.MAX_VALUE) {
         throw new IOException("Cannot buffer entire body for content length: " + contentLength);
@@ -143,41 +268,163 @@
     }
 
     /**
-     * Returns the response bytes as a UTF-8 character stream. Do not call this
-     * method if the response content is not a UTF-8 character stream.
+     * Returns the response as a character stream decoded with the charset
+     * of the Content-Type header. If that header is either absent or lacks a
+     * charset, this will attempt to decode the response body as UTF-8.
      */
-    public Reader charStream() throws IOException {
-      // TODO: parse content-type.
-      return new InputStreamReader(byteStream(), "UTF-8");
+    public final Reader charStream() {
+      Reader r = reader;
+      return r != null ? r : (reader = new InputStreamReader(byteStream(), charset()));
     }
 
     /**
-     * Returns the response bytes as a UTF-8 string. Do not call this method if
-     * the response content is not a UTF-8 character stream.
+     * Returns the response as a string decoded with the charset of the
+     * Content-Type header. If that header is either absent or lacks a charset,
+     * this will attempt to decode the response body as UTF-8.
      */
-    public String string() throws IOException {
-      // TODO: parse content-type.
-      return new String(bytes(), "UTF-8");
+    public final String string() throws IOException {
+      return new String(bytes(), charset().name());
+    }
+
+    private Charset charset() {
+      MediaType contentType = contentType();
+      return contentType != null ? contentType.charset(UTF_8) : UTF_8;
+    }
+
+    @Override public void close() throws IOException {
+      byteStream().close();
+    }
+  }
+
+  private ParsedHeaders parsedHeaders() {
+    ParsedHeaders result = parsedHeaders;
+    return result != null ? result : (parsedHeaders = new ParsedHeaders(headers));
+  }
+
+  /**
+   * Returns the cache control directives for this response. This is never null,
+   * even if this response contains no {@code Cache-Control} header.
+   */
+  public CacheControl cacheControl() {
+    CacheControl result = cacheControl;
+    return result != null ? result : (cacheControl = CacheControl.parse(headers));
+  }
+
+  /** Parsed response headers, computed on-demand and cached. */
+  private static class ParsedHeaders {
+    /** The last modified date of the response, if known. */
+    Date lastModified;
+
+    /** Case-insensitive set of field names. */
+    private Set<String> varyFields = Collections.emptySet();
+
+    private ParsedHeaders(Headers headers) {
+      for (int i = 0; i < headers.size(); i++) {
+        String fieldName = headers.name(i);
+        String value = headers.value(i);
+        if ("Last-Modified".equalsIgnoreCase(fieldName)) {
+          lastModified = HttpDate.parse(value);
+        } else if ("Vary".equalsIgnoreCase(fieldName)) {
+          // Replace the immutable empty set with something we can mutate.
+          if (varyFields.isEmpty()) {
+            varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+          }
+          for (String varyField : value.split(",")) {
+            varyFields.add(varyField.trim());
+          }
+        }
+      }
     }
   }
 
   public interface Receiver {
+    /**
+     * Called when the request could not be executed due to a connectivity
+     * problem or timeout. Because networks can fail during an exchange, it is
+     * possible that the remote server accepted the request before the failure.
+     */
     void onFailure(Failure failure);
-    void onResponse(Response response) throws IOException;
+
+    /**
+     * Called when the HTTP response was successfully returned by the remote
+     * server. The receiver may proceed to read the response body with the
+     * response's {@link #body} method.
+     *
+     * <p>Note that transport-layer success (receiving a HTTP response code,
+     * headers and body) does not necessarily indicate application-layer
+     * success: {@code response} may still indicate an unhappy HTTP response
+     * code like 404 or 500.
+     *
+     * <h3>Non-blocking responses</h3>
+     *
+     * <p>Receivers do not need to block while waiting for the response body to
+     * download. Instead, they can get called back as data arrives. Use {@link
+     * Body#ready} to check if bytes should be read immediately. While there is
+     * data ready, read it. If there isn't, return false: receivers will be
+     * called back with {@code onResponse()} as additional data is downloaded.
+     *
+     * <p>Return true to indicate that the receiver has finished handling the
+     * response body. If the response body has unread data, it will be
+     * discarded.
+     *
+     * <p>When the response body has been fully consumed the returned value is
+     * undefined.
+     *
+     * <p>The current implementation of {@link Body#ready} always returns true
+     * when the underlying transport is HTTP/1. This results in blocking on that
+     * transport. For effective non-blocking your server must support SPDY or
+     * HTTP/2.
+     */
+    boolean onResponse(Response response) throws IOException;
   }
 
   public static class Builder {
-    private final Request request;
-    private final int code;
-    private final RawHeaders headers = new RawHeaders();
+    private Request request;
+    private StatusLine statusLine;
+    private Handshake handshake;
+    private Headers.Builder headers;
     private Body body;
-    private Response redirectedBy;
+    private Response networkResponse;
+    private Response cacheResponse;
+    private Response priorResponse;
 
-    public Builder(Request request, int code) {
-      if (request == null) throw new IllegalArgumentException("request == null");
-      if (code <= 0) throw new IllegalArgumentException("code <= 0");
+    public Builder() {
+      headers = new Headers.Builder();
+    }
+
+    private Builder(Response response) {
+      this.request = response.request;
+      this.statusLine = response.statusLine;
+      this.handshake = response.handshake;
+      this.headers = response.headers.newBuilder();
+      this.body = response.body;
+      this.networkResponse = response.networkResponse;
+      this.cacheResponse = response.cacheResponse;
+      this.priorResponse = response.priorResponse;
+    }
+
+    public Builder request(Request request) {
       this.request = request;
-      this.code = code;
+      return this;
+    }
+
+    public Builder statusLine(StatusLine statusLine) {
+      if (statusLine == null) throw new IllegalArgumentException("statusLine == null");
+      this.statusLine = statusLine;
+      return this;
+    }
+
+    public Builder statusLine(String statusLine) {
+      try {
+        return statusLine(new StatusLine(statusLine));
+      } catch (IOException e) {
+        throw new IllegalArgumentException(e);
+      }
+    }
+
+    public Builder handshake(Handshake handshake) {
+      this.handshake = handshake;
+      return this;
     }
 
     /**
@@ -198,19 +445,59 @@
       return this;
     }
 
+    public Builder removeHeader(String name) {
+      headers.removeAll(name);
+      return this;
+    }
+
+    /** Removes all headers on this builder and adds {@code headers}. */
+    public Builder headers(Headers headers) {
+      this.headers = headers.newBuilder();
+      return this;
+    }
+
     public Builder body(Body body) {
       this.body = body;
       return this;
     }
 
-    public Builder redirectedBy(Response redirectedBy) {
-      this.redirectedBy = redirectedBy;
+    // TODO: move out of public API
+    public Builder setResponseSource(ResponseSource responseSource) {
+      return header(OkHeaders.RESPONSE_SOURCE, responseSource + " " + statusLine.code());
+    }
+
+    public Builder networkResponse(Response networkResponse) {
+      if (networkResponse != null) checkSupportResponse("networkResponse", networkResponse);
+      this.networkResponse = networkResponse;
+      return this;
+    }
+
+    public Builder cacheResponse(Response cacheResponse) {
+      if (cacheResponse != null) checkSupportResponse("cacheResponse", cacheResponse);
+      this.cacheResponse = cacheResponse;
+      return this;
+    }
+
+    private void checkSupportResponse(String name, Response response) {
+      if (response.body != null) {
+        throw new IllegalArgumentException(name + ".body != null");
+      } else if (response.networkResponse != null) {
+        throw new IllegalArgumentException(name + ".networkResponse != null");
+      } else if (response.cacheResponse != null) {
+        throw new IllegalArgumentException(name + ".cacheResponse != null");
+      } else if (response.priorResponse != null) {
+        throw new IllegalArgumentException(name + ".priorResponse != null");
+      }
+    }
+
+    public Builder priorResponse(Response priorResponse) {
+      this.priorResponse = priorResponse;
       return this;
     }
 
     public Response build() {
-      if (request == null) throw new IllegalStateException("Response has no request.");
-      if (code == -1) throw new IllegalStateException("Response has no code.");
+      if (request == null) throw new IllegalStateException("request == null");
+      if (statusLine == null) throw new IllegalStateException("statusLine == null");
       return new Response(this);
     }
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java b/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java
index 4eca172..915fa58 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java
@@ -29,9 +29,20 @@
   CONDITIONAL_CACHE,
 
   /** The response was returned from the network. */
-  NETWORK;
+  NETWORK,
+
+  /**
+   * The request demanded a cached response that the cache couldn't satisfy.
+   * This yields a 504 (Gateway Timeout) response as specified by
+   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
+   */
+  NONE;
 
   public boolean requiresConnection() {
     return this == CONDITIONAL_CACHE || this == NETWORK;
   }
+
+  public boolean usesCache() {
+    return this == CACHE || this == CONDITIONAL_CACHE;
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Route.java b/okhttp/src/main/java/com/squareup/okhttp/Route.java
index 4b8786d..a08a469 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Route.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Route.java
@@ -18,7 +18,21 @@
 import java.net.InetSocketAddress;
 import java.net.Proxy;
 
-/** Represents the route used by a connection to reach an endpoint. */
+/**
+ * The concrete route used by a connection to reach an abstract origin server.
+ * When creating a connection the client has many options:
+ * <ul>
+ *   <li><strong>HTTP proxy:</strong> a proxy server may be explicitly
+ *       configured for the client. Otherwise the {@link java.net.ProxySelector
+ *       proxy selector} is used. It may return multiple proxies to attempt.
+ *   <li><strong>IP address:</strong> whether connecting directly to an origin
+ *       server or a proxy, opening a socket requires an IP address. The DNS
+ *       server may return multiple IP addresses to attempt.
+ *   <li><strong>Modern TLS:</strong> whether to include advanced TLS options
+ *       when attempting a HTTPS connection.
+ * </ul>
+ * Each route is a specific selection of these options.
+ */
 public class Route {
   final Address address;
   final Proxy proxy;
@@ -44,11 +58,8 @@
   /**
    * Returns the {@link Proxy} of this route.
    *
-   * <strong>Warning:</strong> This may be different than the proxy returned
-   * by {@link #getAddress}! That is the proxy that the user asked to be
-   * connected to; this returns the proxy that they were actually connected
-   * to. The two may disagree when a proxy selector selects a different proxy
-   * for a connection.
+   * <strong>Warning:</strong> This may disagree with {@link Address#getProxy}
+   * is null. When the address's proxy is null, the proxy selector will be used.
    */
   public Proxy getProxy() {
     return proxy;
@@ -64,11 +75,6 @@
     return modernTls;
   }
 
-  /** Returns a copy of this route with flipped TLS mode. */
-  Route flipTlsMode() {
-    return new Route(address, proxy, inetSocketAddress, !modernTls);
-  }
-
   @Override public boolean equals(Object obj) {
     if (obj instanceof Route) {
       Route other = (Route) obj;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java b/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java
index 9cbeaa7..4177c0f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java
@@ -15,10 +15,8 @@
  */
 package com.squareup.okhttp;
 
-import java.io.IOException;
 import java.util.LinkedHashSet;
 import java.util.Set;
-import javax.net.ssl.SSLHandshakeException;
 
 /**
  * A blacklist of failed routes to avoid when creating a new connection to a
@@ -31,14 +29,8 @@
   private final Set<Route> failedRoutes = new LinkedHashSet<Route>();
 
   /** Records a failure connecting to {@code failedRoute}. */
-  public synchronized void failed(Route failedRoute, IOException failure) {
+  public synchronized void failed(Route failedRoute) {
     failedRoutes.add(failedRoute);
-
-    if (!(failure instanceof SSLHandshakeException)) {
-      // If the problem was not related to SSL then it will also fail with
-      // a different TLS mode therefore we can be proactive about it.
-      failedRoutes.add(failedRoute.flipTlsMode());
-    }
   }
 
   /** Records success connecting to {@code failedRoute}. */
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java b/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java
index 5260b87..3bcff5a 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java
@@ -15,7 +15,8 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.http.RawHeaders;
+import java.io.IOException;
+import java.net.URL;
 
 import static com.squareup.okhttp.internal.Util.getDefaultPort;
 
@@ -49,27 +50,31 @@
     this.proxyAuthorization = proxyAuthorization;
   }
 
+  String requestLine() {
+    return "CONNECT " + host + ":" + port + " HTTP/1.1";
+  }
+
   /**
    * If we're creating a TLS tunnel, send only the minimum set of headers.
    * This avoids sending potentially sensitive data like HTTP cookies to
    * the proxy unencrypted.
    */
-  RawHeaders getRequestHeaders() {
-    RawHeaders result = new RawHeaders();
-    result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1");
+  Request getRequest() throws IOException {
+    Request.Builder result = new Request.Builder()
+        .url(new URL("https", host, port, "/"));
 
     // Always set Host and User-Agent.
-    result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
-    result.set("User-Agent", userAgent);
+    result.header("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
+    result.header("User-Agent", userAgent);
 
     // Copy over the Proxy-Authorization header if it exists.
     if (proxyAuthorization != null) {
-      result.set("Proxy-Authorization", proxyAuthorization);
+      result.header("Proxy-Authorization", proxyAuthorization);
     }
 
     // Always set the Proxy-Connection to Keep-Alive for the benefit of
     // HTTP/1.0 proxies like Squid.
-    result.set("Proxy-Connection", "Keep-Alive");
-    return result;
+    result.header("Proxy-Connection", "Keep-Alive");
+    return result.build();
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
deleted file mode 100644
index 78c9691..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/AbstractOutputStream.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2010 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;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * An output stream for an HTTP request body.
- *
- * <p>Since a single socket's output stream may be used to write multiple HTTP
- * requests to the same server, subclasses should not close the socket stream.
- */
-public abstract class AbstractOutputStream extends OutputStream {
-  protected boolean closed;
-
-  @Override public final void write(int data) throws IOException {
-    write(new byte[] { (byte) data });
-  }
-
-  protected final void checkNotClosed() throws IOException {
-    if (closed) {
-      throw new IOException("stream closed");
-    }
-  }
-
-  /** Returns true if this stream was closed locally. */
-  public boolean isClosed() {
-    return closed;
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java b/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java
new file mode 100644
index 0000000..c83f1dd
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.lang.String.format;
+
+/** A simple bitset which supports left shifting. */
+public interface BitArray {
+
+  void clear();
+
+  void set(int index);
+
+  void toggle(int index);
+
+  boolean get(int index);
+
+  void shiftLeft(int count);
+
+  /** Bit set that only supports settings bits 0 - 63. */
+  public final class FixedCapacity implements BitArray {
+    long data = 0x0000000000000000L;
+
+    @Override public void clear() {
+      data = 0x0000000000000000L;
+    }
+
+    @Override public void set(int index) {
+      data |= (1L << checkInput(index));
+    }
+
+    @Override public void toggle(int index) {
+      data ^= (1L << checkInput(index));
+    }
+
+    @Override public boolean get(int index) {
+      return ((data >> checkInput(index)) & 1L) == 1;
+    }
+
+    @Override public void shiftLeft(int count) {
+      data = data << checkInput(count);
+    }
+
+    @Override public String toString() {
+      return Long.toBinaryString(data);
+    }
+
+    public BitArray toVariableCapacity() {
+      return new VariableCapacity(this);
+    }
+
+    private static int checkInput(int index) {
+      if (index < 0 || index > 63) {
+        throw new IllegalArgumentException(format("input must be between 0 and 63: %s", index));
+      }
+      return index;
+    }
+  }
+
+  /** Bit set that grows as needed. */
+  public final class VariableCapacity implements BitArray {
+
+    long[] data;
+
+    // Start offset which allows for cheap shifting. Data is always kept on 64-bit bounds but we
+    // offset the outward facing index to support shifts without having to move the underlying bits.
+    private int start; // Valid values are [0..63]
+
+    public VariableCapacity() {
+      data = new long[1];
+    }
+
+    private VariableCapacity(FixedCapacity small) {
+      data = new long[] {small.data, 0};
+    }
+
+    private void growToSize(int size) {
+      long[] newData = new long[size];
+      if (data != null) {
+        System.arraycopy(data, 0, newData, 0, data.length);
+      }
+      data = newData;
+    }
+
+    private int offsetOf(int index) {
+      index += start;
+      int offset = index / 64;
+      if (offset > data.length - 1) {
+        growToSize(offset + 1);
+      }
+      return offset;
+    }
+
+    private int shiftOf(int index) {
+      return (index + start) % 64;
+    }
+
+    @Override public void clear() {
+      Arrays.fill(data, 0);
+    }
+
+    @Override public void set(int index) {
+      checkInput(index);
+      int offset = offsetOf(index);
+      data[offset] |= 1L << shiftOf(index);
+    }
+
+    @Override public void toggle(int index) {
+      checkInput(index);
+      int offset = offsetOf(index);
+      data[offset] ^= 1L << shiftOf(index);
+    }
+
+    @Override public boolean get(int index) {
+      checkInput(index);
+      int offset = offsetOf(index);
+      return (data[offset] & (1L << shiftOf(index))) != 0;
+    }
+
+    @Override public void shiftLeft(int count) {
+      start -= checkInput(count);
+      if (start < 0) {
+        int arrayShift = (start / -64) + 1;
+        long[] newData = new long[data.length + arrayShift];
+        System.arraycopy(data, 0, newData, arrayShift, data.length);
+        data = newData;
+        start = 64 + (start % 64);
+      }
+    }
+
+    @Override public String toString() {
+      StringBuilder builder = new StringBuilder("{");
+      List<Integer> ints = toIntegerList();
+      for (int i = 0, count = ints.size(); i < count; i++) {
+        if (i > 0) {
+          builder.append(',');
+        }
+        builder.append(ints.get(i));
+      }
+      return builder.append('}').toString();
+    }
+
+    List<Integer> toIntegerList() {
+      List<Integer> ints = new ArrayList<Integer>();
+      for (int i = 0, count = data.length * 64 - start; i < count; i++) {
+        if (get(i)) {
+          ints.add(i);
+        }
+      }
+      return ints;
+    }
+
+    private static int checkInput(int index) {
+      if (index < 0) {
+        throw new IllegalArgumentException(format("input must be a positive number: %s", index));
+      }
+      return index;
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index f7fcb1e..7f4fa11 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -16,7 +16,6 @@
 
 package com.squareup.okhttp.internal;
 
-import java.io.BufferedWriter;
 import java.io.Closeable;
 import java.io.EOFException;
 import java.io.File;
@@ -26,20 +25,19 @@
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.concurrent.Callable;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.OkBuffer;
+import okio.Okio;
 
 /**
  * A cache that uses a bounded amount of space on a filesystem. Each cache
@@ -146,7 +144,7 @@
   private long maxSize;
   private final int valueCount;
   private long size = 0;
-  private Writer journalWriter;
+  private BufferedSink journalWriter;
   private final LinkedHashMap<String, Entry> lruEntries =
       new LinkedHashMap<String, Entry>(0, 0.75f, true);
   private int redundantOpCount;
@@ -159,21 +157,24 @@
   private long nextSequenceNumber = 0;
 
   /** This cache uses a single background thread to evict entries. */
-  final ThreadPoolExecutor executorService =
-      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
-  private final Callable<Void> cleanupCallable = new Callable<Void>() {
-    public Void call() throws Exception {
+  final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
+      new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
+  private final Runnable cleanupRunnable = new Runnable() {
+    public void run() {
       synchronized (DiskLruCache.this) {
         if (journalWriter == null) {
-          return null; // Closed.
+          return; // Closed.
         }
-        trimToSize();
-        if (journalRebuildRequired()) {
-          rebuildJournal();
-          redundantOpCount = 0;
+        try {
+          trimToSize();
+          if (journalRebuildRequired()) {
+            rebuildJournal();
+            redundantOpCount = 0;
+          }
+        } catch (IOException e) {
+          throw new RuntimeException(e);
         }
       }
-      return null;
     }
   };
 
@@ -223,8 +224,7 @@
       try {
         cache.readJournal();
         cache.processJournal();
-        cache.journalWriter = new BufferedWriter(
-            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
+        cache.journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(cache.journalFile, true)));
         return cache;
       } catch (IOException journalIsCorrupt) {
         Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
@@ -241,13 +241,13 @@
   }
 
   private void readJournal() throws IOException {
-    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+    BufferedSource source = Okio.buffer(Okio.source(new FileInputStream(journalFile)));
     try {
-      String magic = reader.readLine();
-      String version = reader.readLine();
-      String appVersionString = reader.readLine();
-      String valueCountString = reader.readLine();
-      String blank = reader.readLine();
+      String magic = source.readUtf8LineStrict();
+      String version = source.readUtf8LineStrict();
+      String appVersionString = source.readUtf8LineStrict();
+      String valueCountString = source.readUtf8LineStrict();
+      String blank = source.readUtf8LineStrict();
       if (!MAGIC.equals(magic)
           || !VERSION_1.equals(version)
           || !Integer.toString(appVersion).equals(appVersionString)
@@ -260,7 +260,7 @@
       int lineCount = 0;
       while (true) {
         try {
-          readJournalLine(reader.readLine());
+          readJournalLine(source.readUtf8LineStrict());
           lineCount++;
         } catch (EOFException endOfJournal) {
           break;
@@ -268,7 +268,7 @@
       }
       redundantOpCount = lineCount - lruEntries.size();
     } finally {
-      Util.closeQuietly(reader);
+      Util.closeQuietly(source);
     }
   }
 
@@ -343,24 +343,23 @@
       journalWriter.close();
     }
 
-    Writer writer = new BufferedWriter(
-        new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+    BufferedSink writer = Okio.buffer(Okio.sink(new FileOutputStream(journalFileTmp)));
     try {
-      writer.write(MAGIC);
-      writer.write("\n");
-      writer.write(VERSION_1);
-      writer.write("\n");
-      writer.write(Integer.toString(appVersion));
-      writer.write("\n");
-      writer.write(Integer.toString(valueCount));
-      writer.write("\n");
-      writer.write("\n");
+      writer.writeUtf8(MAGIC);
+      writer.writeUtf8("\n");
+      writer.writeUtf8(VERSION_1);
+      writer.writeUtf8("\n");
+      writer.writeUtf8(Integer.toString(appVersion));
+      writer.writeUtf8("\n");
+      writer.writeUtf8(Integer.toString(valueCount));
+      writer.writeUtf8("\n");
+      writer.writeUtf8("\n");
 
       for (Entry entry : lruEntries.values()) {
         if (entry.currentEditor != null) {
-          writer.write(DIRTY + ' ' + entry.key + '\n');
+          writer.writeUtf8(DIRTY + ' ' + entry.key + '\n');
         } else {
-          writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+          writer.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
         }
       }
     } finally {
@@ -373,13 +372,13 @@
     renameTo(journalFileTmp, journalFile, false);
     journalFileBackup.delete();
 
-    journalWriter = new BufferedWriter(
-        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
+    journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(journalFile, true)));
   }
 
   private static void deleteIfExists(File file) throws IOException {
-    if (file.exists() && !file.delete()) {
-      throw new IOException();
+    // If delete() fails, make sure it's because the file didn't exist!
+    if (!file.delete() && file.exists()) {
+      throw new IOException("failed to delete " + file);
     }
   }
 
@@ -430,9 +429,9 @@
     }
 
     redundantOpCount++;
-    journalWriter.append(READ + ' ' + key + '\n');
+    journalWriter.writeUtf8(READ + ' ' + key + '\n');
     if (journalRebuildRequired()) {
-      executorService.submit(cleanupCallable);
+      executorService.execute(cleanupRunnable);
     }
 
     return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
@@ -465,7 +464,7 @@
     entry.currentEditor = editor;
 
     // Flush the journal before creating files to prevent file leaks.
-    journalWriter.write(DIRTY + ' ' + key + '\n');
+    journalWriter.writeUtf8(DIRTY + ' ' + key + '\n');
     journalWriter.flush();
     return editor;
   }
@@ -479,7 +478,7 @@
    * Returns the maximum number of bytes that this cache should use to store
    * its data.
    */
-  public long getMaxSize() {
+  public synchronized long getMaxSize() {
     return maxSize;
   }
 
@@ -489,7 +488,7 @@
    */
   public synchronized void setMaxSize(long maxSize) {
     this.maxSize = maxSize;
-    executorService.submit(cleanupCallable);
+    executorService.execute(cleanupRunnable);
   }
 
   /**
@@ -541,18 +540,18 @@
     entry.currentEditor = null;
     if (entry.readable | success) {
       entry.readable = true;
-      journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+      journalWriter.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
       if (success) {
         entry.sequenceNumber = nextSequenceNumber++;
       }
     } else {
       lruEntries.remove(entry.key);
-      journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+      journalWriter.writeUtf8(REMOVE + ' ' + entry.key + '\n');
     }
     journalWriter.flush();
 
     if (size > maxSize || journalRebuildRequired()) {
-      executorService.submit(cleanupCallable);
+      executorService.execute(cleanupRunnable);
     }
   }
 
@@ -562,7 +561,7 @@
    */
   private boolean journalRebuildRequired() {
     final int redundantOpCompactThreshold = 2000;
-    return redundantOpCount >= redundantOpCompactThreshold //
+    return redundantOpCount >= redundantOpCompactThreshold
         && redundantOpCount >= lruEntries.size();
   }
 
@@ -582,19 +581,17 @@
 
     for (int i = 0; i < valueCount; i++) {
       File file = entry.getCleanFile(i);
-      if (!file.delete()) {
-        throw new IOException("failed to delete " + file);
-      }
+      deleteIfExists(file);
       size -= entry.lengths[i];
       entry.lengths[i] = 0;
     }
 
     redundantOpCount++;
-    journalWriter.append(REMOVE + ' ' + key + '\n');
+    journalWriter.writeUtf8(REMOVE + ' ' + key + '\n');
     lruEntries.remove(key);
 
     if (journalRebuildRequired()) {
-      executorService.submit(cleanupCallable);
+      executorService.execute(cleanupRunnable);
     }
 
     return true;
@@ -623,7 +620,9 @@
     if (journalWriter == null) {
       return; // Already closed.
     }
-    for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+    // Copying for safe iteration.
+    for (Object next : lruEntries.values().toArray()) {
+      Entry entry = (Entry) next;
       if (entry.currentEditor != null) {
         entry.currentEditor.abort();
       }
@@ -658,7 +657,8 @@
   }
 
   private static String inputStreamToString(InputStream in) throws IOException {
-    return Util.readFully(new InputStreamReader(in, Util.UTF_8));
+    OkBuffer buffer = Util.readFully(Okio.source(in));
+    return buffer.readUtf8(buffer.size());
   }
 
   /** A snapshot of the values for an entry. */
@@ -789,13 +789,9 @@
 
     /** Sets the value at {@code index} to {@code value}. */
     public void set(int index, String value) throws IOException {
-      Writer writer = null;
-      try {
-        writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
-        writer.write(value);
-      } finally {
-        Util.closeQuietly(writer);
-      }
+      BufferedSink writer = Okio.buffer(Okio.sink(newOutputStream(index)));
+      writer.writeUtf8(value);
+      writer.close();
     }
 
     /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
deleted file mode 100644
index c32b27a..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
-
-/**
- * An output stream wrapper that recovers from failures in the underlying stream
- * by replacing it with another stream. This class buffers a fixed amount of
- * data under the assumption that failures occur early in a stream's life.
- * If a failure occurs after the buffer has been exhausted, no recovery is
- * attempted.
- *
- * <p>Subclasses must override {@link #replacementStream} which will request a
- * replacement stream each time an {@link IOException} is encountered on the
- * current stream.
- */
-public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
-  private final int maxReplayBufferLength;
-
-  /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
-  private ByteArrayOutputStream replayBuffer;
-  private OutputStream out;
-
-  /**
-   * @param maxReplayBufferLength the maximum number of successfully written
-   *     bytes to buffer so they can be replayed in the event of an error.
-   *     Failure recoveries are not possible once this limit has been exceeded.
-   */
-  public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
-    if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
-    this.maxReplayBufferLength = maxReplayBufferLength;
-    this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
-    this.out = out;
-  }
-
-  @Override public final void write(byte[] buffer, int offset, int count) throws IOException {
-    if (closed) throw new IOException("stream closed");
-    checkOffsetAndCount(buffer.length, offset, count);
-
-    while (true) {
-      try {
-        out.write(buffer, offset, count);
-
-        if (replayBuffer != null) {
-          if (count + replayBuffer.size() > maxReplayBufferLength) {
-            // Failure recovery is no longer possible once we overflow the replay buffer.
-            replayBuffer = null;
-          } else {
-            // Remember the written bytes to the replay buffer.
-            replayBuffer.write(buffer, offset, count);
-          }
-        }
-        return;
-      } catch (IOException e) {
-        if (!recover(e)) throw e;
-      }
-    }
-  }
-
-  @Override public final void flush() throws IOException {
-    if (closed) {
-      return; // don't throw; this stream might have been closed on the caller's behalf
-    }
-    while (true) {
-      try {
-        out.flush();
-        return;
-      } catch (IOException e) {
-        if (!recover(e)) throw e;
-      }
-    }
-  }
-
-  @Override public final void close() throws IOException {
-    if (closed) {
-      return;
-    }
-    while (true) {
-      try {
-        out.close();
-        closed = true;
-        return;
-      } catch (IOException e) {
-        if (!recover(e)) throw e;
-      }
-    }
-  }
-
-  /**
-   * Attempt to replace {@code out} with another equivalent stream. Returns true
-   * if a suitable replacement stream was found.
-   */
-  private boolean recover(IOException e) {
-    if (replayBuffer == null) {
-      return false; // Can't recover because we've dropped data that we would need to replay.
-    }
-
-    while (true) {
-      OutputStream replacementStream = null;
-      try {
-        replacementStream = replacementStream(e);
-        if (replacementStream == null) {
-          return false;
-        }
-        replaceStream(replacementStream);
-        return true;
-      } catch (IOException replacementStreamFailure) {
-        // The replacement was also broken. Loop to ask for another replacement.
-        Util.closeQuietly(replacementStream);
-        e = replacementStreamFailure;
-      }
-    }
-  }
-
-  /**
-   * Returns true if errors in the underlying stream can currently be recovered.
-   */
-  public boolean isRecoverable() {
-    return replayBuffer != null;
-  }
-
-  /**
-   * Replaces the current output stream with {@code replacementStream}, writing
-   * any replay bytes to it if they exist. The current output stream is closed.
-   */
-  public final void replaceStream(OutputStream replacementStream) throws IOException {
-    if (!isRecoverable()) {
-      throw new IllegalStateException();
-    }
-    if (this.out == replacementStream) {
-      return; // Don't replace a stream with itself.
-    }
-    replayBuffer.writeTo(replacementStream);
-    Util.closeQuietly(out);
-    out = replacementStream;
-  }
-
-  /**
-   * Returns a replacement output stream to recover from {@code e} thrown by the
-   * previous stream. Returns a new OutputStream if recovery was successful, in
-   * which case all previously-written data will be replayed. Returns null if
-   * the failure cannot be recovered.
-   */
-  protected abstract OutputStream replacementStream(IOException e) throws IOException;
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java b/okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
similarity index 100%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Platform.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
similarity index 64%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Platform.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
index 905641a..231ce3b 100644
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,16 +16,15 @@
  */
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.Protocol;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.net.InetSocketAddress;
-import java.net.NetworkInterface;
 import java.net.Socket;
 import java.net.SocketException;
 import java.net.URI;
@@ -38,15 +37,33 @@
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 import javax.net.ssl.SSLSocket;
+import okio.ByteString;
 
 /**
  * Access to Platform-specific features necessary for SPDY and advanced TLS.
  *
- * <h3>SPDY</h3>
- * SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
- * available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
- * also requires a recent version of {@code DeflaterOutputStream} that is
- * public API in Java 7 and callable via reflection in Android 4.1+.
+ * <h3>ALPN and NPN</h3>
+ * This class uses TLS extensions ALPN and NPN to negotiate the upgrade from
+ * HTTP/1.1 (the default protocol to use with TLS on port 443) to either SPDY
+ * or HTTP/2.
+ *
+ * <p>NPN (Next Protocol Negotiation) was developed for SPDY. It is widely
+ * available and we support it on both Android (4.1+) and OpenJDK 7 (via the
+ * Jetty NPN-boot library). NPN is not yet available on Java 8.
+ *
+ * <p>ALPN (Application Layer Protocol Negotiation) is the successor to NPN. It
+ * has some technical advantages over NPN. ALPN first arrived in Android 4.4,
+ * but that release suffers a <a href="http://goo.gl/y5izPP">concurrency bug</a>
+ * so we don't use it. ALPN will be supported in the future.
+ *
+ * <p>On platforms that support both extensions, OkHttp will use both,
+ * preferring ALPN's result. Future versions of OkHttp will drop support for
+ * NPN.
+ *
+ * <h3>Deflater Sync Flush</h3>
+ * SPDY header compression requires a recent version of {@code
+ * DeflaterOutputStream} that is public API in Java 7 and callable via
+ * reflection in Android 4.1+.
  */
 public class Platform {
   private static final Platform PLATFORM = findPlatform();
@@ -93,7 +110,7 @@
   }
 
   /** Returns the negotiated protocol, or null if no protocol was negotiated. */
-  public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+  public ByteString getNpnSelectedProtocol(SSLSocket socket) {
     return null;
   }
 
@@ -101,7 +118,7 @@
    * Sets client-supported protocols on a socket to send to a server. The
    * protocols are only sent if the socket implementation supports NPN.
    */
-  public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+  public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
   }
 
   public void connectSocket(Socket socket, InetSocketAddress address,
@@ -135,27 +152,8 @@
     }
   }
 
-  /**
-   * Returns the maximum transmission unit of the network interface used by
-   * {@code socket}, or a reasonable default if this platform doesn't expose the
-   * MTU to the application layer.
-   *
-   * <p>The returned value should only be used as an optimization; such as to
-   * size buffers efficiently.
-   */
-  public int getMtu(Socket socket) throws IOException {
-    return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
-  }
-
   /** Attempt to match the host runtime to a capable Platform implementation. */
   private static Platform findPlatform() {
-    Method getMtu;
-    try {
-      getMtu = NetworkInterface.class.getMethod("getMTU");
-    } catch (NoSuchMethodException e) {
-      return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
-    }
-
     // Attempt to find Android 2.3+ APIs.
     Class<?> openSslSocketClass;
     Method setUseSessionTickets;
@@ -173,14 +171,16 @@
       setHostname = openSslSocketClass.getMethod("setHostname", String.class);
 
       // Attempt to find Android 4.1+ APIs.
+      Method setNpnProtocols = null;
+      Method getNpnSelectedProtocol = null;
       try {
-        Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
-        Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
-        return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
-            setNpnProtocols, getNpnSelectedProtocol);
+        setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
+        getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
       } catch (NoSuchMethodException ignored) {
-        return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
       }
+
+      return new Android(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
+          getNpnSelectedProtocol);
     } catch (ClassNotFoundException ignored) {
       // This isn't an Android runtime.
     } catch (NoSuchMethodException ignored) {
@@ -196,59 +196,38 @@
       Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
       Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
       Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
-      return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
-          serverProviderClass);
+      return new JdkWithJettyNpnPlatform(
+          putMethod, getMethod, clientProviderClass, serverProviderClass);
     } catch (ClassNotFoundException ignored) {
       // NPN isn't on the classpath.
     } catch (NoSuchMethodException ignored) {
       // The NPN version isn't what we expect.
     }
 
-    return new Java6(getMtu);
+    return new Platform();
   }
 
-  private static class Java6 extends Platform {
-    private final Method getMtu;
-
-    private Java6(Method getMtu) {
-      this.getMtu = getMtu;
-    }
-
-    @Override public int getMtu(Socket socket) throws IOException {
-      try {
-        NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
-            socket.getLocalAddress());
-        if (networkInterface == null) {
-          return super.getMtu(socket); // There's no longer an interface with this local address.
-        }
-        return (Integer) getMtu.invoke(networkInterface);
-      } catch (NullPointerException e) {
-        // Certain Alcatel devices throw on getByInetAddress. Return default.
-        return super.getMtu(socket);
-      } catch (SocketException e) {
-        // Certain Motorola devices always throw on getByInetAddress. Return the default for those.
-        return super.getMtu(socket);
-      } catch (IllegalAccessException e) {
-        throw new AssertionError(e);
-      } catch (InvocationTargetException e) {
-        if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
-        throw new RuntimeException(e.getCause());
-      }
-    }
-  }
-
-  /** Android version 2.3 and newer support TLS session tickets and server name indication (SNI). */
-  private static class Android23 extends Java6 {
+  /**
+   * Android 2.3 or better. Version 2.3 supports TLS session tickets and server
+   * name indication (SNI). Versions 4.1 supports NPN.
+   */
+  private static class Android extends Platform {
+    // Non-null.
     protected final Class<?> openSslSocketClass;
     private final Method setUseSessionTickets;
     private final Method setHostname;
 
-    private Android23(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
-        Method setHostname) {
-      super(getMtu);
+    // Non-null on Android 4.1+.
+    private final Method setNpnProtocols;
+    private final Method getNpnSelectedProtocol;
+
+    private Android(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
+        Method setNpnProtocols, Method getNpnSelectedProtocol) {
       this.openSslSocketClass = openSslSocketClass;
       this.setUseSessionTickets = setUseSessionTickets;
       this.setHostname = setHostname;
+      this.setNpnProtocols = setNpnProtocols;
+      this.getNpnSelectedProtocol = getNpnSelectedProtocol;
     }
 
     @Override public void connectSocket(Socket socket, InetSocketAddress address,
@@ -266,38 +245,23 @@
 
     @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
       super.enableTlsExtensions(socket, uriHost);
-      if (openSslSocketClass.isInstance(socket)) {
-        // This is Android: use reflection on OpenSslSocketImpl.
-        try {
-          setUseSessionTickets.invoke(socket, true);
-          setHostname.invoke(socket, uriHost);
-        } catch (InvocationTargetException e) {
-          throw new RuntimeException(e);
-        } catch (IllegalAccessException e) {
-          throw new AssertionError(e);
-        }
-      }
-    }
-  }
-
-  /** Android version 4.1 and newer support NPN. */
-  private static class Android41 extends Android23 {
-    private final Method setNpnProtocols;
-    private final Method getNpnSelectedProtocol;
-
-    private Android41(Method getMtu, Class<?> openSslSocketClass, Method setUseSessionTickets,
-        Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
-      super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
-      this.setNpnProtocols = setNpnProtocols;
-      this.getNpnSelectedProtocol = getNpnSelectedProtocol;
-    }
-
-    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
-      if (!openSslSocketClass.isInstance(socket)) {
-        return;
-      }
+      if (!openSslSocketClass.isInstance(socket)) return;
       try {
-        setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
+        setUseSessionTickets.invoke(socket, true);
+        setHostname.invoke(socket, uriHost);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    }
+
+    @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
+      if (setNpnProtocols == null) return;
+      if (!openSslSocketClass.isInstance(socket)) return;
+      try {
+        Object[] parameters = { concatLengthPrefixed(npnProtocols) };
+        setNpnProtocols.invoke(socket, parameters);
       } catch (IllegalAccessException e) {
         throw new AssertionError(e);
       } catch (InvocationTargetException e) {
@@ -305,12 +269,13 @@
       }
     }
 
-    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
-      if (!openSslSocketClass.isInstance(socket)) {
-        return null;
-      }
+    @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
+      if (getNpnSelectedProtocol == null) return null;
+      if (!openSslSocketClass.isInstance(socket)) return null;
       try {
-        return (byte[]) getNpnSelectedProtocol.invoke(socket);
+        byte[] npnResult = (byte[]) getNpnSelectedProtocol.invoke(socket);
+        if (npnResult == null) return null;
+        return ByteString.of(npnResult);
       } catch (InvocationTargetException e) {
         throw new RuntimeException(e);
       } catch (IllegalAccessException e) {
@@ -320,35 +285,29 @@
   }
 
   /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
-  private static class JdkWithJettyNpnPlatform extends Java6 {
+  private static class JdkWithJettyNpnPlatform extends Platform {
     private final Method getMethod;
     private final Method putMethod;
     private final Class<?> clientProviderClass;
     private final Class<?> serverProviderClass;
 
-    public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
-        Class<?> clientProviderClass, Class<?> serverProviderClass) {
-      super(getMtu);
+    public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
+        Class<?> serverProviderClass) {
       this.putMethod = putMethod;
       this.getMethod = getMethod;
       this.clientProviderClass = clientProviderClass;
       this.serverProviderClass = serverProviderClass;
     }
 
-    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+    @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
       try {
-        List<String> strings = new ArrayList<String>();
-        for (int i = 0; i < npnProtocols.length; ) {
-          int length = npnProtocols[i++];
-          strings.add(new String(npnProtocols, i, length, "US-ASCII"));
-          i += length;
+        List<String> names = new ArrayList<String>(npnProtocols.size());
+        for (int i = 0, size = npnProtocols.size(); i < size; i++) {
+          names.add(npnProtocols.get(i).name.utf8());
         }
         Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
-            new Class[] {clientProviderClass, serverProviderClass},
-            new JettyNpnProvider(strings));
+            new Class[] { clientProviderClass, serverProviderClass }, new JettyNpnProvider(names));
         putMethod.invoke(null, socket, provider);
-      } catch (UnsupportedEncodingException e) {
-        throw new AssertionError(e);
       } catch (InvocationTargetException e) {
         throw new AssertionError(e);
       } catch (IllegalAccessException e) {
@@ -356,19 +315,17 @@
       }
     }
 
-    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+    @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
       try {
         JettyNpnProvider provider =
             (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
         if (!provider.unsupported && provider.selected == null) {
           Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
           logger.log(Level.INFO,
-              "NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
+              "NPN callback dropped so SPDY is disabled. Is npn-boot on the boot class path?");
           return null;
         }
-        return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
-      } catch (UnsupportedEncodingException e) {
-        throw new AssertionError();
+        return provider.unsupported ? null : ByteString.encodeUtf8(provider.selected);
       } catch (InvocationTargetException e) {
         throw new AssertionError();
       } catch (IllegalAccessException e) {
@@ -382,8 +339,11 @@
    * without a compile-time dependency on those interfaces.
    */
   private static class JettyNpnProvider implements InvocationHandler {
+    /** This peer's supported protocols. */
     private final List<String> protocols;
+    /** Set when remote peer notifies NPN is unsupported. */
     private boolean unsupported;
+    /** The protocol the client selected. */
     private String selected;
 
     public JettyNpnProvider(List<String> protocols) {
@@ -397,26 +357,53 @@
         args = Util.EMPTY_STRING_ARRAY;
       }
       if (methodName.equals("supports") && boolean.class == returnType) {
-        return true;
+        return true; // Client supports NPN.
       } else if (methodName.equals("unsupported") && void.class == returnType) {
-        this.unsupported = true;
+        this.unsupported = true; // Remote peer doesn't support NPN.
         return null;
       } else if (methodName.equals("protocols") && args.length == 0) {
-        return protocols;
-      } else if (methodName.equals("selectProtocol")
+        return protocols; // Server advertises these protocols.
+      } else if (methodName.equals("selectProtocol") // Called when client.
           && String.class == returnType
           && args.length == 1
           && (args[0] == null || args[0] instanceof List)) {
-        // TODO: use OpenSSL's algorithm which uses both lists
-        List<?> serverProtocols = (List) args[0];
-        this.selected = protocols.get(0);
-        return selected;
+        List<String> serverProtocols = (List) args[0];
+        // Pick the first protocol the server advertises and client knows.
+        for (int i = 0, size = serverProtocols.size(); i < size; i++) {
+          if (protocols.contains(serverProtocols.get(i))) {
+            return selected = serverProtocols.get(i);
+          }
+        }
+        // On no intersection, try client's first protocol.
+        return selected = protocols.get(0);
       } else if (methodName.equals("protocolSelected") && args.length == 1) {
-        this.selected = (String) args[0];
+        this.selected = (String) args[0]; // Client selected this protocol.
         return null;
       } else {
         return method.invoke(this, args);
       }
     }
   }
+
+  /**
+   * Concatenation of 8-bit, length prefixed protocol names.
+   *
+   * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
+   */
+  static byte[] concatLengthPrefixed(List<Protocol> protocols) {
+    int size = 0;
+    for (Protocol protocol : protocols) {
+      size += protocol.name.size() + 1; // add a byte for 8-bit length prefix.
+    }
+    byte[] result = new byte[size];
+    int pos = 0;
+    for (Protocol protocol : protocols) {
+      int nameSize = protocol.name.size();
+      result[pos++] = (byte) nameSize;
+      // toByteArray allocates an array, but this is only called on new connections.
+      System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize);
+      pos += nameSize;
+    }
+    return result;
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
deleted file mode 100644
index 74af6fd..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
+++ /dev/null
@@ -1,207 +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;
-
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-
-/**
- * Buffers input from an {@link InputStream} for reading lines.
- *
- * <p>This class is used for buffered reading of lines. For purposes of this class, a line ends with
- * "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
- * end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
- * to detect it after catching the {@code EOFException}.
- *
- * <p>This class is intended for reading input that strictly consists of lines, such as line-based
- * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
- * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
- * end-of-input reporting and a more restrictive definition of a line.
- *
- * <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
- * and 10, respectively, and the representation of no other character contains these values.
- * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
- * The default charset is US_ASCII.
- */
-public class StrictLineReader implements Closeable {
-  private static final byte CR = (byte) '\r';
-  private static final byte LF = (byte) '\n';
-
-  private final InputStream in;
-  private final Charset charset;
-
-  /*
-   * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
-   * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
-   * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
-   * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
-   */
-  private byte[] buf;
-  private int pos;
-  private int end;
-
-  /**
-   * Constructs a new {@code LineReader} with the specified charset and the default capacity.
-   *
-   * @param in the {@code InputStream} to read data from.
-   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
-   *     supported.
-   * @throws NullPointerException if {@code in} or {@code charset} is null.
-   * @throws IllegalArgumentException if the specified charset is not supported.
-   */
-  public StrictLineReader(InputStream in, Charset charset) {
-    this(in, 8192, charset);
-  }
-
-  /**
-   * Constructs a new {@code LineReader} with the specified capacity and charset.
-   *
-   * @param in the {@code InputStream} to read data from.
-   * @param capacity the capacity of the buffer.
-   * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
-   *     supported.
-   * @throws NullPointerException if {@code in} or {@code charset} is null.
-   * @throws IllegalArgumentException if {@code capacity} is negative or zero
-   *     or the specified charset is not supported.
-   */
-  public StrictLineReader(InputStream in, int capacity, Charset charset) {
-    if (in == null || charset == null) {
-      throw new NullPointerException();
-    }
-    if (capacity < 0) {
-      throw new IllegalArgumentException("capacity <= 0");
-    }
-    if (!(charset.equals(Util.US_ASCII))) {
-      throw new IllegalArgumentException("Unsupported encoding");
-    }
-
-    this.in = in;
-    this.charset = charset;
-    buf = new byte[capacity];
-  }
-
-  /**
-   * Closes the reader by closing the underlying {@code InputStream} and
-   * marking this reader as closed.
-   *
-   * @throws IOException for errors when closing the underlying {@code InputStream}.
-   */
-  public void close() throws IOException {
-    synchronized (in) {
-      if (buf != null) {
-        buf = null;
-        in.close();
-      }
-    }
-  }
-
-  /**
-   * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
-   * this end of line marker is not included in the result.
-   *
-   * @return the next line from the input.
-   * @throws IOException for underlying {@code InputStream} errors.
-   * @throws EOFException for the end of source stream.
-   */
-  public String readLine() throws IOException {
-    synchronized (in) {
-      if (buf == null) {
-        throw new IOException("LineReader is closed");
-      }
-
-      // Read more data if we are at the end of the buffered data.
-      // Though it's an error to read after an exception, we will let {@code fillBuf()}
-      // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
-      if (pos >= end) {
-        fillBuf();
-      }
-      // Try to find LF in the buffered data and return the line if successful.
-      for (int i = pos; i != end; ++i) {
-        if (buf[i] == LF) {
-          int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
-          String res = new String(buf, pos, lineEnd - pos, charset.name());
-          pos = i + 1;
-          return res;
-        }
-      }
-
-      // Let's anticipate up to 80 characters on top of those already read.
-      ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
-        @Override public String toString() {
-          int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
-          try {
-            return new String(buf, 0, length, charset.name());
-          } catch (UnsupportedEncodingException e) {
-            throw new AssertionError(e); // Since we control the charset this will never happen.
-          }
-        }
-      };
-
-      while (true) {
-        out.write(buf, pos, end - pos);
-        // Mark unterminated line in case fillBuf throws EOFException or IOException.
-        end = -1;
-        fillBuf();
-        // Try to find LF in the buffered data and return the line if successful.
-        for (int i = pos; i != end; ++i) {
-          if (buf[i] == LF) {
-            if (i != pos) {
-              out.write(buf, pos, i - pos);
-            }
-            pos = i + 1;
-            return out.toString();
-          }
-        }
-      }
-    }
-  }
-
-  /**
-   * Read an {@code int} from a line containing its decimal representation.
-   *
-   * @return the value of the {@code int} from the next line.
-   * @throws IOException for underlying {@code InputStream} errors or conversion error.
-   * @throws EOFException for the end of source stream.
-   */
-  public int readInt() throws IOException {
-    String intString = readLine();
-    try {
-      return Integer.parseInt(intString);
-    } catch (NumberFormatException e) {
-      throw new IOException("expected an int but was \"" + intString + "\"");
-    }
-  }
-
-  /**
-   * Reads new input data into the buffer. Call only with pos == end or end == -1,
-   * depending on the desired outcome if the function throws.
-   */
-  private void fillBuf() throws IOException {
-    int result = in.read(buf, 0, buf.length);
-    if (result == -1) {
-      throw new EOFException();
-    }
-    pos = 0;
-    end = result;
-  }
-}
-
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
new file mode 100644
index 0000000..51e04e8
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -0,0 +1,289 @@
+/*
+ * 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;
+
+import com.squareup.okhttp.internal.spdy.Header;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Source;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+/** Junk drawer of utility methods. */
+public final class Util {
+  public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+  public static final String[] EMPTY_STRING_ARRAY = new String[0];
+  public static final InputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
+
+  /** A cheap and type-safe constant for the US-ASCII Charset. */
+  public static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+  /** A cheap and type-safe constant for the UTF-8 Charset. */
+  public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private Util() {
+  }
+
+  public static int getEffectivePort(URI uri) {
+    return getEffectivePort(uri.getScheme(), uri.getPort());
+  }
+
+  public static int getEffectivePort(URL url) {
+    return getEffectivePort(url.getProtocol(), url.getPort());
+  }
+
+  private static int getEffectivePort(String scheme, int specifiedPort) {
+    return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
+  }
+
+  public static int getDefaultPort(String protocol) {
+    if ("http".equals(protocol)) return 80;
+    if ("https".equals(protocol)) return 443;
+    return -1;
+  }
+
+  public static void checkOffsetAndCount(long arrayLength, long offset, long count) {
+    if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
+      throw new ArrayIndexOutOfBoundsException();
+    }
+  }
+
+  /** Returns true if two possibly-null objects are equal. */
+  public static boolean equal(Object a, Object b) {
+    return a == b || (a != null && a.equals(b));
+  }
+
+  /**
+   * Closes {@code closeable}, ignoring any checked exceptions. Does nothing
+   * if {@code closeable} is null.
+   */
+  public static void closeQuietly(Closeable closeable) {
+    if (closeable != null) {
+      try {
+        closeable.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  /**
+   * Closes {@code socket}, ignoring any checked exceptions. Does nothing if
+   * {@code socket} is null.
+   */
+  public static void closeQuietly(Socket socket) {
+    if (socket != null) {
+      try {
+        socket.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  /**
+   * Closes {@code serverSocket}, ignoring any checked exceptions. Does nothing if
+   * {@code serverSocket} is null.
+   */
+  public static void closeQuietly(ServerSocket serverSocket) {
+    if (serverSocket != null) {
+      try {
+        serverSocket.close();
+      } catch (RuntimeException rethrown) {
+        throw rethrown;
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  /**
+   * Closes {@code a} and {@code b}. If either close fails, this completes
+   * the other close and rethrows the first encountered exception.
+   */
+  public static void closeAll(Closeable a, Closeable b) throws IOException {
+    Throwable thrown = null;
+    try {
+      a.close();
+    } catch (Throwable e) {
+      thrown = e;
+    }
+    try {
+      b.close();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+    if (thrown == null) return;
+    if (thrown instanceof IOException) throw (IOException) thrown;
+    if (thrown instanceof RuntimeException) throw (RuntimeException) thrown;
+    if (thrown instanceof Error) throw (Error) thrown;
+    throw new AssertionError(thrown);
+  }
+
+  /**
+   * Deletes the contents of {@code dir}. Throws an IOException if any file
+   * could not be deleted, or if {@code dir} is not a readable directory.
+   */
+  public static void deleteContents(File dir) throws IOException {
+    File[] files = dir.listFiles();
+    if (files == null) {
+      throw new IOException("not a readable directory: " + dir);
+    }
+    for (File file : files) {
+      if (file.isDirectory()) {
+        deleteContents(file);
+      }
+      if (!file.delete()) {
+        throw new IOException("failed to delete file: " + file);
+      }
+    }
+  }
+
+  /**
+   * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
+   */
+  public static void readFully(InputStream in, byte[] dst) throws IOException {
+    readFully(in, dst, 0, dst.length);
+  }
+
+  /**
+   * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
+   * EOFException if insufficient bytes are available.
+   *
+   * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
+   */
+  public static void readFully(InputStream in, byte[] dst, int offset, int byteCount)
+      throws IOException {
+    if (byteCount == 0) {
+      return;
+    }
+    if (in == null) {
+      throw new NullPointerException("in == null");
+    }
+    if (dst == null) {
+      throw new NullPointerException("dst == null");
+    }
+    checkOffsetAndCount(dst.length, offset, byteCount);
+    while (byteCount > 0) {
+      int bytesRead = in.read(dst, offset, byteCount);
+      if (bytesRead < 0) {
+        throw new EOFException();
+      }
+      offset += bytesRead;
+      byteCount -= bytesRead;
+    }
+  }
+
+  /** Returns the remainder of 'source' as a buffer, closing it when done. */
+  public static OkBuffer readFully(Source source) throws IOException {
+    OkBuffer result = new OkBuffer();
+    while (source.read(result, 2048) != -1) {
+    }
+    source.close();
+    return result;
+  }
+
+  /** Reads until {@code in} is exhausted or the timeout has elapsed. */
+  public static boolean skipAll(Source in, int timeoutMillis) throws IOException {
+    // TODO: Implement deadlines everywhere so they can do this work.
+    long startNanos = System.nanoTime();
+    OkBuffer skipBuffer = new OkBuffer();
+    while (NANOSECONDS.toMillis(System.nanoTime() - startNanos) < timeoutMillis) {
+      long read = in.read(skipBuffer, 2048);
+      if (read == -1) return true; // Successfully exhausted the stream.
+      skipBuffer.clear();
+    }
+    return false; // Ran out of time.
+  }
+
+  /**
+   * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+   * Returns the total number of bytes transferred.
+   */
+  public static int copy(InputStream in, OutputStream out) throws IOException {
+    int total = 0;
+    byte[] buffer = new byte[8192];
+    int c;
+    while ((c = in.read(buffer)) != -1) {
+      total += c;
+      out.write(buffer, 0, c);
+    }
+    return total;
+  }
+
+  /** Returns a 32 character string containing a hash of {@code s}. */
+  public static String hash(String s) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(s.getBytes("UTF-8"));
+      return ByteString.of(md5bytes).hex();
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  /** Returns an immutable copy of {@code list}. */
+  public static <T> List<T> immutableList(List<T> list) {
+    return Collections.unmodifiableList(new ArrayList<T>(list));
+  }
+
+  /** Returns an immutable list containing {@code elements}. */
+  public static <T> List<T> immutableList(T... elements) {
+    return Collections.unmodifiableList(Arrays.asList(elements.clone()));
+  }
+
+  public static ThreadFactory threadFactory(final String name, final boolean daemon) {
+    return new ThreadFactory() {
+      @Override public Thread newThread(Runnable runnable) {
+        Thread result = new Thread(runnable, name);
+        result.setDaemon(daemon);
+        return result;
+      }
+    };
+  }
+
+  public static List<Header> headerEntries(String... elements) {
+    List<Header> result = new ArrayList<Header>(elements.length / 2);
+    for (int i = 0; i < elements.length; i += 2) {
+      result.add(new Header(elements[i], elements[i + 1]));
+    }
+    return result;
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
deleted file mode 100644
index a5d39b3..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2010 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.internal.Util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.CacheRequest;
-
-/**
- * An input stream for the body of an HTTP response.
- *
- * <p>Since a single socket's input stream may be used to read multiple HTTP
- * responses from the same server, subclasses shouldn't close the socket stream.
- *
- * <p>A side effect of reading an HTTP response is that the response cache
- * is populated. If the stream is closed early, that cache entry will be
- * invalidated.
- */
-abstract class AbstractHttpInputStream extends InputStream {
-  protected final InputStream in;
-  protected final HttpEngine httpEngine;
-  private final CacheRequest cacheRequest;
-  private final OutputStream cacheBody;
-  protected boolean closed;
-
-  AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, CacheRequest cacheRequest)
-      throws IOException {
-    this.in = in;
-    this.httpEngine = httpEngine;
-
-    OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
-
-    // some apps return a null body; for compatibility we treat that like a null cache request
-    if (cacheBody == null) {
-      cacheRequest = null;
-    }
-
-    this.cacheBody = cacheBody;
-    this.cacheRequest = cacheRequest;
-  }
-
-  /**
-   * read() is implemented using read(byte[], int, int) so subclasses only
-   * need to override the latter.
-   */
-  @Override public final int read() throws IOException {
-    return Util.readSingleByte(this);
-  }
-
-  protected final void checkNotClosed() throws IOException {
-    if (closed) {
-      throw new IOException("stream closed");
-    }
-  }
-
-  protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException {
-    if (cacheBody != null) {
-      cacheBody.write(buffer, offset, count);
-    }
-  }
-
-  /**
-   * 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() throws IOException {
-    if (cacheRequest != null) {
-      cacheBody.close();
-    }
-    httpEngine.release(false);
-  }
-
-  /**
-   * 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() {
-    if (cacheRequest != null) {
-      cacheRequest.abort();
-    }
-    httpEngine.release(true);
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
new file mode 100644
index 0000000..75e13d9
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
@@ -0,0 +1,321 @@
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.CacheControl;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.Date;
+
+import static com.squareup.okhttp.internal.Util.EMPTY_INPUT_STREAM;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Given a request and cached response, this figures out whether to use the
+ * network, the cache, or both.
+ *
+ * <p>Selecting a cache strategy may add conditions to the request (like the
+ * "If-Modified-Since" header for conditional GETs) or warnings to the cached
+ * response (if the cached data is potentially stale).
+ */
+public final class CacheStrategy {
+  private static final Response.Body EMPTY_BODY = new Response.Body() {
+    @Override public boolean ready() throws IOException {
+      return true;
+    }
+    @Override public MediaType contentType() {
+      return null;
+    }
+    @Override public long contentLength() {
+      return 0;
+    }
+    @Override public InputStream byteStream() {
+      return EMPTY_INPUT_STREAM;
+    }
+  };
+
+  private static final StatusLine GATEWAY_TIMEOUT_STATUS_LINE;
+  static {
+    try {
+      GATEWAY_TIMEOUT_STATUS_LINE = new StatusLine("HTTP/1.1 504 Gateway Timeout");
+    } catch (IOException e) {
+      throw new AssertionError();
+    }
+  }
+
+  /** The request to send on the network, or null if this call doesn't use the network. */
+  public final Request networkRequest;
+
+  /** The cached response to return or validate; or null if this call doesn't use a cache. */
+  public final Response cacheResponse;
+
+  public final ResponseSource source;
+
+  private CacheStrategy(
+      Request networkRequest, Response cacheResponse, ResponseSource source) {
+    this.networkRequest = networkRequest;
+    this.cacheResponse = cacheResponse;
+    this.source = source;
+  }
+
+  /**
+   * Returns true if this response can be stored to later serve another
+   * request.
+   */
+  public static boolean isCacheable(Response response, Request request) {
+    // Always go to network for uncacheable response codes (RFC 2616, 13.4),
+    // This implementation doesn't support caching partial content.
+    int responseCode = response.code();
+    if (responseCode != HttpURLConnection.HTTP_OK
+        && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
+        && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
+        && responseCode != HttpURLConnection.HTTP_MOVED_PERM
+        && responseCode != HttpURLConnection.HTTP_GONE) {
+      return false;
+    }
+
+    // Responses to authorized requests aren't cacheable unless they include
+    // a 'public', 'must-revalidate' or 's-maxage' directive.
+    CacheControl responseCaching = response.cacheControl();
+    if (request.header("Authorization") != null
+        && !responseCaching.isPublic()
+        && !responseCaching.mustRevalidate()
+        && responseCaching.sMaxAgeSeconds() == -1) {
+      return false;
+    }
+
+    if (responseCaching.noStore()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public static class Factory {
+    final long nowMillis;
+    final Request request;
+    final Response cacheResponse;
+
+    /** The server's time when the cached response was served, if known. */
+    private Date servedDate;
+    private String servedDateString;
+
+    /** The last modified date of the cached response, if known. */
+    private Date lastModified;
+    private String lastModifiedString;
+
+    /**
+     * The expiration date of the cached response, if known. If both this field
+     * and the max age are set, the max age is preferred.
+     */
+    private Date expires;
+
+    /**
+     * Extension header set by OkHttp specifying the timestamp when the cached
+     * HTTP request was first initiated.
+     */
+    private long sentRequestMillis;
+
+    /**
+     * Extension header set by OkHttp specifying the timestamp when the cached
+     * HTTP response was first received.
+     */
+    private long receivedResponseMillis;
+
+    /** Etag of the cached response. */
+    private String etag;
+
+    /** Age of the cached response. */
+    private int ageSeconds = -1;
+
+    public Factory(long nowMillis, Request request, Response cacheResponse) {
+      this.nowMillis = nowMillis;
+      this.request = request;
+      this.cacheResponse = cacheResponse;
+
+      if (cacheResponse != null) {
+        for (int i = 0; i < cacheResponse.headers().size(); i++) {
+          String fieldName = cacheResponse.headers().name(i);
+          String value = cacheResponse.headers().value(i);
+          if ("Date".equalsIgnoreCase(fieldName)) {
+            servedDate = HttpDate.parse(value);
+            servedDateString = value;
+          } else if ("Expires".equalsIgnoreCase(fieldName)) {
+            expires = HttpDate.parse(value);
+          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
+            lastModified = HttpDate.parse(value);
+            lastModifiedString = value;
+          } else if ("ETag".equalsIgnoreCase(fieldName)) {
+            etag = value;
+          } else if ("Age".equalsIgnoreCase(fieldName)) {
+            ageSeconds = HeaderParser.parseSeconds(value);
+          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
+            sentRequestMillis = Long.parseLong(value);
+          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
+            receivedResponseMillis = Long.parseLong(value);
+          }
+        }
+      }
+    }
+
+    /**
+     * Returns a strategy to satisfy {@code request} using the a cached response
+     * {@code response}.
+     */
+    public CacheStrategy get() {
+      CacheStrategy candidate = getCandidate();
+
+      if (candidate.source != ResponseSource.CACHE && request.cacheControl().onlyIfCached()) {
+        // We're forbidden from using the network, but the cache is insufficient.
+        Response noneResponse = new Response.Builder()
+            .request(candidate.networkRequest)
+            .statusLine(GATEWAY_TIMEOUT_STATUS_LINE)
+            .setResponseSource(ResponseSource.NONE)
+            .body(EMPTY_BODY)
+            .build();
+        return new CacheStrategy(null, noneResponse, ResponseSource.NONE);
+      }
+
+      return candidate;
+    }
+
+    /** Returns a strategy to use assuming the request can use the network. */
+    private CacheStrategy getCandidate() {
+      // No cached response.
+      if (cacheResponse == null) {
+        return new CacheStrategy(request, null, ResponseSource.NETWORK);
+      }
+
+      // Drop the cached response if it's missing a required handshake.
+      if (request.isHttps() && cacheResponse.handshake() == null) {
+        return new CacheStrategy(request, null, ResponseSource.NETWORK);
+      }
+
+      // If this response shouldn't have been stored, it should never be used
+      // as a response source. This check should be redundant as long as the
+      // persistence store is well-behaved and the rules are constant.
+      if (!isCacheable(cacheResponse, request)) {
+        return new CacheStrategy(request, null, ResponseSource.NETWORK);
+      }
+
+      CacheControl requestCaching = request.cacheControl();
+      if (requestCaching.noCache() || hasConditions(request)) {
+        return new CacheStrategy(request, cacheResponse, ResponseSource.NETWORK);
+      }
+
+      long ageMillis = cacheResponseAge();
+      long freshMillis = computeFreshnessLifetime();
+
+      if (requestCaching.maxAgeSeconds() != -1) {
+        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
+      }
+
+      long minFreshMillis = 0;
+      if (requestCaching.minFreshSeconds() != -1) {
+        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
+      }
+
+      long maxStaleMillis = 0;
+      CacheControl responseCaching = cacheResponse.cacheControl();
+      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
+        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
+      }
+
+      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
+        Response.Builder builder = cacheResponse.newBuilder()
+            .setResponseSource(ResponseSource.CACHE); // Overwrite any stored response source.
+        if (ageMillis + minFreshMillis >= freshMillis) {
+          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
+        }
+        long oneDayMillis = 24 * 60 * 60 * 1000L;
+        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
+          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
+        }
+        return new CacheStrategy(null, builder.build(), ResponseSource.CACHE);
+      }
+
+      Request.Builder conditionalRequestBuilder = request.newBuilder();
+
+      if (lastModified != null) {
+        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
+      } else if (servedDate != null) {
+        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
+      }
+
+      if (etag != null) {
+        conditionalRequestBuilder.header("If-None-Match", etag);
+      }
+
+      Request conditionalRequest = conditionalRequestBuilder.build();
+      return hasConditions(conditionalRequest)
+          ? new CacheStrategy(conditionalRequest, cacheResponse, ResponseSource.CONDITIONAL_CACHE)
+          : new CacheStrategy(conditionalRequest, null, ResponseSource.NETWORK);
+    }
+
+    /**
+     * Returns the number of milliseconds that the response was fresh for,
+     * starting from the served date.
+     */
+    private long computeFreshnessLifetime() {
+      CacheControl responseCaching = cacheResponse.cacheControl();
+      if (responseCaching.maxAgeSeconds() != -1) {
+        return SECONDS.toMillis(responseCaching.maxAgeSeconds());
+      } else if (expires != null) {
+        long servedMillis = servedDate != null
+            ? servedDate.getTime()
+            : receivedResponseMillis;
+        long delta = expires.getTime() - servedMillis;
+        return delta > 0 ? delta : 0;
+      } else if (lastModified != null
+          && cacheResponse.request().url().getQuery() == null) {
+        // As recommended by the HTTP RFC and implemented in Firefox, the
+        // max age of a document should be defaulted to 10% of the
+        // document's age at the time it was served. Default expiration
+        // dates aren't used for URIs containing a query.
+        long servedMillis = servedDate != null
+            ? servedDate.getTime()
+            : sentRequestMillis;
+        long delta = servedMillis - lastModified.getTime();
+        return delta > 0 ? (delta / 10) : 0;
+      }
+      return 0;
+    }
+
+    /**
+     * Returns the current age of the response, in milliseconds. The calculation
+     * is specified by RFC 2616, 13.2.3 Age Calculations.
+     */
+    private long cacheResponseAge() {
+      long apparentReceivedAge = servedDate != null
+          ? Math.max(0, receivedResponseMillis - servedDate.getTime())
+          : 0;
+      long receivedAge = ageSeconds != -1
+          ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
+          : apparentReceivedAge;
+      long responseDuration = receivedResponseMillis - sentRequestMillis;
+      long residentDuration = nowMillis - receivedResponseMillis;
+      return receivedAge + responseDuration + residentDuration;
+    }
+
+    /**
+     * Returns true if computeFreshnessLifetime used a heuristic. If we used a
+     * heuristic to serve a cached response older than 24 hours, we are required
+     * to attach a warning.
+     */
+    private boolean isFreshnessLifetimeHeuristic() {
+      return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
+    }
+
+    /**
+     * Returns true if the request contains conditions that save the server from
+     * sending a response that the client has locally. When a request is enqueued
+     * with its own conditions, the built-in response cache won't be used.
+     */
+    private static boolean hasConditions(Request request) {
+      return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java
new file mode 100644
index 0000000..fedf115
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java
@@ -0,0 +1,292 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Handshake;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Implement an HTTPS connection by delegating to an HTTP connection for
+ * everything but the HTTPS-specific stuff.
+ */
+abstract class DelegatingHttpsURLConnection extends HttpsURLConnection {
+  private final HttpURLConnection delegate;
+
+  public DelegatingHttpsURLConnection(HttpURLConnection delegate) {
+    super(delegate.getURL());
+    this.delegate = delegate;
+  }
+
+  protected abstract Handshake handshake();
+
+  @Override public abstract void setHostnameVerifier(HostnameVerifier hostnameVerifier);
+
+  @Override public abstract HostnameVerifier getHostnameVerifier();
+
+  @Override public abstract void setSSLSocketFactory(SSLSocketFactory sslSocketFactory);
+
+  @Override public abstract SSLSocketFactory getSSLSocketFactory();
+
+  @Override public String getCipherSuite() {
+    Handshake handshake = handshake();
+    return handshake != null ? handshake.cipherSuite() : null;
+  }
+
+  @Override public Certificate[] getLocalCertificates() {
+    Handshake handshake = handshake();
+    if (handshake == null) return null;
+    List<Certificate> result = handshake.localCertificates();
+    return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null;
+  }
+
+  @Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
+    Handshake handshake = handshake();
+    if (handshake == null) return null;
+    List<Certificate> result = handshake.peerCertificates();
+    return !result.isEmpty() ? result.toArray(new Certificate[result.size()]) : null;
+  }
+
+  @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    Handshake handshake = handshake();
+    return handshake != null ? handshake.peerPrincipal() : null;
+  }
+
+  @Override public Principal getLocalPrincipal() {
+    Handshake handshake = handshake();
+    return handshake != null ? handshake.localPrincipal() : null;
+  }
+
+  @Override public void connect() throws IOException {
+    connected = true;
+    delegate.connect();
+  }
+
+  @Override public void disconnect() {
+    delegate.disconnect();
+  }
+
+  @Override public InputStream getErrorStream() {
+    return delegate.getErrorStream();
+  }
+
+  @Override public String getRequestMethod() {
+    return delegate.getRequestMethod();
+  }
+
+  @Override public int getResponseCode() throws IOException {
+    return delegate.getResponseCode();
+  }
+
+  @Override public String getResponseMessage() throws IOException {
+    return delegate.getResponseMessage();
+  }
+
+  @Override public void setRequestMethod(String method) throws ProtocolException {
+    delegate.setRequestMethod(method);
+  }
+
+  @Override public boolean usingProxy() {
+    return delegate.usingProxy();
+  }
+
+  @Override public boolean getInstanceFollowRedirects() {
+    return delegate.getInstanceFollowRedirects();
+  }
+
+  @Override public void setInstanceFollowRedirects(boolean followRedirects) {
+    delegate.setInstanceFollowRedirects(followRedirects);
+  }
+
+  @Override public boolean getAllowUserInteraction() {
+    return delegate.getAllowUserInteraction();
+  }
+
+  @Override public Object getContent() throws IOException {
+    return delegate.getContent();
+  }
+
+  @SuppressWarnings("unchecked") // Spec does not generify
+  @Override public Object getContent(Class[] types) throws IOException {
+    return delegate.getContent(types);
+  }
+
+  @Override public String getContentEncoding() {
+    return delegate.getContentEncoding();
+  }
+
+  @Override public int getContentLength() {
+    return delegate.getContentLength();
+  }
+
+  @Override public String getContentType() {
+    return delegate.getContentType();
+  }
+
+  @Override public long getDate() {
+    return delegate.getDate();
+  }
+
+  @Override public boolean getDefaultUseCaches() {
+    return delegate.getDefaultUseCaches();
+  }
+
+  @Override public boolean getDoInput() {
+    return delegate.getDoInput();
+  }
+
+  @Override public boolean getDoOutput() {
+    return delegate.getDoOutput();
+  }
+
+  @Override public long getExpiration() {
+    return delegate.getExpiration();
+  }
+
+  @Override public String getHeaderField(int pos) {
+    return delegate.getHeaderField(pos);
+  }
+
+  @Override public Map<String, List<String>> getHeaderFields() {
+    return delegate.getHeaderFields();
+  }
+
+  @Override public Map<String, List<String>> getRequestProperties() {
+    return delegate.getRequestProperties();
+  }
+
+  @Override public void addRequestProperty(String field, String newValue) {
+    delegate.addRequestProperty(field, newValue);
+  }
+
+  @Override public String getHeaderField(String key) {
+    return delegate.getHeaderField(key);
+  }
+
+  @Override public long getHeaderFieldDate(String field, long defaultValue) {
+    return delegate.getHeaderFieldDate(field, defaultValue);
+  }
+
+  @Override public int getHeaderFieldInt(String field, int defaultValue) {
+    return delegate.getHeaderFieldInt(field, defaultValue);
+  }
+
+  @Override public String getHeaderFieldKey(int position) {
+    return delegate.getHeaderFieldKey(position);
+  }
+
+  @Override public long getIfModifiedSince() {
+    return delegate.getIfModifiedSince();
+  }
+
+  @Override public InputStream getInputStream() throws IOException {
+    return delegate.getInputStream();
+  }
+
+  @Override public long getLastModified() {
+    return delegate.getLastModified();
+  }
+
+  @Override public OutputStream getOutputStream() throws IOException {
+    return delegate.getOutputStream();
+  }
+
+  @Override public Permission getPermission() throws IOException {
+    return delegate.getPermission();
+  }
+
+  @Override public String getRequestProperty(String field) {
+    return delegate.getRequestProperty(field);
+  }
+
+  @Override public URL getURL() {
+    return delegate.getURL();
+  }
+
+  @Override public boolean getUseCaches() {
+    return delegate.getUseCaches();
+  }
+
+  @Override public void setAllowUserInteraction(boolean newValue) {
+    delegate.setAllowUserInteraction(newValue);
+  }
+
+  @Override public void setDefaultUseCaches(boolean newValue) {
+    delegate.setDefaultUseCaches(newValue);
+  }
+
+  @Override public void setDoInput(boolean newValue) {
+    delegate.setDoInput(newValue);
+  }
+
+  @Override public void setDoOutput(boolean newValue) {
+    delegate.setDoOutput(newValue);
+  }
+
+  @Override public void setIfModifiedSince(long newValue) {
+    delegate.setIfModifiedSince(newValue);
+  }
+
+  @Override public void setRequestProperty(String field, String newValue) {
+    delegate.setRequestProperty(field, newValue);
+  }
+
+  @Override public void setUseCaches(boolean newValue) {
+    delegate.setUseCaches(newValue);
+  }
+
+  @Override public void setConnectTimeout(int timeoutMillis) {
+    delegate.setConnectTimeout(timeoutMillis);
+  }
+
+  @Override public int getConnectTimeout() {
+    return delegate.getConnectTimeout();
+  }
+
+  @Override public void setReadTimeout(int timeoutMillis) {
+    delegate.setReadTimeout(timeoutMillis);
+  }
+
+  @Override public int getReadTimeout() {
+    return delegate.getReadTimeout();
+  }
+
+  @Override public String toString() {
+    return delegate.toString();
+  }
+
+  @Override public void setFixedLengthStreamingMode(int contentLength) {
+    delegate.setFixedLengthStreamingMode(contentLength);
+  }
+
+  @Override public void setChunkedStreamingMode(int chunkLength) {
+    delegate.setChunkedStreamingMode(chunkLength);
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
index 12e6409..e9af130 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
@@ -16,50 +16,7 @@
 
 package com.squareup.okhttp.internal.http;
 
-final class HeaderParser {
-
-  public interface CacheControlHandler {
-    void handle(String directive, String parameter);
-  }
-
-  /** Parse a comma-separated list of cache control header values. */
-  public static void parseCacheControl(String value, CacheControlHandler handler) {
-    int pos = 0;
-    while (pos < value.length()) {
-      int tokenStart = pos;
-      pos = skipUntil(value, pos, "=,");
-      String directive = value.substring(tokenStart, pos).trim();
-
-      if (pos == value.length() || value.charAt(pos) == ',') {
-        pos++; // consume ',' (if necessary)
-        handler.handle(directive, null);
-        continue;
-      }
-
-      pos++; // consume '='
-      pos = skipWhitespace(value, pos);
-
-      String parameter;
-
-      // quoted string
-      if (pos < value.length() && value.charAt(pos) == '\"') {
-        pos++; // consume '"' open quote
-        int parameterStart = pos;
-        pos = skipUntil(value, pos, "\"");
-        parameter = value.substring(parameterStart, pos);
-        pos++; // consume '"' close quote (if necessary)
-
-        // unquoted string
-      } else {
-        int parameterStart = pos;
-        pos = skipUntil(value, pos, ",");
-        parameter = value.substring(parameterStart, pos).trim();
-      }
-
-      handler.handle(directive, parameter);
-    }
-  }
-
+public final class HeaderParser {
   /**
    * Returns the next index in {@code input} at or after {@code pos} that
    * contains a character from {@code characters}. Returns the input length if
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
index 1ad3689..ce40a92 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
@@ -16,8 +16,11 @@
  */
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.OkAuthenticator;
 import com.squareup.okhttp.OkAuthenticator.Challenge;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
 import java.io.IOException;
 import java.net.Authenticator;
 import java.net.InetAddress;
@@ -38,7 +41,8 @@
   public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
     @Override public Credential authenticate(
         Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
-      for (Challenge challenge : challenges) {
+      for (int i = 0, size = challenges.size(); i < size; i++) {
+        Challenge challenge = challenges.get(i);
         if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
           continue;
         }
@@ -55,7 +59,8 @@
 
     @Override public Credential authenticateProxy(
         Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
-      for (Challenge challenge : challenges) {
+      for (int i = 0, size = challenges.size(); i < size; i++) {
+        Challenge challenge = challenges.get(i);
         if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
           continue;
         }
@@ -84,44 +89,40 @@
 
   /**
    * React to a failed authorization response by looking up new credentials.
-   *
-   * @return true if credentials have been added to successorRequestHeaders
-   *         and another request should be attempted.
+   * Returns a request for a subsequent attempt, or null if no further attempts
+   * should be made.
    */
-  public static boolean processAuthHeader(OkAuthenticator authenticator, int responseCode,
-      RawHeaders responseHeaders, RawHeaders successorRequestHeaders, Proxy proxy, URL url)
-      throws IOException {
+  public static Request processAuthHeader(
+      OkAuthenticator authenticator, Response response, Proxy proxy) throws IOException {
     String responseField;
     String requestField;
-    if (responseCode == HTTP_UNAUTHORIZED) {
+    if (response.code() == HTTP_UNAUTHORIZED) {
       responseField = "WWW-Authenticate";
       requestField = "Authorization";
-    } else if (responseCode == HTTP_PROXY_AUTH) {
+    } else if (response.code() == HTTP_PROXY_AUTH) {
       responseField = "Proxy-Authenticate";
       requestField = "Proxy-Authorization";
     } else {
       throw new IllegalArgumentException(); // TODO: ProtocolException?
     }
-    List<Challenge> challenges = parseChallenges(responseHeaders, responseField);
-    if (challenges.isEmpty()) {
-      return false; // Could not find a challenge so end the request cycle.
-    }
-    Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH
-        ? authenticator.authenticateProxy(proxy, url, challenges)
-        : authenticator.authenticate(proxy, url, challenges);
-    if (credential == null) {
-      return false; // Could not satisfy the challenge so end the request cycle.
-    }
+    List<Challenge> challenges = parseChallenges(response.headers(), responseField);
+    if (challenges.isEmpty()) return null; // Could not find a challenge so end the request cycle.
+
+    Request request = response.request();
+    Credential credential = response.code() == HTTP_PROXY_AUTH
+        ? authenticator.authenticateProxy(proxy, request.url(), challenges)
+        : authenticator.authenticate(proxy, request.url(), challenges);
+    if (credential == null) return null; // Couldn't satisfy the challenge so end the request cycle.
+
     // Add authorization credentials, bypassing the already-connected check.
-    successorRequestHeaders.set(requestField, credential.getHeaderValue());
-    return true;
+    return request.newBuilder().header(requestField, credential.getHeaderValue()).build();
   }
 
   /**
    * Parse RFC 2617 challenges. This API is only interested in the scheme
    * name and realm.
    */
-  private static List<Challenge> parseChallenges(RawHeaders responseHeaders,
+  private static List<Challenge> parseChallenges(Headers responseHeaders,
       String challengeHeader) {
     // auth-scheme = token
     // auth-param  = token "=" ( token | quoted-string )
@@ -129,11 +130,11 @@
     // realm       = "realm" "=" realm-value
     // realm-value = quoted-string
     List<Challenge> result = new ArrayList<Challenge>();
-    for (int h = 0; h < responseHeaders.length(); h++) {
-      if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) {
+    for (int h = 0; h < responseHeaders.size(); h++) {
+      if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(h))) {
         continue;
       }
-      String value = responseHeaders.getValue(h);
+      String value = responseHeaders.value(h);
       int pos = 0;
       while (pos < value.length()) {
         int tokenStart = pos;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
new file mode 100644
index 0000000..b12b12d
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -0,0 +1,581 @@
+/*
+ * 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.Connection;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.ProtocolException;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Deadline;
+import okio.OkBuffer;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+
+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;
+
+/**
+ * A socket connection that can be used to send HTTP/1.1 messages. This class
+ * strictly enforces the following lifecycle:
+ * <ol>
+ *   <li>{@link #writeRequest Send request headers}.
+ *   <li>Open a sink to write the request body. Either {@link
+ *       #newFixedLengthSink fixed-length} or {@link #newChunkedSink chunked}.
+ *   <li>Write to and then close that stream.
+ *   <li>{@link #readResponse Read response headers}.
+ *   <li>Open the HTTP response body input stream. Either {@link
+ *       #newFixedLengthSource fixed-length}, {@link #newChunkedSource chunked}
+ *       or {@link #newUnknownLengthSource unknown length}.
+ *   <li>Read from and close that stream.
+ * </ol>
+ * <p>Exchanges that do not have a request body may skip creating and closing
+ * the request body. Exchanges that do not have a response body must call {@link
+ * #emptyResponseBody}.
+ */
+public final class HttpConnection {
+  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;
+  private static final int STATE_READ_RESPONSE_HEADERS = 3;
+  private static final int STATE_OPEN_RESPONSE_BODY = 4;
+  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;
+  private final BufferedSource source;
+  private final BufferedSink sink;
+
+  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.getInputStream()));
+    this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
+  }
+
+  /**
+   * Configure this connection to put itself back into the connection pool when
+   * the HTTP response body is exhausted.
+   */
+  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.
+      pool.recycle(connection);
+    }
+  }
+
+  /**
+   * Configure this connection to close itself when the HTTP response body is
+   * exhausted.
+   */
+  public void closeOnIdle() throws IOException {
+    onIdle = ON_IDLE_CLOSE;
+
+    // If we're already idle, close immediately.
+    if (state == STATE_IDLE) {
+      state = STATE_CLOSED;
+      connection.close();
+    }
+  }
+
+  /** Returns true if this connection is closed. */
+  public boolean isClosed() {
+    return state == STATE_CLOSED;
+  }
+
+  public void flush() 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);
+    sink.writeUtf8(requestLine).writeUtf8("\r\n");
+    for (int i = 0; i < headers.size(); i ++) {
+      sink.writeUtf8(headers.name(i))
+          .writeUtf8(": ")
+          .writeUtf8(headers.value(i))
+          .writeUtf8("\r\n");
+    }
+    sink.writeUtf8("\r\n");
+    state = STATE_OPEN_REQUEST_BODY;
+  }
+
+  /** Parses bytes of a response header from an HTTP transport. */
+  public Response.Builder readResponse() throws IOException {
+    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
+      throw new IllegalStateException("state: " + state);
+    }
+
+    while (true) {
+      String statusLineString = source.readUtf8LineStrict();
+      StatusLine statusLine = new StatusLine(statusLineString);
+
+      Response.Builder responseBuilder = new Response.Builder()
+          .statusLine(statusLine)
+          .header(OkHeaders.SELECTED_PROTOCOL, Protocol.HTTP_11.name.utf8());
+
+      Headers.Builder headersBuilder = new Headers.Builder();
+      readHeaders(headersBuilder);
+      responseBuilder.headers(headersBuilder.build());
+
+      if (statusLine.code() != HTTP_CONTINUE) {
+        state = STATE_OPEN_RESPONSE_BODY;
+        return responseBuilder;
+      }
+    }
+  }
+
+  /** Reads headers or trailers into {@code builder}. */
+  public void readHeaders(Headers.Builder builder) throws IOException {
+    // parse the result headers until the first blank line
+    for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
+      builder.addLine(line);
+    }
+  }
+
+  /**
+   * Discards the response body so that the connection can be reused and the
+   * cache entry can be completed. This needs to be done judiciously, since it
+   * delays the current request in order to speed up a potential future request
+   * that may never occur.
+   */
+  public boolean discard(Source in, int timeoutMillis) {
+    try {
+      int socketTimeout = socket.getSoTimeout();
+      socket.setSoTimeout(timeoutMillis);
+      try {
+        return Util.skipAll(in, timeoutMillis);
+      } finally {
+        socket.setSoTimeout(socketTimeout);
+      }
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  public Sink newChunkedSink() {
+    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
+    state = STATE_WRITING_REQUEST_BODY;
+    return new ChunkedSink();
+  }
+
+  public Sink newFixedLengthSink(long contentLength) {
+    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
+    state = STATE_WRITING_REQUEST_BODY;
+    return new FixedLengthSink(contentLength);
+  }
+
+  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);
+  }
+
+  public Source newFixedLengthSource(CacheRequest cacheRequest, long length)
+      throws IOException {
+    if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+    state = STATE_READING_RESPONSE_BODY;
+    return new FixedLengthSource(cacheRequest, length);
+  }
+
+  /**
+   * Call this to advance past a response body for HTTP responses that do not
+   * have a response body.
+   */
+  public void emptyResponseBody() throws IOException {
+    newFixedLengthSource(null, 0L); // Transition to STATE_IDLE.
+  }
+
+  public Source newChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine)
+      throws IOException {
+    if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+    state = STATE_READING_RESPONSE_BODY;
+    return new ChunkedSource(cacheRequest, httpEngine);
+  }
+
+  public Source newUnknownLengthSource(CacheRequest cacheRequest) throws IOException {
+    if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+    state = STATE_READING_RESPONSE_BODY;
+    return new UnknownLengthSource(cacheRequest);
+  }
+
+  /** An HTTP body with a fixed length known in advance. */
+  private final class FixedLengthSink implements Sink {
+    private boolean closed;
+    private long bytesRemaining;
+
+    private FixedLengthSink(long bytesRemaining) {
+      this.bytesRemaining = bytesRemaining;
+    }
+
+    @Override public Sink deadline(Deadline deadline) {
+      return this; // TODO: honor deadline.
+    }
+
+    @Override public void write(OkBuffer source, long byteCount) throws IOException {
+      if (closed) throw new IllegalStateException("closed");
+      checkOffsetAndCount(source.size(), 0, byteCount);
+      if (byteCount > bytesRemaining) {
+        throw new ProtocolException("expected " + bytesRemaining
+            + " bytes but received " + byteCount);
+      }
+      sink.write(source, byteCount);
+      bytesRemaining -= byteCount;
+    }
+
+    @Override public void flush() throws IOException {
+      if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
+      sink.flush();
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) return;
+      closed = true;
+      if (bytesRemaining > 0) throw new ProtocolException("unexpected end of stream");
+      state = STATE_READ_RESPONSE_HEADERS;
+    }
+  }
+
+  private static final String CRLF = "\r\n";
+  private static final byte[] HEX_DIGITS = {
+      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+  };
+  private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
+
+  /**
+   * An HTTP body with alternating chunk sizes and chunk bodies. It is the
+   * caller's responsibility to buffer chunks; typically by using a buffered
+   * sink with this sink.
+   */
+  private final class ChunkedSink implements Sink {
+    /** Scratch space for up to 16 hex digits, and then a constant CRLF. */
+    private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' };
+
+    private boolean closed;
+
+    @Override public Sink deadline(Deadline deadline) {
+      return this; // TODO: honor deadline.
+    }
+
+    @Override public void write(OkBuffer source, long byteCount) throws IOException {
+      if (closed) throw new IllegalStateException("closed");
+      if (byteCount == 0) return;
+
+      writeHex(byteCount);
+      sink.write(source, byteCount);
+      sink.writeUtf8(CRLF);
+    }
+
+    @Override public synchronized void flush() throws IOException {
+      if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
+      sink.flush();
+    }
+
+    @Override public synchronized void close() throws IOException {
+      if (closed) return;
+      closed = true;
+      sink.write(FINAL_CHUNK);
+      state = STATE_READ_RESPONSE_HEADERS;
+    }
+
+    /**
+     * Equivalent to, but cheaper than writing Long.toHexString().getBytes()
+     * followed by CRLF.
+     */
+    private void writeHex(long i) throws IOException {
+      int cursor = 16;
+      do {
+        hex[--cursor] = HEX_DIGITS[((int) (i & 0xf))];
+      } while ((i >>>= 4) != 0);
+      sink.write(hex, cursor, hex.length - cursor);
+    }
+  }
+
+  private class AbstractSource {
+    private final CacheRequest cacheRequest;
+    protected final OutputStream cacheBody;
+    protected boolean closed;
+
+    AbstractSource(CacheRequest cacheRequest) throws IOException {
+      // Some apps return a null body; for compatibility we treat that like a null cache request.
+      OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
+      if (cacheBody == null) {
+        cacheRequest = null;
+      }
+
+      this.cacheBody = cacheBody;
+      this.cacheRequest = cacheRequest;
+    }
+
+    /** Copy the last {@code byteCount} bytes of {@code source} to the cache body. */
+    protected final void cacheWrite(OkBuffer source, long byteCount) throws IOException {
+      if (cacheBody != null) {
+        Okio.copy(source, source.size() - byteCount, byteCount, cacheBody);
+      }
+    }
+
+    /**
+     * 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 {
+      if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+
+      if (cacheRequest != null) {
+        cacheBody.close();
+      }
+
+      state = STATE_IDLE;
+      if (recyclable && onIdle == ON_IDLE_POOL) {
+        onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
+        pool.recycle(connection);
+      } else if (onIdle == ON_IDLE_CLOSE) {
+        state = STATE_CLOSED;
+        connection.close();
+      }
+    }
+
+    /**
+     * 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() {
+      if (cacheRequest != null) {
+        cacheRequest.abort();
+      }
+      Util.closeQuietly(connection);
+      state = STATE_CLOSED;
+    }
+  }
+
+  /** An HTTP body with a fixed length specified in advance. */
+  private class FixedLengthSource extends AbstractSource implements Source {
+    private long bytesRemaining;
+
+    public FixedLengthSource(CacheRequest cacheRequest, long length) throws IOException {
+      super(cacheRequest);
+      bytesRemaining = length;
+      if (bytesRemaining == 0) {
+        endOfInput(true);
+      }
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount)
+        throws IOException {
+      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+      if (closed) throw new IllegalStateException("closed");
+      if (bytesRemaining == 0) return -1;
+
+      long read = source.read(sink, Math.min(bytesRemaining, byteCount));
+      if (read == -1) {
+        unexpectedEndOfInput(); // the server didn't supply the promised content length
+        throw new ProtocolException("unexpected end of stream");
+      }
+
+      bytesRemaining -= read;
+      cacheWrite(sink, read);
+      if (bytesRemaining == 0) {
+        endOfInput(true);
+      }
+      return read;
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) return;
+
+      if (bytesRemaining != 0 && !discard(this, DISCARD_STREAM_TIMEOUT_MILLIS)) {
+        unexpectedEndOfInput();
+      }
+
+      closed = true;
+    }
+  }
+
+  /** An HTTP body with alternating chunk sizes and chunk bodies. */
+  private class ChunkedSource extends AbstractSource implements Source {
+    private static final int NO_CHUNK_YET = -1;
+    private int bytesRemainingInChunk = NO_CHUNK_YET;
+    private boolean hasMoreChunks = true;
+    private final HttpEngine httpEngine;
+
+    ChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine) throws IOException {
+      super(cacheRequest);
+      this.httpEngine = httpEngine;
+    }
+
+    @Override public long read(
+        OkBuffer sink, long byteCount) throws IOException {
+      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+      if (closed) throw new IllegalStateException("closed");
+      if (!hasMoreChunks) return -1;
+
+      if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
+        readChunkSize();
+        if (!hasMoreChunks) return -1;
+      }
+
+      long read = source.read(sink, Math.min(byteCount, bytesRemainingInChunk));
+      if (read == -1) {
+        unexpectedEndOfInput(); // the server didn't supply the promised chunk length
+        throw new IOException("unexpected end of stream");
+      }
+      bytesRemainingInChunk -= read;
+      cacheWrite(sink, read);
+      return read;
+    }
+
+    private void readChunkSize() throws IOException {
+      // read the suffix of the previous chunk
+      if (bytesRemainingInChunk != NO_CHUNK_YET) {
+        source.readUtf8LineStrict();
+      }
+      String chunkSizeString = source.readUtf8LineStrict();
+      int index = chunkSizeString.indexOf(";");
+      if (index != -1) {
+        chunkSizeString = chunkSizeString.substring(0, index);
+      }
+      try {
+        bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
+      } catch (NumberFormatException e) {
+        throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
+      }
+      if (bytesRemainingInChunk == 0) {
+        hasMoreChunks = false;
+        Headers.Builder trailersBuilder = new Headers.Builder();
+        readHeaders(trailersBuilder);
+        httpEngine.receiveHeaders(trailersBuilder.build());
+        endOfInput(true);
+      }
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) return;
+      if (hasMoreChunks && !discard(this, DISCARD_STREAM_TIMEOUT_MILLIS)) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+
+  /** An HTTP message body terminated by the end of the underlying stream. */
+  class UnknownLengthSource extends AbstractSource implements Source {
+    private boolean inputExhausted;
+
+    UnknownLengthSource(CacheRequest cacheRequest) throws IOException {
+      super(cacheRequest);
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount)
+        throws IOException {
+      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+      if (closed) throw new IllegalStateException("closed");
+      if (inputExhausted) return -1;
+
+      long read = source.read(sink, byteCount);
+      if (read == -1) {
+        inputExhausted = true;
+        endOfInput(false);
+        return -1;
+      }
+      cacheWrite(sink, read);
+      return read;
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) return;
+      // TODO: discard unknown length streams for best caching?
+      if (!inputExhausted) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
index b4d2c7c..cdb6973 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
@@ -26,7 +26,7 @@
 /**
  * Best-effort parser for HTTP dates.
  */
-final class HttpDate {
+public final class HttpDate {
 
   /**
    * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
@@ -45,13 +45,21 @@
   private static final String[] BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS = new String[] {
       "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036
       "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime()
-      "EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z",
-      "EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z",
-      "EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z",
-      "EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z",
+      "EEE, dd-MMM-yyyy HH:mm:ss z",
+      "EEE, dd-MMM-yyyy HH-mm-ss z",
+      "EEE, dd MMM yy HH:mm:ss z",
+      "EEE dd-MMM-yyyy HH:mm:ss z",
+      "EEE dd MMM yyyy HH:mm:ss z",
+      "EEE dd-MMM-yyyy HH-mm-ss z",
+      "EEE dd-MMM-yy HH:mm:ss z",
+      "EEE dd MMM yy HH:mm:ss z",
+      "EEE,dd-MMM-yy HH:mm:ss z",
+      "EEE,dd-MMM-yyyy HH:mm:ss z",
+      "EEE, dd-MM-yyyy HH:mm:ss z",
 
-            /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
-      "EEE MMM d yyyy HH:mm:ss z", };
+      /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
+      "EEE MMM d yyyy HH:mm:ss z",
+  };
 
   private static final DateFormat[] BROWSER_COMPATIBLE_DATE_FORMATS =
       new DateFormat[BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.length];
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 fee0e01..f00fbe7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -19,38 +19,40 @@
 
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.HostResolver;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
 import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.Route;
 import com.squareup.okhttp.TunnelRequest;
-import com.squareup.okhttp.internal.Dns;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.CacheRequest;
-import java.net.CacheResponse;
 import java.net.CookieHandler;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.net.ProtocolException;
 import java.net.URL;
 import java.net.UnknownHostException;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
+import java.security.cert.CertificateException;
 import java.util.List;
 import java.util.Map;
-import java.util.zip.GZIPInputStream;
 import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSocketFactory;
+import okio.BufferedSink;
+import okio.GzipSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
 
-import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
+import static com.squareup.okhttp.internal.Util.closeQuietly;
 import static com.squareup.okhttp.internal.Util.getDefaultPort;
 import static com.squareup.okhttp.internal.Util.getEffectivePort;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
 
 /**
  * Handles a single HTTP request/response pair. Each HTTP engine follows this
@@ -69,106 +71,108 @@
  *
  * <p>The request and response may be served by the HTTP response cache, by the
  * network, or by both in the event of a conditional GET.
- *
- * <p>This class may hold a socket connection that needs to be released or
- * recycled. By default, this socket connection is held when the last byte of
- * the response is consumed. To release the connection when it is no longer
- * required, use {@link #automaticallyReleaseConnectionToPool()}.
  */
 public class HttpEngine {
-  private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() {
-    @Override public Map<String, List<String>> getHeaders() throws IOException {
-      Map<String, List<String>> result = new HashMap<String, List<String>>();
-      result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout"));
-      return result;
-    }
-    @Override public InputStream getBody() throws IOException {
-      return new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
-    }
-  };
-  public static final int HTTP_CONTINUE = 100;
+  final OkHttpClient client;
 
-  protected final Policy policy;
-  protected final OkHttpClient client;
-
-  protected final String method;
-
-  private ResponseSource responseSource;
-
-  protected Connection connection;
-  protected RouteSelector routeSelector;
-  private OutputStream requestBodyOut;
+  private Connection connection;
+  private RouteSelector routeSelector;
+  private Route route;
+  private final Response priorResponse;
 
   private Transport transport;
 
-  private InputStream responseTransferIn;
-  private InputStream responseBodyIn;
-
-  private CacheResponse cacheResponse;
-  private CacheRequest cacheRequest;
-
   /** The time when the request headers were written, or -1 if they haven't been written yet. */
   long sentRequestMillis = -1;
 
-  /** Whether the connection has been established. */
-  boolean connected;
-
   /**
    * True if this client added an "Accept-Encoding: gzip" header field and is
    * therefore responsible for also decompressing the transfer stream.
    */
   private boolean transparentGzip;
 
-  final URI uri;
+  /**
+   * True if the request body must be completely buffered before transmission;
+   * false if it can be streamed. Buffering has two advantages: we don't need
+   * the content-length in advance and we can retransmit if necessary. The
+   * upside of streaming is that we can save memory.
+   */
+  public final boolean bufferRequestBody;
 
-  final RequestHeaders requestHeaders;
+  /**
+   * The original application-provided request. Never modified by OkHttp. When
+   * follow-up requests are necessary, they are derived from this request.
+   */
+  private final Request userRequest;
+
+  /**
+   * The request to send on the network, or null for no network request. This is
+   * derived from the user request, and customized to support OkHttp features
+   * like compression and caching.
+   */
+  private Request networkRequest;
+
+  /**
+   * The cached response, or null if the cache doesn't exist or cannot be used
+   * for this request. Conditional caching means this may be non-null even when
+   * the network request is non-null. Never modified by OkHttp.
+   */
+  private Response cacheResponse;
+
+  /**
+   * The response read from the network. Null if the network response hasn't
+   * been read yet, or if the network is not used. Never modified by OkHttp.
+   */
+  private Response networkResponse;
+
+  /**
+   * The user-visible response. This is derived from either the network
+   * response, cache response, or both. It is customized to support OkHttp
+   * features like compression and caching.
+   */
+  private Response userResponse;
+
+  private Sink requestBodyOut;
+  private BufferedSink bufferedRequestBody;
+
+  private ResponseSource responseSource;
 
   /** Null until a response is received from the network or the cache. */
-  ResponseHeaders responseHeaders;
+  private Source responseTransferSource;
+  private Source responseBody;
+  private InputStream responseBodyBytes;
 
-  // The cache response currently being validated on a conditional get. Null
-  // if the cached response doesn't exist or doesn't need validation. If the
-  // conditional get succeeds, these will be used for the response headers and
-  // body. If it fails, these be closed and set to null.
-  private ResponseHeaders cachedResponseHeaders;
-  private InputStream cachedResponseBody;
+  /** The cache request currently being populated from a network response. */
+  private CacheRequest storeRequest;
 
   /**
-   * True if the socket connection should be released to the connection pool
-   * when the response has been fully read.
-   */
-  private boolean automaticallyReleaseConnectionToPool;
-
-  /** True if the socket connection is no longer needed by this engine. */
-  private boolean connectionReleased;
-
-  /**
-   * @param requestHeaders the client's supplied request headers. This class
-   *     creates a private copy that it can mutate.
+   * @param request the HTTP request without a body. The body must be
+   *     written via the engine's request body stream.
    * @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 attempt, or null if this request doesn't
+   *     recover from a failure.
    */
-  public HttpEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
-      Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
+  public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
+      Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut,
+      Response priorResponse) {
     this.client = client;
-    this.policy = policy;
-    this.method = method;
+    this.userRequest = request;
+    this.bufferRequestBody = bufferRequestBody;
     this.connection = connection;
+    this.routeSelector = routeSelector;
     this.requestBodyOut = requestBodyOut;
+    this.priorResponse = priorResponse;
 
-    try {
-      uri = Platform.get().toUriLenient(policy.getURL());
-    } catch (URISyntaxException e) {
-      throw new IOException(e.getMessage());
+    if (connection != null) {
+      connection.setOwner(this);
+      this.route = connection.getRoute();
+    } else {
+      this.route = null;
     }
-
-    this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
-  }
-
-  public URI getUri() {
-    return uri;
   }
 
   /**
@@ -177,140 +181,106 @@
    * writing the request body if it exists.
    */
   public final void sendRequest() throws IOException {
-    if (responseSource != null) {
-      return;
-    }
+    if (responseSource != null) return; // Already sent.
+    if (transport != null) throw new IllegalStateException();
 
-    prepareRawRequestHeaders();
-    initResponseSource();
+    Request request = networkRequest(userRequest);
+
     OkResponseCache responseCache = client.getOkResponseCache();
+    Response cacheCandidate = responseCache != null
+        ? responseCache.get(request)
+        : null;
+    long now = System.currentTimeMillis();
+    CacheStrategy cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
+    responseSource = cacheStrategy.source;
+    networkRequest = cacheStrategy.networkRequest;
+    cacheResponse = cacheStrategy.cacheResponse;
+
     if (responseCache != null) {
       responseCache.trackResponse(responseSource);
     }
 
-    // The raw response source may require the network, but the request
-    // headers may forbid network use. In that case, dispose of the network
-    // response and use a GATEWAY_TIMEOUT response instead, as specified
-    // by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
-    if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
-      if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
-        Util.closeQuietly(cachedResponseBody);
+    if (cacheCandidate != null
+        && (responseSource == ResponseSource.NONE || cacheResponse == null)) {
+      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
+    }
+
+    if (networkRequest != null) {
+      // Open a connection unless we inherited one from a redirect.
+      if (connection == null) {
+        connect(networkRequest);
       }
-      this.responseSource = ResponseSource.CACHE;
-      this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
-      RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
-      setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
-    }
 
-    if (responseSource.requiresConnection()) {
-      sendSocketRequest();
-    } else if (connection != null) {
-      client.getConnectionPool().recycle(connection);
-      connection = null;
-    }
-  }
+      // Blow up if we aren't the current owner of the connection.
+      if (connection.getOwner() != this && !connection.isSpdy()) throw new AssertionError();
 
-  /**
-   * Initialize the source for this response. It may be corrected later if the
-   * request headers forbids network use.
-   */
-  private void initResponseSource() throws IOException {
-    responseSource = ResponseSource.NETWORK;
-    if (!policy.getUseCaches()) return;
+      transport = (Transport) connection.newTransport(this);
 
-    OkResponseCache responseCache = client.getOkResponseCache();
-    if (responseCache == null) return;
+      // Create a request body if we don't have one already. We'll already have
+      // one if we're retrying a failed POST.
+      if (hasRequestBody() && requestBodyOut == null) {
+        requestBodyOut = transport.createRequestBody(request);
+      }
 
-    CacheResponse candidate = responseCache.get(
-        uri, method, requestHeaders.getHeaders().toMultimap(false));
-    if (candidate == null) return;
-
-    Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
-    cachedResponseBody = candidate.getBody();
-    if (!acceptCacheResponseType(candidate)
-        || responseHeadersMap == null
-        || cachedResponseBody == null) {
-      Util.closeQuietly(cachedResponseBody);
-      return;
-    }
-
-    RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true);
-    cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
-    long now = System.currentTimeMillis();
-    this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
-    if (responseSource == ResponseSource.CACHE) {
-      this.cacheResponse = candidate;
-      setResponse(cachedResponseHeaders, cachedResponseBody);
-    } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
-      this.cacheResponse = candidate;
-    } else if (responseSource == ResponseSource.NETWORK) {
-      Util.closeQuietly(cachedResponseBody);
     } else {
-      throw new AssertionError();
+      // We're using a cached response. Recycle a connection we may have inherited from a redirect.
+      if (connection != null) {
+        client.getConnectionPool().recycle(connection);
+        connection = null;
+      }
+
+      // No need for the network! Promote the cached response immediately.
+      this.userResponse = cacheResponse.newBuilder()
+          .request(userRequest)
+          .priorResponse(stripBody(priorResponse))
+          .cacheResponse(stripBody(cacheResponse))
+          .build();
+      if (userResponse.body() != null) {
+        initContentStream(userResponse.body().source());
+      }
     }
   }
 
-  private void sendSocketRequest() throws IOException {
-    if (connection == null) {
-      connect();
-    }
-
-    if (transport != null) {
-      throw new IllegalStateException();
-    }
-
-    transport = (Transport) connection.newTransport(this);
-
-    if (hasRequestBody() && requestBodyOut == null) {
-      // Create a request body if we don't have one already. We'll already
-      // have one if we're retrying a failed POST.
-      requestBodyOut = transport.createRequestBody();
-    }
+  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. */
-  protected final void connect() throws IOException {
-    if (connection != null) {
-      return;
-    }
+  private void connect(Request request) throws IOException {
+    if (connection != null) throw new IllegalStateException();
+
     if (routeSelector == null) {
-      String uriHost = uri.getHost();
-      if (uriHost == null) {
-        throw new UnknownHostException(uri.toString());
+      String uriHost = request.url().getHost();
+      if (uriHost == null || uriHost.length() == 0) {
+        throw new UnknownHostException(request.url().toString());
       }
       SSLSocketFactory sslSocketFactory = null;
       HostnameVerifier hostnameVerifier = null;
-      if (uri.getScheme().equalsIgnoreCase("https")) {
+      if (request.isHttps()) {
         sslSocketFactory = client.getSslSocketFactory();
         hostnameVerifier = client.getHostnameVerifier();
       }
-      Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
-          hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getTransports());
-      routeSelector = new RouteSelector(address, uri, client.getProxySelector(),
-          client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase());
+      Address address = new Address(uriHost, getEffectivePort(request.url()),
+          client.getSocketFactory(), sslSocketFactory, hostnameVerifier, client.getAuthenticator(),
+          client.getProxy(), client.getProtocols());
+      routeSelector = new RouteSelector(address, request.uri(), client.getProxySelector(),
+          client.getConnectionPool(), client.getHostResolver(), client.getRoutesDatabase());
     }
-    connection = routeSelector.next(method);
+
+    connection = routeSelector.next(request.method());
+    connection.setOwner(this);
+
     if (!connection.isConnected()) {
       connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
-      client.getConnectionPool().maybeShare(connection);
+      if (connection.isSpdy()) client.getConnectionPool().share(connection);
       client.getRoutesDatabase().connected(connection.getRoute());
-    } else {
+    } else if (!connection.isSpdy()) {
       connection.updateReadTimeout(client.getReadTimeout());
     }
-    connected(connection);
-    if (connection.getRoute().getProxy() != client.getProxy()) {
-      // Update the request line if the proxy changed; it may need a host name.
-      requestHeaders.getHeaders().setRequestLine(getRequestLine());
-    }
-  }
 
-  /**
-   * Called after a socket connection has been created or retrieved from the
-   * pool. Subclasses use this hook to get a reference to the TLS data.
-   */
-  protected void connected(Connection connection) {
-    policy.setSelectedProxy(connection.getRoute().getProxy());
-    connected = true;
+    route = connection.getRoute();
   }
 
   /**
@@ -318,69 +288,54 @@
    * This is used to observe the sent time should the request be cached.
    */
   public void writingRequestHeaders() {
-    if (sentRequestMillis != -1) {
-      throw new IllegalStateException();
-    }
+    if (sentRequestMillis != -1) throw new IllegalStateException();
     sentRequestMillis = System.currentTimeMillis();
   }
 
-  /**
-   * @param body the response body, or null if it doesn't exist or isn't
-   * available.
-   */
-  private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
-    if (this.responseBodyIn != null) {
-      throw new IllegalStateException();
-    }
-    this.responseHeaders = headers;
-    if (body != null) {
-      initContentStream(body);
-    }
-  }
-
   boolean hasRequestBody() {
-    return method.equals("POST") || method.equals("PUT") || method.equals("PATCH");
+    return HttpMethod.hasRequestBody(userRequest.method());
   }
 
   /** Returns the request body or null if this request doesn't have a body. */
-  public final OutputStream getRequestBody() {
-    if (responseSource == null) {
-      throw new IllegalStateException();
-    }
+  public final Sink getRequestBody() {
+    if (responseSource == null) throw new IllegalStateException();
     return requestBodyOut;
   }
 
+  public final BufferedSink getBufferedRequestBody() {
+    BufferedSink result = bufferedRequestBody;
+    if (result != null) return result;
+    Sink requestBody = getRequestBody();
+    return requestBody != null
+        ? (bufferedRequestBody = Okio.buffer(requestBody))
+        : null;
+  }
+
   public final boolean hasResponse() {
-    return responseHeaders != null;
+    return userResponse != null;
   }
 
-  public final RequestHeaders getRequestHeaders() {
-    return requestHeaders;
+  public final Request getRequest() {
+    return userRequest;
   }
 
-  public final ResponseHeaders getResponseHeaders() {
-    if (responseHeaders == null) {
-      throw new IllegalStateException();
-    }
-    return responseHeaders;
+  /** Returns the engine's response. */
+  // TODO: the returned body will always be null.
+  public final Response getResponse() {
+    if (userResponse == null) throw new IllegalStateException();
+    return userResponse;
   }
 
-  public final int getResponseCode() {
-    if (responseHeaders == null) {
-      throw new IllegalStateException();
-    }
-    return responseHeaders.getHeaders().getResponseCode();
+  public final Source getResponseBody() {
+    if (userResponse == null) throw new IllegalStateException();
+    return responseBody;
   }
 
-  public final InputStream getResponseBody() {
-    if (responseHeaders == null) {
-      throw new IllegalStateException();
-    }
-    return responseBodyIn;
-  }
-
-  public final CacheResponse getCacheResponse() {
-    return cacheResponse;
+  public final InputStream getResponseBodyBytes() {
+    InputStream result = responseBodyBytes;
+    return result != null
+        ? result
+        : (responseBodyBytes = Okio.buffer(getResponseBody()).inputStream());
   }
 
   public final Connection getConnection() {
@@ -388,86 +343,141 @@
   }
 
   /**
-   * Returns true if {@code cacheResponse} is of the right type. This
-   * condition is necessary but not sufficient for the cached response to
-   * be used.
+   * Report and attempt to recover from {@code e}. Returns a new HTTP engine
+   * that should be used for the retry if {@code e} is recoverable, or null if
+   * the failure is permanent.
    */
-  protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
-    return true;
+  public HttpEngine recover(IOException e) {
+    if (routeSelector != null && connection != null) {
+      routeSelector.connectFailed(connection, 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) {
+      return null;
+    }
+
+    Connection connection = close();
+
+    // For failure recovery, use the same route selector with a new connection.
+    return new HttpEngine(client, userRequest, bufferRequestBody, connection, routeSelector,
+        (RetryableSink) requestBodyOut, priorResponse);
+  }
+
+  private boolean isRecoverable(IOException e) {
+    // If the problem was a CertificateException from the X509TrustManager,
+    // do not retry, we didn't have an abrupt server-initiated exception.
+    boolean sslFailure =
+        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
+    boolean protocolFailure = e instanceof ProtocolException;
+    return !sslFailure && !protocolFailure;
+  }
+
+  /**
+   * 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 {
-    // Are we caching at all?
-    if (!policy.getUseCaches()) return;
     OkResponseCache responseCache = client.getOkResponseCache();
     if (responseCache == null) return;
 
-    HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
-
     // Should we cache this response for this request?
-    if (!responseHeaders.isCacheable(requestHeaders)) {
-      responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
+    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
+      responseCache.maybeRemove(networkRequest);
       return;
     }
 
     // Offer this request to the cache.
-    cacheRequest = responseCache.put(uri, connectionToCache);
+    storeRequest = responseCache.put(stripBody(userResponse));
   }
 
   /**
-   * Cause the socket connection to be released to the connection pool when
-   * it is no longer needed. If it is already unneeded, it will be pooled
-   * immediately. Otherwise the connection is held so that redirects can be
-   * handled by the same connection.
+   * Configure the socket connection to be either pooled or closed when it is
+   * either exhausted or closed. If it is unneeded when this is called, it will
+   * be released immediately.
    */
-  public final void automaticallyReleaseConnectionToPool() {
-    automaticallyReleaseConnectionToPool = true;
-    if (connection != null && connectionReleased) {
-      client.getConnectionPool().recycle(connection);
+  public final void releaseConnection() throws IOException {
+    if (transport != null && connection != null) {
+      transport.releaseConnectionOnIdle();
+    }
+    connection = null;
+  }
+
+  /**
+   * Release any resources held by this engine. If a connection is still held by
+   * this engine, it is returned.
+   */
+  public final Connection close() {
+    if (bufferedRequestBody != null) {
+      // This also closes the wrapped requestBodyOut.
+      closeQuietly(bufferedRequestBody);
+    } else if (requestBodyOut != null) {
+      closeQuietly(requestBodyOut);
+    }
+
+    // If this engine never achieved a response body, its connection cannot be reused.
+    if (responseBody == null) {
+      closeQuietly(connection);
+      connection = null;
+      return null;
+    }
+
+    // Close the response body. This will recycle the connection if it is eligible.
+    closeQuietly(responseBody);
+
+    // Clear the buffer held by the response body input stream adapter.
+    closeQuietly(responseBodyBytes);
+
+    // Close the connection if it cannot be reused.
+    if (transport != null && !transport.canReuseConnection()) {
+      closeQuietly(connection);
+      connection = null;
+      return null;
+    }
+
+    // Prevent this engine from disconnecting a connection it no longer owns.
+    if (connection != null && !connection.clearOwner()) {
       connection = null;
     }
+
+    Connection result = connection;
+    connection = null;
+    return result;
   }
 
   /**
-   * Releases this engine so that its resources may be either reused or
-   * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
-   * the connection will be used to follow a redirect.
+   * Initialize the response content stream from the response transfer source.
+   * These two sources are the same unless we're doing transparent gzip, in
+   * which case the content source is decompressed.
+   *
+   * <p>Whenever we do transparent gzip we also strip the corresponding headers.
+   * We strip the Content-Encoding header to prevent the application from
+   * attempting to double decompress. We strip the Content-Length header because
+   * it is the length of the compressed content, but the application is only
+   * interested in the length of the uncompressed content.
+   *
+   * <p>This method should only be used for non-empty response bodies. Response
+   * codes like "304 Not Modified" can include "Content-Encoding: gzip" without
+   * a response body and we will crash if we attempt to decompress the zero-byte
+   * source.
    */
-  public final void release(boolean streamCanceled) {
-    // If the response body comes from the cache, close it.
-    if (responseBodyIn == cachedResponseBody) {
-      Util.closeQuietly(responseBodyIn);
-    }
-
-    if (!connectionReleased && connection != null) {
-      connectionReleased = true;
-
-      if (transport == null
-          || !transport.makeReusable(streamCanceled, requestBodyOut, responseTransferIn)) {
-        Util.closeQuietly(connection);
-        connection = null;
-      } else if (automaticallyReleaseConnectionToPool) {
-        client.getConnectionPool().recycle(connection);
-        connection = null;
-      }
-    }
-  }
-
-  private void initContentStream(InputStream transferStream) throws IOException {
-    responseTransferIn = transferStream;
-    if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
-      // If the response was transparently gzipped, remove the gzip header field
-      // so clients don't double decompress. http://b/3009828
-      //
-      // Also remove the Content-Length in this case because it contains the
-      // length 528 of the gzipped response. This isn't terribly useful and is
-      // dangerous because 529 clients can query the content length, but not
-      // the content encoding.
-      responseHeaders.stripContentEncoding();
-      responseHeaders.stripContentLength();
-      responseBodyIn = new GZIPInputStream(transferStream);
+  private void initContentStream(Source transferSource) throws IOException {
+    responseTransferSource = transferSource;
+    if (transparentGzip && "gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) {
+      userResponse = userResponse.newBuilder()
+          .removeHeader("Content-Encoding")
+          .removeHeader("Content-Length")
+          .build();
+      responseBody = new GzipSource(transferSource);
     } else {
-      responseBodyIn = transferStream;
+      responseBody = transferSource;
     }
   }
 
@@ -476,23 +486,23 @@
    * See RFC 2616 section 4.3.
    */
   public final boolean hasResponseBody() {
-    int responseCode = responseHeaders.getHeaders().getResponseCode();
-
     // HEAD requests never yield a body regardless of the response headers.
-    if (method.equals("HEAD")) {
+    if (userRequest.method().equals("HEAD")) {
       return false;
     }
 
+    int responseCode = userResponse.code();
     if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
-        && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
-        && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
+        && responseCode != HTTP_NO_CONTENT
+        && responseCode != HTTP_NOT_MODIFIED) {
       return true;
     }
 
     // If the Content-Length or Transfer-Encoding headers disagree with the
     // response code, the response is malformed. For best compatibility, we
     // honor the headers.
-    if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
+    if (OkHeaders.contentLength(networkResponse) != -1
+        || "chunked".equalsIgnoreCase(networkResponse.header("Transfer-Encoding"))) {
       return true;
     }
 
@@ -500,96 +510,50 @@
   }
 
   /**
-   * Populates requestHeaders with defaults and cookies.
+   * Populates request with defaults and cookies.
    *
    * <p>This client doesn't specify a default {@code Accept} header because it
    * doesn't know what content types the application is interested in.
    */
-  private void prepareRawRequestHeaders() throws IOException {
-    requestHeaders.getHeaders().setRequestLine(getRequestLine());
+  private Request networkRequest(Request request) throws IOException {
+    Request.Builder result = request.newBuilder();
 
-    if (requestHeaders.getUserAgent() == null) {
-      requestHeaders.setUserAgent(getDefaultUserAgent());
+    if (request.getUserAgent() == null) {
+      result.setUserAgent(getDefaultUserAgent());
     }
 
-    if (requestHeaders.getHost() == null) {
-      requestHeaders.setHost(getOriginAddress(policy.getURL()));
+    if (request.header("Host") == null) {
+      result.header("Host", hostHeader(request.url()));
     }
 
     if ((connection == null || connection.getHttpMinorVersion() != 0)
-        && requestHeaders.getConnection() == null) {
-      requestHeaders.setConnection("Keep-Alive");
+        && request.header("Connection") == null) {
+      result.header("Connection", "Keep-Alive");
     }
 
-    if (requestHeaders.getAcceptEncoding() == null) {
+    if (request.header("Accept-Encoding") == null) {
       transparentGzip = true;
-      requestHeaders.setAcceptEncoding("gzip");
+      result.header("Accept-Encoding", "gzip");
     }
 
-    if (hasRequestBody() && requestHeaders.getContentType() == null) {
-      requestHeaders.setContentType("application/x-www-form-urlencoded");
-    }
-
-    long ifModifiedSince = policy.getIfModifiedSince();
-    if (ifModifiedSince != 0) {
-      requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
+    if (hasRequestBody() && request.header("Content-Type") == null) {
+      result.header("Content-Type", "application/x-www-form-urlencoded");
     }
 
     CookieHandler cookieHandler = client.getCookieHandler();
     if (cookieHandler != null) {
-      requestHeaders.addCookies(
-          cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
+      // Capture the request headers added so far so that they can be offered to the CookieHandler.
+      // This is mostly to stay close to the RI; it is unlikely any of the headers above would
+      // affect cookie choice besides "Host".
+      Map<String, List<String>> headers = OkHeaders.toMultimap(result.build().headers(), null);
+
+      Map<String, List<String>> cookies = cookieHandler.get(request.uri(), headers);
+
+      // Add any new cookies to the request.
+      OkHeaders.addCookies(result, cookies);
     }
-  }
 
-  /**
-   * Returns the request status line, like "GET / HTTP/1.1". This is exposed
-   * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
-   * it needs to be set even if the transport is SPDY.
-   */
-  String getRequestLine() {
-    String protocol =
-        (connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
-    return method + " " + requestString() + " " + protocol;
-  }
-
-  private String requestString() {
-    URL url = policy.getURL();
-    if (includeAuthorityInRequestLine()) {
-      return url.toString();
-    } else {
-      return requestPath(url);
-    }
-  }
-
-  /**
-   * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
-   * empty, even if the request URL is. Includes the query component if it
-   * exists.
-   */
-  public static String requestPath(URL url) {
-    String fileOnly = url.getFile();
-    if (fileOnly == null) {
-      return "/";
-    } else if (!fileOnly.startsWith("/")) {
-      return "/" + fileOnly;
-    } else {
-      return fileOnly;
-    }
-  }
-
-  /**
-   * Returns true if the request line should contain the full URL with host
-   * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
-   * (like "GET /foo HTTP/1.1").
-   *
-   * <p>This is non-final because for HTTPS it's never necessary to supply the
-   * full URL, even if a proxy is in use.
-   */
-  protected boolean includeAuthorityInRequestLine() {
-    return connection == null
-        ? policy.usingProxy() // A proxy was requested.
-        : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
+    return result.build();
   }
 
   public static String getDefaultUserAgent() {
@@ -597,13 +561,10 @@
     return agent != null ? agent : ("Java" + System.getProperty("java.version"));
   }
 
-  public static String getOriginAddress(URL url) {
-    int port = url.getPort();
-    String result = url.getHost();
-    if (port > 0 && port != getDefaultPort(url.getProtocol())) {
-      result = result + ":" + port;
-    }
-    return result;
+  public static String hostHeader(URL url) {
+    return getEffectivePort(url) != getDefaultPort(url.getProtocol())
+        ? url.getHost() + ":" + url.getPort()
+        : url.getHost();
   }
 
   /**
@@ -611,76 +572,160 @@
    * headers and starts reading the HTTP response body if it exists.
    */
   public final void readResponse() throws IOException {
-    if (hasResponse()) {
-      responseHeaders.setResponseSource(responseSource);
-      return;
+    if (userResponse != null) {
+      return; // Already ready.
+    }
+    if (networkRequest == null && cacheResponse == null) {
+      throw new IllegalStateException("call sendRequest() first!");
+    }
+    if (networkRequest == null) {
+      return; // No network response to read.
     }
 
-    if (responseSource == null) {
-      throw new IllegalStateException("readResponse() without sendRequest()");
-    }
-
-    if (!responseSource.requiresConnection()) {
-      return;
+    // Flush the request body if there's data outstanding.
+    if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) {
+      bufferedRequestBody.flush();
     }
 
     if (sentRequestMillis == -1) {
-      if (requestBodyOut instanceof RetryableOutputStream) {
-        int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
-        requestHeaders.setContentLength(contentLength);
+      if (OkHeaders.contentLength(networkRequest) == -1
+          && requestBodyOut instanceof RetryableSink) {
+        // We might not learn the Content-Length until the request body has been buffered.
+        long contentLength = ((RetryableSink) requestBodyOut).contentLength();
+        networkRequest = networkRequest.newBuilder()
+            .header("Content-Length", Long.toString(contentLength))
+            .build();
       }
-      transport.writeRequestHeaders();
+      transport.writeRequestHeaders(networkRequest);
     }
 
     if (requestBodyOut != null) {
-      requestBodyOut.close();
-      if (requestBodyOut instanceof RetryableOutputStream) {
-        transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
+      if (bufferedRequestBody != null) {
+        // This also closes the wrapped requestBodyOut.
+        bufferedRequestBody.close();
+      } else {
+        requestBodyOut.close();
+      }
+      if (requestBodyOut instanceof RetryableSink) {
+        transport.writeRequestBody((RetryableSink) requestBodyOut);
       }
     }
 
     transport.flushRequest();
 
-    responseHeaders = transport.readResponseHeaders();
-    responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
-    responseHeaders.setResponseSource(responseSource);
+    networkResponse = transport.readResponseHeaders()
+        .request(networkRequest)
+        .handshake(connection.getHandshake())
+        .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
+        .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
+        .setResponseSource(responseSource)
+        .build();
+    connection.setHttpMinorVersion(networkResponse.httpMinorVersion());
+    receiveHeaders(networkResponse.headers());
 
     if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
-      if (cachedResponseHeaders.validate(responseHeaders)) {
-        release(false);
-        ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
-        this.responseHeaders = combinedHeaders;
+      if (cacheResponse.validate(networkResponse)) {
+        userResponse = cacheResponse.newBuilder()
+            .request(userRequest)
+            .priorResponse(stripBody(priorResponse))
+            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
+            .cacheResponse(stripBody(cacheResponse))
+            .networkResponse(stripBody(networkResponse))
+            .build();
+        transport.emptyTransferStream();
+        releaseConnection();
 
-        // Update the cache after applying the combined headers but before initializing the content
-        // stream, otherwise the Content-Encoding header (if present) will be stripped from the
-        // combined headers and not end up in the cache file if transparent gzip compression is
-        // turned on.
+        // Update the cache after combining headers but before stripping the
+        // Content-Encoding header (as performed by initContentStream()).
         OkResponseCache responseCache = client.getOkResponseCache();
         responseCache.trackConditionalCacheHit();
-        responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
+        responseCache.update(cacheResponse, stripBody(userResponse));
+        if (cacheResponse.body() != null) {
+          initContentStream(cacheResponse.body().source());
+        }
 
-        initContentStream(cachedResponseBody);
         return;
       } else {
-        Util.closeQuietly(cachedResponseBody);
+        closeQuietly(cacheResponse.body());
       }
     }
 
-    if (hasResponseBody()) {
-      maybeCache(); // reentrant. this calls into user code which may call back into this!
+    userResponse = networkResponse.newBuilder()
+        .request(userRequest)
+        .priorResponse(stripBody(priorResponse))
+        .cacheResponse(stripBody(cacheResponse))
+        .networkResponse(stripBody(networkResponse))
+        .build();
+
+    if (!hasResponseBody()) {
+      // Don't call initContentStream() when the response doesn't have any content.
+      responseTransferSource = transport.getTransferStream(storeRequest);
+      responseBody = responseTransferSource;
+      return;
     }
 
-    initContentStream(transport.getTransferStream(cacheRequest));
+    maybeCache();
+    initContentStream(transport.getTransferStream(storeRequest));
   }
 
-  protected TunnelRequest getTunnelConfig() {
-    return null;
+  /**
+   * Combines cached headers with a network headers as defined by RFC 2616,
+   * 13.5.3.
+   */
+  private static Headers combine(Headers cachedHeaders, Headers networkHeaders) throws IOException {
+    Headers.Builder result = new Headers.Builder();
+
+    for (int i = 0; i < cachedHeaders.size(); i++) {
+      String fieldName = cachedHeaders.name(i);
+      String value = cachedHeaders.value(i);
+      if ("Warning".equals(fieldName) && value.startsWith("1")) {
+        continue; // drop 100-level freshness warnings
+      }
+      if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
+        result.add(fieldName, value);
+      }
+    }
+
+    for (int i = 0; i < networkHeaders.size(); i++) {
+      String fieldName = networkHeaders.name(i);
+      if (isEndToEnd(fieldName)) {
+        result.add(fieldName, networkHeaders.value(i));
+      }
+    }
+
+    return result.build();
   }
 
-  public void receiveHeaders(RawHeaders headers) throws IOException {
+  /**
+   * Returns true if {@code fieldName} is an end-to-end HTTP header, as
+   * defined by RFC 2616, 13.5.1.
+   */
+  private static boolean isEndToEnd(String fieldName) {
+    return !"Connection".equalsIgnoreCase(fieldName)
+        && !"Keep-Alive".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+        && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+        && !"TE".equalsIgnoreCase(fieldName)
+        && !"Trailers".equalsIgnoreCase(fieldName)
+        && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+        && !"Upgrade".equalsIgnoreCase(fieldName);
+  }
+
+  private TunnelRequest getTunnelConfig() {
+    if (!userRequest.isHttps()) return null;
+
+    String userAgent = userRequest.getUserAgent();
+    if (userAgent == null) userAgent = getDefaultUserAgent();
+
+    URL url = userRequest.url();
+    return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
+        userRequest.getProxyAuthorization());
+  }
+
+  public void receiveHeaders(Headers headers) throws IOException {
     CookieHandler cookieHandler = client.getCookieHandler();
     if (cookieHandler != null) {
-      cookieHandler.put(uri, headers.toMultimap(true));
+      cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null));
     }
   }
 }
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
new file mode 100644
index 0000000..1577d10
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public final class HttpMethod {
+  public static final Set<String> METHODS = new LinkedHashSet<String>(Arrays.asList(
+      "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH"));
+
+  public static boolean invalidatesCache(String method) {
+    return method.equals("POST")
+        || method.equals("PATCH")
+        || method.equals("PUT")
+        || method.equals("DELETE");
+  }
+
+  public static boolean hasRequestBody(String method) {
+    return method.equals("POST")
+        || method.equals("PUT")
+        || method.equals("PATCH")
+        || method.equals("DELETE"); // Permitted as spec is ambiguous.
+  }
+
+  private HttpMethod() {
+  }
+}
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
index f8f9f17..a1b367f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -16,100 +16,65 @@
 
 package com.squareup.okhttp.internal.http;
 
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.internal.AbstractOutputStream;
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayOutputStream;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.CacheRequest;
-import java.net.ProtocolException;
-import java.net.Socket;
-
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+import okio.Sink;
+import okio.Source;
 
 public final class HttpTransport implements Transport {
-  /**
-   * 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
-   * the time it takes to establish a new connection.
-   */
-  private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
-
-  public static final int DEFAULT_CHUNK_LENGTH = 1024;
-
   private final HttpEngine httpEngine;
-  private final InputStream socketIn;
-  private final OutputStream socketOut;
+  private final HttpConnection httpConnection;
 
-  /**
-   * This stream buffers the request headers and the request body when their
-   * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
-   * we can save socket writes, which in turn saves a packet transmission.
-   * This is socketOut if the request size is large or unknown.
-   */
-  private OutputStream requestOut;
-
-  public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) {
+  public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
     this.httpEngine = httpEngine;
-    this.socketOut = outputStream;
-    this.requestOut = outputStream;
-    this.socketIn = inputStream;
+    this.httpConnection = httpConnection;
   }
 
-  @Override public OutputStream createRequestBody() throws IOException {
-    boolean chunked = httpEngine.requestHeaders.isChunked();
-    if (!chunked
-        && httpEngine.policy.getChunkLength() > 0
-        && httpEngine.connection.getHttpMinorVersion() != 0) {
-      httpEngine.requestHeaders.setChunked();
-      chunked = true;
-    }
+  @Override public Sink createRequestBody(Request request) throws IOException {
+    long contentLength = OkHeaders.contentLength(request);
 
-    // Stream a request body of unknown length.
-    if (chunked) {
-      int chunkLength = httpEngine.policy.getChunkLength();
-      if (chunkLength == -1) {
-        chunkLength = DEFAULT_CHUNK_LENGTH;
+    if (httpEngine.bufferRequestBody) {
+      if (contentLength > Integer.MAX_VALUE) {
+        throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
+            + "setChunkedStreamingMode() for requests larger than 2 GiB.");
       }
-      writeRequestHeaders();
-      return new ChunkedOutputStream(requestOut, chunkLength);
+
+      if (contentLength != -1) {
+        // Buffer a request body of a known length.
+        writeRequestHeaders(request);
+        return new RetryableSink((int) contentLength);
+      } else {
+        // Buffer a request body of an unknown length. Don't write request
+        // headers until the entire body is ready; otherwise we can't set the
+        // Content-Length header correctly.
+        return new RetryableSink();
+      }
     }
 
-    // Stream a request body of a known length.
-    long fixedContentLength = httpEngine.policy.getFixedContentLength();
-    if (fixedContentLength != -1) {
-      httpEngine.requestHeaders.setContentLength(fixedContentLength);
-      writeRequestHeaders();
-      return new FixedLengthOutputStream(requestOut, fixedContentLength);
+    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
+      // Stream a request body of unknown length.
+      writeRequestHeaders(request);
+      return httpConnection.newChunkedSink();
     }
 
-    long contentLength = httpEngine.requestHeaders.getContentLength();
-    if (contentLength > Integer.MAX_VALUE) {
-      throw new IllegalArgumentException("Use setFixedLengthStreamingMode() or "
-          + "setChunkedStreamingMode() for requests larger than 2 GiB.");
-    }
-
-    // Buffer a request body of a known length.
     if (contentLength != -1) {
-      writeRequestHeaders();
-      return new RetryableOutputStream((int) contentLength);
+      // Stream a request body of a known length.
+      writeRequestHeaders(request);
+      return httpConnection.newFixedLengthSink(contentLength);
     }
 
-    // Buffer a request body of an unknown length. Don't write request
-    // headers until the entire body is ready; otherwise we can't set the
-    // Content-Length header correctly.
-    return new RetryableOutputStream();
+    throw new IllegalStateException(
+        "Cannot stream a request body without chunked encoding or a known content length!");
   }
 
   @Override public void flushRequest() throws IOException {
-    requestOut.flush();
-    requestOut = socketOut;
+    httpConnection.flush();
   }
 
-  @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
-    requestBody.writeToSocket(requestOut);
+  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
+    httpConnection.writeRequestBody(requestBody);
   }
 
   /**
@@ -124,374 +89,65 @@
    * This ensures that the {@code Content-Length} header field receives the
    * proper value.
    */
-  public void writeRequestHeaders() throws IOException {
+  public void writeRequestHeaders(Request request) throws IOException {
     httpEngine.writingRequestHeaders();
-    RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
-    byte[] bytes = headersToSend.toBytes();
-    requestOut.write(bytes);
+    String requestLine = RequestLine.get(request,
+        httpEngine.getConnection().getRoute().getProxy().type(),
+        httpEngine.getConnection().getHttpMinorVersion());
+    httpConnection.writeRequest(request.getHeaders(), requestLine);
   }
 
-  @Override public ResponseHeaders readResponseHeaders() throws IOException {
-    RawHeaders rawHeaders = RawHeaders.fromBytes(socketIn);
-    httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion());
-    httpEngine.receiveHeaders(rawHeaders);
-
-    ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
-    headers.setTransport("http/1.1");
-    return headers;
+  @Override public Response.Builder readResponseHeaders() throws IOException {
+    return httpConnection.readResponse();
   }
 
-  public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
-      InputStream responseBodyIn) {
-    if (streamCanceled) {
-      return false;
+  @Override public void releaseConnectionOnIdle() throws IOException {
+    if (canReuseConnection()) {
+      httpConnection.poolOnIdle();
+    } else {
+      httpConnection.closeOnIdle();
     }
+  }
 
-    // We cannot reuse sockets that have incomplete output.
-    if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
-      return false;
-    }
-
+  @Override public boolean canReuseConnection() {
     // If the request specified that the connection shouldn't be reused, don't reuse it.
-    if (httpEngine.requestHeaders.hasConnectionClose()) {
+    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 (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
+    if ("close".equalsIgnoreCase(httpEngine.getResponse().header("Connection"))) {
       return false;
     }
 
-    if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
+    if (httpConnection.isClosed()) {
       return false;
     }
 
-    if (responseBodyIn != null) {
-      return discardStream(httpEngine, responseBodyIn);
-    }
-
     return true;
   }
 
-  /**
-   * Discards the response body so that the connection can be reused. This
-   * needs to be done judiciously, since it delays the current request in
-   * order to speed up a potential future request that may never occur.
-   *
-   * <p>A stream may be discarded to encourage response caching (a response
-   * cannot be cached unless it is consumed completely) or to enable connection
-   * reuse.
-   */
-  private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
-    Connection connection = httpEngine.connection;
-    if (connection == null) return false;
-    Socket socket = connection.getSocket();
-    if (socket == null) return false;
-    try {
-      int socketTimeout = socket.getSoTimeout();
-      socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
-      try {
-        Util.skipAll(responseBodyIn);
-        return true;
-      } finally {
-        socket.setSoTimeout(socketTimeout);
-      }
-    } catch (IOException e) {
-      return false;
-    }
+  @Override public void emptyTransferStream() throws IOException {
+    httpConnection.emptyResponseBody();
   }
 
-  @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+  @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException {
     if (!httpEngine.hasResponseBody()) {
-      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
+      return httpConnection.newFixedLengthSource(cacheRequest, 0);
     }
 
-    if (httpEngine.responseHeaders.isChunked()) {
-      return new ChunkedInputStream(socketIn, cacheRequest, this);
+    if ("chunked".equalsIgnoreCase(httpEngine.getResponse().header("Transfer-Encoding"))) {
+      return httpConnection.newChunkedSource(cacheRequest, httpEngine);
     }
 
-    if (httpEngine.responseHeaders.getContentLength() != -1) {
-      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
-          httpEngine.responseHeaders.getContentLength());
+    long contentLength = OkHeaders.contentLength(httpEngine.getResponse());
+    if (contentLength != -1) {
+      return httpConnection.newFixedLengthSource(cacheRequest, 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 new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine);
-  }
-
-  /** An HTTP body with a fixed length known in advance. */
-  private static final class FixedLengthOutputStream extends AbstractOutputStream {
-    private final OutputStream socketOut;
-    private long bytesRemaining;
-
-    private FixedLengthOutputStream(OutputStream socketOut, long bytesRemaining) {
-      this.socketOut = socketOut;
-      this.bytesRemaining = bytesRemaining;
-    }
-
-    @Override public void write(byte[] buffer, int offset, int count) throws IOException {
-      checkNotClosed();
-      checkOffsetAndCount(buffer.length, offset, count);
-      if (count > bytesRemaining) {
-        throw new ProtocolException("expected " + bytesRemaining + " bytes but received " + count);
-      }
-      socketOut.write(buffer, offset, count);
-      bytesRemaining -= count;
-    }
-
-    @Override public void flush() throws IOException {
-      if (closed) {
-        return; // don't throw; this stream might have been closed on the caller's behalf
-      }
-      socketOut.flush();
-    }
-
-    @Override public void close() throws IOException {
-      if (closed) {
-        return;
-      }
-      closed = true;
-      if (bytesRemaining > 0) {
-        throw new ProtocolException("unexpected end of stream");
-      }
-    }
-  }
-
-  /**
-   * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are
-   * buffered until {@code maxChunkLength} bytes are ready, at which point the
-   * chunk is written and the buffer is cleared.
-   */
-  private static final class ChunkedOutputStream extends AbstractOutputStream {
-    private static final byte[] CRLF = { '\r', '\n' };
-    private static final byte[] HEX_DIGITS = {
-        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
-    };
-    private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
-
-    /** Scratch space for up to 8 hex digits, and then a constant CRLF. */
-    private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' };
-
-    private final OutputStream socketOut;
-    private final int maxChunkLength;
-    private final ByteArrayOutputStream bufferedChunk;
-
-    private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) {
-      this.socketOut = socketOut;
-      this.maxChunkLength = Math.max(1, dataLength(maxChunkLength));
-      this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength);
-    }
-
-    /**
-     * Returns the amount of data that can be transmitted in a chunk whose total
-     * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably
-     * useful to match sizes with wire-protocol packets.
-     */
-    private int dataLength(int dataPlusHeaderLength) {
-      int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data
-      for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) {
-        headerLength++;
-      }
-      return dataPlusHeaderLength - headerLength;
-    }
-
-    @Override public synchronized void write(byte[] buffer, int offset, int count)
-        throws IOException {
-      checkNotClosed();
-      checkOffsetAndCount(buffer.length, offset, count);
-
-      while (count > 0) {
-        int numBytesWritten;
-
-        if (bufferedChunk.size() > 0 || count < maxChunkLength) {
-          // fill the buffered chunk and then maybe write that to the stream
-          numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size());
-          // TODO: skip unnecessary copies from buffer->bufferedChunk?
-          bufferedChunk.write(buffer, offset, numBytesWritten);
-          if (bufferedChunk.size() == maxChunkLength) {
-            writeBufferedChunkToSocket();
-          }
-        } else {
-          // write a single chunk of size maxChunkLength to the stream
-          numBytesWritten = maxChunkLength;
-          writeHex(numBytesWritten);
-          socketOut.write(buffer, offset, numBytesWritten);
-          socketOut.write(CRLF);
-        }
-
-        offset += numBytesWritten;
-        count -= numBytesWritten;
-      }
-    }
-
-    /**
-     * Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
-     * followed by CRLF.
-     */
-    private void writeHex(int i) throws IOException {
-      int cursor = 8;
-      do {
-        hex[--cursor] = HEX_DIGITS[i & 0xf];
-      } while ((i >>>= 4) != 0);
-      socketOut.write(hex, cursor, hex.length - cursor);
-    }
-
-    @Override public synchronized void flush() throws IOException {
-      if (closed) {
-        return; // don't throw; this stream might have been closed on the caller's behalf
-      }
-      writeBufferedChunkToSocket();
-      socketOut.flush();
-    }
-
-    @Override public synchronized void close() throws IOException {
-      if (closed) {
-        return;
-      }
-      closed = true;
-      writeBufferedChunkToSocket();
-      socketOut.write(FINAL_CHUNK);
-    }
-
-    private void writeBufferedChunkToSocket() throws IOException {
-      int size = bufferedChunk.size();
-      if (size <= 0) {
-        return;
-      }
-
-      writeHex(size);
-      bufferedChunk.writeTo(socketOut);
-      bufferedChunk.reset();
-      socketOut.write(CRLF);
-    }
-  }
-
-  /** An HTTP body with a fixed length specified in advance. */
-  private static class FixedLengthInputStream extends AbstractHttpInputStream {
-    private int bytesRemaining;
-
-    public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
-        int length) throws IOException {
-      super(is, httpEngine, cacheRequest);
-      bytesRemaining = length;
-      if (bytesRemaining == 0) {
-        endOfInput();
-      }
-    }
-
-    @Override public int read(byte[] buffer, int offset, int count) throws IOException {
-      checkOffsetAndCount(buffer.length, offset, count);
-      checkNotClosed();
-      if (bytesRemaining == 0) {
-        return -1;
-      }
-      int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
-      if (read == -1) {
-        unexpectedEndOfInput(); // the server didn't supply the promised content length
-        throw new ProtocolException("unexpected end of stream");
-      }
-      bytesRemaining -= read;
-      cacheWrite(buffer, offset, read);
-      if (bytesRemaining == 0) {
-        endOfInput();
-      }
-      return read;
-    }
-
-    @Override public int available() throws IOException {
-      checkNotClosed();
-      return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
-    }
-
-    @Override public void close() throws IOException {
-      if (closed) {
-        return;
-      }
-      if (bytesRemaining != 0 && !discardStream(httpEngine, this)) {
-        unexpectedEndOfInput();
-      }
-      closed = true;
-    }
-  }
-
-  /** An HTTP body with alternating chunk sizes and chunk bodies. */
-  private static class ChunkedInputStream extends AbstractHttpInputStream {
-    private static final int NO_CHUNK_YET = -1;
-    private final HttpTransport transport;
-    private int bytesRemainingInChunk = NO_CHUNK_YET;
-    private boolean hasMoreChunks = true;
-
-    ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport)
-        throws IOException {
-      super(is, transport.httpEngine, cacheRequest);
-      this.transport = transport;
-    }
-
-    @Override public int read(byte[] buffer, int offset, int count) throws IOException {
-      checkOffsetAndCount(buffer.length, offset, count);
-      checkNotClosed();
-
-      if (!hasMoreChunks) {
-        return -1;
-      }
-      if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
-        readChunkSize();
-        if (!hasMoreChunks) {
-          return -1;
-        }
-      }
-      int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk));
-      if (read == -1) {
-        unexpectedEndOfInput(); // the server didn't supply the promised chunk length
-        throw new IOException("unexpected end of stream");
-      }
-      bytesRemainingInChunk -= read;
-      cacheWrite(buffer, offset, read);
-      return read;
-    }
-
-    private void readChunkSize() throws IOException {
-      // read the suffix of the previous chunk
-      if (bytesRemainingInChunk != NO_CHUNK_YET) {
-        Util.readAsciiLine(in);
-      }
-      String chunkSizeString = Util.readAsciiLine(in);
-      int index = chunkSizeString.indexOf(";");
-      if (index != -1) {
-        chunkSizeString = chunkSizeString.substring(0, index);
-      }
-      try {
-        bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
-      } catch (NumberFormatException e) {
-        throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
-      }
-      if (bytesRemainingInChunk == 0) {
-        hasMoreChunks = false;
-        RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
-        RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
-        httpEngine.receiveHeaders(rawResponseHeaders);
-        endOfInput();
-      }
-    }
-
-    @Override public int available() throws IOException {
-      checkNotClosed();
-      if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) {
-        return 0;
-      }
-      return Math.min(in.available(), bytesRemainingInChunk);
-    }
-
-    @Override public void close() throws IOException {
-      if (closed) {
-        return;
-      }
-      if (hasMoreChunks && !discardStream(httpEngine, this)) {
-        unexpectedEndOfInput();
-      }
-      closed = true;
-    }
+    return httpConnection.newUnknownLengthSource(cacheRequest);
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index 0ba0228..899d914 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -18,7 +18,13 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.OkHttpClient;
+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.Platform;
 import com.squareup.okhttp.internal.Util;
 import java.io.FileNotFoundException;
@@ -33,15 +39,18 @@
 import java.net.SocketPermission;
 import java.net.URL;
 import java.security.Permission;
-import java.security.cert.CertificateException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLHandshakeException;
+import okio.BufferedSink;
+import okio.ByteString;
+import okio.Sink;
 
 import static com.squareup.okhttp.internal.Util.getEffectivePort;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
 
 /**
  * This implementation uses HttpEngine to send requests and receive responses.
@@ -53,30 +62,37 @@
  * is <strong>not</strong> used to indicate not 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. Test the {@code
- * connection} field on this class for null/non-null to determine of an instance
- * is currently connected to a server.
+ * header fields, request method, etc.) are immutable.
  */
-public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
-
-  /** Numeric status code, 307: Temporary Redirect. */
-  static final int HTTP_TEMP_REDIRECT = 307;
+public class HttpURLConnectionImpl extends HttpURLConnection {
 
   /**
    * How many redirects should we follow? Chrome follows 21; Firefox, curl,
    * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
    */
-  private static final int MAX_REDIRECTS = 20;
+  public static final int MAX_REDIRECTS = 20;
 
   final OkHttpClient client;
 
-  private final RawHeaders rawRequestHeaders = new RawHeaders();
+  private Headers.Builder requestHeaders = new Headers.Builder();
+
   /** Like the superclass field of the same name, but a long and available on all platforms. */
   private long fixedContentLength = -1;
   private int redirectionCount;
   protected IOException httpEngineFailure;
   protected HttpEngine httpEngine;
-  private Proxy selectedProxy;
+
+  /**
+   * The most recently attempted route. This will be null if we haven't sent a
+   * request yet, or if the response comes from a cache.
+   */
+  private Route route;
+
+  /**
+   * The most recently received TLS handshake. This will be null if we haven't
+   * connected yet, or if the most recent connection was HTTP (and not HTTPS).
+   */
+  Handshake handshake;
 
   public HttpURLConnectionImpl(URL url, OkHttpClient client) {
     super(url);
@@ -94,15 +110,7 @@
   @Override public final void disconnect() {
     // Calling disconnect() before a connection exists should have no effect.
     if (httpEngine != null) {
-      // We close the response body here instead of in
-      // HttpEngine.release because that is called when input
-      // has been completely read from the underlying socket.
-      // However the response body can be a GZIPInputStream that
-      // still has unread data.
-      if (httpEngine.hasResponse()) {
-        Util.closeQuietly(httpEngine.getResponseBody());
-      }
-      httpEngine.release(true);
+      httpEngine.close();
     }
   }
 
@@ -113,8 +121,8 @@
   @Override public final InputStream getErrorStream() {
     try {
       HttpEngine response = getResponse();
-      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
-        return response.getResponseBody();
+      if (response.hasResponseBody() && response.getResponse().code() >= HTTP_BAD_REQUEST) {
+        return response.getResponseBodyBytes();
       }
       return null;
     } catch (IOException e) {
@@ -128,7 +136,7 @@
    */
   @Override public final String getHeaderField(int position) {
     try {
-      return getResponse().getResponseHeaders().getHeaders().getValue(position);
+      return getResponse().getResponse().headers().value(position);
     } catch (IOException e) {
       return null;
     }
@@ -141,8 +149,8 @@
    */
   @Override public final String getHeaderField(String fieldName) {
     try {
-      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
-      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
+      Response response = getResponse().getResponse();
+      return fieldName == null ? response.statusLine() : response.headers().get(fieldName);
     } catch (IOException e) {
       return null;
     }
@@ -150,7 +158,7 @@
 
   @Override public final String getHeaderFieldKey(int position) {
     try {
-      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
+      return getResponse().getResponse().headers().name(position);
     } catch (IOException e) {
       return null;
     }
@@ -158,7 +166,8 @@
 
   @Override public final Map<String, List<String>> getHeaderFields() {
     try {
-      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
+      Response response = getResponse().getResponse();
+      return OkHeaders.toMultimap(response.headers(), response.statusLine());
     } catch (IOException e) {
       return Collections.emptyMap();
     }
@@ -169,7 +178,8 @@
       throw new IllegalStateException(
           "Cannot access request header fields after connection is set");
     }
-    return rawRequestHeaders.toMultimap(false);
+
+    return OkHeaders.toMultimap(requestHeaders.build(), null);
   }
 
   @Override public final InputStream getInputStream() throws IOException {
@@ -187,7 +197,7 @@
       throw new FileNotFoundException(url.toString());
     }
 
-    InputStream result = response.getResponseBody();
+    InputStream result = response.getResponseBodyBytes();
     if (result == null) {
       throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
     }
@@ -197,14 +207,14 @@
   @Override public final OutputStream getOutputStream() throws IOException {
     connect();
 
-    OutputStream out = httpEngine.getRequestBody();
-    if (out == null) {
+    BufferedSink sink = httpEngine.getBufferedRequestBody();
+    if (sink == null) {
       throw new ProtocolException("method does not support a request body: " + method);
     } else if (httpEngine.hasResponse()) {
       throw new ProtocolException("cannot write request body after response has been read");
     }
 
-    return out;
+    return sink.outputStream();
   }
 
   @Override public final Permission getPermission() throws IOException {
@@ -219,10 +229,8 @@
   }
 
   @Override public final String getRequestProperty(String field) {
-    if (field == null) {
-      return null;
-    }
-    return rawRequestHeaders.get(field);
+    if (field == null) return null;
+    return requestHeaders.get(field);
   }
 
   @Override public void setConnectTimeout(int timeoutMillis) {
@@ -254,31 +262,49 @@
         if (method.equals("GET")) {
           // they are requesting a stream to write to. This implies a POST method
           method = "POST";
-        } else if (!method.equals("POST") && !method.equals("PUT") && !method.equals("PATCH")) {
+        } else if (!HttpMethod.hasRequestBody(method)) {
           // If the request method is neither POST nor PUT nor PATCH, then you're not writing
           throw new ProtocolException(method + " does not support writing");
         }
       }
-      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
+      httpEngine = newHttpEngine(method, null, null, null);
     } catch (IOException e) {
       httpEngineFailure = e;
       throw e;
     }
   }
 
-  @Override public HttpURLConnection getHttpConnectionToCache() {
-    return this;
-  }
-
-  private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
-      Connection connection, RetryableOutputStream requestBody) throws IOException {
-    if (url.getProtocol().equals("http")) {
-      return new HttpEngine(client, this, method, requestHeaders, connection, requestBody);
-    } else if (url.getProtocol().equals("https")) {
-      return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
-    } else {
-      throw new AssertionError();
+  private HttpEngine newHttpEngine(String method, Connection connection,
+      RetryableSink requestBody, Response priorResponse) {
+    Request.Builder builder = new Request.Builder()
+        .url(getURL())
+        .method(method, null /* No body; that's passed separately. */);
+    Headers headers = requestHeaders.build();
+    for (int i = 0; i < headers.size(); i++) {
+      builder.addHeader(headers.name(i), headers.value(i));
     }
+
+    boolean bufferRequestBody = false;
+    if (HttpMethod.hasRequestBody(method)) {
+      if (fixedContentLength != -1) {
+        builder.header("Content-Length", Long.toString(fixedContentLength));
+      } else if (chunkLength > 0) {
+        builder.header("Transfer-Encoding", "chunked");
+      } else {
+        bufferRequestBody = true;
+      }
+    }
+
+    Request request = builder.build();
+
+    // If we're currently not using caches, make sure the engine's client doesn't have one.
+    OkHttpClient engineClient = client;
+    if (engineClient.getOkResponseCache() != null && !getUseCaches()) {
+      engineClient = client.clone().setOkResponseCache(null);
+    }
+
+    return new HttpEngine(engineClient, request, bufferRequestBody, connection, null, requestBody,
+        priorResponse);
   }
 
   /**
@@ -298,46 +324,42 @@
         continue;
       }
 
+      Response response = httpEngine.getResponse();
+
       Retry retry = processResponseHeaders();
       if (retry == Retry.NONE) {
-        httpEngine.automaticallyReleaseConnectionToPool();
+        httpEngine.releaseConnection();
         return httpEngine;
       }
 
       // The first request was insufficient. Prepare for another...
       String retryMethod = method;
-      OutputStream requestBody = httpEngine.getRequestBody();
+      Sink requestBody = httpEngine.getRequestBody();
 
       // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
       // redirect should keep the same method, Chrome, Firefox and the
       // RI all issue GETs when following any redirect.
-      int responseCode = getResponseCode();
+      int responseCode = httpEngine.getResponse().code();
       if (responseCode == HTTP_MULT_CHOICE
           || responseCode == HTTP_MOVED_PERM
           || responseCode == HTTP_MOVED_TEMP
           || responseCode == HTTP_SEE_OTHER) {
         retryMethod = "GET";
+        requestHeaders.removeAll("Content-Length");
         requestBody = null;
       }
 
-      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
-        throw new HttpRetryException("Cannot retry streamed HTTP body",
-            httpEngine.getResponseCode());
+      if (requestBody != null && !(requestBody instanceof RetryableSink)) {
+        throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
       }
 
       if (retry == Retry.DIFFERENT_CONNECTION) {
-        httpEngine.automaticallyReleaseConnectionToPool();
+        httpEngine.releaseConnection();
       }
 
-      httpEngine.release(false);
-
-      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
-          (RetryableOutputStream) requestBody);
-
-      if (requestBody == null) {
-        // Drop the Content-Length header when redirected from POST to GET.
-        httpEngine.getRequestHeaders().removeContentLength();
-      }
+      Connection connection = httpEngine.close();
+      httpEngine = newHttpEngine(retryMethod, connection, (RetryableSink) requestBody,
+          response);
     }
   }
 
@@ -349,60 +371,26 @@
   private boolean execute(boolean readResponse) throws IOException {
     try {
       httpEngine.sendRequest();
+      route = httpEngine.getRoute();
+      handshake = httpEngine.getConnection() != null
+          ? httpEngine.getConnection().getHandshake()
+          : null;
       if (readResponse) {
         httpEngine.readResponse();
       }
 
       return true;
     } catch (IOException e) {
-      if (handleFailure(e)) {
+      HttpEngine retryEngine = httpEngine.recover(e);
+      if (retryEngine != null) {
+        httpEngine = retryEngine;
         return false;
-      } else {
-        throw e;
       }
-    }
-  }
 
-  /**
-   * Report and attempt to recover from {@code e}. Returns true if the HTTP
-   * engine was replaced and the request should be retried. Otherwise the
-   * failure is permanent.
-   */
-  private boolean handleFailure(IOException e) throws IOException {
-    RouteSelector routeSelector = httpEngine.routeSelector;
-    if (routeSelector != null && httpEngine.connection != null) {
-      routeSelector.connectFailed(httpEngine.connection, e);
-    }
-
-    OutputStream requestBody = httpEngine.getRequestBody();
-    boolean canRetryRequestBody = requestBody == null
-        || requestBody instanceof RetryableOutputStream;
-    if (routeSelector == null && httpEngine.connection == null // No connection.
-        || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
-        || !isRecoverable(e)
-        || !canRetryRequestBody) {
+      // Give up; recovery is not possible.
       httpEngineFailure = e;
-      return false;
+      throw e;
     }
-
-    httpEngine.release(true);
-    RetryableOutputStream retryableOutputStream = (RetryableOutputStream) requestBody;
-    httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
-    httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
-    return true;
-  }
-
-  private boolean isRecoverable(IOException e) {
-    // If the problem was a CertificateException from the X509TrustManager,
-    // do not retry, we didn't have an abrupt server initiated exception.
-    boolean sslFailure =
-        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
-    boolean protocolFailure = e instanceof ProtocolException;
-    return !sslFailure && !protocolFailure;
-  }
-
-  public HttpEngine getHttpEngine() {
-    return httpEngine;
   }
 
   enum Retry {
@@ -413,12 +401,13 @@
 
   /**
    * Returns the retry action to take for the current response headers. The
-   * headers, proxy and target URL or this connection may be adjusted to
+   * headers, proxy and target URL for this connection may be adjusted to
    * prepare for a follow up request.
    */
   private Retry processResponseHeaders() throws IOException {
-    Proxy selectedProxy = httpEngine.connection != null
-        ? httpEngine.connection.getRoute().getProxy()
+    Connection connection = httpEngine.getConnection();
+    Proxy selectedProxy = connection != null
+        ? connection.getRoute().getProxy()
         : client.getProxy();
     final int responseCode = getResponseCode();
     switch (responseCode) {
@@ -428,10 +417,11 @@
         }
         // fall-through
       case HTTP_UNAUTHORIZED:
-        boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
-            getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
-            selectedProxy, url);
-        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
+        Request successorRequest = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
+            httpEngine.getResponse(), selectedProxy);
+        if (successorRequest == null) return Retry.NONE;
+        requestHeaders = successorRequest.getHeaders().newBuilder();
+        return Retry.SAME_CONNECTION;
 
       case HTTP_MULT_CHOICE:
       case HTTP_MOVED_PERM:
@@ -475,38 +465,30 @@
     }
   }
 
-  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
-  @Override public final long getFixedContentLength() {
-    return fixedContentLength;
-  }
-
-  @Override public final int getChunkLength() {
-    return chunkLength;
-  }
-
+  /**
+   * Returns true if either:
+   * <ul>
+   *   <li>A specific proxy was explicitly configured for this connection.
+   *   <li>The response has already been retrieved, and a proxy was {@link
+   *       java.net.ProxySelector selected} in order to get it.
+   * </ul>
+   *
+   * <p><strong>Warning:</strong> This method may return false before attempting
+   * to connect and true afterwards.
+   */
   @Override public final boolean usingProxy() {
-    if (selectedProxy != null) {
-      return isValidNonDirectProxy(selectedProxy);
-    }
-
-    // This behavior is a bit odd (but is probably justified by the
-    // oddness of the APIs involved). Before a connection is established,
-    // this method will return true only if this connection was explicitly
-    // opened with a Proxy. We don't attempt to query the ProxySelector
-    // at all.
-    return isValidNonDirectProxy(client.getProxy());
-  }
-
-  private static boolean isValidNonDirectProxy(Proxy proxy) {
+    Proxy proxy = route != null
+        ? route.getProxy()
+        : client.getProxy();
     return proxy != null && proxy.type() != Proxy.Type.DIRECT;
   }
 
   @Override public String getResponseMessage() throws IOException {
-    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
+    return getResponse().getResponse().statusMessage();
   }
 
   @Override public final int getResponseCode() throws IOException {
-    return getResponse().getResponseCode();
+    return getResponse().getResponse().code();
   }
 
   @Override public final void setRequestProperty(String field, String newValue) {
@@ -526,10 +508,20 @@
       return;
     }
 
-    if ("X-Android-Transports".equals(field)) {
-      setTransports(newValue, false /* append */);
+    // TODO: Deprecate use of X-Android-Transports header?
+    if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) {
+      setProtocols(newValue, false /* append */);
     } else {
-      rawRequestHeaders.set(field, newValue);
+      requestHeaders.set(field, newValue);
+    }
+  }
+
+  @Override public void setIfModifiedSince(long newValue) {
+    super.setIfModifiedSince(newValue);
+    if (ifModifiedSince != 0) {
+      requestHeaders.set("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince)));
+    } else {
+      requestHeaders.removeAll("If-Modified-Since");
     }
   }
 
@@ -550,26 +542,41 @@
       return;
     }
 
-    if ("X-Android-Transports".equals(field)) {
-      setTransports(value, true /* append */);
+    // TODO: Deprecate use of X-Android-Transports header?
+    if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) {
+      setProtocols(value, true /* append */);
     } else {
-      rawRequestHeaders.add(field, value);
+      requestHeaders.add(field, value);
     }
   }
 
   /*
-   * Splits and validates a comma-separated string of transports.
+   * Splits and validates a comma-separated string of protocols.
    * When append == false, we require that the transport list contains "http/1.1".
+   * Throws {@link IllegalStateException} when one of the protocols isn't
+   * defined in {@link Protocol OkHttp's protocol enumeration}.
    */
-  private void setTransports(String transportsString, boolean append) {
-    List<String> transportsList = new ArrayList<String>();
+  private void setProtocols(String protocolsString, boolean append) {
+    List<Protocol> protocolsList = new ArrayList<Protocol>();
     if (append) {
-      transportsList.addAll(client.getTransports());
+      protocolsList.addAll(client.getProtocols());
     }
-    for (String transport : transportsString.split(",", -1)) {
-      transportsList.add(transport);
+    for (String protocol : protocolsString.split(",", -1)) {
+      try {
+        protocolsList.add(Protocol.find(ByteString.encodeUtf8(protocol)));
+      } catch (IOException e) {
+        throw new IllegalStateException(e);
+      }
     }
-    client.setTransports(transportsList);
+    client.setProtocols(protocolsList);
+  }
+
+  @Override public void setRequestMethod(String method) throws ProtocolException {
+    if (!HttpMethod.METHODS.contains(method)) {
+      throw new ProtocolException(
+          "Expected one of " + HttpMethod.METHODS + " but was " + method);
+    }
+    this.method = method;
   }
 
   @Override public void setFixedLengthStreamingMode(int contentLength) {
@@ -584,8 +591,4 @@
     this.fixedContentLength = contentLength;
     super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
   }
-
-  @Override public final void setSelectedProxy(Proxy proxy) {
-    this.selectedProxy = proxy;
-  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java
deleted file mode 100644
index 2bc1d68..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.TunnelRequest;
-import java.io.IOException;
-import java.net.CacheResponse;
-import java.net.SecureCacheResponse;
-import java.net.URL;
-import javax.net.ssl.SSLSocket;
-
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-
-public final class HttpsEngine extends HttpEngine {
-  /**
-   * Stash of HttpsEngine.connection.socket to implement requests like {@code
-   * HttpsURLConnection#getCipherSuite} even after the connection has been
-   * recycled.
-   */
-  private SSLSocket sslSocket;
-
-  public HttpsEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders,
-      Connection connection, RetryableOutputStream requestBody) throws IOException {
-    super(client, policy, method, requestHeaders, connection, requestBody);
-    this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
-  }
-
-  @Override protected void connected(Connection connection) {
-    this.sslSocket = (SSLSocket) connection.getSocket();
-    super.connected(connection);
-  }
-
-  @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
-    return cacheResponse instanceof SecureCacheResponse;
-  }
-
-  @Override protected boolean includeAuthorityInRequestLine() {
-    // Even if there is a proxy, it isn't involved. Always request just the path.
-    return false;
-  }
-
-  public SSLSocket getSslSocket() {
-    return sslSocket;
-  }
-
-  @Override protected TunnelRequest getTunnelConfig() {
-    String userAgent = requestHeaders.getUserAgent();
-    if (userAgent == null) {
-      userAgent = getDefaultUserAgent();
-    }
-
-    URL url = policy.getURL();
-    return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
-        requestHeaders.getProxyAuthorization());
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index e8c656a..232e1ca 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -16,314 +16,35 @@
  */
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.Handshake;
 import com.squareup.okhttp.OkHttpClient;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.ProtocolException;
-import java.net.SecureCacheResponse;
 import java.net.URL;
-import java.security.Permission;
-import java.security.Principal;
-import java.security.cert.Certificate;
-import java.util.List;
-import java.util.Map;
 import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
-public final class HttpsURLConnectionImpl extends HttpsURLConnection {
-
-  /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
-  private final HttpUrlConnectionDelegate delegate;
+public final class HttpsURLConnectionImpl extends DelegatingHttpsURLConnection {
+  private final HttpURLConnectionImpl delegate;
 
   public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
-    super(url);
-    delegate = new HttpUrlConnectionDelegate(url, client);
+    this(new HttpURLConnectionImpl(url, client));
   }
 
-  @Override public String getCipherSuite() {
-    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
-    if (cacheResponse != null) {
-      return cacheResponse.getCipherSuite();
-    }
-    SSLSocket sslSocket = getSslSocket();
-    if (sslSocket != null) {
-      return sslSocket.getSession().getCipherSuite();
-    }
-    return null;
+  public HttpsURLConnectionImpl(HttpURLConnectionImpl delegate) {
+    super(delegate);
+    this.delegate = delegate;
   }
 
-  @Override public Certificate[] getLocalCertificates() {
-    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
-    if (cacheResponse != null) {
-      List<Certificate> result = cacheResponse.getLocalCertificateChain();
-      return result != null ? result.toArray(new Certificate[result.size()]) : null;
-    }
-    SSLSocket sslSocket = getSslSocket();
-    if (sslSocket != null) {
-      return sslSocket.getSession().getLocalCertificates();
-    }
-    return null;
-  }
-
-  @Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
-    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
-    if (cacheResponse != null) {
-      List<Certificate> result = cacheResponse.getServerCertificateChain();
-      return result != null ? result.toArray(new Certificate[result.size()]) : null;
-    }
-    SSLSocket sslSocket = getSslSocket();
-    if (sslSocket != null) {
-      return sslSocket.getSession().getPeerCertificates();
-    }
-    return null;
-  }
-
-  @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
-    if (cacheResponse != null) {
-      return cacheResponse.getPeerPrincipal();
-    }
-    SSLSocket sslSocket = getSslSocket();
-    if (sslSocket != null) {
-      return sslSocket.getSession().getPeerPrincipal();
-    }
-    return null;
-  }
-
-  @Override public Principal getLocalPrincipal() {
-    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
-    if (cacheResponse != null) {
-      return cacheResponse.getLocalPrincipal();
-    }
-    SSLSocket sslSocket = getSslSocket();
-    if (sslSocket != null) {
-      return sslSocket.getSession().getLocalPrincipal();
-    }
-    return null;
-  }
-
-  public HttpEngine getHttpEngine() {
-    return delegate.getHttpEngine();
-  }
-
-  private SSLSocket getSslSocket() {
-    if (delegate.httpEngine == null || !delegate.httpEngine.connected) {
+  @Override protected Handshake handshake() {
+    if (delegate.httpEngine == null) {
       throw new IllegalStateException("Connection has not yet been established");
     }
-    return delegate.httpEngine instanceof HttpsEngine
-        ? ((HttpsEngine) delegate.httpEngine).getSslSocket()
-        : null; // Not HTTPS! Probably an https:// to http:// redirect.
-  }
 
-  @Override public void disconnect() {
-    delegate.disconnect();
-  }
-
-  @Override public InputStream getErrorStream() {
-    return delegate.getErrorStream();
-  }
-
-  @Override public String getRequestMethod() {
-    return delegate.getRequestMethod();
-  }
-
-  @Override public int getResponseCode() throws IOException {
-    return delegate.getResponseCode();
-  }
-
-  @Override public String getResponseMessage() throws IOException {
-    return delegate.getResponseMessage();
-  }
-
-  @Override public void setRequestMethod(String method) throws ProtocolException {
-    delegate.setRequestMethod(method);
-  }
-
-  @Override public boolean usingProxy() {
-    return delegate.usingProxy();
-  }
-
-  @Override public boolean getInstanceFollowRedirects() {
-    return delegate.getInstanceFollowRedirects();
-  }
-
-  @Override public void setInstanceFollowRedirects(boolean followRedirects) {
-    delegate.setInstanceFollowRedirects(followRedirects);
-  }
-
-  @Override public void connect() throws IOException {
-    connected = true;
-    delegate.connect();
-  }
-
-  @Override public boolean getAllowUserInteraction() {
-    return delegate.getAllowUserInteraction();
-  }
-
-  @Override public Object getContent() throws IOException {
-    return delegate.getContent();
-  }
-
-  @SuppressWarnings("unchecked") // Spec does not generify
-  @Override public Object getContent(Class[] types) throws IOException {
-    return delegate.getContent(types);
-  }
-
-  @Override public String getContentEncoding() {
-    return delegate.getContentEncoding();
-  }
-
-  @Override public int getContentLength() {
-    return delegate.getContentLength();
-  }
-
-  @Override public String getContentType() {
-    return delegate.getContentType();
-  }
-
-  @Override public long getDate() {
-    return delegate.getDate();
-  }
-
-  @Override public boolean getDefaultUseCaches() {
-    return delegate.getDefaultUseCaches();
-  }
-
-  @Override public boolean getDoInput() {
-    return delegate.getDoInput();
-  }
-
-  @Override public boolean getDoOutput() {
-    return delegate.getDoOutput();
-  }
-
-  @Override public long getExpiration() {
-    return delegate.getExpiration();
-  }
-
-  @Override public String getHeaderField(int pos) {
-    return delegate.getHeaderField(pos);
-  }
-
-  @Override public Map<String, List<String>> getHeaderFields() {
-    return delegate.getHeaderFields();
-  }
-
-  @Override public Map<String, List<String>> getRequestProperties() {
-    return delegate.getRequestProperties();
-  }
-
-  @Override public void addRequestProperty(String field, String newValue) {
-    delegate.addRequestProperty(field, newValue);
-  }
-
-  @Override public String getHeaderField(String key) {
-    return delegate.getHeaderField(key);
-  }
-
-  @Override public long getHeaderFieldDate(String field, long defaultValue) {
-    return delegate.getHeaderFieldDate(field, defaultValue);
-  }
-
-  @Override public int getHeaderFieldInt(String field, int defaultValue) {
-    return delegate.getHeaderFieldInt(field, defaultValue);
-  }
-
-  @Override public String getHeaderFieldKey(int position) {
-    return delegate.getHeaderFieldKey(position);
-  }
-
-  @Override public long getIfModifiedSince() {
-    return delegate.getIfModifiedSince();
-  }
-
-  @Override public InputStream getInputStream() throws IOException {
-    return delegate.getInputStream();
-  }
-
-  @Override public long getLastModified() {
-    return delegate.getLastModified();
-  }
-
-  @Override public OutputStream getOutputStream() throws IOException {
-    return delegate.getOutputStream();
-  }
-
-  @Override public Permission getPermission() throws IOException {
-    return delegate.getPermission();
-  }
-
-  @Override public String getRequestProperty(String field) {
-    return delegate.getRequestProperty(field);
-  }
-
-  @Override public URL getURL() {
-    return delegate.getURL();
-  }
-
-  @Override public boolean getUseCaches() {
-    return delegate.getUseCaches();
-  }
-
-  @Override public void setAllowUserInteraction(boolean newValue) {
-    delegate.setAllowUserInteraction(newValue);
-  }
-
-  @Override public void setDefaultUseCaches(boolean newValue) {
-    delegate.setDefaultUseCaches(newValue);
-  }
-
-  @Override public void setDoInput(boolean newValue) {
-    delegate.setDoInput(newValue);
-  }
-
-  @Override public void setDoOutput(boolean newValue) {
-    delegate.setDoOutput(newValue);
-  }
-
-  @Override public void setIfModifiedSince(long newValue) {
-    delegate.setIfModifiedSince(newValue);
-  }
-
-  @Override public void setRequestProperty(String field, String newValue) {
-    delegate.setRequestProperty(field, newValue);
-  }
-
-  @Override public void setUseCaches(boolean newValue) {
-    delegate.setUseCaches(newValue);
-  }
-
-  @Override public void setConnectTimeout(int timeoutMillis) {
-    delegate.setConnectTimeout(timeoutMillis);
-  }
-
-  @Override public int getConnectTimeout() {
-    return delegate.getConnectTimeout();
-  }
-
-  @Override public void setReadTimeout(int timeoutMillis) {
-    delegate.setReadTimeout(timeoutMillis);
-  }
-
-  @Override public int getReadTimeout() {
-    return delegate.getReadTimeout();
-  }
-
-  @Override public String toString() {
-    return delegate.toString();
-  }
-
-  @Override public void setFixedLengthStreamingMode(int contentLength) {
-    delegate.setFixedLengthStreamingMode(contentLength);
-  }
-
-  @Override public void setChunkedStreamingMode(int chunkLength) {
-    delegate.setChunkedStreamingMode(chunkLength);
+    // If there's a response, get the handshake from there so that caching
+    // works. Otherwise get the handshake from the connection because we might
+    // have not connected yet.
+    return delegate.httpEngine.hasResponse()
+        ? delegate.httpEngine.getResponse().handshake()
+        : delegate.handshake;
   }
 
   @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
@@ -342,19 +63,7 @@
     return delegate.client.getSslSocketFactory();
   }
 
-  private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
-    private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
-      super(url, client);
-    }
-
-    @Override public HttpURLConnection getHttpConnectionToCache() {
-      return HttpsURLConnectionImpl.this;
-    }
-
-    public SecureCacheResponse getSecureCacheResponse() {
-      return httpEngine instanceof HttpsEngine
-          ? (SecureCacheResponse) httpEngine.getCacheResponse()
-          : null;
-    }
+  @Override public void setFixedLengthStreamingMode(long contentLength) {
+    delegate.setFixedLengthStreamingMode(contentLength);
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java
new file mode 100644
index 0000000..24ecea5
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Helper methods that convert between Java and OkHttp representations.
+ */
+public final class JavaApiConverter {
+
+  private JavaApiConverter() {
+  }
+
+  /**
+   * Creates an OkHttp {@link Response} using the supplied {@link URI} and {@link URLConnection}
+   * to supply the data. The URLConnection is assumed to already be connected.
+   */
+  public static Response createOkResponse(URI uri, URLConnection urlConnection) throws IOException {
+    HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+
+    Response.Builder okResponseBuilder = new Response.Builder();
+
+    // Request: Create one from the URL connection.
+    // A connected HttpURLConnection does not permit access to request headers.
+    Map<String, List<String>> requestHeaders = null;
+    Request okRequest = createOkRequest(uri, httpUrlConnection.getRequestMethod(), requestHeaders);
+    okResponseBuilder.request(okRequest);
+
+    // Status line
+    String statusLine = extractStatusLine(httpUrlConnection);
+    okResponseBuilder.statusLine(statusLine);
+
+    // Response headers
+    Headers okHeaders = extractOkResponseHeaders(httpUrlConnection);
+    okResponseBuilder.headers(okHeaders);
+
+    // Meta data: Defaulted
+    okResponseBuilder.setResponseSource(ResponseSource.NETWORK);
+
+    // Response body
+    Response.Body okBody = createOkBody(okHeaders, urlConnection.getInputStream());
+    okResponseBuilder.body(okBody);
+
+    // Handle SSL handshake information as needed.
+    if (httpUrlConnection instanceof HttpsURLConnection) {
+      HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) httpUrlConnection;
+
+      Certificate[] peerCertificates;
+      try {
+        peerCertificates = httpsUrlConnection.getServerCertificates();
+      } catch (SSLPeerUnverifiedException e) {
+        peerCertificates = null;
+      }
+
+      Certificate[] localCertificates = httpsUrlConnection.getLocalCertificates();
+
+      Handshake handshake = Handshake.get(
+          httpsUrlConnection.getCipherSuite(), nullSafeImmutableList(peerCertificates),
+          nullSafeImmutableList(localCertificates));
+      okResponseBuilder.handshake(handshake);
+    }
+
+    return okResponseBuilder.build();
+  }
+
+  /**
+   * Creates an OkHttp {@link Response} using the supplied {@link Request} and {@link CacheResponse}
+   * to supply the data.
+   */
+  static Response createOkResponse(Request request, CacheResponse javaResponse)
+      throws IOException {
+    Response.Builder okResponseBuilder = new Response.Builder();
+
+    // Request: Use the one provided.
+    okResponseBuilder.request(request);
+
+    // Status line: Java has this as one of the headers.
+    okResponseBuilder.statusLine(extractStatusLine(javaResponse));
+
+    // Response headers
+    Headers okHeaders = extractOkHeaders(javaResponse);
+    okResponseBuilder.headers(okHeaders);
+
+    // Meta data: Defaulted
+    okResponseBuilder.setResponseSource(ResponseSource.CACHE);
+
+    // Response body
+    Response.Body okBody = createOkBody(okHeaders, javaResponse.getBody());
+    okResponseBuilder.body(okBody);
+
+    // Handle SSL handshake information as needed.
+    if (javaResponse instanceof SecureCacheResponse) {
+      SecureCacheResponse javaSecureCacheResponse = (SecureCacheResponse) javaResponse;
+
+      // Handshake doesn't support null lists.
+      List<Certificate> peerCertificates;
+      try {
+        peerCertificates = javaSecureCacheResponse.getServerCertificateChain();
+      } catch (SSLPeerUnverifiedException e) {
+        peerCertificates = Collections.emptyList();
+      }
+      List<Certificate> localCertificates = javaSecureCacheResponse.getLocalCertificateChain();
+      if (localCertificates == null) {
+        localCertificates = Collections.emptyList();
+      }
+      Handshake handshake = Handshake.get(
+          javaSecureCacheResponse.getCipherSuite(), peerCertificates, localCertificates);
+      okResponseBuilder.handshake(handshake);
+    }
+
+    return okResponseBuilder.build();
+  }
+
+  /**
+   * Creates an OkHttp {@link Request} from the supplied information.
+   *
+   * <p>This method allows a {@code null} value for {@code requestHeaders} for situations
+   * where a connection is already connected and access to the headers has been lost.
+   * See {@link java.net.HttpURLConnection#getRequestProperties()} for details.
+   */
+  public static Request createOkRequest(
+      URI uri, String requestMethod, Map<String, List<String>> requestHeaders) {
+
+    Request.Builder builder = new Request.Builder()
+        .url(uri.toString())
+        .method(requestMethod, null);
+
+    if (requestHeaders != null) {
+      Headers headers = extractOkHeaders(requestHeaders);
+      builder.headers(headers);
+    }
+    return builder.build();
+  }
+
+  /**
+   * Creates a {@link java.net.CacheResponse} of the correct (sub)type using information
+   * gathered from the supplied {@link Response}.
+   */
+  public static CacheResponse createJavaCacheResponse(final Response response) {
+    final Headers headers = response.headers();
+    final Response.Body body = response.body();
+    if (response.request().isHttps()) {
+      final Handshake handshake = response.handshake();
+      return new SecureCacheResponse() {
+        @Override
+        public String getCipherSuite() {
+          return handshake != null ? handshake.cipherSuite() : null;
+        }
+
+        @Override
+        public List<Certificate> getLocalCertificateChain() {
+          if (handshake == null) return null;
+          // Java requires null, not an empty list here.
+          List<Certificate> certificates = handshake.localCertificates();
+          return certificates.size() > 0 ? certificates : null;
+        }
+
+        @Override
+        public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
+          if (handshake == null) return null;
+          // Java requires null, not an empty list here.
+          List<Certificate> certificates = handshake.peerCertificates();
+          return certificates.size() > 0 ? certificates : null;
+        }
+
+        @Override
+        public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+          if (handshake == null) return null;
+          return handshake.peerPrincipal();
+        }
+
+        @Override
+        public Principal getLocalPrincipal() {
+          if (handshake == null) return null;
+          return handshake.localPrincipal();
+        }
+
+        @Override
+        public Map<String, List<String>> getHeaders() throws IOException {
+          // Java requires that the entry with a null key be the status line.
+          return OkHeaders.toMultimap(headers, response.statusLine());
+        }
+
+        @Override
+        public InputStream getBody() throws IOException {
+          if (body == null) return null;
+          return body.byteStream();
+        }
+      };
+    } else {
+      return new CacheResponse() {
+        @Override
+        public Map<String, List<String>> getHeaders() throws IOException {
+          // Java requires that the entry with a null key be the status line.
+          return OkHeaders.toMultimap(headers, response.statusLine());
+        }
+
+        @Override
+        public InputStream getBody() throws IOException {
+          if (body == null) return null;
+          return body.byteStream();
+        }
+      };
+    }
+  }
+
+  /**
+   * Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp
+   * {@link Response}.
+   */
+  static HttpURLConnection createJavaUrlConnection(Response okResponse) {
+    Request request = okResponse.request();
+    // Create an object of the correct class in case the ResponseCache uses instanceof.
+    if (request.isHttps()) {
+      return new CacheHttpsURLConnection(new CacheHttpURLConnection(okResponse));
+    } else {
+      return new CacheHttpURLConnection(okResponse);
+    }
+  }
+
+  /**
+   * Extracts an immutable request header map from the supplied {@link com.squareup.okhttp.Headers}.
+   */
+  static Map<String, List<String>> extractJavaHeaders(Request request) {
+    return OkHeaders.toMultimap(request.headers(), null);
+  }
+
+  /**
+   * Extracts OkHttp headers from the supplied {@link java.net.CacheResponse}. Only real headers are
+   * extracted. See {@link #extractStatusLine(java.net.CacheResponse)}.
+   */
+  private static Headers extractOkHeaders(CacheResponse javaResponse) throws IOException {
+    Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
+    return extractOkHeaders(javaResponseHeaders);
+  }
+
+  /**
+   * Extracts OkHttp headers from the supplied {@link java.net.HttpURLConnection}. Only real headers
+   * are extracted. See {@link #extractStatusLine(java.net.HttpURLConnection)}.
+   */
+  private static Headers extractOkResponseHeaders(HttpURLConnection httpUrlConnection) {
+    Map<String, List<String>> javaResponseHeaders = httpUrlConnection.getHeaderFields();
+    return extractOkHeaders(javaResponseHeaders);
+  }
+
+  /**
+   * Extracts OkHttp headers from the supplied {@link Map}. Only real headers are
+   * extracted. Any entry (one with a {@code null} key) is discarded.
+   */
+  // @VisibleForTesting
+  static Headers extractOkHeaders(Map<String, List<String>> javaHeaders) {
+    Headers.Builder okHeadersBuilder = new Headers.Builder();
+    for (Map.Entry<String, List<String>> javaHeader : javaHeaders.entrySet()) {
+      String name = javaHeader.getKey();
+      if (name == null) {
+        // The Java API uses the null key to store the status line in responses.
+        // Earlier versions of OkHttp would use the null key to store the "request line" in
+        // requests. e.g. "GET / HTTP 1.1". Although this is no longer the case it must be
+        // explicitly ignored because Headers.Builder does not support null keys.
+        continue;
+      }
+      for (String value : javaHeader.getValue()) {
+        okHeadersBuilder.add(name, value);
+      }
+    }
+    return okHeadersBuilder.build();
+  }
+
+  /**
+   * Extracts the status line from the supplied Java API {@link java.net.HttpURLConnection}.
+   * As per the spec, the status line is held as the header with the null key. Returns {@code null}
+   * if there is no status line.
+   */
+  private static String extractStatusLine(HttpURLConnection httpUrlConnection) {
+    // Java specifies that this will be be response header with a null key.
+    return httpUrlConnection.getHeaderField(null);
+  }
+
+  /**
+   * Extracts the status line from the supplied Java API {@link java.net.CacheResponse}.
+   * As per the spec, the status line is held as the header with the null key. Returns {@code null}
+   * if there is no status line.
+   */
+  private static String extractStatusLine(CacheResponse javaResponse) throws IOException {
+    Map<String, List<String>> javaResponseHeaders = javaResponse.getHeaders();
+    return extractStatusLine(javaResponseHeaders);
+  }
+
+  // VisibleForTesting
+  static String extractStatusLine(Map<String, List<String>> javaResponseHeaders) {
+    List<String> values = javaResponseHeaders.get(null);
+    if (values == null || values.size() == 0) {
+      return null;
+    }
+    return values.get(0);
+  }
+
+  /**
+   * Creates an OkHttp Response.Body containing the supplied information.
+   */
+  private static Response.Body createOkBody(final Headers okHeaders, final InputStream body) {
+    return new Response.Body() {
+
+      @Override
+      public boolean ready() throws IOException {
+        return true;
+      }
+
+      @Override
+      public MediaType contentType() {
+        String contentTypeHeader = okHeaders.get("Content-Type");
+        return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
+      }
+
+      @Override
+      public long contentLength() {
+        return OkHeaders.contentLength(okHeaders);
+      }
+
+      @Override
+      public InputStream byteStream() {
+        return body;
+      }
+    };
+  }
+
+  /**
+   * An {@link java.net.HttpURLConnection} that represents an HTTP request at the point where
+   * the request has been made, and the response headers have been received, but the body content,
+   * if present, has not been read yet. This intended to provide enough information for
+   * {@link java.net.ResponseCache} subclasses and no more.
+   *
+   * <p>Much of the method implementations are overrides to delegate to the OkHttp request and
+   * response, or to deny access to information as a real HttpURLConnection would after connection.
+   */
+  private static final class CacheHttpURLConnection extends HttpURLConnection {
+
+    private final Request request;
+    private final Response response;
+
+    public CacheHttpURLConnection(Response response) {
+      super(response.request().url());
+      this.request = response.request();
+      this.response = response;
+
+      // Configure URLConnection inherited fields.
+      this.connected = true;
+      this.doOutput = response.body() == null;
+
+      // Configure HttpUrlConnection inherited fields.
+      this.method = request.method();
+    }
+
+    // HTTP connection lifecycle methods
+
+    @Override
+    public void connect() throws IOException {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void disconnect() {
+      throw throwRequestModificationException();
+    }
+
+    // HTTP Request methods
+
+    @Override
+    public void setRequestProperty(String key, String value) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void addRequestProperty(String key, String value) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public String getRequestProperty(String key) {
+      return request.header(key);
+    }
+
+    @Override
+    public Map<String, List<String>> getRequestProperties() {
+      // This is to preserve RI and compatibility with OkHttp's HttpURLConnectionImpl. There seems
+      // no good reason why this should fail while getRequestProperty() is ok.
+      throw throwRequestHeaderAccessException();
+    }
+
+    @Override
+    public void setFixedLengthStreamingMode(int contentLength) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void setFixedLengthStreamingMode(long contentLength) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void setChunkedStreamingMode(int chunklen) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void setInstanceFollowRedirects(boolean followRedirects) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public boolean getInstanceFollowRedirects() {
+      // Return the platform default.
+      return super.getInstanceFollowRedirects();
+    }
+
+    @Override
+    public void setRequestMethod(String method) throws ProtocolException {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public String getRequestMethod() {
+      return request.method();
+    }
+
+    // HTTP Response methods
+
+    @Override
+    public String getHeaderFieldKey(int position) {
+      // Deal with index 0 meaning "status line"
+      if (position < 0) {
+        throw new IllegalArgumentException("Invalid header index: " + position);
+      }
+      if (position == 0) {
+        return null;
+      }
+      return response.headers().name(position - 1);
+    }
+
+    @Override
+    public String getHeaderField(int position) {
+      // Deal with index 0 meaning "status line"
+      if (position < 0) {
+        throw new IllegalArgumentException("Invalid header index: " + position);
+      }
+      if (position == 0) {
+        return response.statusLine();
+      }
+      return response.headers().value(position - 1);
+    }
+
+    @Override
+    public String getHeaderField(String fieldName) {
+      return fieldName == null ? response.statusLine() : response.headers().get(fieldName);
+    }
+
+    @Override
+    public Map<String, List<String>> getHeaderFields() {
+      return OkHeaders.toMultimap(response.headers(), response.statusLine());
+    }
+
+    @Override
+    public int getResponseCode() throws IOException {
+      return response.code();
+    }
+
+    @Override
+    public String getResponseMessage() throws IOException {
+      return response.statusMessage();
+    }
+
+    @Override
+    public InputStream getErrorStream() {
+      return null;
+    }
+
+    // HTTP miscellaneous methods
+
+    @Override
+    public boolean usingProxy() {
+      // It's safe to return false here, even if a proxy is in use. The problem is we don't
+      // necessarily know if we're going to use a proxy by the time we ask the cache for a response.
+      return false;
+    }
+
+    // URLConnection methods
+
+    @Override
+    public void setConnectTimeout(int timeout) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public int getConnectTimeout() {
+      // Impossible to say.
+      return 0;
+    }
+
+    @Override
+    public void setReadTimeout(int timeout) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public int getReadTimeout() {
+      // Impossible to say.
+      return 0;
+    }
+
+    @Override
+    public Object getContent() throws IOException {
+      throw throwResponseBodyAccessException();
+    }
+
+    @Override
+    public Object getContent(Class[] classes) throws IOException {
+      throw throwResponseBodyAccessException();
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+      throw throwResponseBodyAccessException();
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public void setDoInput(boolean doInput) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public boolean getDoInput() {
+      return true;
+    }
+
+    @Override
+    public void setDoOutput(boolean doOutput) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public boolean getDoOutput() {
+      return request.body() != null;
+    }
+
+    @Override
+    public void setAllowUserInteraction(boolean allowUserInteraction) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public boolean getAllowUserInteraction() {
+      return false;
+    }
+
+    @Override
+    public void setUseCaches(boolean useCaches) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public boolean getUseCaches() {
+      return super.getUseCaches();
+    }
+
+    @Override
+    public void setIfModifiedSince(long ifModifiedSince) {
+      throw throwRequestModificationException();
+    }
+
+    @Override
+    public long getIfModifiedSince() {
+      return 0;
+    }
+
+    @Override
+    public boolean getDefaultUseCaches() {
+      return super.getDefaultUseCaches();
+    }
+
+    @Override
+    public void setDefaultUseCaches(boolean defaultUseCaches) {
+      super.setDefaultUseCaches(defaultUseCaches);
+    }
+  }
+
+  /** An HttpsURLConnection to offer to the cache. */
+  private static final class CacheHttpsURLConnection extends DelegatingHttpsURLConnection {
+    private final CacheHttpURLConnection delegate;
+
+    public CacheHttpsURLConnection(CacheHttpURLConnection delegate) {
+      super(delegate);
+      this.delegate = delegate;
+    }
+
+    @Override protected Handshake handshake() {
+      return delegate.response.handshake();
+    }
+
+    @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+      throw throwRequestModificationException();
+    }
+
+    @Override public HostnameVerifier getHostnameVerifier() {
+      throw throwRequestSslAccessException();
+    }
+
+    @Override public void setSSLSocketFactory(SSLSocketFactory socketFactory) {
+      throw throwRequestModificationException();
+    }
+
+    @Override public SSLSocketFactory getSSLSocketFactory() {
+      throw throwRequestSslAccessException();
+    }
+
+    @Override public void setFixedLengthStreamingMode(long contentLength) {
+      delegate.setFixedLengthStreamingMode(contentLength);
+    }
+  }
+
+  private static RuntimeException throwRequestModificationException() {
+    throw new UnsupportedOperationException("ResponseCache cannot modify the request.");
+  }
+
+  private static RuntimeException throwRequestHeaderAccessException() {
+    throw new UnsupportedOperationException("ResponseCache cannot access request headers");
+  }
+
+  private static RuntimeException throwRequestSslAccessException() {
+    throw new UnsupportedOperationException("ResponseCache cannot access SSL internals");
+  }
+
+  private static RuntimeException throwResponseBodyAccessException() {
+    throw new UnsupportedOperationException("ResponseCache cannot access the response body.");
+  }
+
+  private static <T> List<T> nullSafeImmutableList(T[] elements) {
+    return elements == null ? Collections.<T>emptyList() : Util.immutableList(elements);
+  }
+
+}
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
new file mode 100644
index 0000000..456c9c7
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
@@ -0,0 +1,128 @@
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.Platform;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** Headers and utilities for internal use by OkHttp. */
+public final class OkHeaders {
+  private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+    // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
+    @Override public int compare(String a, String b) {
+      if (a == b) {
+        return 0;
+      } else if (a == null) {
+        return -1;
+      } else if (b == null) {
+        return 1;
+      } else {
+        return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+      }
+    }
+  };
+
+  static final String PREFIX = Platform.get().getPrefix();
+
+  /**
+   * Synthetic response header: the local time when the request was sent.
+   */
+  public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";
+
+  /**
+   * Synthetic response header: the local time when the response was received.
+   */
+  public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
+
+  /**
+   * Synthetic response header: the response source and status code like
+   * "CONDITIONAL_CACHE 304".
+   */
+  public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
+
+  /**
+   * Synthetic response header: the selected
+   * {@link com.squareup.okhttp.Protocol protocol} ("spdy/3.1", "http/1.1", etc).
+   */
+  public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
+
+  private OkHeaders() {
+  }
+
+  public static long contentLength(Request request) {
+    return contentLength(request.headers());
+  }
+
+  public static long contentLength(Response response) {
+    return contentLength(response.headers());
+  }
+
+  public static long contentLength(Headers headers) {
+    return stringToLong(headers.get("Content-Length"));
+  }
+
+  private static long stringToLong(String s) {
+    if (s == null) return -1;
+    try {
+      return Long.parseLong(s);
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+
+  /**
+   * Returns an immutable map containing each field to its list of values.
+   *
+   * @param valueForNullKey the request line for requests, or the status line
+   *     for responses. If non-null, this value is mapped to the null key.
+   */
+  public static Map<String, List<String>> toMultimap(Headers headers, String valueForNullKey) {
+    Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+    for (int i = 0; i < headers.size(); i++) {
+      String fieldName = headers.name(i);
+      String value = headers.value(i);
+
+      List<String> allValues = new ArrayList<String>();
+      List<String> otherValues = result.get(fieldName);
+      if (otherValues != null) {
+        allValues.addAll(otherValues);
+      }
+      allValues.add(value);
+      result.put(fieldName, Collections.unmodifiableList(allValues));
+    }
+    if (valueForNullKey != null) {
+      result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
+  public static void addCookies(Request.Builder builder, Map<String, List<String>> cookieHeaders) {
+    for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
+      String key = entry.getKey();
+      if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
+          && !entry.getValue().isEmpty()) {
+        builder.addHeader(key, buildCookieHeader(entry.getValue()));
+      }
+    }
+  }
+
+  /**
+   * Send all cookies in one big header, as recommended by
+   * <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
+   */
+  private static String buildCookieHeader(List<String> cookies) {
+    if (cookies.size() == 1) return cookies.get(0);
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < cookies.size(); i++) {
+      if (i > 0) sb.append("; ");
+      sb.append(cookies.get(i));
+    }
+    return sb.toString();
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
deleted file mode 100644
index 5335c2b..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.OkResponseCache;
-import com.squareup.okhttp.ResponseSource;
-import java.io.IOException;
-import java.net.CacheRequest;
-import java.net.CacheResponse;
-import java.net.HttpURLConnection;
-import java.net.ResponseCache;
-import java.net.URI;
-import java.net.URLConnection;
-import java.util.List;
-import java.util.Map;
-
-public final class OkResponseCacheAdapter implements OkResponseCache {
-  private final ResponseCache responseCache;
-  public OkResponseCacheAdapter(ResponseCache responseCache) {
-    this.responseCache = responseCache;
-  }
-
-  @Override public CacheResponse get(URI uri, String requestMethod,
-      Map<String, List<String>> requestHeaders) throws IOException {
-    return responseCache.get(uri, requestMethod, requestHeaders);
-  }
-
-  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
-    return responseCache.put(uri, urlConnection);
-  }
-
-  @Override public void maybeRemove(String requestMethod, URI uri) throws IOException {
-  }
-
-  @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
-      throws IOException {
-  }
-
-  @Override public void trackConditionalCacheHit() {
-  }
-
-  @Override public void trackResponse(ResponseSource source) {
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java
deleted file mode 100644
index 0a29d4b..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.URL;
-
-public interface Policy {
-  /** Returns true if HTTP response caches should be used. */
-  boolean getUseCaches();
-
-  /** Returns the HttpURLConnection instance to store in the cache. */
-  HttpURLConnection getHttpConnectionToCache();
-
-  /** Returns the current destination URL, possibly a redirect. */
-  URL getURL();
-
-  /** Returns the If-Modified-Since timestamp, or 0 if none is set. */
-  long getIfModifiedSince();
-
-  /** Returns true if a non-direct proxy is specified. */
-  boolean usingProxy();
-
-  /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
-  int getChunkLength();
-
-  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
-  long getFixedContentLength();
-
-  /**
-   * Sets the current proxy that this connection is using.
-   * @see java.net.HttpURLConnection#usingProxy
-   */
-  void setSelectedProxy(Proxy proxy);
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
deleted file mode 100644
index 8b45320..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ /dev/null
@@ -1,447 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.internal.Util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.ProtocolException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-/**
- * The HTTP status and unparsed header fields of a single HTTP message. Values
- * are represented as uninterpreted strings; use {@link RequestHeaders} and
- * {@link ResponseHeaders} for interpreted headers. This class maintains the
- * order of the header fields within the HTTP message.
- *
- * <p>This class tracks fields line-by-line. A field with multiple comma-
- * separated values on the same line will be treated as a field with a single
- * value by this class. It is the caller's responsibility to detect and split
- * on commas if their field permits multiple values. This simplifies use of
- * single-valued fields whose values routinely contain commas, such as cookies
- * or dates.
- *
- * <p>This class trims whitespace from values. It never returns values with
- * leading or trailing whitespace.
- */
-public final class RawHeaders {
-  private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
-    // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
-    @Override public int compare(String a, String b) {
-      if (a == b) {
-        return 0;
-      } else if (a == null) {
-        return -1;
-      } else if (b == null) {
-        return 1;
-      } else {
-        return String.CASE_INSENSITIVE_ORDER.compare(a, b);
-      }
-    }
-  };
-
-  private final List<String> namesAndValues = new ArrayList<String>(20);
-  private String requestLine;
-  private String statusLine;
-  private int httpMinorVersion = 1;
-  private int responseCode = -1;
-  private String responseMessage;
-
-  public RawHeaders() {
-  }
-
-  public RawHeaders(RawHeaders copyFrom) {
-    namesAndValues.addAll(copyFrom.namesAndValues);
-    requestLine = copyFrom.requestLine;
-    statusLine = copyFrom.statusLine;
-    httpMinorVersion = copyFrom.httpMinorVersion;
-    responseCode = copyFrom.responseCode;
-    responseMessage = copyFrom.responseMessage;
-  }
-
-  /** Sets the request line (like "GET / HTTP/1.1"). */
-  public void setRequestLine(String requestLine) {
-    requestLine = requestLine.trim();
-    this.requestLine = requestLine;
-  }
-
-  /** Sets the response status line (like "HTTP/1.0 200 OK"). */
-  public void setStatusLine(String statusLine) throws IOException {
-    // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
-    // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
-    if (this.responseMessage != null) {
-      throw new IllegalStateException("statusLine is already set");
-    }
-    // We allow empty message without leading white space since some servers
-    // do not send the white space when the message is empty.
-    boolean hasMessage = statusLine.length() > 13;
-    if (!statusLine.startsWith("HTTP/1.")
-        || statusLine.length() < 12
-        || statusLine.charAt(8) != ' '
-        || (hasMessage && statusLine.charAt(12) != ' ')) {
-      throw new ProtocolException("Unexpected status line: " + statusLine);
-    }
-    int httpMinorVersion = statusLine.charAt(7) - '0';
-    if (httpMinorVersion < 0 || httpMinorVersion > 9) {
-      throw new ProtocolException("Unexpected status line: " + statusLine);
-    }
-    int responseCode;
-    try {
-      responseCode = Integer.parseInt(statusLine.substring(9, 12));
-    } catch (NumberFormatException e) {
-      throw new ProtocolException("Unexpected status line: " + statusLine);
-    }
-    this.responseMessage = hasMessage ? statusLine.substring(13) : "";
-    this.responseCode = responseCode;
-    this.statusLine = statusLine;
-    this.httpMinorVersion = httpMinorVersion;
-  }
-
-  /**
-   * @param method like "GET", "POST", "HEAD", etc.
-   * @param path like "/foo/bar.html"
-   * @param version like "HTTP/1.1"
-   * @param host like "www.android.com:1234"
-   * @param scheme like "https"
-   */
-  public void addSpdyRequestHeaders(String method, String path, String version, String host,
-      String scheme) {
-    // TODO: populate the statusLine for the client's benefit?
-    add(":method", method);
-    add(":scheme", scheme);
-    add(":path", path);
-    add(":version", version);
-    add(":host", host);
-  }
-
-  public String getStatusLine() {
-    return statusLine;
-  }
-
-  /**
-   * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
-   * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
-   */
-  public int getHttpMinorVersion() {
-    return httpMinorVersion != -1 ? httpMinorVersion : 1;
-  }
-
-  /** Returns the HTTP status code or -1 if it is unknown. */
-  public int getResponseCode() {
-    return responseCode;
-  }
-
-  /** Returns the HTTP status message or null if it is unknown. */
-  public String getResponseMessage() {
-    return responseMessage;
-  }
-
-  /**
-   * Add an HTTP header line containing a field name, a literal colon, and a
-   * value. This works around empty header names and header names that start
-   * with a colon (created by old broken SPDY versions of the response cache).
-   */
-  public void addLine(String line) {
-    int index = line.indexOf(":", 1);
-    if (index != -1) {
-      addLenient(line.substring(0, index), line.substring(index + 1));
-    } else if (line.startsWith(":")) {
-      addLenient("", line.substring(1)); // Empty header name.
-    } else {
-      addLenient("", line); // No header name.
-    }
-  }
-
-  /** Add a field with the specified value. */
-  public void add(String fieldName, String value) {
-    if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
-    if (value == null) throw new IllegalArgumentException("value == null");
-    if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
-      throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
-    }
-    addLenient(fieldName, value);
-  }
-
-  /**
-   * Add a field with the specified value without any validation. Only
-   * appropriate for headers from the remote peer.
-   */
-  private void addLenient(String fieldName, String value) {
-    namesAndValues.add(fieldName);
-    namesAndValues.add(value.trim());
-  }
-
-  public void removeAll(String fieldName) {
-    for (int i = 0; i < namesAndValues.size(); i += 2) {
-      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
-        namesAndValues.remove(i); // field name
-        namesAndValues.remove(i); // value
-      }
-    }
-  }
-
-  public void addAll(String fieldName, List<String> headerFields) {
-    for (String value : headerFields) {
-      add(fieldName, value);
-    }
-  }
-
-  /**
-   * Set a field with the specified value. If the field is not found, it is
-   * added. If the field is found, the existing values are replaced.
-   */
-  public void set(String fieldName, String value) {
-    removeAll(fieldName);
-    add(fieldName, value);
-  }
-
-  /** Returns the number of field values. */
-  public int length() {
-    return namesAndValues.size() / 2;
-  }
-
-  /** Returns the field at {@code position} or null if that is out of range. */
-  public String getFieldName(int index) {
-    int fieldNameIndex = index * 2;
-    if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
-      return null;
-    }
-    return namesAndValues.get(fieldNameIndex);
-  }
-
-  /** Returns an immutable case-insensitive set of header names. */
-  public Set<String> names() {
-    TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
-    for (int i = 0; i < length(); i++) {
-      result.add(getFieldName(i));
-    }
-    return Collections.unmodifiableSet(result);
-  }
-
-  /** Returns the value at {@code index} or null if that is out of range. */
-  public String getValue(int index) {
-    int valueIndex = index * 2 + 1;
-    if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
-      return null;
-    }
-    return namesAndValues.get(valueIndex);
-  }
-
-  /** Returns the last value corresponding to the specified field, or null. */
-  public String get(String fieldName) {
-    for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
-      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
-        return namesAndValues.get(i + 1);
-      }
-    }
-    return null;
-  }
-
-  /** Returns an immutable list of the header values for {@code name}. */
-  public List<String> values(String name) {
-    List<String> result = null;
-    for (int i = 0; i < length(); i++) {
-      if (name.equalsIgnoreCase(getFieldName(i))) {
-        if (result == null) result = new ArrayList<String>(2);
-        result.add(getValue(i));
-      }
-    }
-    return result != null
-        ? Collections.unmodifiableList(result)
-        : Collections.<String>emptyList();
-  }
-
-  /** @param fieldNames a case-insensitive set of HTTP header field names. */
-  public RawHeaders getAll(Set<String> fieldNames) {
-    RawHeaders result = new RawHeaders();
-    for (int i = 0; i < namesAndValues.size(); i += 2) {
-      String fieldName = namesAndValues.get(i);
-      if (fieldNames.contains(fieldName)) {
-        result.add(fieldName, namesAndValues.get(i + 1));
-      }
-    }
-    return result;
-  }
-
-  /** Returns bytes of a request header for sending on an HTTP transport. */
-  public byte[] toBytes() throws UnsupportedEncodingException {
-    StringBuilder result = new StringBuilder(256);
-    result.append(requestLine).append("\r\n");
-    for (int i = 0; i < namesAndValues.size(); i += 2) {
-      result.append(namesAndValues.get(i))
-          .append(": ")
-          .append(namesAndValues.get(i + 1))
-          .append("\r\n");
-    }
-    result.append("\r\n");
-    return result.toString().getBytes("ISO-8859-1");
-  }
-
-  /** Parses bytes of a response header from an HTTP transport. */
-  public static RawHeaders fromBytes(InputStream in) throws IOException {
-    RawHeaders headers;
-    do {
-      headers = new RawHeaders();
-      headers.setStatusLine(Util.readAsciiLine(in));
-      readHeaders(in, headers);
-    } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
-    return headers;
-  }
-
-  /** Reads headers or trailers into {@code out}. */
-  public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
-    // parse the result headers until the first blank line
-    String line;
-    while ((line = Util.readAsciiLine(in)).length() != 0) {
-      out.addLine(line);
-    }
-  }
-
-  /**
-   * Returns an immutable map containing each field to its list of values. The
-   * status line is mapped to null.
-   */
-  public Map<String, List<String>> toMultimap(boolean response) {
-    Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
-    for (int i = 0; i < namesAndValues.size(); i += 2) {
-      String fieldName = namesAndValues.get(i);
-      String value = namesAndValues.get(i + 1);
-
-      List<String> allValues = new ArrayList<String>();
-      List<String> otherValues = result.get(fieldName);
-      if (otherValues != null) {
-        allValues.addAll(otherValues);
-      }
-      allValues.add(value);
-      result.put(fieldName, Collections.unmodifiableList(allValues));
-    }
-    if (response && statusLine != null) {
-      result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
-    } else if (requestLine != null) {
-      result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine)));
-    }
-    return Collections.unmodifiableMap(result);
-  }
-
-  /**
-   * Creates a new instance from the given map of fields to values. If
-   * present, the null field's last element will be used to set the status
-   * line.
-   */
-  public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
-      throws IOException {
-    if (!response) throw new UnsupportedOperationException();
-    RawHeaders result = new RawHeaders();
-    for (Entry<String, List<String>> entry : map.entrySet()) {
-      String fieldName = entry.getKey();
-      List<String> values = entry.getValue();
-      if (fieldName != null) {
-        for (String value : values) {
-          result.addLenient(fieldName, value);
-        }
-      } else if (!values.isEmpty()) {
-        result.setStatusLine(values.get(values.size() - 1));
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Returns a list of alternating names and values. Names are all lower case.
-   * No names are repeated. If any name has multiple values, they are
-   * concatenated using "\0" as a delimiter.
-   */
-  public List<String> toNameValueBlock() {
-    Set<String> names = new HashSet<String>();
-    List<String> result = new ArrayList<String>();
-    for (int i = 0; i < namesAndValues.size(); i += 2) {
-      String name = namesAndValues.get(i).toLowerCase(Locale.US);
-      String value = namesAndValues.get(i + 1);
-
-      // Drop headers that are forbidden when layering HTTP over SPDY.
-      if (name.equals("connection")
-          || name.equals("host")
-          || name.equals("keep-alive")
-          || name.equals("proxy-connection")
-          || name.equals("transfer-encoding")) {
-        continue;
-      }
-
-      // If we haven't seen this name before, add the pair to the end of the list...
-      if (names.add(name)) {
-        result.add(name);
-        result.add(value);
-        continue;
-      }
-
-      // ...otherwise concatenate the existing values and this value.
-      for (int j = 0; j < result.size(); j += 2) {
-        if (name.equals(result.get(j))) {
-          result.set(j + 1, result.get(j + 1) + "\0" + value);
-          break;
-        }
-      }
-    }
-    return result;
-  }
-
-  /** Returns headers for a name value block containing a SPDY response. */
-  public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) throws IOException {
-    if (nameValueBlock.size() % 2 != 0) {
-      throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
-    }
-    String status = null;
-    String version = null;
-    RawHeaders result = new RawHeaders();
-    for (int i = 0; i < nameValueBlock.size(); i += 2) {
-      String name = nameValueBlock.get(i);
-      String values = nameValueBlock.get(i + 1);
-      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 (":status".equals(name)) {
-          status = value;
-        } else if (":version".equals(name)) {
-          version = value;
-        } else {
-          result.namesAndValues.add(name);
-          result.namesAndValues.add(value);
-        }
-        start = end + 1;
-      }
-    }
-    if (status == null) throw new ProtocolException("Expected ':status' header not present");
-    if (version == null) throw new ProtocolException("Expected ':version' header not present");
-    result.setStatusLine(version + " " + status);
-    return result;
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
deleted file mode 100644
index 71c3cd0..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ /dev/null
@@ -1,317 +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.http;
-
-import java.net.URI;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-/** Parsed HTTP request headers. */
-public final class RequestHeaders {
-  private final URI uri;
-  private final RawHeaders headers;
-
-  /** Don't use a cache to satisfy this request. */
-  private boolean noCache;
-  private int maxAgeSeconds = -1;
-  private int maxStaleSeconds = -1;
-  private int minFreshSeconds = -1;
-
-  /**
-   * This field's name "only-if-cached" is misleading. It actually means "do
-   * not use the network". It is set by a client who only wants to make a
-   * request if it can be fully satisfied by the cache. Cached responses that
-   * would require validation (ie. conditional gets) are not permitted if this
-   * header is set.
-   */
-  private boolean onlyIfCached;
-
-  /**
-   * True if the request contains an authorization field. Although this isn't
-   * necessarily a shared cache, it follows the spec's strict requirements for
-   * shared caches.
-   */
-  private boolean hasAuthorization;
-
-  private long contentLength = -1;
-  private String transferEncoding;
-  private String userAgent;
-  private String host;
-  private String connection;
-  private String acceptEncoding;
-  private String contentType;
-  private String ifModifiedSince;
-  private String ifNoneMatch;
-  private String proxyAuthorization;
-
-  public RequestHeaders(URI uri, RawHeaders headers) {
-    this.uri = uri;
-    this.headers = headers;
-
-    HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
-      @Override public void handle(String directive, String parameter) {
-        if ("no-cache".equalsIgnoreCase(directive)) {
-          noCache = true;
-        } else if ("max-age".equalsIgnoreCase(directive)) {
-          maxAgeSeconds = HeaderParser.parseSeconds(parameter);
-        } else if ("max-stale".equalsIgnoreCase(directive)) {
-          maxStaleSeconds = HeaderParser.parseSeconds(parameter);
-        } else if ("min-fresh".equalsIgnoreCase(directive)) {
-          minFreshSeconds = HeaderParser.parseSeconds(parameter);
-        } else if ("only-if-cached".equalsIgnoreCase(directive)) {
-          onlyIfCached = true;
-        }
-      }
-    };
-
-    for (int i = 0; i < headers.length(); i++) {
-      String fieldName = headers.getFieldName(i);
-      String value = headers.getValue(i);
-      if ("Cache-Control".equalsIgnoreCase(fieldName)) {
-        HeaderParser.parseCacheControl(value, handler);
-      } else if ("Pragma".equalsIgnoreCase(fieldName)) {
-        if ("no-cache".equalsIgnoreCase(value)) {
-          noCache = true;
-        }
-      } else if ("If-None-Match".equalsIgnoreCase(fieldName)) {
-        ifNoneMatch = value;
-      } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) {
-        ifModifiedSince = value;
-      } else if ("Authorization".equalsIgnoreCase(fieldName)) {
-        hasAuthorization = true;
-      } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
-        try {
-          contentLength = Integer.parseInt(value);
-        } catch (NumberFormatException ignored) {
-        }
-      } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
-        transferEncoding = value;
-      } else if ("User-Agent".equalsIgnoreCase(fieldName)) {
-        userAgent = value;
-      } else if ("Host".equalsIgnoreCase(fieldName)) {
-        host = value;
-      } else if ("Connection".equalsIgnoreCase(fieldName)) {
-        connection = value;
-      } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) {
-        acceptEncoding = value;
-      } else if ("Content-Type".equalsIgnoreCase(fieldName)) {
-        contentType = value;
-      } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
-        proxyAuthorization = value;
-      }
-    }
-  }
-
-  public boolean isChunked() {
-    return "chunked".equalsIgnoreCase(transferEncoding);
-  }
-
-  public boolean hasConnectionClose() {
-    return "close".equalsIgnoreCase(connection);
-  }
-
-  public URI getUri() {
-    return uri;
-  }
-
-  public RawHeaders getHeaders() {
-    return headers;
-  }
-
-  public boolean isNoCache() {
-    return noCache;
-  }
-
-  public int getMaxAgeSeconds() {
-    return maxAgeSeconds;
-  }
-
-  public int getMaxStaleSeconds() {
-    return maxStaleSeconds;
-  }
-
-  public int getMinFreshSeconds() {
-    return minFreshSeconds;
-  }
-
-  public boolean isOnlyIfCached() {
-    return onlyIfCached;
-  }
-
-  public boolean hasAuthorization() {
-    return hasAuthorization;
-  }
-
-  public long getContentLength() {
-    return contentLength;
-  }
-
-  public String getTransferEncoding() {
-    return transferEncoding;
-  }
-
-  public String getUserAgent() {
-    return userAgent;
-  }
-
-  public String getHost() {
-    return host;
-  }
-
-  public String getConnection() {
-    return connection;
-  }
-
-  public String getAcceptEncoding() {
-    return acceptEncoding;
-  }
-
-  public String getContentType() {
-    return contentType;
-  }
-
-  public String getIfModifiedSince() {
-    return ifModifiedSince;
-  }
-
-  public String getIfNoneMatch() {
-    return ifNoneMatch;
-  }
-
-  public String getProxyAuthorization() {
-    return proxyAuthorization;
-  }
-
-  public void setChunked() {
-    if (this.transferEncoding != null) {
-      headers.removeAll("Transfer-Encoding");
-    }
-    headers.add("Transfer-Encoding", "chunked");
-    this.transferEncoding = "chunked";
-  }
-
-  public void setContentLength(long contentLength) {
-    if (this.contentLength != -1) {
-      headers.removeAll("Content-Length");
-    }
-    headers.add("Content-Length", Long.toString(contentLength));
-    this.contentLength = contentLength;
-  }
-
-  /**
-   * Remove the Content-Length headers. Call this when dropping the body on a
-   * request or response, such as when a redirect changes the method from POST
-   * to GET.
-   */
-  public void removeContentLength() {
-    if (contentLength != -1) {
-      headers.removeAll("Content-Length");
-      contentLength = -1;
-    }
-  }
-
-  public void setUserAgent(String userAgent) {
-    if (this.userAgent != null) {
-      headers.removeAll("User-Agent");
-    }
-    headers.add("User-Agent", userAgent);
-    this.userAgent = userAgent;
-  }
-
-  public void setHost(String host) {
-    if (this.host != null) {
-      headers.removeAll("Host");
-    }
-    headers.add("Host", host);
-    this.host = host;
-  }
-
-  public void setConnection(String connection) {
-    if (this.connection != null) {
-      headers.removeAll("Connection");
-    }
-    headers.add("Connection", connection);
-    this.connection = connection;
-  }
-
-  public void setAcceptEncoding(String acceptEncoding) {
-    if (this.acceptEncoding != null) {
-      headers.removeAll("Accept-Encoding");
-    }
-    headers.add("Accept-Encoding", acceptEncoding);
-    this.acceptEncoding = acceptEncoding;
-  }
-
-  public void setContentType(String contentType) {
-    if (this.contentType != null) {
-      headers.removeAll("Content-Type");
-    }
-    headers.add("Content-Type", contentType);
-    this.contentType = contentType;
-  }
-
-  public void setIfModifiedSince(Date date) {
-    if (ifModifiedSince != null) {
-      headers.removeAll("If-Modified-Since");
-    }
-    String formattedDate = HttpDate.format(date);
-    headers.add("If-Modified-Since", formattedDate);
-    ifModifiedSince = formattedDate;
-  }
-
-  public void setIfNoneMatch(String ifNoneMatch) {
-    if (this.ifNoneMatch != null) {
-      headers.removeAll("If-None-Match");
-    }
-    headers.add("If-None-Match", ifNoneMatch);
-    this.ifNoneMatch = ifNoneMatch;
-  }
-
-  /**
-   * Returns true if the request contains conditions that save the server from
-   * sending a response that the client has locally. When the caller adds
-   * conditions, this cache won't participate in the request.
-   */
-  public boolean hasConditions() {
-    return ifModifiedSince != null || ifNoneMatch != null;
-  }
-
-  public void addCookies(Map<String, List<String>> allCookieHeaders) {
-    for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
-      String key = entry.getKey();
-      if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
-          && !entry.getValue().isEmpty()) {
-        headers.add(key, buildCookieHeader(entry.getValue()));
-      }
-    }
-  }
-
-  /**
-   * Send all cookies in one big header, as recommended by
-   * <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
-   */
-  private String buildCookieHeader(List<String> cookies) {
-    if (cookies.size() == 1) return cookies.get(0);
-    StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < cookies.size(); i++) {
-      if (i > 0) sb.append("; ");
-      sb.append(cookies.get(i));
-    }
-    return sb.toString();
-  }
-}
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
new file mode 100644
index 0000000..c918df3
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
@@ -0,0 +1,55 @@
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Request;
+import java.net.Proxy;
+import java.net.URL;
+
+public final class RequestLine {
+  private RequestLine() {
+  }
+
+  /**
+   * Returns the request status line, like "GET / HTTP/1.1". This is exposed
+   * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
+   * it needs to be set even if the transport is SPDY.
+   */
+  static String get(Request request, Proxy.Type proxyType, int httpMinorVersion) {
+    StringBuilder result = new StringBuilder();
+    result.append(request.method());
+    result.append(" ");
+
+    if (includeAuthorityInRequestLine(request, proxyType)) {
+      result.append(request.url());
+    } else {
+      result.append(requestPath(request.url()));
+    }
+
+    result.append(" ");
+    result.append(version(httpMinorVersion));
+    return result.toString();
+  }
+
+  /**
+   * Returns true if the request line should contain the full URL with host
+   * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
+   * (like "GET /foo HTTP/1.1").
+   */
+  private static boolean includeAuthorityInRequestLine(Request request, Proxy.Type proxyType) {
+    return !request.isHttps() && proxyType == Proxy.Type.HTTP;
+  }
+
+  /**
+   * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never empty,
+   * even if the request URL is. Includes the query component if it exists.
+   */
+  public static String requestPath(URL url) {
+    String pathAndQuery = url.getFile();
+    if (pathAndQuery == null) return "/";
+    if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery;
+    return pathAndQuery;
+  }
+
+  public static String version(int httpMinorVersion) {
+    return httpMinorVersion == 1 ? "HTTP/1.1" : "HTTP/1.0";
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java
new file mode 100644
index 0000000..9231307
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An adapter from {@link ResponseCache} to {@link com.squareup.okhttp.OkResponseCache}. This class
+ * enables OkHttp to continue supporting Java standard response cache implementations.
+ */
+public class ResponseCacheAdapter implements OkResponseCache {
+
+  private final ResponseCache delegate;
+
+  public ResponseCacheAdapter(ResponseCache delegate) {
+    this.delegate = delegate;
+  }
+
+  public ResponseCache getDelegate() {
+    return delegate;
+  }
+
+  @Override
+  public Response get(Request request) throws IOException {
+    CacheResponse javaResponse = getJavaCachedResponse(request);
+    if (javaResponse == null) {
+      return null;
+    }
+    return JavaApiConverter.createOkResponse(request, javaResponse);
+  }
+
+  @Override
+  public CacheRequest put(Response response) throws IOException {
+    URI uri = response.request().uri();
+    HttpURLConnection connection = JavaApiConverter.createJavaUrlConnection(response);
+    return delegate.put(uri, connection);
+  }
+
+  @Override
+  public boolean maybeRemove(Request request) throws IOException {
+    // This method is treated as optional and there is no obvious way of implementing it with
+    // ResponseCache. Removing items from the cache due to modifications made from this client is
+    // not essential given that modifications could be made from any other client. We have to assume
+    // that it's ok to keep using the cached data. Otherwise the server shouldn't declare it as
+    // cacheable or the client should be careful about caching it.
+    return false;
+  }
+
+  @Override
+  public void update(Response cached, Response network) throws IOException {
+    // This method is treated as optional and there is no obvious way of implementing it with
+    // ResponseCache. Updating headers is useful if the server changes the metadata for a resource
+    // (e.g. max age) to extend or truncate the life of that resource in the cache. If the metadata
+    // is not updated the caching behavior may not be optimal, but will obey the metadata sent
+    // with the original cached response.
+  }
+
+  @Override
+  public void trackConditionalCacheHit() {
+    // This method is treated as optional.
+  }
+
+  @Override
+  public void trackResponse(ResponseSource source) {
+    // This method is treated as optional.
+  }
+
+  /**
+   * Returns the {@link CacheResponse} from the delegate by converting the
+   * OkHttp {@link Request} into the arguments required by the {@link ResponseCache}.
+   */
+  private CacheResponse getJavaCachedResponse(Request request) throws IOException {
+    Map<String, List<String>> headers = JavaApiConverter.extractJavaHeaders(request);
+    return delegate.get(request.uri(), request.method(), headers);
+  }
+
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
deleted file mode 100644
index 461de8e..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ /dev/null
@@ -1,505 +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.http;
-
-import com.squareup.okhttp.ResponseSource;
-import com.squareup.okhttp.internal.Platform;
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-
-import static com.squareup.okhttp.internal.Util.equal;
-
-/** Parsed HTTP response headers. */
-public final class ResponseHeaders {
-
-  /** HTTP header name for the local time when the request was sent. */
-  private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
-
-  /** HTTP header name for the local time when the response was received. */
-  private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
-
-  /** HTTP synthetic header with the response source. */
-  static final String RESPONSE_SOURCE = Platform.get().getPrefix() + "-Response-Source";
-
-  /** HTTP synthetic header with the selected transport (spdy/3, http/1.1, etc). */
-  static final String SELECTED_TRANSPORT = Platform.get().getPrefix() + "-Selected-Transport";
-
-  private final URI uri;
-  private final RawHeaders headers;
-
-  /** The server's time when this response was served, if known. */
-  private Date servedDate;
-
-  /** The last modified date of the response, if known. */
-  private Date lastModified;
-
-  /**
-   * The expiration date of the response, if known. If both this field and the
-   * max age are set, the max age is preferred.
-   */
-  private Date expires;
-
-  /**
-   * Extension header set by HttpURLConnectionImpl specifying the timestamp
-   * when the HTTP request was first initiated.
-   */
-  private long sentRequestMillis;
-
-  /**
-   * Extension header set by HttpURLConnectionImpl specifying the timestamp
-   * when the HTTP response was first received.
-   */
-  private long receivedResponseMillis;
-
-  /**
-   * In the response, this field's name "no-cache" is misleading. It doesn't
-   * prevent us from caching the response; it only means we have to validate
-   * the response with the origin server before returning it. We can do this
-   * with a conditional get.
-   */
-  private boolean noCache;
-
-  /** If true, this response should not be cached. */
-  private boolean noStore;
-
-  /**
-   * The duration past the response's served date that it can be served
-   * without validation.
-   */
-  private int maxAgeSeconds = -1;
-
-  /**
-   * The "s-maxage" directive is the max age for shared caches. Not to be
-   * confused with "max-age" for non-shared caches, As in Firefox and Chrome,
-   * this directive is not honored by this cache.
-   */
-  private int sMaxAgeSeconds = -1;
-
-  /**
-   * This request header field's name "only-if-cached" is misleading. It
-   * actually means "do not use the network". It is set by a client who only
-   * wants to make a request if it can be fully satisfied by the cache.
-   * Cached responses that would require validation (ie. conditional gets) are
-   * not permitted if this header is set.
-   */
-  private boolean isPublic;
-  private boolean mustRevalidate;
-  private String etag;
-  private int ageSeconds = -1;
-
-  /** Case-insensitive set of field names. */
-  private Set<String> varyFields = Collections.emptySet();
-
-  private String contentEncoding;
-  private String transferEncoding;
-  private int contentLength = -1;
-  private String connection;
-
-  public ResponseHeaders(URI uri, RawHeaders headers) {
-    this.uri = uri;
-    this.headers = headers;
-
-    HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() {
-      @Override public void handle(String directive, String parameter) {
-        if ("no-cache".equalsIgnoreCase(directive)) {
-          noCache = true;
-        } else if ("no-store".equalsIgnoreCase(directive)) {
-          noStore = true;
-        } else if ("max-age".equalsIgnoreCase(directive)) {
-          maxAgeSeconds = HeaderParser.parseSeconds(parameter);
-        } else if ("s-maxage".equalsIgnoreCase(directive)) {
-          sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
-        } else if ("public".equalsIgnoreCase(directive)) {
-          isPublic = true;
-        } else if ("must-revalidate".equalsIgnoreCase(directive)) {
-          mustRevalidate = true;
-        }
-      }
-    };
-
-    for (int i = 0; i < headers.length(); i++) {
-      String fieldName = headers.getFieldName(i);
-      String value = headers.getValue(i);
-      if ("Cache-Control".equalsIgnoreCase(fieldName)) {
-        HeaderParser.parseCacheControl(value, handler);
-      } else if ("Date".equalsIgnoreCase(fieldName)) {
-        servedDate = HttpDate.parse(value);
-      } else if ("Expires".equalsIgnoreCase(fieldName)) {
-        expires = HttpDate.parse(value);
-      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
-        lastModified = HttpDate.parse(value);
-      } else if ("ETag".equalsIgnoreCase(fieldName)) {
-        etag = value;
-      } else if ("Pragma".equalsIgnoreCase(fieldName)) {
-        if ("no-cache".equalsIgnoreCase(value)) {
-          noCache = true;
-        }
-      } else if ("Age".equalsIgnoreCase(fieldName)) {
-        ageSeconds = HeaderParser.parseSeconds(value);
-      } else if ("Vary".equalsIgnoreCase(fieldName)) {
-        // Replace the immutable empty set with something we can mutate.
-        if (varyFields.isEmpty()) {
-          varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
-        }
-        for (String varyField : value.split(",")) {
-          varyFields.add(varyField.trim());
-        }
-      } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) {
-        contentEncoding = value;
-      } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) {
-        transferEncoding = value;
-      } else if ("Content-Length".equalsIgnoreCase(fieldName)) {
-        try {
-          contentLength = Integer.parseInt(value);
-        } catch (NumberFormatException ignored) {
-        }
-      } else if ("Connection".equalsIgnoreCase(fieldName)) {
-        connection = value;
-      } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) {
-        sentRequestMillis = Long.parseLong(value);
-      } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
-        receivedResponseMillis = Long.parseLong(value);
-      }
-    }
-  }
-
-  public boolean isContentEncodingGzip() {
-    return "gzip".equalsIgnoreCase(contentEncoding);
-  }
-
-  public void stripContentEncoding() {
-    contentEncoding = null;
-    headers.removeAll("Content-Encoding");
-  }
-
-  public void stripContentLength() {
-    contentLength = -1;
-    headers.removeAll("Content-Length");
-  }
-
-  public boolean isChunked() {
-    return "chunked".equalsIgnoreCase(transferEncoding);
-  }
-
-  public boolean hasConnectionClose() {
-    return "close".equalsIgnoreCase(connection);
-  }
-
-  public URI getUri() {
-    return uri;
-  }
-
-  public RawHeaders getHeaders() {
-    return headers;
-  }
-
-  public Date getServedDate() {
-    return servedDate;
-  }
-
-  public Date getLastModified() {
-    return lastModified;
-  }
-
-  public Date getExpires() {
-    return expires;
-  }
-
-  public boolean isNoCache() {
-    return noCache;
-  }
-
-  public boolean isNoStore() {
-    return noStore;
-  }
-
-  public int getMaxAgeSeconds() {
-    return maxAgeSeconds;
-  }
-
-  public int getSMaxAgeSeconds() {
-    return sMaxAgeSeconds;
-  }
-
-  public boolean isPublic() {
-    return isPublic;
-  }
-
-  public boolean isMustRevalidate() {
-    return mustRevalidate;
-  }
-
-  public String getEtag() {
-    return etag;
-  }
-
-  public Set<String> getVaryFields() {
-    return varyFields;
-  }
-
-  public String getContentEncoding() {
-    return contentEncoding;
-  }
-
-  public int getContentLength() {
-    return contentLength;
-  }
-
-  public String getConnection() {
-    return connection;
-  }
-
-  public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) {
-    this.sentRequestMillis = sentRequestMillis;
-    headers.add(SENT_MILLIS, Long.toString(sentRequestMillis));
-    this.receivedResponseMillis = receivedResponseMillis;
-    headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis));
-  }
-
-  public void setResponseSource(ResponseSource responseSource) {
-    headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode());
-  }
-
-  public void setTransport(String transport) {
-    headers.set(SELECTED_TRANSPORT, transport);
-  }
-
-  /**
-   * Returns the current age of the response, in milliseconds. The calculation
-   * is specified by RFC 2616, 13.2.3 Age Calculations.
-   */
-  private long computeAge(long nowMillis) {
-    long apparentReceivedAge =
-        servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0;
-    long receivedAge =
-        ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds))
-            : apparentReceivedAge;
-    long responseDuration = receivedResponseMillis - sentRequestMillis;
-    long residentDuration = nowMillis - receivedResponseMillis;
-    return receivedAge + responseDuration + residentDuration;
-  }
-
-  /**
-   * Returns the number of milliseconds that the response was fresh for,
-   * starting from the served date.
-   */
-  private long computeFreshnessLifetime() {
-    if (maxAgeSeconds != -1) {
-      return TimeUnit.SECONDS.toMillis(maxAgeSeconds);
-    } else if (expires != null) {
-      long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis;
-      long delta = expires.getTime() - servedMillis;
-      return delta > 0 ? delta : 0;
-    } else if (lastModified != null && uri.getRawQuery() == null) {
-      // As recommended by the HTTP RFC and implemented in Firefox, the
-      // max age of a document should be defaulted to 10% of the
-      // document's age at the time it was served. Default expiration
-      // dates aren't used for URIs containing a query.
-      long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis;
-      long delta = servedMillis - lastModified.getTime();
-      return delta > 0 ? (delta / 10) : 0;
-    }
-    return 0;
-  }
-
-  /**
-   * Returns true if computeFreshnessLifetime used a heuristic. If we used a
-   * heuristic to serve a cached response older than 24 hours, we are required
-   * to attach a warning.
-   */
-  private boolean isFreshnessLifetimeHeuristic() {
-    return maxAgeSeconds == -1 && expires == null;
-  }
-
-  /**
-   * Returns true if this response can be stored to later serve another
-   * request.
-   */
-  public boolean isCacheable(RequestHeaders request) {
-    // Always go to network for uncacheable response codes (RFC 2616, 13.4),
-    // This implementation doesn't support caching partial content.
-    int responseCode = headers.getResponseCode();
-    if (responseCode != HttpURLConnection.HTTP_OK
-        && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
-        && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
-        && responseCode != HttpURLConnection.HTTP_MOVED_PERM
-        && responseCode != HttpURLConnection.HTTP_GONE) {
-      return false;
-    }
-
-    // Responses to authorized requests aren't cacheable unless they include
-    // a 'public', 'must-revalidate' or 's-maxage' directive.
-    if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) {
-      return false;
-    }
-
-    if (noStore) {
-      return false;
-    }
-
-    return true;
-  }
-
-  /**
-   * Returns true if a Vary header contains an asterisk. Such responses cannot
-   * be cached.
-   */
-  public boolean hasVaryAll() {
-    return varyFields.contains("*");
-  }
-
-  /**
-   * Returns true if none of the Vary headers on this response have changed
-   * between {@code cachedRequest} and {@code newRequest}.
-   */
-  public boolean varyMatches(Map<String, List<String>> cachedRequest,
-      Map<String, List<String>> newRequest) {
-    for (String field : varyFields) {
-      if (!equal(cachedRequest.get(field), newRequest.get(field))) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  /** Returns the source to satisfy {@code request} given this cached response. */
-  public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) {
-    // If this response shouldn't have been stored, it should never be used
-    // as a response source. This check should be redundant as long as the
-    // persistence store is well-behaved and the rules are constant.
-    if (!isCacheable(request)) {
-      return ResponseSource.NETWORK;
-    }
-
-    if (request.isNoCache() || request.hasConditions()) {
-      return ResponseSource.NETWORK;
-    }
-
-    long ageMillis = computeAge(nowMillis);
-    long freshMillis = computeFreshnessLifetime();
-
-    if (request.getMaxAgeSeconds() != -1) {
-      freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds()));
-    }
-
-    long minFreshMillis = 0;
-    if (request.getMinFreshSeconds() != -1) {
-      minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds());
-    }
-
-    long maxStaleMillis = 0;
-    if (!mustRevalidate && request.getMaxStaleSeconds() != -1) {
-      maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds());
-    }
-
-    if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
-      if (ageMillis + minFreshMillis >= freshMillis) {
-        headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
-      }
-      long oneDayMillis = 24 * 60 * 60 * 1000L;
-      if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
-        headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
-      }
-      return ResponseSource.CACHE;
-    }
-
-    if (lastModified != null) {
-      request.setIfModifiedSince(lastModified);
-    } else if (servedDate != null) {
-      request.setIfModifiedSince(servedDate);
-    }
-
-    if (etag != null) {
-      request.setIfNoneMatch(etag);
-    }
-
-    return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK;
-  }
-
-  /**
-   * Returns true if this cached response should be used; false if the
-   * network response should be used.
-   */
-  public boolean validate(ResponseHeaders networkResponse) {
-    if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
-      return true;
-    }
-
-    // The HTTP spec says that if the network's response is older than our
-    // cached response, we may return the cache's response. Like Chrome (but
-    // unlike Firefox), this client prefers to return the newer response.
-    if (lastModified != null
-        && networkResponse.lastModified != null
-        && networkResponse.lastModified.getTime() < lastModified.getTime()) {
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
-   * Combines this cached header with a network header as defined by RFC 2616,
-   * 13.5.3.
-   */
-  public ResponseHeaders combine(ResponseHeaders network) throws IOException {
-    RawHeaders result = new RawHeaders();
-    result.setStatusLine(headers.getStatusLine());
-
-    for (int i = 0; i < headers.length(); i++) {
-      String fieldName = headers.getFieldName(i);
-      String value = headers.getValue(i);
-      if ("Warning".equals(fieldName) && value.startsWith("1")) {
-        continue; // drop 100-level freshness warnings
-      }
-      if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) {
-        result.add(fieldName, value);
-      }
-    }
-
-    for (int i = 0; i < network.headers.length(); i++) {
-      String fieldName = network.headers.getFieldName(i);
-      if (isEndToEnd(fieldName)) {
-        result.add(fieldName, network.headers.getValue(i));
-      }
-    }
-
-    return new ResponseHeaders(uri, result);
-  }
-
-  /**
-   * Returns true if {@code fieldName} is an end-to-end HTTP header, as
-   * defined by RFC 2616, 13.5.1.
-   */
-  private static boolean isEndToEnd(String fieldName) {
-    return !"Connection".equalsIgnoreCase(fieldName)
-        && !"Keep-Alive".equalsIgnoreCase(fieldName)
-        && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
-        && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
-        && !"TE".equalsIgnoreCase(fieldName)
-        && !"Trailers".equalsIgnoreCase(fieldName)
-        && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
-        && !"Upgrade".equalsIgnoreCase(fieldName);
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
deleted file mode 100644
index 5eb6b76..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2010 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.internal.AbstractOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.ProtocolException;
-
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
-
-/**
- * An HTTP request body that's completely buffered in memory. This allows
- * the post body to be transparently re-sent if the HTTP request must be
- * sent multiple times.
- */
-final class RetryableOutputStream extends AbstractOutputStream {
-  private final int limit;
-  private final ByteArrayOutputStream content;
-
-  public RetryableOutputStream(int limit) {
-    this.limit = limit;
-    this.content = new ByteArrayOutputStream(limit);
-  }
-
-  public RetryableOutputStream() {
-    this.limit = -1;
-    this.content = new ByteArrayOutputStream();
-  }
-
-  @Override public synchronized void close() throws IOException {
-    if (closed) {
-      return;
-    }
-    closed = true;
-    if (content.size() < limit) {
-      throw new ProtocolException(
-          "content-length promised " + limit + " bytes, but received " + content.size());
-    }
-  }
-
-  @Override public synchronized void write(byte[] buffer, int offset, int count)
-      throws IOException {
-    checkNotClosed();
-    checkOffsetAndCount(buffer.length, offset, count);
-    if (limit != -1 && content.size() > limit - count) {
-      throw new ProtocolException("exceeded content-length limit of " + limit + " bytes");
-    }
-    content.write(buffer, offset, count);
-  }
-
-  public synchronized int contentLength() throws IOException {
-    close();
-    return content.size();
-  }
-
-  public void writeToSocket(OutputStream socketOut) throws IOException {
-    content.writeTo(socketOut);
-  }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java
new file mode 100644
index 0000000..b8f53a3
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 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 java.io.IOException;
+import java.net.ProtocolException;
+import okio.BufferedSink;
+import okio.Deadline;
+import okio.OkBuffer;
+import okio.Sink;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An HTTP request body that's completely buffered in memory. This allows
+ * the post body to be transparently re-sent if the HTTP request must be
+ * sent multiple times.
+ */
+final class RetryableSink implements Sink {
+  private boolean closed;
+  private final int limit;
+  private final OkBuffer content = new OkBuffer();
+
+  public RetryableSink(int limit) {
+    this.limit = limit;
+  }
+
+  public RetryableSink() {
+    this(-1);
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) return;
+    closed = true;
+    if (content.size() < limit) {
+      throw new ProtocolException(
+          "content-length promised " + limit + " bytes, but received " + content.size());
+    }
+  }
+
+  @Override public void write(OkBuffer source, long byteCount) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    checkOffsetAndCount(source.size(), 0, byteCount);
+    if (limit != -1 && content.size() > limit - byteCount) {
+      throw new ProtocolException("exceeded content-length limit of " + limit + " bytes");
+    }
+    content.write(source, byteCount);
+  }
+
+  @Override public void flush() throws IOException {
+  }
+
+  @Override public Sink deadline(Deadline deadline) {
+    return this;
+  }
+
+  public long contentLength() throws IOException {
+    return content.size();
+  }
+
+  public void writeToSocket(BufferedSink socketOut) throws IOException {
+    // Clone the content; otherwise we won't have data to retry.
+    socketOut.write(content.clone(), content.size());
+  }
+}
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 1055e4f..c634bab 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
@@ -18,9 +18,9 @@
 import com.squareup.okhttp.Address;
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.HostResolver;
 import com.squareup.okhttp.Route;
 import com.squareup.okhttp.RouteDatabase;
-import com.squareup.okhttp.internal.Dns;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -33,6 +33,8 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.NoSuchElementException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLProtocolException;
 
 import static com.squareup.okhttp.internal.Util.getEffectivePort;
 
@@ -53,7 +55,7 @@
   private final URI uri;
   private final ProxySelector proxySelector;
   private final ConnectionPool pool;
-  private final Dns dns;
+  private final HostResolver hostResolver;
   private final RouteDatabase routeDatabase;
 
   /* The most recently attempted route. */
@@ -77,12 +79,12 @@
   private final List<Route> postponedRoutes;
 
   public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
-      Dns dns, RouteDatabase routeDatabase) {
+      HostResolver hostResolver, RouteDatabase routeDatabase) {
     this.address = address;
     this.uri = uri;
     this.proxySelector = proxySelector;
     this.pool = pool;
-    this.dns = dns;
+    this.hostResolver = hostResolver;
     this.routeDatabase = routeDatabase;
     this.postponedRoutes = new LinkedList<Route>();
 
@@ -116,7 +118,7 @@
           if (!hasNextPostponed()) {
             throw new NoSuchElementException();
           }
-          return new Connection(nextPostponed());
+          return new Connection(pool, nextPostponed());
         }
         lastProxy = nextProxy();
         resetNextInetSocketAddress(lastProxy);
@@ -134,7 +136,7 @@
       return next(method);
     }
 
-    return new Connection(route);
+    return new Connection(pool, route);
   }
 
   /**
@@ -142,13 +144,27 @@
    * failure on a connection returned by this route selector.
    */
   public void connectFailed(Connection connection, IOException failure) {
+    // If this is a recycled connection, don't count its failure against the route.
+    if (connection.recycleCount() > 0) return;
+
     Route failedRoute = connection.getRoute();
     if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
       // Tell the proxy selector when we fail to connect on a fresh connection.
       proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
     }
 
-    routeDatabase.failed(failedRoute, failure);
+    routeDatabase.failed(failedRoute);
+
+    // If the previously returned route's problem was not related to TLS, and
+    // the next route only changes the TLS mode, we shouldn't even attempt it.
+    // This suppresses it in both this selector and also in the route database.
+    if (hasNextTlsMode()
+        && !(failure instanceof SSLHandshakeException)
+        && !(failure instanceof SSLProtocolException)) {
+      boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
+      Route routeToSuppress = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
+      routeDatabase.failed(routeToSuppress);
+    }
   }
 
   /** Resets {@link #nextProxy} to the first option. */
@@ -213,7 +229,7 @@
     }
 
     // Try each address for best behavior in mixed IPv4/IPv6 environments.
-    socketAddresses = dns.getAllByName(socketHost);
+    socketAddresses = hostResolver.getAllByName(socketHost);
     nextSocketAddressIndex = 0;
   }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index fce58f4..e775d34 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -16,17 +16,59 @@
 
 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.internal.Util;
 import com.squareup.okhttp.internal.spdy.ErrorCode;
+import com.squareup.okhttp.internal.spdy.Header;
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
 import com.squareup.okhttp.internal.spdy.SpdyStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.CacheRequest;
-import java.net.URL;
+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 okio.ByteString;
+import okio.Deadline;
+import okio.OkBuffer;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.spdy.Header.RESPONSE_STATUS;
+import static com.squareup.okhttp.internal.spdy.Header.TARGET_AUTHORITY;
+import static com.squareup.okhttp.internal.spdy.Header.TARGET_HOST;
+import static com.squareup.okhttp.internal.spdy.Header.TARGET_METHOD;
+import static com.squareup.okhttp.internal.spdy.Header.TARGET_PATH;
+import static com.squareup.okhttp.internal.spdy.Header.TARGET_SCHEME;
+import static com.squareup.okhttp.internal.spdy.Header.VERSION;
 
 public final class SpdyTransport 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 SpdyConnection spdyConnection;
   private SpdyStream stream;
@@ -36,64 +78,244 @@
     this.spdyConnection = spdyConnection;
   }
 
-  @Override public OutputStream createRequestBody() throws IOException {
-    // TODO: if we aren't streaming up to the server, we should buffer the whole request
-    writeRequestHeaders();
-    return stream.getOutputStream();
+  @Override public Sink createRequestBody(Request request) throws IOException {
+    // TODO: if bufferRequestBody is set, we must buffer the whole request
+    writeRequestHeaders(request);
+    return stream.getSink();
   }
 
-  @Override public void writeRequestHeaders() throws IOException {
-    if (stream != null) {
-      return;
-    }
+  @Override public void writeRequestHeaders(Request request) throws IOException {
+    if (stream != null) return;
+
     httpEngine.writingRequestHeaders();
-    RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
-    String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
-    URL url = httpEngine.policy.getURL();
-    requestHeaders.addSpdyRequestHeaders(httpEngine.method, HttpEngine.requestPath(url), version,
-        HttpEngine.getOriginAddress(url), httpEngine.uri.getScheme());
     boolean hasRequestBody = httpEngine.hasRequestBody();
     boolean hasResponseBody = true;
-    stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), hasRequestBody,
+    String version = RequestLine.version(httpEngine.getConnection().getHttpMinorVersion());
+    stream = spdyConnection.newStream(
+        writeNameValueBlock(request, spdyConnection.getProtocol(), version), hasRequestBody,
         hasResponseBody);
     stream.setReadTimeout(httpEngine.client.getReadTimeout());
   }
 
-  @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+  @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
     throw new UnsupportedOperationException();
   }
 
   @Override public void flushRequest() throws IOException {
-    stream.getOutputStream().close();
+    stream.getSink().close();
   }
 
-  @Override public ResponseHeaders readResponseHeaders() throws IOException {
-    List<String> nameValueBlock = stream.getResponseHeaders();
-    RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
-    httpEngine.receiveHeaders(rawHeaders);
-
-    ResponseHeaders headers = new ResponseHeaders(httpEngine.uri, rawHeaders);
-    headers.setTransport("spdy/3");
-    return headers;
+  @Override public Response.Builder readResponseHeaders() throws IOException {
+    return readNameValueBlock(stream.getResponseHeaders(), spdyConnection.getProtocol());
   }
 
-  @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
-    return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
+  /**
+   * 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();
+    // TODO: make the known header names constants.
+    List<Header> result = new ArrayList<Header>(headers.size() + 10);
+    result.add(new Header(TARGET_METHOD, request.method()));
+    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
+    String host = HttpEngine.hostHeader(request.url());
+    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));
+    } else {
+      throw new AssertionError();
+    }
+    result.add(new Header(TARGET_SCHEME, request.url().getProtocol()));
+
+    Set<ByteString> names = new LinkedHashSet<ByteString>();
+    for (int i = 0; i < headers.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;
   }
 
-  @Override public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
-      InputStream responseBodyIn) {
-    if (streamCanceled) {
-      if (stream != null) {
+  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.name.utf8());
+    for (int i = 0; i < headerBlock.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");
+    if (version == null) throw new ProtocolException("Expected ':version' header not present");
+
+    return new Response.Builder()
+        .statusLine(new StatusLine(version + " " + status))
+        .headers(headersBuilder.build());
+  }
+
+  @Override public void emptyTransferStream() {
+    // Do nothing.
+  }
+
+  @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException {
+    return new SpdySource(stream, cacheRequest);
+  }
+
+  @Override public void releaseConnectionOnIdle() {
+  }
+
+  @Override public boolean canReuseConnection() {
+    return true; // TODO: spdyConnection.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);
+    }
+  }
+
+  /** An HTTP message body terminated by the end of the underlying stream. */
+  private static class SpdySource implements Source {
+    private final SpdyStream stream;
+    private final Source source;
+    private final CacheRequest cacheRequest;
+    private final OutputStream cacheBody;
+
+    private boolean inputExhausted;
+    private boolean closed;
+
+    SpdySource(SpdyStream stream, CacheRequest cacheRequest) throws IOException {
+      this.stream = stream;
+      this.source = stream.getSource();
+
+      // Some apps return a null body; for compatibility we treat that like a null cache request.
+      OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
+      if (cacheBody == null) {
+        cacheRequest = null;
+      }
+
+      this.cacheBody = cacheBody;
+      this.cacheRequest = cacheRequest;
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount)
+        throws IOException {
+      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+      if (closed) throw new IllegalStateException("closed");
+      if (inputExhausted) return -1;
+
+      long read = source.read(sink, byteCount);
+      if (read == -1) {
+        inputExhausted = true;
+        if (cacheRequest != null) {
+          cacheBody.close();
+        }
+        return -1;
+      }
+
+      if (cacheBody != null) {
+        Okio.copy(sink, sink.size() - read, read, cacheBody);
+      }
+
+      return read;
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) return;
+
+      if (!inputExhausted && cacheBody != null) {
+        discardStream(); // Could make inputExhausted true!
+      }
+
+      closed = true;
+
+      if (!inputExhausted) {
         stream.closeLater(ErrorCode.CANCEL);
-        return true;
-      } else {
-        // If stream is null, it either means that writeRequestHeaders wasn't called
-        // or that SpdyConnection#newStream threw an IOException. In both cases there's
-        // nothing to do here and this stream can't be reused.
+        if (cacheRequest != null) {
+          cacheRequest.abort();
+        }
+      }
+    }
+
+    private boolean discardStream() {
+      try {
+        long socketTimeout = stream.getReadTimeoutMillis();
+        stream.setReadTimeout(socketTimeout);
+        stream.setReadTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
+        try {
+          Util.skipAll(this, DISCARD_STREAM_TIMEOUT_MILLIS);
+          return true;
+        } finally {
+          stream.setReadTimeout(socketTimeout);
+        }
+      } catch (IOException e) {
         return false;
       }
     }
-    return true;
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java
new file mode 100644
index 0000000..d295891
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java
@@ -0,0 +1,89 @@
+package com.squareup.okhttp.internal.http;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+
+public final class StatusLine {
+  /** Numeric status code, 307: Temporary Redirect. */
+  public static final int HTTP_TEMP_REDIRECT = 307;
+  public static final int HTTP_CONTINUE = 100;
+
+  private final String statusLine;
+  private final int httpMinorVersion;
+  private final int responseCode;
+  private final String responseMessage;
+
+  /** Sets the response status line (like "HTTP/1.0 200 OK"). */
+  public StatusLine(String statusLine) throws IOException {
+    // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
+    // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
+
+    // Parse protocol like "HTTP/1.1" followed by a space.
+    int codeStart;
+    int httpMinorVersion;
+    if (statusLine.startsWith("HTTP/1.")) {
+      if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') {
+        throw new ProtocolException("Unexpected status line: " + statusLine);
+      }
+      httpMinorVersion = statusLine.charAt(7) - '0';
+      codeStart = 9;
+      if (httpMinorVersion < 0 || httpMinorVersion > 9) {
+        throw new ProtocolException("Unexpected status line: " + statusLine);
+      }
+    } else if (statusLine.startsWith("ICY ")) {
+      // Shoutcast uses ICY instead of "HTTP/1.0".
+      httpMinorVersion = 0;
+      codeStart = 4;
+    } else {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+
+    // Parse response code like "200". Always 3 digits.
+    if (statusLine.length() < codeStart + 3) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    int responseCode;
+    try {
+      responseCode = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3));
+    } catch (NumberFormatException e) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+
+    // Parse an optional response message like "OK" or "Not Modified". If it
+    // exists, it is separated from the response code by a space.
+    String responseMessage = "";
+    if (statusLine.length() > codeStart + 3) {
+      if (statusLine.charAt(codeStart + 3) != ' ') {
+        throw new ProtocolException("Unexpected status line: " + statusLine);
+      }
+      responseMessage = statusLine.substring(codeStart + 4);
+    }
+
+    this.responseMessage = responseMessage;
+    this.responseCode = responseCode;
+    this.statusLine = statusLine;
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  public String getStatusLine() {
+    return statusLine;
+  }
+
+  /**
+   * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
+   * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
+   */
+  public int httpMinorVersion() {
+    return httpMinorVersion != -1 ? httpMinorVersion : 1;
+  }
+
+  /** Returns the HTTP status code or -1 if it is unknown. */
+  public int code() {
+    return responseCode;
+  }
+
+  /** Returns the HTTP status message or null if it is unknown. */
+  public String message() {
+    return responseMessage;
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index d408bfe..94c90d4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -16,13 +16,22 @@
 
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.CacheRequest;
+import okio.Sink;
+import okio.Source;
 
 interface Transport {
   /**
+   * 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
+   * the time it takes to establish a new connection.
+   */
+  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
+
+  /**
    * Returns an output stream where the request body can be written. The
    * returned stream will of one of two types:
    * <ul>
@@ -38,27 +47,38 @@
    */
   // TODO: don't bother retransmitting the request body? It's quite a corner
   // case and there's uncertainty whether Firefox or Chrome do this
-  OutputStream createRequestBody() throws IOException;
+  Sink createRequestBody(Request request) throws IOException;
 
   /** This should update the HTTP engine's sentRequestMillis field. */
-  void writeRequestHeaders() throws IOException;
+  void writeRequestHeaders(Request request) throws IOException;
 
   /**
    * Sends the request body returned by {@link #createRequestBody} to the
    * remote peer.
    */
-  void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
+  void writeRequestBody(RetryableSink requestBody) throws IOException;
 
   /** Flush the request body to the underlying socket. */
   void flushRequest() throws IOException;
 
   /** Read response headers and update the cookie manager. */
-  ResponseHeaders readResponseHeaders() throws IOException;
+  Response.Builder readResponseHeaders() throws IOException;
+
+  /** Notify the transport that no response body will be read. */
+  void emptyTransferStream() throws IOException;
 
   // TODO: make this the content stream?
-  InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
+  Source getTransferStream(CacheRequest cacheRequest) throws IOException;
 
-  /** Returns true if the underlying connection can be recycled. */
-  boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut,
-      InputStream responseBodyIn);
+  /**
+   * Configures the response body to pool or close the socket connection when
+   * the response body is closed.
+   */
+  void releaseConnectionOnIdle() throws IOException;
+
+  /**
+   * Returns true if the socket connection held by this transport can be reused
+   * for a follow-up exchange.
+   */
+  boolean canReuseConnection();
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
deleted file mode 100644
index ca6bb59..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
+++ /dev/null
@@ -1,63 +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 java.io.IOException;
-import java.io.InputStream;
-import java.net.CacheRequest;
-
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
-
-/** An HTTP message body terminated by the end of the underlying stream. */
-final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
-  private boolean inputExhausted;
-
-  UnknownLengthHttpInputStream(InputStream in, CacheRequest cacheRequest, HttpEngine httpEngine)
-      throws IOException {
-    super(in, httpEngine, cacheRequest);
-  }
-
-  @Override public int read(byte[] buffer, int offset, int count) throws IOException {
-    checkOffsetAndCount(buffer.length, offset, count);
-    checkNotClosed();
-    if (in == null || inputExhausted) {
-      return -1;
-    }
-    int read = in.read(buffer, offset, count);
-    if (read == -1) {
-      inputExhausted = true;
-      endOfInput();
-      return -1;
-    }
-    cacheWrite(buffer, offset, read);
-    return read;
-  }
-
-  @Override public int available() throws IOException {
-    checkNotClosed();
-    return in == null ? 0 : in.available();
-  }
-
-  @Override public void close() throws IOException {
-    if (closed) {
-      return;
-    }
-    closed = true;
-    if (!inputExhausted) {
-      unexpectedEndOfInput();
-    }
-  }
-}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
similarity index 93%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
index d3a32e1..045677b 100644
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
@@ -1,5 +1,7 @@
 package com.squareup.okhttp.internal.spdy;
 
+// TODO: revisit for http/2 draft 9
+// http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-7
 public enum ErrorCode {
   /** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */
   NO_ERROR(0, -1, 0),
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
new file mode 100644
index 0000000..c87226a
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
@@ -0,0 +1,108 @@
+/*
+ * 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.spdy;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import okio.BufferedSource;
+import okio.ByteString;
+
+/** Reads transport frames for SPDY/3 or HTTP/2. */
+public interface FrameReader extends Closeable {
+  void readConnectionHeader() throws IOException;
+  boolean nextFrame(Handler handler) throws IOException;
+
+  public interface Handler {
+    void data(boolean inFinished, int streamId, BufferedSource source, int length)
+        throws IOException;
+
+    /**
+     * Create or update incoming headers, creating the corresponding streams
+     * if necessary. Frames that trigger this are SPDY SYN_STREAM, HEADERS, and
+     * SYN_REPLY, and HTTP/2 HEADERS and PUSH_PROMISE.
+     *
+     * @param outFinished true if the receiver should not send further frames.
+     * @param inFinished true if the sender will not send further frames.
+     * @param streamId the stream owning these headers.
+     * @param associatedStreamId the stream that triggered the sender to create
+     * this stream.
+     * @param priority or -1 for no priority. For SPDY, priorities range from 0
+     * (highest) thru 7 (lowest). For HTTP/2, priorities range from 0
+     * (highest) thru 2^31-1 (lowest), defaulting to 2^30.
+     */
+    void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
+        int priority, List<Header> headerBlock, HeadersMode headersMode);
+    void rstStream(int streamId, ErrorCode errorCode);
+    void settings(boolean clearPrevious, Settings settings);
+
+    /** HTTP/2 only. */
+    void ackSettings();
+
+    /**
+     *  Read a connection-level ping from the peer.  {@code ack} indicates this
+     *  is a reply.  Payload parameters are different between SPDY/3 and HTTP/2.
+     *  <p>
+     *  In SPDY/3, only the first {@code payload1} parameter is set.  If the
+     *  reader is a client, it is an unsigned even number.  Likewise, a server
+     *  will receive an odd number.
+     *  <p>
+     *  In HTTP/2, both {@code payload1} and {@code payload2} parameters are
+     *  set. The data is opaque binary, and there are no rules on the content.
+     */
+    void ping(boolean ack, int payload1, int payload2);
+
+    /**
+     * The peer tells us to stop creating streams.  It is safe to replay
+     * streams with {@code ID > lastGoodStreamId} on a new connection.  In-
+     * flight streams with {@code ID <= lastGoodStreamId} can only be replayed
+     * on a new connection if they are idempotent.
+     *
+     * @param lastGoodStreamId the last stream ID the peer processed before
+     *     sending this message. If {@code lastGoodStreamId} is zero, the peer
+     *     processed no frames.
+     * @param errorCode reason for closing the connection.
+     * @param debugData only valid for http/2; opaque debug data to send.
+     */
+    void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData);
+
+    /**
+     * Notifies that an additional {@code windowSizeIncrement} bytes can be
+     * sent on {@code streamId}, or the connection if {@code streamId} is zero.
+     */
+    void windowUpdate(int streamId, long windowSizeIncrement);
+    void priority(int streamId, int priority);
+
+    /**
+     * HTTP/2 only. Receive a push promise header block.
+     * <p>
+     * A push promise contains all the headers that pertain to a server-initiated
+     * request, and a {@code promisedStreamId} to which response frames will be
+     * delivered. Push promise frames are sent as a part of the response to
+     * {@code streamId}.  The {@code promisedStreamId} has a priority of one
+     * greater than {@code streamId}.
+     *
+     * @param streamId client-initiated stream ID.  Must be an odd number.
+     * @param promisedStreamId server-initiated stream ID.  Must be an even
+     * number.
+     * @param requestHeaders minimally includes {@code :method}, {@code :scheme},
+     * {@code :authority}, and (@code :path}.
+     */
+    void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders)
+        throws IOException;
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
new file mode 100644
index 0000000..f96c2aa
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
@@ -0,0 +1,99 @@
+/*
+ * 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.spdy;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import okio.OkBuffer;
+
+/** Writes transport frames for SPDY/3 or HTTP/2. */
+public interface FrameWriter extends Closeable {
+  /** HTTP/2 only. */
+  void connectionHeader() throws IOException;
+  void ackSettings() throws IOException;
+
+  /**
+   * HTTP/2 only. Send a push promise header block.
+   * <p>
+   * A push promise contains all the headers that pertain to a server-initiated
+   * request, and a {@code promisedStreamId} to which response frames will be
+   * delivered. Push promise frames are sent as a part of the response to
+   * {@code streamId}.  The {@code promisedStreamId} has a priority of one
+   * greater than {@code streamId}.
+   *
+   * @param streamId client-initiated stream ID.  Must be an odd number.
+   * @param promisedStreamId server-initiated stream ID.  Must be an even
+   * number.
+   * @param requestHeaders minimally includes {@code :method}, {@code :scheme},
+   * {@code :authority}, and (@code :path}.
+   */
+  void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders)
+      throws IOException;
+
+  /** SPDY/3 only. */
+  void flush() throws IOException;
+  void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
+      int priority, int slot, List<Header> headerBlock) throws IOException;
+  void synReply(boolean outFinished, int streamId, List<Header> headerBlock)
+      throws IOException;
+  void headers(int streamId, List<Header> headerBlock) throws IOException;
+  void rstStream(int streamId, ErrorCode errorCode) throws IOException;
+
+  /**
+   * {@code data.length} may be longer than the max length of the variant's data frame.
+   * Implementations must send multiple frames as necessary.
+   *
+   * @param source the buffer to draw bytes from. May be null if byteCount is 0.
+   */
+  void data(boolean outFinished, int streamId, OkBuffer source, int byteCount) throws IOException;
+
+  void data(boolean outFinished, int streamId, OkBuffer source) throws IOException;
+
+  /** Write okhttp's settings to the peer. */
+  void settings(Settings okHttpSettings) throws IOException;
+
+  /**
+   *  Send a connection-level ping to the peer.  {@code ack} indicates this is
+   *  a reply.  Payload parameters are different between SPDY/3 and HTTP/2.
+   *  <p>
+   *  In SPDY/3, only the first {@code payload1} parameter is sent.  If the
+   *  sender is a client, it is an unsigned odd number.  Likewise, a server
+   *  will send an even number.
+   *  <p>
+   *  In HTTP/2, both {@code payload1} and {@code payload2} parameters are
+   *  sent.  The data is opaque binary, and there are no rules on the content.
+   */
+  void ping(boolean ack, int payload1, int payload2) throws IOException;
+
+  /**
+   * Tell the peer to stop creating streams and that we last processed
+   * {@code lastGoodStreamId}, or zero if no streams were processed.
+   *
+   * @param lastGoodStreamId the last stream ID processed, or zero if no
+   * streams were processed.
+   * @param errorCode reason for closing the connection.
+   * @param debugData only valid for http/2; opaque debug data to send.
+   */
+  void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException;
+
+  /**
+   * Inform peer that an additional {@code windowSizeIncrement} bytes can be
+   * sent on {@code streamId}, or the connection if {@code streamId} is zero.
+   */
+  void windowUpdate(int streamId, long windowSizeIncrement) throws IOException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
new file mode 100644
index 0000000..1e9b503
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
@@ -0,0 +1,56 @@
+package com.squareup.okhttp.internal.spdy;
+
+import okio.ByteString;
+
+/** HTTP header: the name is an ASCII string, but the value can be UTF-8. */
+public final class Header {
+  // Special header names defined in the SPDY and HTTP/2 specs.
+  public static final ByteString RESPONSE_STATUS = ByteString.encodeUtf8(":status");
+  public static final ByteString TARGET_METHOD = ByteString.encodeUtf8(":method");
+  public static final ByteString TARGET_PATH = ByteString.encodeUtf8(":path");
+  public static final ByteString TARGET_SCHEME = ByteString.encodeUtf8(":scheme");
+  public static final ByteString TARGET_AUTHORITY = ByteString.encodeUtf8(":authority"); // http/2
+  public static final ByteString TARGET_HOST = ByteString.encodeUtf8(":host"); // spdy/3
+  public static final ByteString VERSION = ByteString.encodeUtf8(":version"); // spdy/3
+
+  /** Name in case-insensitive ASCII encoding. */
+  public final ByteString name;
+  /** Value in UTF-8 encoding. */
+  public final ByteString value;
+  final int hpackSize;
+
+  // TODO: search for toLowerCase and consider moving logic here.
+  public Header(String name, String value) {
+    this(ByteString.encodeUtf8(name), ByteString.encodeUtf8(value));
+  }
+
+  public Header(ByteString name, String value) {
+    this(name, ByteString.encodeUtf8(value));
+  }
+
+  public Header(ByteString name, ByteString value) {
+    this.name = name;
+    this.value = value;
+    this.hpackSize = 32 + name.size() + value.size();
+  }
+
+  @Override public boolean equals(Object other) {
+    if (other instanceof Header) {
+      Header that = (Header) other;
+      return this.name.equals(that.name)
+          && this.value.equals(that.value);
+    }
+    return false;
+  }
+
+  @Override public int hashCode() {
+    int result = 17;
+    result = 31 * result + name.hashCode();
+    result = 31 * result + value.hashCode();
+    return result;
+  }
+
+  @Override public String toString() {
+    return String.format("%s: %s", name.utf8(), value.utf8());
+  }
+}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
similarity index 100%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java
new file mode 100644
index 0000000..645f162
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java
@@ -0,0 +1,448 @@
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.BitArray;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * Read and write HPACK v05.
+ *
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05
+ *
+ * This implementation uses an array for the header table with a bitset for
+ * references.  Dynamic entries are added to the array, starting in the last
+ * position moving forward.  When the array fills, it is doubled.
+ */
+final class HpackDraft05 {
+  private static final int PREFIX_6_BITS = 0x3f;
+  private static final int PREFIX_7_BITS = 0x7f;
+
+  private static final Header[] STATIC_HEADER_TABLE = new Header[] {
+      new Header(Header.TARGET_AUTHORITY, ""),
+      new Header(Header.TARGET_METHOD, "GET"),
+      new Header(Header.TARGET_METHOD, "POST"),
+      new Header(Header.TARGET_PATH, "/"),
+      new Header(Header.TARGET_PATH, "/index.html"),
+      new Header(Header.TARGET_SCHEME, "http"),
+      new Header(Header.TARGET_SCHEME, "https"),
+      new Header(Header.RESPONSE_STATUS, "200"),
+      new Header(Header.RESPONSE_STATUS, "500"),
+      new Header(Header.RESPONSE_STATUS, "404"),
+      new Header(Header.RESPONSE_STATUS, "403"),
+      new Header(Header.RESPONSE_STATUS, "400"),
+      new Header(Header.RESPONSE_STATUS, "401"),
+      new Header("accept-charset", ""),
+      new Header("accept-encoding", ""),
+      new Header("accept-language", ""),
+      new Header("accept-ranges", ""),
+      new Header("accept", ""),
+      new Header("access-control-allow-origin", ""),
+      new Header("age", ""),
+      new Header("allow", ""),
+      new Header("authorization", ""),
+      new Header("cache-control", ""),
+      new Header("content-disposition", ""),
+      new Header("content-encoding", ""),
+      new Header("content-language", ""),
+      new Header("content-length", ""),
+      new Header("content-location", ""),
+      new Header("content-range", ""),
+      new Header("content-type", ""),
+      new Header("cookie", ""),
+      new Header("date", ""),
+      new Header("etag", ""),
+      new Header("expect", ""),
+      new Header("expires", ""),
+      new Header("from", ""),
+      new Header("host", ""),
+      new Header("if-match", ""),
+      new Header("if-modified-since", ""),
+      new Header("if-none-match", ""),
+      new Header("if-range", ""),
+      new Header("if-unmodified-since", ""),
+      new Header("last-modified", ""),
+      new Header("link", ""),
+      new Header("location", ""),
+      new Header("max-forwards", ""),
+      new Header("proxy-authenticate", ""),
+      new Header("proxy-authorization", ""),
+      new Header("range", ""),
+      new Header("referer", ""),
+      new Header("refresh", ""),
+      new Header("retry-after", ""),
+      new Header("server", ""),
+      new Header("set-cookie", ""),
+      new Header("strict-transport-security", ""),
+      new Header("transfer-encoding", ""),
+      new Header("user-agent", ""),
+      new Header("vary", ""),
+      new Header("via", ""),
+      new Header("www-authenticate", "")
+  };
+
+  private HpackDraft05() {
+  }
+
+  // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2
+  static final class Reader {
+    private final Huffman.Codec huffmanCodec;
+
+    private final List<Header> emittedHeaders = new ArrayList<Header>();
+    private final BufferedSource source;
+    private int maxHeaderTableByteCount;
+
+    // Visible for testing.
+    Header[] headerTable = new Header[8];
+    // Array is populated back to front, so new entries always have lowest index.
+    int nextHeaderIndex = headerTable.length - 1;
+    int headerCount = 0;
+
+    /**
+     * Set bit positions indicate {@code headerTable[pos]} should be emitted.
+     */
+    // Using a BitArray as it has left-shift operator.
+    BitArray referencedHeaders = new BitArray.FixedCapacity();
+
+    /**
+     * Set bit positions indicate {@code headerTable[pos]} was already emitted.
+     */
+    BitArray emittedReferencedHeaders = new BitArray.FixedCapacity();
+    int headerTableByteCount = 0;
+
+    Reader(boolean client, int maxHeaderTableByteCount, Source source) {
+      this.huffmanCodec = client ? Huffman.Codec.RESPONSE : Huffman.Codec.REQUEST;
+      this.maxHeaderTableByteCount = maxHeaderTableByteCount;
+      this.source = Okio.buffer(source);
+    }
+
+    int maxHeaderTableByteCount() {
+      return maxHeaderTableByteCount;
+    }
+
+    /**
+     * Called by the reader when the peer sent a new header table size setting.
+     * <p>
+     * Evicts entries or clears the table as needed.
+     */
+    void maxHeaderTableByteCount(int newMaxHeaderTableByteCount) {
+      this.maxHeaderTableByteCount = newMaxHeaderTableByteCount;
+      if (maxHeaderTableByteCount < headerTableByteCount) {
+        if (maxHeaderTableByteCount == 0) {
+          clearHeaderTable();
+        } else {
+          evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount);
+        }
+      }
+    }
+
+    private void clearHeaderTable() {
+      clearReferenceSet();
+      Arrays.fill(headerTable, null);
+      nextHeaderIndex = headerTable.length - 1;
+      headerCount = 0;
+      headerTableByteCount = 0;
+    }
+
+    /** Returns the count of entries evicted. */
+    private int evictToRecoverBytes(int bytesToRecover) {
+      int entriesToEvict = 0;
+      if (bytesToRecover > 0) {
+        // determine how many headers need to be evicted.
+        for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
+          bytesToRecover -= headerTable[j].hpackSize;
+          headerTableByteCount -= headerTable[j].hpackSize;
+          headerCount--;
+          entriesToEvict++;
+        }
+        referencedHeaders.shiftLeft(entriesToEvict);
+        emittedReferencedHeaders.shiftLeft(entriesToEvict);
+        System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable,
+            nextHeaderIndex + 1 + entriesToEvict, headerCount);
+        nextHeaderIndex += entriesToEvict;
+      }
+      return entriesToEvict;
+    }
+
+    /**
+     * Read {@code byteCount} bytes of headers from the source stream into the
+     * set of emitted headers.
+     */
+    void readHeaders() throws IOException {
+      while (!source.exhausted()) {
+        int b = source.readByte() & 0xff;
+        if (b == 0x80) { // 10000000
+          clearReferenceSet();
+        } else if ((b & 0x80) == 0x80) { // 1NNNNNNN
+          int index = readInt(b, PREFIX_7_BITS);
+          readIndexedHeader(index - 1);
+        } else { // 0NNNNNNN
+          if (b == 0x40) { // 01000000
+            readLiteralHeaderWithoutIndexingNewName();
+          } else if ((b & 0x40) == 0x40) {  // 01NNNNNN
+            int index = readInt(b, PREFIX_6_BITS);
+            readLiteralHeaderWithoutIndexingIndexedName(index - 1);
+          } else if (b == 0) { // 00000000
+            readLiteralHeaderWithIncrementalIndexingNewName();
+          } else if ((b & 0xc0) == 0) { // 00NNNNNN
+            int index = readInt(b, PREFIX_6_BITS);
+            readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
+          } else {
+            // TODO: we should throw something that we can coerce to a PROTOCOL_ERROR
+            throw new AssertionError("unhandled byte: " + Integer.toBinaryString(b));
+          }
+        }
+      }
+    }
+
+    private void clearReferenceSet() {
+      referencedHeaders.clear();
+      emittedReferencedHeaders.clear();
+    }
+
+    void emitReferenceSet() {
+      for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) {
+        if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) {
+          emittedHeaders.add(headerTable[i]);
+        }
+      }
+    }
+
+    /**
+     * Returns all headers emitted since they were last cleared, then clears the
+     * emitted headers.
+     */
+    List<Header> getAndReset() {
+      List<Header> result = new ArrayList<Header>(emittedHeaders);
+      emittedHeaders.clear();
+      emittedReferencedHeaders.clear();
+      return result;
+    }
+
+    private void readIndexedHeader(int index) {
+      if (isStaticHeader(index)) {
+        Header staticEntry = STATIC_HEADER_TABLE[index - headerCount];
+        if (maxHeaderTableByteCount == 0) {
+          emittedHeaders.add(staticEntry);
+        } else {
+          insertIntoHeaderTable(-1, staticEntry);
+        }
+      } else {
+        int headerTableIndex = headerTableIndex(index);
+        if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately.
+          emittedHeaders.add(headerTable[headerTableIndex]);
+          emittedReferencedHeaders.set(headerTableIndex);
+        }
+        referencedHeaders.toggle(headerTableIndex);
+      }
+    }
+
+    // referencedHeaders is relative to nextHeaderIndex + 1.
+    private int headerTableIndex(int index) {
+      return nextHeaderIndex + 1 + index;
+    }
+
+    private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException {
+      ByteString name = getName(index);
+      ByteString value = readByteString(false);
+      emittedHeaders.add(new Header(name, value));
+    }
+
+    private void readLiteralHeaderWithoutIndexingNewName() throws IOException {
+      ByteString name = readByteString(true);
+      ByteString value = readByteString(false);
+      emittedHeaders.add(new Header(name, value));
+    }
+
+    private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
+        throws IOException {
+      ByteString name = getName(nameIndex);
+      ByteString value = readByteString(false);
+      insertIntoHeaderTable(-1, new Header(name, value));
+    }
+
+    private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
+      ByteString name = readByteString(true);
+      ByteString value = readByteString(false);
+      insertIntoHeaderTable(-1, new Header(name, value));
+    }
+
+    private ByteString getName(int index) {
+      if (isStaticHeader(index)) {
+        return STATIC_HEADER_TABLE[index - headerCount].name;
+      } else {
+        return headerTable[headerTableIndex(index)].name;
+      }
+    }
+
+    private boolean isStaticHeader(int index) {
+      return index >= headerCount;
+    }
+
+    /** index == -1 when new. */
+    private void insertIntoHeaderTable(int index, Header entry) {
+      int delta = entry.hpackSize;
+      if (index != -1) { // Index -1 == new header.
+        delta -= headerTable[headerTableIndex(index)].hpackSize;
+      }
+
+      // if the new or replacement header is too big, drop all entries.
+      if (delta > maxHeaderTableByteCount) {
+        clearHeaderTable();
+        // emit the large header to the callback.
+        emittedHeaders.add(entry);
+        return;
+      }
+
+      // Evict headers to the required length.
+      int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount;
+      int entriesEvicted = evictToRecoverBytes(bytesToRecover);
+
+      if (index == -1) { // Adding a value to the header table.
+        if (headerCount + 1 > headerTable.length) { // Need to grow the header table.
+          Header[] doubled = new Header[headerTable.length * 2];
+          System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length);
+          if (doubled.length == 64) {
+            referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity();
+            emittedReferencedHeaders =
+                ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity();
+          }
+          referencedHeaders.shiftLeft(headerTable.length);
+          emittedReferencedHeaders.shiftLeft(headerTable.length);
+          nextHeaderIndex = headerTable.length - 1;
+          headerTable = doubled;
+        }
+        index = nextHeaderIndex--;
+        referencedHeaders.set(index);
+        headerTable[index] = entry;
+        headerCount++;
+      } else { // Replace value at same position.
+        index += headerTableIndex(index) + entriesEvicted;
+        referencedHeaders.set(index);
+        headerTable[index] = entry;
+      }
+      headerTableByteCount += delta;
+    }
+
+    private int readByte() throws IOException {
+      return source.readByte() & 0xff;
+    }
+
+    int readInt(int firstByte, int prefixMask) throws IOException {
+      int prefix = firstByte & prefixMask;
+      if (prefix < prefixMask) {
+        return prefix; // This was a single byte value.
+      }
+
+      // This is a multibyte value. Read 7 bits at a time.
+      int result = prefixMask;
+      int shift = 0;
+      while (true) {
+        int b = readByte();
+        if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
+          result += (b & 0x7f) << shift;
+          shift += 7;
+        } else {
+          result += b << shift; // Last byte.
+          break;
+        }
+      }
+      return result;
+    }
+
+    /**
+     * Reads a potentially Huffman encoded string byte string. When
+     * {@code asciiLowercase} is true, bytes will be converted to lowercase.
+     */
+    ByteString readByteString(boolean asciiLowercase) throws IOException {
+      int firstByte = readByte();
+      boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN
+      int length = readInt(firstByte, PREFIX_7_BITS);
+
+      ByteString byteString = source.readByteString(length);
+
+      if (huffmanDecode) {
+        byteString = huffmanCodec.decode(byteString); // TODO: streaming Huffman!
+      }
+
+      if (asciiLowercase) {
+        byteString = byteString.toAsciiLowercase();
+      }
+
+      return byteString;
+    }
+  }
+
+  private static final Map<ByteString, Integer> NAME_TO_FIRST_INDEX = nameToFirstIndex();
+
+  private static Map<ByteString, Integer> nameToFirstIndex() {
+    Map<ByteString, Integer> result =
+        new LinkedHashMap<ByteString, Integer>(STATIC_HEADER_TABLE.length);
+    for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) {
+      if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) {
+        result.put(STATIC_HEADER_TABLE[i].name, i);
+      }
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
+  static final class Writer {
+    private final OkBuffer out;
+
+    Writer(OkBuffer out) {
+      this.out = out;
+    }
+
+    void writeHeaders(List<Header> headerBlock) throws IOException {
+      // TODO: implement index tracking
+      for (int i = 0, size = headerBlock.size(); i < size; i++) {
+        ByteString name = headerBlock.get(i).name;
+        Integer staticIndex = NAME_TO_FIRST_INDEX.get(name);
+        if (staticIndex != null) {
+          // Literal Header Field without Indexing - Indexed Name.
+          writeInt(staticIndex + 1, PREFIX_6_BITS, 0x40);
+          writeByteString(headerBlock.get(i).value);
+        } else {
+          out.writeByte(0x40); // Literal Header without Indexing - New Name.
+          writeByteString(name);
+          writeByteString(headerBlock.get(i).value);
+        }
+      }
+    }
+
+    // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.1
+    void writeInt(int value, int prefixMask, int bits) throws IOException {
+      // Write the raw value for a single byte value.
+      if (value < prefixMask) {
+        out.writeByte(bits | value);
+        return;
+      }
+
+      // Write the mask to start a multibyte value.
+      out.writeByte(bits | prefixMask);
+      value -= prefixMask;
+
+      // Write 7 bits at a time 'til we're done.
+      while (value >= 0x80) {
+        int b = value & 0x7f;
+        out.writeByte(b | 0x80);
+        value >>>= 7;
+      }
+      out.writeByte(value);
+    }
+
+    void writeByteString(ByteString data) throws IOException {
+      writeInt(data.size(), PREFIX_7_BITS, 0);
+      out.write(data);
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java
new file mode 100644
index 0000000..a88b747
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.Protocol;
+import java.io.IOException;
+import java.util.List;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Deadline;
+import okio.OkBuffer;
+import okio.Source;
+
+/**
+ * Read and write http/2 v09 frames.
+ * http://tools.ietf.org/html/draft-ietf-httpbis-http2-09
+ */
+public final class Http20Draft09 implements Variant {
+
+  @Override public Protocol getProtocol() {
+    return Protocol.HTTP_2;
+  }
+
+  private static final ByteString CONNECTION_HEADER
+      = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
+
+  static final byte TYPE_DATA = 0x0;
+  static final byte TYPE_HEADERS = 0x1;
+  static final byte TYPE_PRIORITY = 0x2;
+  static final byte TYPE_RST_STREAM = 0x3;
+  static final byte TYPE_SETTINGS = 0x4;
+  static final byte TYPE_PUSH_PROMISE = 0x5;
+  static final byte TYPE_PING = 0x6;
+  static final byte TYPE_GOAWAY = 0x7;
+  static final byte TYPE_WINDOW_UPDATE = 0x9;
+  static final byte TYPE_CONTINUATION = 0xa;
+
+  static final byte FLAG_NONE = 0x0;
+  static final byte FLAG_ACK = 0x1;
+  static final byte FLAG_END_STREAM = 0x1;
+  static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation.
+  static final byte FLAG_END_PUSH_PROMISE = 0x4;
+  static final byte FLAG_PRIORITY = 0x8;
+
+  @Override public FrameReader newReader(BufferedSource source, boolean client) {
+    return new Reader(source, 4096, client);
+  }
+
+  @Override public FrameWriter newWriter(BufferedSink sink, boolean client) {
+    return new Writer(sink, client);
+  }
+
+  @Override public int maxFrameSize() {
+    return 16383;
+  }
+
+  static final class Reader implements FrameReader {
+    private final BufferedSource source;
+    private final ContinuationSource continuation;
+    private final boolean client;
+
+    // Visible for testing.
+    final HpackDraft05.Reader hpackReader;
+
+    Reader(BufferedSource source, int headerTableSize, boolean client) {
+      this.source = source;
+      this.client = client;
+      this.continuation = new ContinuationSource(this.source);
+      this.hpackReader = new HpackDraft05.Reader(client, headerTableSize, continuation);
+    }
+
+    @Override public void readConnectionHeader() throws IOException {
+      if (client) return; // Nothing to read; servers don't send connection headers!
+      ByteString connectionHeader = source.readByteString(CONNECTION_HEADER.size());
+      if (!CONNECTION_HEADER.equals(connectionHeader)) {
+        throw ioException("Expected a connection header but was %s", connectionHeader.utf8());
+      }
+    }
+
+    @Override public boolean nextFrame(Handler handler) throws IOException {
+      int w1;
+      int w2;
+      try {
+        w1 = source.readInt();
+        w2 = source.readInt();
+      } catch (IOException e) {
+        return false; // This might be a normal socket close.
+      }
+
+      // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits.
+      short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == max 16383
+      byte type = (byte) ((w1 & 0xff00) >> 8);
+      byte flags = (byte) (w1 & 0xff);
+      // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit.
+      int streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier.
+
+      switch (type) {
+        case TYPE_DATA:
+          readData(handler, length, flags, streamId);
+          break;
+
+        case TYPE_HEADERS:
+          readHeaders(handler, length, flags, streamId);
+          break;
+
+        case TYPE_PRIORITY:
+          readPriority(handler, length, flags, streamId);
+          break;
+
+        case TYPE_RST_STREAM:
+          readRstStream(handler, length, flags, streamId);
+          break;
+
+        case TYPE_SETTINGS:
+          readSettings(handler, length, flags, streamId);
+          break;
+
+        case TYPE_PUSH_PROMISE:
+          readPushPromise(handler, length, flags, streamId);
+          break;
+
+        case TYPE_PING:
+          readPing(handler, length, flags, streamId);
+          break;
+
+        case TYPE_GOAWAY:
+          readGoAway(handler, length, flags, streamId);
+          break;
+
+        case TYPE_WINDOW_UPDATE:
+          readWindowUpdate(handler, length, flags, streamId);
+          break;
+
+        default:
+          // Implementations MUST ignore frames of unsupported or unrecognized types.
+          source.skip(length);
+      }
+      return true;
+    }
+
+    private void readHeaders(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0");
+
+      boolean endStream = (flags & FLAG_END_STREAM) != 0;
+
+      int priority = -1;
+      if ((flags & FLAG_PRIORITY) != 0) {
+        priority = source.readInt() & 0x7fffffff;
+        length -= 4; // account for above read.
+      }
+
+      List<Header> headerBlock = readHeaderBlock(length, flags, streamId);
+
+      handler.headers(false, endStream, streamId, -1, priority, headerBlock,
+          HeadersMode.HTTP_20_HEADERS);
+    }
+
+    private List<Header> readHeaderBlock(short length, byte flags, int streamId)
+        throws IOException {
+      continuation.length = continuation.left = length;
+      continuation.flags = flags;
+      continuation.streamId = streamId;
+
+      hpackReader.readHeaders();
+      hpackReader.emitReferenceSet();
+      // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20.
+      // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3
+      return hpackReader.getAndReset();
+    }
+
+    private void readData(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      boolean inFinished = (flags & FLAG_END_STREAM) != 0;
+      // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED
+      handler.data(inFinished, streamId, source, length);
+    }
+
+    private void readPriority(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (length != 4) throw ioException("TYPE_PRIORITY length: %d != 4", length);
+      if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0");
+      int w1 = source.readInt();
+      // boolean r = (w1 & 0x80000000) != 0; // Reserved.
+      int priority = (w1 & 0x7fffffff);
+      handler.priority(streamId, priority);
+    }
+
+    private void readRstStream(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length);
+      if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0");
+      int errorCodeInt = source.readInt();
+      ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
+      if (errorCode == null) {
+        throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
+      }
+      handler.rstStream(streamId, errorCode);
+    }
+
+    private void readSettings(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0");
+      if ((flags & FLAG_ACK) != 0) {
+        if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!");
+        handler.ackSettings();
+        return;
+      }
+
+      if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length);
+      Settings settings = new Settings();
+      for (int i = 0; i < length; i += 8) {
+        int w1 = source.readInt();
+        int value = source.readInt();
+        // int r = (w1 & 0xff000000) >>> 24; // Reserved.
+        int id = w1 & 0xffffff;
+        settings.set(id, 0, value);
+      }
+      handler.settings(false, settings);
+      if (settings.getHeaderTableSize() >= 0) {
+        hpackReader.maxHeaderTableByteCount(settings.getHeaderTableSize());
+      }
+    }
+
+    private void readPushPromise(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (streamId == 0) {
+        throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0");
+      }
+      int promisedStreamId = source.readInt() & 0x7fffffff;
+      length -= 4; // account for above read.
+      List<Header> headerBlock = readHeaderBlock(length, flags, streamId);
+      handler.pushPromise(streamId, promisedStreamId, headerBlock);
+    }
+
+    private void readPing(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (length != 8) throw ioException("TYPE_PING length != 8: %s", length);
+      if (streamId != 0) throw ioException("TYPE_PING streamId != 0");
+      int payload1 = source.readInt();
+      int payload2 = source.readInt();
+      boolean ack = (flags & FLAG_ACK) != 0;
+      handler.ping(ack, payload1, payload2);
+    }
+
+    private void readGoAway(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length);
+      if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0");
+      int lastStreamId = source.readInt();
+      int errorCodeInt = source.readInt();
+      int opaqueDataLength = length - 8;
+      ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
+      if (errorCode == null) {
+        throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
+      }
+      ByteString debugData = ByteString.EMPTY;
+      if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection.
+        debugData = source.readByteString(opaqueDataLength);
+      }
+      handler.goAway(lastStreamId, errorCode, debugData);
+    }
+
+    private void readWindowUpdate(Handler handler, short length, byte flags, int streamId)
+        throws IOException {
+      if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length);
+      long increment = (source.readInt() & 0x7fffffff);
+      if (increment == 0) throw ioException("windowSizeIncrement was 0", increment);
+      handler.windowUpdate(streamId, increment);
+    }
+
+    @Override public void close() throws IOException {
+      source.close();
+    }
+  }
+
+  static final class Writer implements FrameWriter {
+    private final BufferedSink sink;
+    private final boolean client;
+    private final OkBuffer hpackBuffer;
+    private final HpackDraft05.Writer hpackWriter;
+    private boolean closed;
+
+    Writer(BufferedSink sink, boolean client) {
+      this.sink = sink;
+      this.client = client;
+      this.hpackBuffer = new OkBuffer();
+      this.hpackWriter = new HpackDraft05.Writer(hpackBuffer);
+    }
+
+    @Override public synchronized void flush() throws IOException {
+      if (closed) throw new IOException("closed");
+      sink.flush();
+    }
+
+    @Override public synchronized void ackSettings() throws IOException {
+      if (closed) throw new IOException("closed");
+      int length = 0;
+      byte type = TYPE_SETTINGS;
+      byte flags = FLAG_ACK;
+      int streamId = 0;
+      frameHeader(length, type, flags, streamId);
+      sink.flush();
+    }
+
+    @Override public synchronized void connectionHeader() throws IOException {
+      if (closed) throw new IOException("closed");
+      if (!client) return; // Nothing to write; servers don't send connection headers!
+      sink.write(CONNECTION_HEADER.toByteArray());
+      sink.flush();
+    }
+
+    @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
+        int streamId, int associatedStreamId, int priority, int slot, List<Header> headerBlock)
+        throws IOException {
+      if (inFinished) throw new UnsupportedOperationException();
+      if (closed) throw new IOException("closed");
+      headers(outFinished, streamId, priority, headerBlock);
+    }
+
+    @Override public synchronized void synReply(boolean outFinished, int streamId,
+        List<Header> headerBlock) throws IOException {
+      if (closed) throw new IOException("closed");
+      headers(outFinished, streamId, -1, headerBlock);
+    }
+
+    @Override public synchronized void headers(int streamId, List<Header> headerBlock)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      headers(false, streamId, -1, headerBlock);
+    }
+
+    @Override public synchronized void pushPromise(int streamId, int promisedStreamId,
+        List<Header> requestHeaders) throws IOException {
+      if (closed) throw new IOException("closed");
+      if (hpackBuffer.size() != 0) throw new IllegalStateException();
+      hpackWriter.writeHeaders(requestHeaders);
+
+      int length = (int) (4 + hpackBuffer.size());
+      byte type = TYPE_PUSH_PROMISE;
+      byte flags = FLAG_END_HEADERS;
+      frameHeader(length, type, flags, streamId); // TODO: CONTINUATION
+      sink.writeInt(promisedStreamId & 0x7fffffff);
+      sink.write(hpackBuffer, hpackBuffer.size());
+    }
+
+    private void headers(boolean outFinished, int streamId, int priority,
+        List<Header> headerBlock) throws IOException {
+      if (closed) throw new IOException("closed");
+      if (hpackBuffer.size() != 0) throw new IllegalStateException();
+      hpackWriter.writeHeaders(headerBlock);
+
+      int length = (int) hpackBuffer.size();
+      byte type = TYPE_HEADERS;
+      byte flags = FLAG_END_HEADERS;
+      if (outFinished) flags |= FLAG_END_STREAM;
+      if (priority != -1) flags |= FLAG_PRIORITY;
+      if (priority != -1) length += 4;
+      frameHeader(length, type, flags, streamId); // TODO: CONTINUATION
+      if (priority != -1) sink.writeInt(priority & 0x7fffffff);
+      sink.write(hpackBuffer, hpackBuffer.size());
+    }
+
+    @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
+
+      int length = 4;
+      byte type = TYPE_RST_STREAM;
+      byte flags = FLAG_NONE;
+      frameHeader(length, type, flags, streamId);
+      sink.writeInt(errorCode.httpCode);
+      sink.flush();
+    }
+
+    @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source)
+        throws IOException {
+      data(outFinished, streamId, source, (int) source.size());
+    }
+
+    @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source,
+        int byteCount) throws IOException {
+      if (closed) throw new IOException("closed");
+      byte flags = FLAG_NONE;
+      if (outFinished) flags |= FLAG_END_STREAM;
+      dataFrame(streamId, flags, source, byteCount);
+    }
+
+    void dataFrame(int streamId, byte flags, OkBuffer buffer, int byteCount) throws IOException {
+      byte type = TYPE_DATA;
+      frameHeader(byteCount, type, flags, streamId);
+      if (byteCount > 0) {
+        sink.write(buffer, byteCount);
+      }
+    }
+
+    @Override public synchronized void settings(Settings settings) throws IOException {
+      if (closed) throw new IOException("closed");
+      int length = settings.size() * 8;
+      byte type = TYPE_SETTINGS;
+      byte flags = FLAG_NONE;
+      int streamId = 0;
+      frameHeader(length, type, flags, streamId);
+      for (int i = 0; i < Settings.COUNT; i++) {
+        if (!settings.isSet(i)) continue;
+        sink.writeInt(i & 0xffffff);
+        sink.writeInt(settings.get(i));
+      }
+      sink.flush();
+    }
+
+    @Override public synchronized void ping(boolean ack, int payload1, int payload2)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      int length = 8;
+      byte type = TYPE_PING;
+      byte flags = ack ? FLAG_ACK : FLAG_NONE;
+      int streamId = 0;
+      frameHeader(length, type, flags, streamId);
+      sink.writeInt(payload1);
+      sink.writeInt(payload2);
+      sink.flush();
+    }
+
+    @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode,
+        byte[] debugData) throws IOException {
+      if (closed) throw new IOException("closed");
+      if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1");
+      int length = 8 + debugData.length;
+      byte type = TYPE_GOAWAY;
+      byte flags = FLAG_NONE;
+      int streamId = 0;
+      frameHeader(length, type, flags, streamId);
+      sink.writeInt(lastGoodStreamId);
+      sink.writeInt(errorCode.httpCode);
+      if (debugData.length > 0) {
+        sink.write(debugData);
+      }
+      sink.flush();
+    }
+
+    @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) {
+        throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s",
+            windowSizeIncrement);
+      }
+      int length = 4;
+      byte type = TYPE_WINDOW_UPDATE;
+      byte flags = FLAG_NONE;
+      frameHeader(length, type, flags, streamId);
+      sink.writeInt((int) windowSizeIncrement);
+      sink.flush();
+    }
+
+    @Override public synchronized void close() throws IOException {
+      closed = true;
+      sink.close();
+    }
+
+    void frameHeader(int length, byte type, byte flags, int streamId) throws IOException {
+      if (length > 16383) throw illegalArgument("FRAME_SIZE_ERROR length > 16383: %s", length);
+      if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId);
+      sink.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
+      sink.writeInt(streamId & 0x7fffffff);
+    }
+  }
+
+  private static IllegalArgumentException illegalArgument(String message, Object... args) {
+    throw new IllegalArgumentException(String.format(message, args));
+  }
+
+  private static IOException ioException(String message, Object... args) throws IOException {
+    throw new IOException(String.format(message, args));
+  }
+
+  /**
+   * Decompression of the header block occurs above the framing layer. This
+   * class lazily reads continuation frames as they are needed by {@link
+   * HpackDraft05.Reader#readHeaders()}.
+   */
+  static final class ContinuationSource implements Source {
+    private final BufferedSource source;
+
+    int length;
+    byte flags;
+    int streamId;
+
+    int left;
+
+    public ContinuationSource(BufferedSource source) {
+      this.source = source;
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+      while (left == 0) {
+        if ((flags & FLAG_END_HEADERS) != 0) return -1;
+        readContinuationHeader();
+        // TODO: test case for empty continuation header?
+      }
+
+      long read = source.read(sink, Math.min(byteCount, left));
+      if (read == -1) return -1;
+      left -= read;
+      return read;
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+    }
+
+    private void readContinuationHeader() throws IOException {
+      int previousStreamId = streamId;
+      int w1 = source.readInt();
+      int w2 = source.readInt();
+      length = left = (short) ((w1 & 0x3fff0000) >> 16);
+      byte type = (byte) ((w1 & 0xff00) >> 8);
+      flags = (byte) (w1 & 0xff);
+      streamId = (w2 & 0x7fffffff);
+      if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type);
+      if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed");
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
new file mode 100644
index 0000000..45d882f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2013 Twitter, 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.spdy;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import okio.ByteString;
+
+/**
+ * This class was originally composed from the following classes in
+ * <a href="https://github.com/twitter/hpack">Twitter Hpack</a>.
+ * <ul>
+ * <li>{@code com.twitter.hpack.HuffmanEncoder}</li>
+ * <li>{@code com.twitter.hpack.HuffmanDecoder}</li>
+ * <li>{@code com.twitter.hpack.HpackUtil}</li>
+ * </ul>
+ */
+class Huffman {
+  enum Codec {
+    REQUEST(REQUEST_CODES, REQUEST_CODE_LENGTHS),
+    RESPONSE(RESPONSE_CODES, RESPONSE_CODE_LENGTHS);
+
+    private final Node root = new Node();
+    private final int[] codes;
+    private final byte[] lengths;
+
+    /**
+     * @param codes Index designates the symbol this code represents.
+     * @param lengths Index designates the symbol this code represents.
+     */
+    Codec(int[] codes, byte[] lengths) {
+      buildTree(codes, lengths);
+      this.codes = codes;
+      this.lengths = lengths;
+    }
+
+    void encode(byte[] data, OutputStream out) throws IOException {
+      long current = 0;
+      int n = 0;
+
+      for (int i = 0; i < data.length; i++) {
+        int b = data[i] & 0xFF;
+        int code = codes[b];
+        int nbits = lengths[b];
+
+        current <<= nbits;
+        current |= code;
+        n += nbits;
+
+        while (n >= 8) {
+          n -= 8;
+          out.write(((int) (current >> n)));
+        }
+      }
+
+      if (n > 0) {
+        current <<= (8 - n);
+        current |= (0xFF >>> n);
+        out.write((int) current);
+      }
+    }
+
+    int encodedLength(byte[] bytes) {
+      long len = 0;
+
+      for (int i = 0; i < bytes.length; i++) {
+        int b = bytes[i] & 0xFF;
+        len += lengths[b];
+      }
+
+      return (int) ((len + 7) >> 3);
+    }
+
+    ByteString decode(ByteString buf) throws IOException {
+      return ByteString.of(decode(buf.toByteArray()));
+    }
+
+    byte[] decode(byte[] buf) throws IOException {
+      // FIXME
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      Node node = root;
+      int current = 0;
+      int nbits = 0;
+      for (int i = 0; i < buf.length; i++) {
+        int b = buf[i] & 0xFF;
+        current = (current << 8) | b;
+        nbits += 8;
+        while (nbits >= 8) {
+          int c = (current >>> (nbits - 8)) & 0xFF;
+          node = node.children[c];
+          if (node.children == null) {
+            // terminal node
+            baos.write(node.symbol);
+            nbits -= node.terminalBits;
+            node = root;
+          } else {
+            // non-terminal node
+            nbits -= 8;
+          }
+        }
+      }
+
+      while (nbits > 0) {
+        int c = (current << (8 - nbits)) & 0xFF;
+        node = node.children[c];
+        if (node.children != null || node.terminalBits > nbits) {
+          break;
+        }
+        baos.write(node.symbol);
+        nbits -= node.terminalBits;
+        node = root;
+      }
+
+      return baos.toByteArray();
+    }
+
+    private void buildTree(int[] codes, byte[] lengths) {
+      for (int i = 0; i < lengths.length; i++) {
+        addCode(i, codes[i], lengths[i]);
+      }
+    }
+
+    private void addCode(int sym, int code, byte len) {
+      Node terminal = new Node(sym, len);
+
+      Node current = root;
+      while (len > 8) {
+        len -= 8;
+        int i = ((code >>> len) & 0xFF);
+        if (current.children == null) {
+          throw new IllegalStateException("invalid dictionary: prefix not unique");
+        }
+        if (current.children[i] == null) {
+          current.children[i] = new Node();
+        }
+        current = current.children[i];
+      }
+
+      int shift = 8 - len;
+      int start = (code << shift) & 0xFF;
+      int end = 1 << shift;
+      for (int i = start; i < start + end; i++) {
+        current.children[i] = terminal;
+      }
+    }
+  }
+
+  private static final class Node {
+
+    // Null if terminal.
+    private final Node[] children;
+
+    // Terminal nodes have a symbol.
+    private final int symbol;
+
+    // Number of bits represented in the terminal node.
+    private final int terminalBits;
+
+    /** Construct an internal node. */
+    Node() {
+      this.children = new Node[256];
+      this.symbol = 0; // Not read.
+      this.terminalBits = 0; // Not read.
+    }
+
+    /**
+     * Construct a terminal node.
+     *
+     * @param symbol symbol the node represents
+     * @param bits length of Huffman code in bits
+     */
+    Node(int symbol, int bits) {
+      this.children = null;
+      this.symbol = symbol;
+      int b = bits & 0x07;
+      this.terminalBits = b == 0 ? 8 : b;
+    }
+  }
+
+  // Appendix C: Huffman Codes For Requests
+  // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-C
+  private static final int[] REQUEST_CODES = {
+      0x7ffffba, 0x7ffffbb, 0x7ffffbc, 0x7ffffbd, 0x7ffffbe, 0x7ffffbf, 0x7ffffc0, 0x7ffffc1,
+      0x7ffffc2, 0x7ffffc3, 0x7ffffc4, 0x7ffffc5, 0x7ffffc6, 0x7ffffc7, 0x7ffffc8, 0x7ffffc9,
+      0x7ffffca, 0x7ffffcb, 0x7ffffcc, 0x7ffffcd, 0x7ffffce, 0x7ffffcf, 0x7ffffd0, 0x7ffffd1,
+      0x7ffffd2, 0x7ffffd3, 0x7ffffd4, 0x7ffffd5, 0x7ffffd6, 0x7ffffd7, 0x7ffffd8, 0x7ffffd9, 0xe8,
+      0xffc, 0x3ffa, 0x7ffc, 0x7ffd, 0x24, 0x6e, 0x7ffe, 0x7fa, 0x7fb, 0x3fa, 0x7fc, 0xe9, 0x25,
+      0x4, 0x0, 0x5, 0x6, 0x7, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x1ec, 0xea, 0x3fffe, 0x2d,
+      0x1fffc, 0x1ed, 0x3ffb, 0x6f, 0xeb, 0xec, 0xed, 0xee, 0x70, 0x1ee, 0x1ef, 0x1f0, 0x1f1, 0x3fb,
+      0x1f2, 0xef, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0xf0, 0xf1, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc,
+      0x3fc, 0x3ffc, 0x7ffffda, 0x1ffc, 0x3ffd, 0x2e, 0x7fffe, 0x8, 0x2f, 0x9, 0x30, 0x1, 0x31,
+      0x32, 0x33, 0xa, 0x71, 0x72, 0xb, 0x34, 0xc, 0xd, 0xe, 0xf2, 0xf, 0x10, 0x11, 0x35, 0x73,
+      0x36, 0xf3, 0xf4, 0xf5, 0x1fffd, 0x7fd, 0x1fffe, 0xffd, 0x7ffffdb, 0x7ffffdc, 0x7ffffdd,
+      0x7ffffde, 0x7ffffdf, 0x7ffffe0, 0x7ffffe1, 0x7ffffe2, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5,
+      0x7ffffe6, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0x7ffffec, 0x7ffffed,
+      0x7ffffee, 0x7ffffef, 0x7fffff0, 0x7fffff1, 0x7fffff2, 0x7fffff3, 0x7fffff4, 0x7fffff5,
+      0x7fffff6, 0x7fffff7, 0x7fffff8, 0x7fffff9, 0x7fffffa, 0x7fffffb, 0x7fffffc, 0x7fffffd,
+      0x7fffffe, 0x7ffffff, 0x3ffff80, 0x3ffff81, 0x3ffff82, 0x3ffff83, 0x3ffff84, 0x3ffff85,
+      0x3ffff86, 0x3ffff87, 0x3ffff88, 0x3ffff89, 0x3ffff8a, 0x3ffff8b, 0x3ffff8c, 0x3ffff8d,
+      0x3ffff8e, 0x3ffff8f, 0x3ffff90, 0x3ffff91, 0x3ffff92, 0x3ffff93, 0x3ffff94, 0x3ffff95,
+      0x3ffff96, 0x3ffff97, 0x3ffff98, 0x3ffff99, 0x3ffff9a, 0x3ffff9b, 0x3ffff9c, 0x3ffff9d,
+      0x3ffff9e, 0x3ffff9f, 0x3ffffa0, 0x3ffffa1, 0x3ffffa2, 0x3ffffa3, 0x3ffffa4, 0x3ffffa5,
+      0x3ffffa6, 0x3ffffa7, 0x3ffffa8, 0x3ffffa9, 0x3ffffaa, 0x3ffffab, 0x3ffffac, 0x3ffffad,
+      0x3ffffae, 0x3ffffaf, 0x3ffffb0, 0x3ffffb1, 0x3ffffb2, 0x3ffffb3, 0x3ffffb4, 0x3ffffb5,
+      0x3ffffb6, 0x3ffffb7, 0x3ffffb8, 0x3ffffb9, 0x3ffffba, 0x3ffffbb, 0x3ffffbc, 0x3ffffbd,
+      0x3ffffbe, 0x3ffffbf, 0x3ffffc0, 0x3ffffc1, 0x3ffffc2, 0x3ffffc3, 0x3ffffc4, 0x3ffffc5,
+      0x3ffffc6, 0x3ffffc7, 0x3ffffc8, 0x3ffffc9, 0x3ffffca, 0x3ffffcb, 0x3ffffcc, 0x3ffffcd,
+      0x3ffffce, 0x3ffffcf, 0x3ffffd0, 0x3ffffd1, 0x3ffffd2, 0x3ffffd3, 0x3ffffd4, 0x3ffffd5,
+      0x3ffffd6, 0x3ffffd7, 0x3ffffd8, 0x3ffffd9, 0x3ffffda, 0x3ffffdb
+  };
+
+  private static final byte[] REQUEST_CODE_LENGTHS = {
+      27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+      27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 12, 14, 15, 15, 6, 7, 15, 11, 11, 10, 11, 8, 6, 5, 4,
+      5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 9, 8, 18, 6, 17, 9, 14, 7, 8, 8, 8, 8, 7, 9, 9, 9, 9, 10, 9, 8,
+      9, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 10, 14, 27, 13, 14, 6, 19, 5, 6, 5, 6, 4, 6, 6, 6, 5, 7,
+      7, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6, 7, 6, 8, 8, 8, 17, 11, 17, 12, 27, 27, 27, 27, 27, 27, 27,
+      27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+      27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+      26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+      26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+      26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+      26, 26, 26, 26, 26, 26, 26
+  };
+
+  // Appendix D: Huffman Codes For Responses
+  // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-D
+  private static final int[] RESPONSE_CODES = {
+      0x1ffffbc, 0x1ffffbd, 0x1ffffbe, 0x1ffffbf, 0x1ffffc0, 0x1ffffc1, 0x1ffffc2, 0x1ffffc3,
+      0x1ffffc4, 0x1ffffc5, 0x1ffffc6, 0x1ffffc7, 0x1ffffc8, 0x1ffffc9, 0x1ffffca, 0x1ffffcb,
+      0x1ffffcc, 0x1ffffcd, 0x1ffffce, 0x1ffffcf, 0x1ffffd0, 0x1ffffd1, 0x1ffffd2, 0x1ffffd3,
+      0x1ffffd4, 0x1ffffd5, 0x1ffffd6, 0x1ffffd7, 0x1ffffd8, 0x1ffffd9, 0x1ffffda, 0x1ffffdb, 0x0,
+      0xffa, 0x6a, 0x1ffa, 0x3ffc, 0x1ec, 0x3f8, 0x1ffb, 0x1ed, 0x1ee, 0xffb, 0x7fa, 0x22, 0x23,
+      0x24, 0x6b, 0x1, 0x2, 0x3, 0x8, 0x9, 0xa, 0x25, 0x26, 0xb, 0xc, 0xd, 0x1ef, 0xfffa, 0x6c,
+      0x1ffc, 0xffc, 0xfffb, 0x6d, 0xea, 0xeb, 0xec, 0xed, 0xee, 0x27, 0x1f0, 0xef, 0xf0, 0x3f9,
+      0x1f1, 0x28, 0xf1, 0xf2, 0x1f2, 0x3fa, 0x1f3, 0x29, 0xe, 0x1f4, 0x1f5, 0xf3, 0x3fb, 0x1f6,
+      0x3fc, 0x7fb, 0x1ffd, 0x7fc, 0x7ffc, 0x1f7, 0x1fffe, 0xf, 0x6e, 0x2a, 0x2b, 0x10, 0x6f, 0x70,
+      0x71, 0x2c, 0x1f8, 0x1f9, 0x72, 0x2d, 0x2e, 0x2f, 0x30, 0x1fa, 0x31, 0x32, 0x33, 0x34, 0x73,
+      0xf4, 0x74, 0xf5, 0x1fb, 0xfffc, 0x3ffd, 0xfffd, 0xfffe, 0x1ffffdc, 0x1ffffdd, 0x1ffffde,
+      0x1ffffdf, 0x1ffffe0, 0x1ffffe1, 0x1ffffe2, 0x1ffffe3, 0x1ffffe4, 0x1ffffe5, 0x1ffffe6,
+      0x1ffffe7, 0x1ffffe8, 0x1ffffe9, 0x1ffffea, 0x1ffffeb, 0x1ffffec, 0x1ffffed, 0x1ffffee,
+      0x1ffffef, 0x1fffff0, 0x1fffff1, 0x1fffff2, 0x1fffff3, 0x1fffff4, 0x1fffff5, 0x1fffff6,
+      0x1fffff7, 0x1fffff8, 0x1fffff9, 0x1fffffa, 0x1fffffb, 0x1fffffc, 0x1fffffd, 0x1fffffe,
+      0x1ffffff, 0xffff80, 0xffff81, 0xffff82, 0xffff83, 0xffff84, 0xffff85, 0xffff86, 0xffff87,
+      0xffff88, 0xffff89, 0xffff8a, 0xffff8b, 0xffff8c, 0xffff8d, 0xffff8e, 0xffff8f, 0xffff90,
+      0xffff91, 0xffff92, 0xffff93, 0xffff94, 0xffff95, 0xffff96, 0xffff97, 0xffff98, 0xffff99,
+      0xffff9a, 0xffff9b, 0xffff9c, 0xffff9d, 0xffff9e, 0xffff9f, 0xffffa0, 0xffffa1, 0xffffa2,
+      0xffffa3, 0xffffa4, 0xffffa5, 0xffffa6, 0xffffa7, 0xffffa8, 0xffffa9, 0xffffaa, 0xffffab,
+      0xffffac, 0xffffad, 0xffffae, 0xffffaf, 0xffffb0, 0xffffb1, 0xffffb2, 0xffffb3, 0xffffb4,
+      0xffffb5, 0xffffb6, 0xffffb7, 0xffffb8, 0xffffb9, 0xffffba, 0xffffbb, 0xffffbc, 0xffffbd,
+      0xffffbe, 0xffffbf, 0xffffc0, 0xffffc1, 0xffffc2, 0xffffc3, 0xffffc4, 0xffffc5, 0xffffc6,
+      0xffffc7, 0xffffc8, 0xffffc9, 0xffffca, 0xffffcb, 0xffffcc, 0xffffcd, 0xffffce, 0xffffcf,
+      0xffffd0, 0xffffd1, 0xffffd2, 0xffffd3, 0xffffd4, 0xffffd5, 0xffffd6, 0xffffd7, 0xffffd8,
+      0xffffd9, 0xffffda, 0xffffdb, 0xffffdc
+  };
+
+  private static final byte[] RESPONSE_CODE_LENGTHS = {
+      25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+      25, 25, 25, 25, 25, 25, 25, 25, 25, 4, 12, 7, 13, 14, 9, 10, 13, 9, 9, 12, 11, 6, 6, 6, 7, 4,
+      4, 4, 5, 5, 5, 6, 6, 5, 5, 5, 9, 16, 7, 13, 12, 16, 7, 8, 8, 8, 8, 8, 6, 9, 8, 8, 10, 9, 6, 8,
+      8, 9, 10, 9, 6, 5, 9, 9, 8, 10, 9, 10, 11, 13, 11, 15, 9, 17, 5, 7, 6, 6, 5, 7, 7, 7, 6, 9, 9,
+      7, 6, 6, 6, 6, 9, 6, 6, 6, 6, 7, 8, 7, 8, 9, 16, 14, 16, 16, 25, 25, 25, 25, 25, 25, 25, 25,
+      25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+      25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+      24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+      24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+      24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+      24, 24, 24, 24, 24, 24
+  };
+}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
similarity index 100%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
new file mode 100644
index 0000000..293d817
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
@@ -0,0 +1,113 @@
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Deadline;
+import okio.InflaterSource;
+import okio.OkBuffer;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * Reads a SPDY/3 Name/Value header block. This class is made complicated by the
+ * requirement that we're strict with which bytes we put in the compressed bytes
+ * buffer. We need to put all compressed bytes into that buffer -- but no other
+ * bytes.
+ */
+class NameValueBlockReader {
+  /** This source transforms compressed bytes into uncompressed bytes. */
+  private final InflaterSource inflaterSource;
+
+  /**
+   * How many compressed bytes must be read into inflaterSource before
+   * {@link #readNameValueBlock} returns.
+   */
+  private int compressedLimit;
+
+  /** This source holds inflated bytes. */
+  private final BufferedSource source;
+
+  public NameValueBlockReader(final BufferedSource source) {
+    // Limit the inflater input stream to only those bytes in the Name/Value
+    // block. We cut the inflater off at its source because we can't predict the
+    // ratio of compressed bytes to uncompressed bytes.
+    Source throttleSource = new Source() {
+      @Override public long read(OkBuffer sink, long byteCount)
+          throws IOException {
+        if (compressedLimit == 0) return -1; // Out of data for the current block.
+        long read = source.read(sink, Math.min(byteCount, compressedLimit));
+        if (read == -1) return -1;
+        compressedLimit -= read;
+        return read;
+      }
+
+      @Override public void close() throws IOException {
+        source.close();
+      }
+
+      @Override public Source deadline(Deadline deadline) {
+        source.deadline(deadline);
+        return this;
+      }
+    };
+
+    // Subclass inflater to install a dictionary when it's needed.
+    Inflater inflater = new Inflater() {
+      @Override public int inflate(byte[] buffer, int offset, int count)
+          throws DataFormatException {
+        int result = super.inflate(buffer, offset, count);
+        if (result == 0 && needsDictionary()) {
+          setDictionary(Spdy3.DICTIONARY);
+          result = super.inflate(buffer, offset, count);
+        }
+        return result;
+      }
+    };
+
+    this.inflaterSource = new InflaterSource(throttleSource, inflater);
+    this.source = Okio.buffer(inflaterSource);
+  }
+
+  public List<Header> readNameValueBlock(int length) throws IOException {
+    this.compressedLimit += length;
+
+    int numberOfPairs = source.readInt();
+    if (numberOfPairs < 0) throw new IOException("numberOfPairs < 0: " + numberOfPairs);
+    if (numberOfPairs > 1024) throw new IOException("numberOfPairs > 1024: " + numberOfPairs);
+
+    List<Header> entries = new ArrayList<Header>(numberOfPairs);
+    for (int i = 0; i < numberOfPairs; i++) {
+      ByteString name = readByteString().toAsciiLowercase();
+      ByteString values = readByteString();
+      if (name.size() == 0) throw new IOException("name.size == 0");
+      entries.add(new Header(name, values));
+    }
+
+    doneReading();
+    return entries;
+  }
+
+  private ByteString readByteString() throws IOException {
+    int length = source.readInt();
+    return source.readByteString(length);
+  }
+
+  private void doneReading() throws IOException {
+    // Move any outstanding unread bytes into the inflater. One side-effect of
+    // deflate compression is that sometimes there are bytes remaining in the
+    // stream after we've consumed all of the content.
+    if (compressedLimit > 0) {
+      inflaterSource.refill();
+      if (compressedLimit != 0) throw new IOException("compressedLimit > 0: " + compressedLimit);
+    }
+  }
+
+  public void close() throws IOException {
+    source.close();
+  }
+}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
similarity index 100%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
new file mode 100644
index 0000000..8eecf6b
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.List;
+import okio.BufferedSource;
+
+/**
+ * {@link com.squareup.okhttp.Protocol#HTTP_2 HTTP/2} only.
+ * Processes server-initiated HTTP requests on the client.
+ *
+ * <p>Use the stream ID to correlate response headers and data.
+ *
+ * <p>Return true to request cancellation of a pushed stream.  Note that this
+ * does not guarantee future frames won't arrive on the stream ID.
+ */
+public interface PushObserver {
+  /**
+   * Describes the request that the server intends to push a response for.
+   *
+   * @param streamId server-initiated stream ID: an even number.
+   * @param requestHeaders minimally includes {@code :method}, {@code :scheme},
+   * {@code :authority}, and (@code :path}.
+   */
+  boolean onRequest(int streamId, List<Header> requestHeaders);
+
+  /**
+   * The response headers corresponding to a pushed request.  When {@code last}
+   * is true, there are no data frames to follow.
+   *
+   * @param streamId server-initiated stream ID: an even number.
+   * @param responseHeaders minimally includes {@code :status}.
+   * @param last when true, there is no response data.
+   */
+  boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last);
+
+  /**
+   * A chunk of response data corresponding to a pushed request.  This data
+   * must either be read or skipped.
+   *
+   * @param streamId server-initiated stream ID: an even number.
+   * @param source location of data corresponding with this stream ID.
+   * @param byteCount number of bytes to read or skip from the source.
+   * @param last when true, there are no data frames to follow.
+   */
+  boolean onData(int streamId, BufferedSource source, int byteCount, boolean last)
+      throws IOException;
+
+  /** Indicates the reason why this stream was cancelled. */
+  void onReset(int streamId, ErrorCode errorCode);
+
+  PushObserver CANCEL = new PushObserver() {
+
+    @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
+      return true;
+    }
+
+    @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
+      return true;
+    }
+
+    @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
+        boolean last) throws IOException {
+      source.skip(byteCount);
+      return true;
+    }
+
+    @Override public void onReset(int streamId, ErrorCode errorCode) {
+    }
+  };
+}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
similarity index 74%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
index 05380e2..bf43088 100644
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
@@ -15,10 +15,16 @@
  */
 package com.squareup.okhttp.internal.spdy;
 
+import java.util.Arrays;
+
+/**
+ * Settings describe characteristics of the sending peer, which are used by the receiving peer.
+ * Settings are {@link com.squareup.okhttp.internal.spdy.SpdyConnection connection} scoped.
+ */
 final class Settings {
   /**
-   * From the spdy/3 spec, the default initial window size for all streams is
-   * 64 KiB. (Chrome 25 uses 10 MiB).
+   * From the SPDY/3 and HTTP/2 specs, the default initial window size for all
+   * streams is 64 KiB. (Chrome 25 uses 10 MiB).
    */
   static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024;
 
@@ -30,24 +36,28 @@
   /** Sent by clients only. The client is reminding the server of a persisted value. */
   static final int PERSISTED = 0x2;
 
-  /** Sender's estimate of max incoming kbps. */
+  /** spdy/3: Sender's estimate of max incoming kbps. */
   static final int UPLOAD_BANDWIDTH = 1;
-  /** Sender's estimate of max outgoing kbps. */
+  /** http/2: Size in bytes of the table used to decode the sender's header blocks. */
+  static final int HEADER_TABLE_SIZE = 1;
+  /** spdy/3: Sender's estimate of max outgoing kbps. */
   static final int DOWNLOAD_BANDWIDTH = 2;
-  /** Sender's estimate of milliseconds between sending a request and receiving a response. */
+  /** http/2: An endpoint must not send a PUSH_PROMISE frame when this is 0. */
+  static final int ENABLE_PUSH = 2;
+  /** spdy/3: Sender's estimate of millis between sending a request and receiving a response. */
   static final int ROUND_TRIP_TIME = 3;
   /** Sender's maximum number of concurrent streams. */
   static final int MAX_CONCURRENT_STREAMS = 4;
-  /** Current CWND in Packets. */
+  /** spdy/3: Current CWND in Packets. */
   static final int CURRENT_CWND = 5;
-  /** Retransmission rate. Percentage */
+  /** spdy/3: Retransmission rate. Percentage */
   static final int DOWNLOAD_RETRANS_RATE = 6;
   /** Window size in bytes. */
   static final int INITIAL_WINDOW_SIZE = 7;
-  /** Window size in bytes. */
+  /** spdy/3: Window size in bytes. */
   static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8;
   /** Flow control options. */
-  static final int FLOW_CONTROL_OPTIONS = 9;
+  static final int FLOW_CONTROL_OPTIONS = 10;
 
   /** Total number of settings. */
   static final int COUNT = 10;
@@ -67,9 +77,14 @@
   /** Flag values. */
   private final int[] values = new int[COUNT];
 
-  void set(int id, int idFlags, int value) {
+  void clear() {
+    set = persistValue = persisted = 0;
+    Arrays.fill(values, 0);
+  }
+
+  Settings set(int id, int idFlags, int value) {
     if (id >= values.length) {
-      return; // Discard unknown settings.
+      return this; // Discard unknown settings.
     }
 
     int bit = 1 << id;
@@ -86,6 +101,7 @@
     }
 
     values[id] = value;
+    return this;
   }
 
   /** Returns true if a value has been assigned for the setting {@code id}. */
@@ -112,31 +128,50 @@
     return Integer.bitCount(set);
   }
 
+  /** spdy/3 only. */
   int getUploadBandwidth(int defaultValue) {
     int bit = 1 << UPLOAD_BANDWIDTH;
     return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
   }
 
+  /** http/2 only. Returns -1 if unset. */
+  int getHeaderTableSize() {
+    int bit = 1 << HEADER_TABLE_SIZE;
+    return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1;
+  }
+
+  /** spdy/3 only. */
   int getDownloadBandwidth(int defaultValue) {
     int bit = 1 << DOWNLOAD_BANDWIDTH;
     return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
   }
 
+  /** http/2 only. */
+  // TODO: honor this setting in http/2.
+  boolean getEnablePush(boolean defaultValue) {
+    int bit = 1 << ENABLE_PUSH;
+    return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1;
+  }
+
+  /** spdy/3 only. */
   int getRoundTripTime(int defaultValue) {
     int bit = 1 << ROUND_TRIP_TIME;
     return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
   }
 
+  // TODO: honor this setting in spdy/3 and http/2.
   int getMaxConcurrentStreams(int defaultValue) {
     int bit = 1 << MAX_CONCURRENT_STREAMS;
     return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
   }
 
+  /** spdy/3 only. */
   int getCurrentCwnd(int defaultValue) {
     int bit = 1 << CURRENT_CWND;
     return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
   }
 
+  /** spdy/3 only. */
   int getDownloadRetransRate(int defaultValue) {
     int bit = 1 << DOWNLOAD_RETRANS_RATE;
     return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
@@ -147,12 +182,13 @@
     return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
   }
 
+  /** spdy/3 only. */
   int getClientCertificateVectorSize(int defaultValue) {
     int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
     return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
   }
 
-  // TODO: honor this setting.
+  // TODO: honor this setting in spdy/3 and http/2.
   boolean isFlowControlDisabled() {
     int bit = 1 << FLOW_CONTROL_OPTIONS;
     int value = (bit & set) != 0 ? values[FLOW_CONTROL_OPTIONS] : 0;
@@ -160,7 +196,7 @@
   }
 
   /**
-   * Returns true if this user agent should use this setting in future SPDY
+   * Returns true if this user agent should use this setting in future spdy/3
    * connections to the same host.
    */
   boolean persistValue(int id) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
new file mode 100644
index 0000000..a71bc6f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
@@ -0,0 +1,499 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
+import java.util.List;
+import java.util.zip.Deflater;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.DeflaterSink;
+import okio.OkBuffer;
+import okio.Okio;
+
+/**
+ * Read and write spdy/3.1 frames.
+ * http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1
+ */
+final class Spdy3 implements Variant {
+
+  @Override public Protocol getProtocol() {
+    return Protocol.SPDY_3;
+  }
+
+  static final int TYPE_DATA = 0x0;
+  static final int TYPE_SYN_STREAM = 0x1;
+  static final int TYPE_SYN_REPLY = 0x2;
+  static final int TYPE_RST_STREAM = 0x3;
+  static final int TYPE_SETTINGS = 0x4;
+  static final int TYPE_PING = 0x6;
+  static final int TYPE_GOAWAY = 0x7;
+  static final int TYPE_HEADERS = 0x8;
+  static final int TYPE_WINDOW_UPDATE = 0x9;
+
+  static final int FLAG_FIN = 0x1;
+  static final int FLAG_UNIDIRECTIONAL = 0x2;
+
+  static final int VERSION = 3;
+
+  static final byte[] DICTIONARY;
+  static {
+    try {
+      DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+          + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+          + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+          + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+          + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+          + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+          + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+          + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+          + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+          + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+          + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+          + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+          + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+          + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+          + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+          + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+          + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+          + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+          + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+          + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+          + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+          + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+          + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+          + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+          + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+          + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+          + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+          + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+          + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+          + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+          + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+          + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+          + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError();
+    }
+  }
+
+  @Override public FrameReader newReader(BufferedSource source, boolean client) {
+    return new Reader(source, client);
+  }
+
+  @Override public FrameWriter newWriter(BufferedSink sink, boolean client) {
+    return new Writer(sink, client);
+  }
+
+  @Override public int maxFrameSize() {
+    return 16383;
+  }
+
+  /** Read spdy/3 frames. */
+  static final class Reader implements FrameReader {
+    private final BufferedSource source;
+    private final boolean client;
+    private final NameValueBlockReader headerBlockReader;
+
+    Reader(BufferedSource source, boolean client) {
+      this.source = source;
+      this.headerBlockReader = new NameValueBlockReader(this.source);
+      this.client = client;
+    }
+
+    @Override public void readConnectionHeader() {
+    }
+
+    /**
+     * Send the next frame to {@code handler}. Returns true unless there are no
+     * more frames on the stream.
+     */
+    @Override public boolean nextFrame(Handler handler) throws IOException {
+      int w1;
+      int w2;
+      try {
+        w1 = source.readInt();
+        w2 = source.readInt();
+      } catch (IOException e) {
+        return false; // This might be a normal socket close.
+      }
+
+      boolean control = (w1 & 0x80000000) != 0;
+      int flags = (w2 & 0xff000000) >>> 24;
+      int length = (w2 & 0xffffff);
+
+      if (control) {
+        int version = (w1 & 0x7fff0000) >>> 16;
+        int type = (w1 & 0xffff);
+
+        if (version != 3) {
+          throw new ProtocolException("version != 3: " + version);
+        }
+
+        switch (type) {
+          case TYPE_SYN_STREAM:
+            readSynStream(handler, flags, length);
+            return true;
+
+          case TYPE_SYN_REPLY:
+            readSynReply(handler, flags, length);
+            return true;
+
+          case TYPE_RST_STREAM:
+            readRstStream(handler, flags, length);
+            return true;
+
+          case TYPE_SETTINGS:
+            readSettings(handler, flags, length);
+            return true;
+
+          case TYPE_PING:
+            readPing(handler, flags, length);
+            return true;
+
+          case TYPE_GOAWAY:
+            readGoAway(handler, flags, length);
+            return true;
+
+          case TYPE_HEADERS:
+            readHeaders(handler, flags, length);
+            return true;
+
+          case TYPE_WINDOW_UPDATE:
+            readWindowUpdate(handler, flags, length);
+            return true;
+
+          default:
+            source.skip(length);
+            return true;
+        }
+      } else {
+        int streamId = w1 & 0x7fffffff;
+        boolean inFinished = (flags & FLAG_FIN) != 0;
+        handler.data(inFinished, streamId, source, length);
+        return true;
+      }
+    }
+
+    private void readSynStream(Handler handler, int flags, int length) throws IOException {
+      int w1 = source.readInt();
+      int w2 = source.readInt();
+      int s3 = source.readShort();
+      int streamId = w1 & 0x7fffffff;
+      int associatedStreamId = w2 & 0x7fffffff;
+      int priority = (s3 & 0xe000) >>> 13;
+      // int slot = s3 & 0xff;
+      List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 10);
+
+      boolean inFinished = (flags & FLAG_FIN) != 0;
+      boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0;
+      handler.headers(outFinished, inFinished, streamId, associatedStreamId, priority,
+          headerBlock, HeadersMode.SPDY_SYN_STREAM);
+    }
+
+    private void readSynReply(Handler handler, int flags, int length) throws IOException {
+      int w1 = source.readInt();
+      int streamId = w1 & 0x7fffffff;
+      List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 4);
+      boolean inFinished = (flags & FLAG_FIN) != 0;
+      handler.headers(false, inFinished, streamId, -1, -1, headerBlock, HeadersMode.SPDY_REPLY);
+    }
+
+    private void readRstStream(Handler handler, int flags, int length) throws IOException {
+      if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length);
+      int streamId = source.readInt() & 0x7fffffff;
+      int errorCodeInt = source.readInt();
+      ErrorCode errorCode = ErrorCode.fromSpdy3Rst(errorCodeInt);
+      if (errorCode == null) {
+        throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
+      }
+      handler.rstStream(streamId, errorCode);
+    }
+
+    private void readHeaders(Handler handler, int flags, int length) throws IOException {
+      int w1 = source.readInt();
+      int streamId = w1 & 0x7fffffff;
+      List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 4);
+      handler.headers(false, false, streamId, -1, -1, headerBlock, HeadersMode.SPDY_HEADERS);
+    }
+
+    private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
+      if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length);
+      int w1 = source.readInt();
+      int w2 = source.readInt();
+      int streamId = w1 & 0x7fffffff;
+      long increment = w2 & 0x7fffffff;
+      if (increment == 0) throw ioException("windowSizeIncrement was 0", increment);
+      handler.windowUpdate(streamId, increment);
+    }
+
+    private void readPing(Handler handler, int flags, int length) throws IOException {
+      if (length != 4) throw ioException("TYPE_PING length: %d != 4", length);
+      int id = source.readInt();
+      boolean ack = client == ((id & 1) == 1);
+      handler.ping(ack, id, 0);
+    }
+
+    private void readGoAway(Handler handler, int flags, int length) throws IOException {
+      if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length);
+      int lastGoodStreamId = source.readInt() & 0x7fffffff;
+      int errorCodeInt = source.readInt();
+      ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt);
+      if (errorCode == null) {
+        throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
+      }
+      handler.goAway(lastGoodStreamId, errorCode, ByteString.EMPTY);
+    }
+
+    private void readSettings(Handler handler, int flags, int length) throws IOException {
+      int numberOfEntries = source.readInt();
+      if (length != 4 + 8 * numberOfEntries) {
+        throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries);
+      }
+      Settings settings = new Settings();
+      for (int i = 0; i < numberOfEntries; i++) {
+        int w1 = source.readInt();
+        int value = source.readInt();
+        int idFlags = (w1 & 0xff000000) >>> 24;
+        int id = w1 & 0xffffff;
+        settings.set(id, idFlags, value);
+      }
+      boolean clearPrevious = (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0;
+      handler.settings(clearPrevious, settings);
+    }
+
+    private static IOException ioException(String message, Object... args) throws IOException {
+      throw new IOException(String.format(message, args));
+    }
+
+    @Override public void close() throws IOException {
+      headerBlockReader.close();
+    }
+  }
+
+  /** Write spdy/3 frames. */
+  static final class Writer implements FrameWriter {
+    private final BufferedSink sink;
+    private final OkBuffer headerBlockBuffer;
+    private final BufferedSink headerBlockOut;
+    private final boolean client;
+    private boolean closed;
+
+    Writer(BufferedSink sink, boolean client) {
+      this.sink = sink;
+      this.client = client;
+
+      Deflater deflater = new Deflater();
+      deflater.setDictionary(DICTIONARY);
+      headerBlockBuffer = new OkBuffer();
+      headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater));
+    }
+
+    @Override public void ackSettings() {
+      // Do nothing: no ACK for SPDY/3 settings.
+    }
+
+    @Override
+    public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders)
+        throws IOException {
+      // Do nothing: no push promise for SPDY/3.
+    }
+
+    @Override public synchronized void connectionHeader() {
+      // Do nothing: no connection header for SPDY/3.
+    }
+
+    @Override public synchronized void flush() throws IOException {
+      if (closed) throw new IOException("closed");
+      sink.flush();
+    }
+
+    @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
+        int streamId, int associatedStreamId, int priority, int slot, List<Header> headerBlock)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      writeNameValueBlockToBuffer(headerBlock);
+      int length = (int) (10 + headerBlockBuffer.size());
+      int type = TYPE_SYN_STREAM;
+      int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0);
+
+      int unused = 0;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(streamId & 0x7fffffff);
+      sink.writeInt(associatedStreamId & 0x7fffffff);
+      sink.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
+      sink.write(headerBlockBuffer, headerBlockBuffer.size());
+      sink.flush();
+    }
+
+    @Override public synchronized void synReply(boolean outFinished, int streamId,
+        List<Header> headerBlock) throws IOException {
+      if (closed) throw new IOException("closed");
+      writeNameValueBlockToBuffer(headerBlock);
+      int type = TYPE_SYN_REPLY;
+      int flags = (outFinished ? FLAG_FIN : 0);
+      int length = (int) (headerBlockBuffer.size() + 4);
+
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(streamId & 0x7fffffff);
+      sink.write(headerBlockBuffer, headerBlockBuffer.size());
+      sink.flush();
+    }
+
+    @Override public synchronized void headers(int streamId, List<Header> headerBlock)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      writeNameValueBlockToBuffer(headerBlock);
+      int flags = 0;
+      int type = TYPE_HEADERS;
+      int length = (int) (headerBlockBuffer.size() + 4);
+
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(streamId & 0x7fffffff);
+      sink.write(headerBlockBuffer, headerBlockBuffer.size());
+    }
+
+    @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
+      int flags = 0;
+      int type = TYPE_RST_STREAM;
+      int length = 8;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(streamId & 0x7fffffff);
+      sink.writeInt(errorCode.spdyRstCode);
+      sink.flush();
+    }
+
+    @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source)
+        throws IOException {
+      data(outFinished, streamId, source, (int) source.size());
+    }
+
+    @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source,
+        int byteCount) throws IOException {
+      int flags = (outFinished ? FLAG_FIN : 0);
+      sendDataFrame(streamId, flags, source, byteCount);
+    }
+
+    void sendDataFrame(int streamId, int flags, OkBuffer buffer, int byteCount)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      if (byteCount > 0xffffffL) {
+        throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount);
+      }
+      sink.writeInt(streamId & 0x7fffffff);
+      sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff);
+      if (byteCount > 0) {
+        sink.write(buffer, byteCount);
+      }
+    }
+
+    private void writeNameValueBlockToBuffer(List<Header> headerBlock) throws IOException {
+      if (headerBlockBuffer.size() != 0) throw new IllegalStateException();
+      headerBlockOut.writeInt(headerBlock.size());
+      for (int i = 0, size = headerBlock.size(); i < size; i++) {
+        ByteString name = headerBlock.get(i).name;
+        headerBlockOut.writeInt(name.size());
+        headerBlockOut.write(name);
+        ByteString value = headerBlock.get(i).value;
+        headerBlockOut.writeInt(value.size());
+        headerBlockOut.write(value);
+      }
+      headerBlockOut.flush();
+    }
+
+    @Override public synchronized void settings(Settings settings) throws IOException {
+      if (closed) throw new IOException("closed");
+      int type = TYPE_SETTINGS;
+      int flags = 0;
+      int size = settings.size();
+      int length = 4 + size * 8;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(size);
+      for (int i = 0; i <= Settings.COUNT; i++) {
+        if (!settings.isSet(i)) continue;
+        int settingsFlags = settings.flags(i);
+        sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff));
+        sink.writeInt(settings.get(i));
+      }
+      sink.flush();
+    }
+
+    @Override public synchronized void ping(boolean reply, int payload1, int payload2)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      boolean payloadIsReply = client != ((payload1 & 1) == 1);
+      if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply");
+      int type = TYPE_PING;
+      int flags = 0;
+      int length = 4;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(payload1);
+      sink.flush();
+    }
+
+    @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode,
+        byte[] ignored) throws IOException {
+      if (closed) throw new IOException("closed");
+      if (errorCode.spdyGoAwayCode == -1) {
+        throw new IllegalArgumentException("errorCode.spdyGoAwayCode == -1");
+      }
+      int type = TYPE_GOAWAY;
+      int flags = 0;
+      int length = 8;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(lastGoodStreamId);
+      sink.writeInt(errorCode.spdyGoAwayCode);
+      sink.flush();
+    }
+
+    @Override public synchronized void windowUpdate(int streamId, long increment)
+        throws IOException {
+      if (closed) throw new IOException("closed");
+      if (increment == 0 || increment > 0x7fffffffL) {
+        throw new IllegalArgumentException(
+            "windowSizeIncrement must be between 1 and 0x7fffffff: " + increment);
+      }
+      int type = TYPE_WINDOW_UPDATE;
+      int flags = 0;
+      int length = 8;
+      sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
+      sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+      sink.writeInt(streamId);
+      sink.writeInt((int) increment);
+      sink.flush();
+    }
+
+    @Override public synchronized void close() throws IOException {
+      closed = true;
+      Util.closeAll(sink, headerBlockOut);
+    }
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
new file mode 100644
index 0000000..da7c4e1
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -0,0 +1,842 @@
+/*
+ * 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.spdy;
+
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.OkBuffer;
+import okio.Okio;
+
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+
+/**
+ * A socket connection to a remote peer. A connection hosts streams which can
+ * send and receive data.
+ *
+ * <p>Many methods in this API are <strong>synchronous:</strong> the call is
+ * completed before the method returns. This is typical for Java but atypical
+ * for SPDY. This is motivated by exception transparency: an IOException that
+ * was triggered by a certain caller can be caught and handled by that caller.
+ */
+public final class SpdyConnection implements Closeable {
+
+  // Internal state of this connection is guarded by 'this'. No blocking
+  // operations may be performed while holding this lock!
+  //
+  // Socket writes are guarded by frameWriter.
+  //
+  // Socket reads are unguarded but are only made by the reader thread.
+  //
+  // Certain operations (like SYN_STREAM) need to synchronize on both the
+  // frameWriter (to do blocking I/O) and this (to create streams). Such
+  // operations must synchronize on 'this' last. This ensures that we never
+  // wait for a blocking operation while holding 'this'.
+
+  private static final ExecutorService executor = new ThreadPoolExecutor(0,
+      Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
+      Util.threadFactory("OkHttp SpdyConnection", true));
+
+  /** The protocol variant, like {@link com.squareup.okhttp.internal.spdy.Spdy3}. */
+  final Protocol protocol;
+
+  /** True if this peer initiated the connection. */
+  final boolean client;
+
+  /**
+   * User code to run in response to an incoming stream. Callbacks must not be
+   * run on the callback executor.
+   */
+  private final IncomingStreamHandler handler;
+  private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
+  private final String hostName;
+  private int lastGoodStreamId;
+  private int nextStreamId;
+  private boolean shutdown;
+  private long idleStartTimeNs = System.nanoTime();
+
+  /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
+  private Map<Integer, Ping> pings;
+  /** User code to run in response to push promise events. */
+  private final PushObserver pushObserver;
+  private int nextPingId;
+
+  /**
+   * The total number of bytes consumed by the application, but not yet
+   * acknowledged by sending a {@code WINDOW_UPDATE} frame on this connection.
+   */
+  // Visible for testing
+  long unacknowledgedBytesRead = 0;
+
+  /**
+   * Count of bytes that can be written on the connection before receiving a
+   * window update.
+   */
+  // Visible for testing
+  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 we receive from the peer. */
+  // TODO: MWS will need to guard on this setting before attempting to push.
+  final Settings peerSettings = new Settings();
+
+  private boolean receivedInitialPeerSettings = false;
+  final FrameReader frameReader;
+  final FrameWriter frameWriter;
+  final long maxFrameSize;
+
+  // Visible for testing
+  final Reader readerRunnable;
+
+  private SpdyConnection(Builder builder) {
+    protocol = builder.protocol;
+    pushObserver = builder.pushObserver;
+    client = builder.client;
+    handler = builder.handler;
+    nextStreamId = builder.client ? 1 : 2;
+    nextPingId = builder.client ? 1 : 2;
+
+    // Flow control was designed more for servers, or proxies than edge clients.
+    // If we are a client, set the flow control window to 16MiB.  This avoids
+    // thrashing window updates every 64KiB, yet small enough to avoid blowing
+    // up the heap.
+    if (builder.client) {
+      okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 16 * 1024 * 1024);
+    }
+
+    hostName = builder.hostName;
+
+    Variant variant;
+    if (protocol == Protocol.HTTP_2) {
+      variant = new Http20Draft09();
+    } else if (protocol == Protocol.SPDY_3) {
+      variant = new Spdy3();
+    } else {
+      throw new AssertionError(protocol);
+    }
+    bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+    frameReader = variant.newReader(builder.source, client);
+    frameWriter = variant.newWriter(builder.sink, client);
+    maxFrameSize = variant.maxFrameSize();
+
+    readerRunnable = new Reader();
+    new Thread(readerRunnable).start(); // Not a daemon thread.
+  }
+
+  /** The protocol as selected using NPN or ALPN. */
+  public Protocol getProtocol() {
+    return protocol;
+  }
+
+  /**
+   * Returns the number of {@link SpdyStream#isOpen() open streams} on this
+   * connection.
+   */
+  public synchronized int openStreamCount() {
+    return streams.size();
+  }
+
+  synchronized SpdyStream getStream(int id) {
+    return streams.get(id);
+  }
+
+  synchronized SpdyStream removeStream(int streamId) {
+    SpdyStream stream = streams.remove(streamId);
+    if (stream != null && streams.isEmpty()) {
+      setIdle(true);
+    }
+    return stream;
+  }
+
+  private synchronized void setIdle(boolean value) {
+    idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE;
+  }
+
+  /** Returns true if this connection is idle. */
+  public synchronized boolean isIdle() {
+    return idleStartTimeNs != Long.MAX_VALUE;
+  }
+
+  /**
+   * Returns the time in ns when this connection became idle or Long.MAX_VALUE
+   * if connection is not idle.
+   */
+  public synchronized long getIdleStartTimeNs() {
+    return idleStartTimeNs;
+  }
+
+  /**
+   * Returns a new server-initiated stream.
+   *
+   * @param associatedStreamId the stream that triggered the sender to create
+   *     this stream.
+   * @param out true to create an output stream that we can use to send data
+   *     to the remote peer. Corresponds to {@code FLAG_FIN}.
+   */
+  public SpdyStream pushStream(int associatedStreamId, List<Header> requestHeaders, boolean out)
+      throws IOException {
+    if (client) throw new IllegalStateException("Client cannot push requests.");
+    if (protocol != Protocol.HTTP_2) throw new IllegalStateException("protocol != HTTP_2");
+    return newStream(associatedStreamId, requestHeaders, out, false);
+  }
+
+  /**
+   * Returns a new locally-initiated stream.
+   *
+   * @param out true to create an output stream that we can use to send data to the remote peer.
+   *     Corresponds to {@code FLAG_FIN}.
+   * @param in true to create an input stream that the remote peer can use to send data to us.
+   *     Corresponds to {@code FLAG_UNIDIRECTIONAL}.
+   */
+  public SpdyStream newStream(List<Header> requestHeaders, boolean out, boolean in)
+      throws IOException {
+    return newStream(0, requestHeaders, out, in);
+  }
+
+  private SpdyStream newStream(int associatedStreamId, List<Header> requestHeaders, boolean out,
+      boolean in) throws IOException {
+    boolean outFinished = !out;
+    boolean inFinished = !in;
+    int priority = -1; // TODO: permit the caller to specify a priority?
+    int slot = 0; // TODO: permit the caller to specify a slot?
+    SpdyStream stream;
+    int streamId;
+
+    synchronized (frameWriter) {
+      synchronized (this) {
+        if (shutdown) {
+          throw new IOException("shutdown");
+        }
+        streamId = nextStreamId;
+        nextStreamId += 2;
+        stream = new SpdyStream(streamId, this, outFinished, inFinished, priority, requestHeaders);
+        if (stream.isOpen()) {
+          streams.put(streamId, stream);
+          setIdle(false);
+        }
+      }
+      if (associatedStreamId == 0) {
+        frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, priority, slot,
+            requestHeaders);
+      } else if (client) {
+        throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
+      } else { // HTTP/2 has a PUSH_PROMISE frame.
+        frameWriter.pushPromise(associatedStreamId, streamId, requestHeaders);
+      }
+    }
+
+    if (!out) {
+      frameWriter.flush();
+    }
+
+    return stream;
+  }
+
+  void writeSynReply(int streamId, boolean outFinished, List<Header> alternating)
+      throws IOException {
+    frameWriter.synReply(outFinished, streamId, alternating);
+  }
+
+  /**
+   * Callers of this method are not thread safe, and sometimes on application
+   * threads.  Most often, this method will be called to send a buffer worth of
+   * data to the peer.
+   * <p>
+   * Writes are subject to the write window of the stream and the connection.
+   * Until there is a window sufficient to send {@code byteCount}, the caller
+   * will block.  For example, a user of {@code HttpURLConnection} who flushes
+   * more bytes to the output stream than the connection's write window will
+   * block.
+   * <p>
+   * Zero {@code byteCount} writes are not subject to flow control and
+   * will not block.  The only use case for zero {@code byteCount} is closing
+   * a flushed output stream.
+   */
+  public void writeData(int streamId, boolean outFinished, OkBuffer buffer, long byteCount)
+      throws IOException {
+    if (byteCount == 0) { // Empty data frames are not flow-controlled.
+      frameWriter.data(outFinished, streamId, buffer, 0);
+      return;
+    }
+
+    while (byteCount > 0) {
+      int toWrite;
+      synchronized (SpdyConnection.this) {
+        try {
+          while (bytesLeftInWriteWindow <= 0) {
+            SpdyConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE.
+          }
+        } catch (InterruptedException e) {
+          throw new InterruptedIOException();
+        }
+
+        toWrite = (int) Math.min(Math.min(byteCount, bytesLeftInWriteWindow), maxFrameSize);
+        bytesLeftInWriteWindow -= toWrite;
+      }
+
+      byteCount -= toWrite;
+      frameWriter.data(outFinished && byteCount == 0, streamId, buffer, toWrite);
+    }
+  }
+
+  /**
+   * {@code delta} will be negative if a settings frame initial window is
+   * smaller than the last.
+   */
+  void addBytesToWriteWindow(long delta) {
+    bytesLeftInWriteWindow += delta;
+    if (delta > 0) SpdyConnection.this.notifyAll();
+  }
+
+  void writeSynResetLater(final int streamId, final ErrorCode errorCode) {
+    executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
+      @Override public void execute() {
+        try {
+          writeSynReset(streamId, errorCode);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  void writeSynReset(int streamId, ErrorCode statusCode) throws IOException {
+    frameWriter.rstStream(streamId, statusCode);
+  }
+
+  void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) {
+    executor.submit(new NamedRunnable("OkHttp Window Update %s stream %d", hostName, streamId) {
+      @Override public void execute() {
+        try {
+          frameWriter.windowUpdate(streamId, unacknowledgedBytesRead);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  /**
+   * Sends a ping frame to the peer. Use the returned object to await the
+   * ping's response and observe its round trip time.
+   */
+  public Ping ping() throws IOException {
+    Ping ping = new Ping();
+    int pingId;
+    synchronized (this) {
+      if (shutdown) {
+        throw new IOException("shutdown");
+      }
+      pingId = nextPingId;
+      nextPingId += 2;
+      if (pings == null) pings = new HashMap<Integer, Ping>();
+      pings.put(pingId, ping);
+    }
+    writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping);
+    return ping;
+  }
+
+  private void writePingLater(
+      final boolean reply, final int payload1, final int payload2, final Ping ping) {
+    executor.submit(new NamedRunnable("OkHttp %s ping %08x%08x",
+        hostName, payload1, payload2) {
+      @Override public void execute() {
+        try {
+          writePing(reply, payload1, payload2, ping);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException {
+    synchronized (frameWriter) {
+      // Observe the sent time immediately before performing I/O.
+      if (ping != null) ping.send();
+      frameWriter.ping(reply, payload1, payload2);
+    }
+  }
+
+  private synchronized Ping removePing(int id) {
+    return pings != null ? pings.remove(id) : null;
+  }
+
+  public void flush() throws IOException {
+    frameWriter.flush();
+  }
+
+  /**
+   * Degrades this connection such that new streams can neither be created
+   * locally, nor accepted from the remote peer. Existing streams are not
+   * impacted. This is intended to permit an endpoint to gracefully stop
+   * accepting new requests without harming previously established streams.
+   */
+  public void shutdown(ErrorCode statusCode) throws IOException {
+    synchronized (frameWriter) {
+      int lastGoodStreamId;
+      synchronized (this) {
+        if (shutdown) {
+          return;
+        }
+        shutdown = true;
+        lastGoodStreamId = this.lastGoodStreamId;
+      }
+      // TODO: propagate exception message into debugData
+      frameWriter.goAway(lastGoodStreamId, statusCode, Util.EMPTY_BYTE_ARRAY);
+    }
+  }
+
+  /**
+   * Closes this connection. This cancels all open streams and unanswered
+   * pings. It closes the underlying input and output streams and shuts down
+   * internal executor services.
+   */
+  @Override public void close() throws IOException {
+    close(ErrorCode.NO_ERROR, ErrorCode.CANCEL);
+  }
+
+  private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException {
+    assert (!Thread.holdsLock(this));
+    IOException thrown = null;
+    try {
+      shutdown(connectionCode);
+    } catch (IOException e) {
+      thrown = e;
+    }
+
+    SpdyStream[] streamsToClose = null;
+    Ping[] pingsToCancel = null;
+    synchronized (this) {
+      if (!streams.isEmpty()) {
+        streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
+        streams.clear();
+        setIdle(false);
+      }
+      if (pings != null) {
+        pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
+        pings = null;
+      }
+    }
+
+    if (streamsToClose != null) {
+      for (SpdyStream stream : streamsToClose) {
+        try {
+          stream.close(streamCode);
+        } catch (IOException e) {
+          if (thrown != null) thrown = e;
+        }
+      }
+    }
+
+    if (pingsToCancel != null) {
+      for (Ping ping : pingsToCancel) {
+        ping.cancel();
+      }
+    }
+
+    try {
+      frameReader.close();
+    } catch (IOException e) {
+      thrown = e;
+    }
+    try {
+      frameWriter.close();
+    } catch (IOException e) {
+      if (thrown == null) thrown = e;
+    }
+
+    if (thrown != null) throw thrown;
+  }
+
+  /**
+   * Sends a connection header if the current variant requires it. This should
+   * be called after {@link Builder#build} for all new connections.
+   */
+  public void sendConnectionHeader() throws IOException {
+    frameWriter.connectionHeader();
+    frameWriter.settings(okHttpSettings);
+  }
+
+  public static class Builder {
+    private String hostName;
+    private BufferedSource source;
+    private BufferedSink sink;
+    private IncomingStreamHandler handler = IncomingStreamHandler.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;
+      this.client = client;
+      this.source = Okio.buffer(Okio.source(socket.getInputStream()));
+      this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
+    }
+
+    public Builder handler(IncomingStreamHandler handler) {
+      this.handler = handler;
+      return this;
+    }
+
+    public Builder protocol(Protocol protocol) {
+      this.protocol = protocol;
+      return this;
+    }
+
+    public Builder pushObserver(PushObserver pushObserver) {
+      this.pushObserver = pushObserver;
+      return this;
+    }
+
+    public SpdyConnection build() {
+      return new SpdyConnection(this);
+    }
+  }
+
+  /**
+   * Methods in this class must not lock FrameWriter.  If a method needs to
+   * write a frame, create an async task to do so.
+   */
+  class Reader extends NamedRunnable implements FrameReader.Handler {
+    private Reader() {
+      super("OkHttp %s", hostName);
+    }
+
+    @Override protected void execute() {
+      ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
+      ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
+      try {
+        if (!client) {
+          frameReader.readConnectionHeader();
+        }
+        while (frameReader.nextFrame(this)) {
+        }
+        connectionErrorCode = ErrorCode.NO_ERROR;
+        streamErrorCode = ErrorCode.CANCEL;
+      } catch (IOException e) {
+        connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
+        streamErrorCode = ErrorCode.PROTOCOL_ERROR;
+      } finally {
+        try {
+          close(connectionErrorCode, streamErrorCode);
+        } catch (IOException ignored) {
+        }
+      }
+    }
+
+    @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)
+        throws IOException {
+      if (pushedStream(streamId)) {
+        pushDataLater(streamId, source, length, inFinished);
+        return;
+      }
+      SpdyStream dataStream = getStream(streamId);
+      if (dataStream == null) {
+        writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
+        source.skip(length);
+        return;
+      }
+      dataStream.receiveData(source, length);
+      if (inFinished) {
+        dataStream.receiveFin();
+      }
+    }
+
+    @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
+        int associatedStreamId, int priority, List<Header> headerBlock, HeadersMode headersMode) {
+      if (pushedStream(streamId)) {
+        pushHeadersLater(streamId, headerBlock, inFinished);
+        return;
+      }
+      SpdyStream stream;
+      synchronized (SpdyConnection.this) {
+        // If we're shutdown, don't bother with this stream.
+        if (shutdown) return;
+
+        stream = getStream(streamId);
+
+        if (stream == null) {
+          // The headers claim to be for an existing stream, but we don't have one.
+          if (headersMode.failIfStreamAbsent()) {
+            writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
+            return;
+          }
+
+          // If the stream ID is less than the last created ID, assume it's already closed.
+          if (streamId <= lastGoodStreamId) return;
+
+          // If the stream ID is in the client's namespace, assume it's already closed.
+          if (streamId % 2 == nextStreamId % 2) return;
+
+          // Create a stream.
+          final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished,
+              inFinished, priority, headerBlock);
+          lastGoodStreamId = streamId;
+          streams.put(streamId, newStream);
+          executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
+            @Override public void execute() {
+              try {
+                handler.receive(newStream);
+              } catch (IOException e) {
+                throw new RuntimeException(e);
+              }
+            }
+          });
+          return;
+        }
+      }
+
+      // The headers claim to be for a new stream, but we already have one.
+      if (headersMode.failIfStreamPresent()) {
+        stream.closeLater(ErrorCode.PROTOCOL_ERROR);
+        removeStream(streamId);
+        return;
+      }
+
+      // Update an existing stream.
+      stream.receiveHeaders(headerBlock, headersMode);
+      if (inFinished) stream.receiveFin();
+    }
+
+    @Override public void rstStream(int streamId, ErrorCode errorCode) {
+      if (pushedStream(streamId)) {
+        pushResetLater(streamId, errorCode);
+        return;
+      }
+      SpdyStream rstStream = removeStream(streamId);
+      if (rstStream != null) {
+        rstStream.receiveRstStream(errorCode);
+      }
+    }
+
+    @Override public void settings(boolean clearPrevious, Settings newSettings) {
+      long delta = 0;
+      SpdyStream[] streamsToNotify = null;
+      synchronized (SpdyConnection.this) {
+        int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+        if (clearPrevious) peerSettings.clear();
+        peerSettings.merge(newSettings);
+        if (getProtocol() == Protocol.HTTP_2) {
+          ackSettingsLater();
+        }
+        int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+        if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) {
+          delta = peerInitialWindowSize - priorWriteWindowSize;
+          if (!receivedInitialPeerSettings) {
+            addBytesToWriteWindow(delta);
+            receivedInitialPeerSettings = true;
+          }
+          if (!streams.isEmpty()) {
+            streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]);
+          }
+        }
+      }
+      if (streamsToNotify != null && delta != 0) {
+        for (SpdyStream stream : streams.values()) {
+          synchronized (stream) {
+            stream.addBytesToWriteWindow(delta);
+          }
+        }
+      }
+    }
+
+    private void ackSettingsLater() {
+      executor.submit(new NamedRunnable("OkHttp %s ACK Settings", hostName) {
+        @Override public void execute() {
+          try {
+            frameWriter.ackSettings();
+          } catch (IOException ignored) {
+          }
+        }
+      });
+    }
+
+    @Override public void ackSettings() {
+      // TODO: If we don't get this callback after sending settings to the peer, SETTINGS_TIMEOUT.
+    }
+
+    @Override public void ping(boolean reply, int payload1, int payload2) {
+      if (reply) {
+        Ping ping = removePing(payload1);
+        if (ping != null) {
+          ping.receive();
+        }
+      } else {
+        // Send a reply to a client ping if this is a server and vice versa.
+        writePingLater(true, payload1, payload2, null);
+      }
+    }
+
+    @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+      if (debugData.size() > 0) { // TODO: log the debugData
+      }
+      synchronized (SpdyConnection.this) {
+        shutdown = true;
+
+        // Fail all streams created after the last good stream ID.
+        for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
+            i.hasNext(); ) {
+          Map.Entry<Integer, SpdyStream> entry = i.next();
+          int streamId = entry.getKey();
+          if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
+            entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM);
+            i.remove();
+          }
+        }
+      }
+    }
+
+    @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
+      if (streamId == 0) {
+        synchronized (SpdyConnection.this) {
+          bytesLeftInWriteWindow += windowSizeIncrement;
+          SpdyConnection.this.notifyAll();
+        }
+      } else {
+        SpdyStream stream = getStream(streamId);
+        if (stream != null) {
+          synchronized (stream) {
+            stream.addBytesToWriteWindow(windowSizeIncrement);
+          }
+        }
+      }
+    }
+
+    @Override public void priority(int streamId, int priority) {
+      // TODO: honor priority.
+    }
+
+    @Override
+    public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {
+      pushRequestLater(promisedStreamId, requestHeaders);
+    }
+  }
+
+  /** Even, positive numbered streams are pushed streams in HTTP/2. */
+  private boolean pushedStream(int streamId) {
+    return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0;
+  }
+
+  // Guarded by this.
+  private final Set<Integer> currentPushRequests = new LinkedHashSet<Integer>();
+
+  private void pushRequestLater(final int streamId, final List<Header> requestHeaders) {
+    synchronized (this) {
+      if (currentPushRequests.contains(streamId)) {
+        writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR);
+        return;
+      }
+      currentPushRequests.add(streamId);
+    }
+    executor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) {
+      @Override public void execute() {
+        boolean cancel = pushObserver.onRequest(streamId, requestHeaders);
+        try {
+          if (cancel) {
+            frameWriter.rstStream(streamId, ErrorCode.CANCEL);
+            synchronized (SpdyConnection.this) {
+              currentPushRequests.remove(streamId);
+            }
+          }
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  private void pushHeadersLater(final int streamId, final List<Header> requestHeaders,
+      final boolean inFinished) {
+    executor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) {
+      @Override public void execute() {
+        boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished);
+        try {
+          if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL);
+          if (cancel || inFinished) {
+            synchronized (SpdyConnection.this) {
+              currentPushRequests.remove(streamId);
+            }
+          }
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  /**
+   * Eagerly reads {@code byteCount} bytes from the source before launching a background task to
+   * process the data.  This avoids corrupting the stream.
+   */
+  private void pushDataLater(final int streamId, final BufferedSource source, final int byteCount,
+      final boolean inFinished) throws IOException {
+    final OkBuffer buffer = new OkBuffer();
+    source.require(byteCount); // Eagerly read the frame before firing client thread.
+    source.read(buffer, byteCount);
+    if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount);
+    executor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) {
+      @Override public void execute() {
+        try {
+          boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished);
+          if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL);
+          if (cancel || inFinished) {
+            synchronized (SpdyConnection.this) {
+              currentPushRequests.remove(streamId);
+            }
+          }
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  private void pushResetLater(final int streamId, final ErrorCode errorCode) {
+    executor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) {
+      @Override public void execute() {
+        pushObserver.onReset(streamId, errorCode);
+        synchronized (SpdyConnection.this) {
+          currentPushRequests.remove(streamId);
+        }
+      }
+    });
+  }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
new file mode 100644
index 0000000..0fcde2d
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -0,0 +1,591 @@
+/*
+ * 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.spdy;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import okio.BufferedSource;
+import okio.Deadline;
+import okio.OkBuffer;
+import okio.Sink;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+
+/** A logical bidirectional stream. */
+public final class SpdyStream {
+  // Internal state is guarded by this. No long-running or potentially
+  // blocking operations are performed while the lock is held.
+
+  /**
+   * The total number of bytes consumed by the application (with {@link
+   * SpdyDataSource#read}), but not yet acknowledged by sending a {@code
+   * WINDOW_UPDATE} frame on this stream.
+   */
+  // Visible for testing
+  long unacknowledgedBytesRead = 0;
+
+  /**
+   * Count of bytes that can be written on the stream before receiving a
+   * window update. Even if this is positive, writes will block until there
+   * available bytes in {@code connection.bytesLeftInWriteWindow}.
+   */
+  // guarded by this
+  long bytesLeftInWriteWindow;
+
+  private final int id;
+  private final SpdyConnection connection;
+  private final int priority;
+  private long readTimeoutMillis = 0;
+
+  /** Headers sent by the stream initiator. Immutable and non null. */
+  private final List<Header> requestHeaders;
+
+  /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
+  private List<Header> responseHeaders;
+
+  private final SpdyDataSource source;
+  final SpdyDataSink sink;
+
+  /**
+   * The reason why this stream was abnormally closed. If there are multiple
+   * reasons to abnormally close this stream (such as both peers closing it
+   * near-simultaneously) then this is the first reason known to this peer.
+   */
+  private ErrorCode errorCode = null;
+
+  SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished,
+      int priority, List<Header> requestHeaders) {
+    if (connection == null) throw new NullPointerException("connection == null");
+    if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
+    this.id = id;
+    this.connection = connection;
+    this.bytesLeftInWriteWindow =
+        connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+    this.source = new SpdyDataSource(
+        connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
+    this.sink = new SpdyDataSink();
+    this.source.finished = inFinished;
+    this.sink.finished = outFinished;
+    this.priority = priority;
+    this.requestHeaders = requestHeaders;
+  }
+
+  public int getId() {
+    return id;
+  }
+
+  /**
+   * Returns true if this stream is open. A stream is open until either:
+   * <ul>
+   * <li>A {@code SYN_RESET} frame abnormally terminates the stream.
+   * <li>Both input and output streams have transmitted all data and
+   * headers.
+   * </ul>
+   * Note that the input stream may continue to yield data even after a stream
+   * reports itself as not open. This is because input data is buffered.
+   */
+  public synchronized boolean isOpen() {
+    if (errorCode != null) {
+      return false;
+    }
+    if ((source.finished || source.closed)
+        && (sink.finished || sink.closed)
+        && responseHeaders != null) {
+      return false;
+    }
+    return true;
+  }
+
+  /** Returns true if this stream was created by this peer. */
+  public boolean isLocallyInitiated() {
+    boolean streamIsClient = ((id & 1) == 1);
+    return connection.client == streamIsClient;
+  }
+
+  public SpdyConnection getConnection() {
+    return connection;
+  }
+
+  public List<Header> getRequestHeaders() {
+    return requestHeaders;
+  }
+
+  /**
+   * Returns the stream's response headers, blocking if necessary if they
+   * have not been received yet.
+   */
+  public synchronized List<Header> getResponseHeaders() throws IOException {
+    long remaining = 0;
+    long start = 0;
+    if (readTimeoutMillis != 0) {
+      start = (System.nanoTime() / 1000000);
+      remaining = readTimeoutMillis;
+    }
+    try {
+      while (responseHeaders == null && errorCode == null) {
+        if (readTimeoutMillis == 0) { // No timeout configured.
+          wait();
+        } else if (remaining > 0) {
+          wait(remaining);
+          remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
+        } else {
+          throw new SocketTimeoutException("Read response header timeout. readTimeoutMillis: "
+                            + readTimeoutMillis);
+        }
+      }
+      if (responseHeaders != null) {
+        return responseHeaders;
+      }
+      throw new IOException("stream was reset: " + errorCode);
+    } catch (InterruptedException e) {
+      InterruptedIOException rethrow = new InterruptedIOException();
+      rethrow.initCause(e);
+      throw rethrow;
+    }
+  }
+
+  /**
+   * Returns the reason why this stream was closed, or null if it closed
+   * normally or has not yet been closed.
+   */
+  public synchronized ErrorCode getErrorCode() {
+    return errorCode;
+  }
+
+  /**
+   * Sends a reply to an incoming stream.
+   *
+   * @param out true to create an output stream that we can use to send data
+   * to the remote peer. Corresponds to {@code FLAG_FIN}.
+   */
+  public void reply(List<Header> responseHeaders, boolean out) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean outFinished = false;
+    synchronized (this) {
+      if (responseHeaders == null) {
+        throw new NullPointerException("responseHeaders == null");
+      }
+      if (this.responseHeaders != null) {
+        throw new IllegalStateException("reply already sent");
+      }
+      this.responseHeaders = responseHeaders;
+      if (!out) {
+        this.sink.finished = true;
+        outFinished = true;
+      }
+    }
+    connection.writeSynReply(id, outFinished, responseHeaders);
+
+    if (outFinished) {
+      connection.flush();
+    }
+  }
+
+  /**
+   * Sets the maximum time to wait on input stream reads before failing with a
+   * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely.
+   */
+  public void setReadTimeout(long readTimeoutMillis) {
+    this.readTimeoutMillis = readTimeoutMillis;
+  }
+
+  public long getReadTimeoutMillis() {
+    return readTimeoutMillis;
+  }
+
+  /** Returns a source that reads data from the peer. */
+  public Source getSource() {
+    return source;
+  }
+
+  /**
+   * Returns a sink that can be used to write data to the peer.
+   *
+   * @throws IllegalStateException if this stream was initiated by the peer
+   *     and a {@link #reply} has not yet been sent.
+   */
+  public Sink getSink() {
+    synchronized (this) {
+      if (responseHeaders == null && !isLocallyInitiated()) {
+        throw new IllegalStateException("reply before requesting the sink");
+      }
+    }
+    return sink;
+  }
+
+  /**
+   * Abnormally terminate this stream. This blocks until the {@code RST_STREAM}
+   * frame has been transmitted.
+   */
+  public void close(ErrorCode rstStatusCode) throws IOException {
+    if (!closeInternal(rstStatusCode)) {
+      return; // Already closed.
+    }
+    connection.writeSynReset(id, rstStatusCode);
+  }
+
+  /**
+   * Abnormally terminate this stream. This enqueues a {@code RST_STREAM}
+   * frame and returns immediately.
+   */
+  public void closeLater(ErrorCode errorCode) {
+    if (!closeInternal(errorCode)) {
+      return; // Already closed.
+    }
+    connection.writeSynResetLater(id, errorCode);
+  }
+
+  /** Returns true if this stream was closed. */
+  private boolean closeInternal(ErrorCode errorCode) {
+    assert (!Thread.holdsLock(this));
+    synchronized (this) {
+      if (this.errorCode != null) {
+        return false;
+      }
+      if (source.finished && sink.finished) {
+        return false;
+      }
+      this.errorCode = errorCode;
+      notifyAll();
+    }
+    connection.removeStream(id);
+    return true;
+  }
+
+  void receiveHeaders(List<Header> headers, HeadersMode headersMode) {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    ErrorCode errorCode = null;
+    boolean open = true;
+    synchronized (this) {
+      if (responseHeaders == null) {
+        if (headersMode.failIfHeadersAbsent()) {
+          errorCode = ErrorCode.PROTOCOL_ERROR;
+        } else {
+          responseHeaders = headers;
+          open = isOpen();
+          notifyAll();
+        }
+      } else {
+        if (headersMode.failIfHeadersPresent()) {
+          errorCode = ErrorCode.STREAM_IN_USE;
+        } else {
+          List<Header> newHeaders = new ArrayList<Header>();
+          newHeaders.addAll(responseHeaders);
+          newHeaders.addAll(headers);
+          this.responseHeaders = newHeaders;
+        }
+      }
+    }
+    if (errorCode != null) {
+      closeLater(errorCode);
+    } else if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  void receiveData(BufferedSource in, int length) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    this.source.receive(in, length);
+  }
+
+  void receiveFin() {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean open;
+    synchronized (this) {
+      this.source.finished = true;
+      open = isOpen();
+      notifyAll();
+    }
+    if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  synchronized void receiveRstStream(ErrorCode errorCode) {
+    if (this.errorCode == null) {
+      this.errorCode = errorCode;
+      notifyAll();
+    }
+  }
+
+  int getPriority() {
+    return priority;
+  }
+
+  /**
+   * A source that reads the incoming data frames of a stream. Although this
+   * class uses synchronization to safely receive incoming data frames, it is
+   * not intended for use by multiple readers.
+   */
+  private final class SpdyDataSource implements Source {
+    /** Buffer to receive data from the network into. Only accessed by the reader thread. */
+    private final OkBuffer receiveBuffer = new OkBuffer();
+
+    /** Buffer with readable data. Guarded by SpdyStream.this. */
+    private final OkBuffer readBuffer = new OkBuffer();
+
+    /** Maximum number of bytes to buffer before reporting a flow control error. */
+    private final long maxByteCount;
+
+    /** True if the caller has closed this stream. */
+    private boolean closed;
+
+    /**
+     * True if either side has cleanly shut down this stream. We will
+     * receive no more bytes beyond those already in the buffer.
+     */
+    private boolean finished;
+
+    private SpdyDataSource(long maxByteCount) {
+      this.maxByteCount = maxByteCount;
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount)
+        throws IOException {
+      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+
+      long read;
+      synchronized (SpdyStream.this) {
+        waitUntilReadable();
+        checkNotClosed();
+        if (readBuffer.size() == 0) return -1; // This source is exhausted.
+
+        // Move bytes from the read buffer into the caller's buffer.
+        read = readBuffer.read(sink, Math.min(byteCount, readBuffer.size()));
+
+        // Flow control: notify the peer that we're ready for more data!
+        unacknowledgedBytesRead += read;
+        if (unacknowledgedBytesRead
+            >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
+          connection.writeWindowUpdateLater(id, unacknowledgedBytesRead);
+          unacknowledgedBytesRead = 0;
+        }
+      }
+
+      // Update connection.unacknowledgedBytesRead outside the stream lock.
+      synchronized (connection) { // Multiple application threads may hit this section.
+        connection.unacknowledgedBytesRead += read;
+        if (connection.unacknowledgedBytesRead
+            >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
+          connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead);
+          connection.unacknowledgedBytesRead = 0;
+        }
+      }
+
+      return read;
+    }
+
+    /**
+     * Returns once the input stream is either readable or finished. Throws
+     * a {@link SocketTimeoutException} if the read timeout elapses before
+     * that happens.
+     */
+    private void waitUntilReadable() throws IOException {
+      long start = 0;
+      long remaining = 0;
+      if (readTimeoutMillis != 0) {
+        start = (System.nanoTime() / 1000000);
+        remaining = readTimeoutMillis;
+      }
+      try {
+        while (readBuffer.size() == 0 && !finished && !closed && errorCode == null) {
+          if (readTimeoutMillis == 0) {
+            SpdyStream.this.wait();
+          } else if (remaining > 0) {
+            SpdyStream.this.wait(remaining);
+            remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
+          } else {
+            throw new SocketTimeoutException("Read timed out");
+          }
+        }
+      } catch (InterruptedException e) {
+        throw new InterruptedIOException();
+      }
+    }
+
+    void receive(BufferedSource in, long byteCount) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+
+      while (byteCount > 0) {
+        boolean finished;
+        boolean flowControlError;
+        synchronized (SpdyStream.this) {
+          finished = this.finished;
+          flowControlError = byteCount + readBuffer.size() > maxByteCount;
+        }
+
+        // If the peer sends more data than we can handle, discard it and close the connection.
+        if (flowControlError) {
+          in.skip(byteCount);
+          closeLater(ErrorCode.FLOW_CONTROL_ERROR);
+          return;
+        }
+
+        // Discard data received after the stream is finished. It's probably a benign race.
+        if (finished) {
+          in.skip(byteCount);
+          return;
+        }
+
+        // Fill the receive buffer without holding any locks.
+        long read = in.read(receiveBuffer, byteCount);
+        if (read == -1) throw new EOFException();
+        byteCount -= read;
+
+        // Move the received data to the read buffer to the reader can read it.
+        synchronized (SpdyStream.this) {
+          boolean wasEmpty = readBuffer.size() == 0;
+          readBuffer.write(receiveBuffer, receiveBuffer.size());
+          if (wasEmpty) {
+            SpdyStream.this.notifyAll();
+          }
+        }
+      }
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      // TODO: honor deadlines.
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      synchronized (SpdyStream.this) {
+        closed = true;
+        readBuffer.clear();
+        SpdyStream.this.notifyAll();
+      }
+      cancelStreamIfNecessary();
+    }
+
+    private void checkNotClosed() throws IOException {
+      if (closed) {
+        throw new IOException("stream closed");
+      }
+      if (errorCode != null) {
+        throw new IOException("stream was reset: " + errorCode);
+      }
+    }
+  }
+
+  private void cancelStreamIfNecessary() throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean open;
+    boolean cancel;
+    synchronized (this) {
+      cancel = !source.finished && source.closed && (sink.finished || sink.closed);
+      open = isOpen();
+    }
+    if (cancel) {
+      // RST this stream to prevent additional data from being sent. This
+      // is safe because the input stream is closed (we won't use any
+      // further bytes) and the output stream is either finished or closed
+      // (so RSTing both streams doesn't cause harm).
+      SpdyStream.this.close(ErrorCode.CANCEL);
+    } else if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  /**
+   * A sink that writes outgoing data frames of a stream. This class is not
+   * thread safe.
+   */
+  final class SpdyDataSink implements Sink {
+    private boolean closed;
+
+    /**
+     * True if either side has cleanly shut down this stream. We shall send
+     * no more bytes.
+     */
+    private boolean finished;
+
+    @Override public void write(OkBuffer source, long byteCount) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      while (byteCount > 0) {
+        long toWrite;
+        synchronized (SpdyStream.this) {
+          try {
+            while (bytesLeftInWriteWindow <= 0) {
+              SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
+            }
+          } catch (InterruptedException e) {
+            throw new InterruptedIOException();
+          }
+
+          checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting.
+          toWrite = Math.min(bytesLeftInWriteWindow, byteCount);
+          bytesLeftInWriteWindow -= toWrite;
+        }
+
+        byteCount -= toWrite;
+        connection.writeData(id, false, source, toWrite);
+      }
+    }
+
+    @Override public void flush() throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      synchronized (SpdyStream.this) {
+        checkOutNotClosed();
+      }
+      connection.flush();
+    }
+
+    @Override public Sink deadline(Deadline deadline) {
+      // TODO: honor deadlines.
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      synchronized (SpdyStream.this) {
+        if (closed) return;
+      }
+      if (!sink.finished) {
+        connection.writeData(id, true, null, 0);
+      }
+      synchronized (SpdyStream.this) {
+        closed = true;
+      }
+      connection.flush();
+      cancelStreamIfNecessary();
+    }
+  }
+
+  /**
+   * {@code delta} will be negative if a settings frame initial window is
+   * smaller than the last.
+   */
+  void addBytesToWriteWindow(long delta) {
+    bytesLeftInWriteWindow += delta;
+    if (delta > 0) SpdyStream.this.notifyAll();
+  }
+
+  private void checkOutNotClosed() throws IOException {
+    if (sink.closed) {
+      throw new IOException("stream closed");
+    } else if (sink.finished) {
+      throw new IOException("stream finished");
+    } else if (errorCode != null) {
+      throw new IOException("stream was reset: " + errorCode);
+    }
+  }
+}
diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
similarity index 70%
rename from okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
index 06de317..f8b14ac 100644
--- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
@@ -15,23 +15,25 @@
  */
 package com.squareup.okhttp.internal.spdy;
 
-import java.io.InputStream;
-import java.io.OutputStream;
+import com.squareup.okhttp.Protocol;
+import okio.BufferedSink;
+import okio.BufferedSource;
 
 /** A version and dialect of the framed socket protocol. */
 interface Variant {
-  Variant SPDY3 = new Spdy3();
-  Variant HTTP_20_DRAFT_04 = new Http20Draft04();
+
+  /** The protocol as selected using NPN or ALPN. */
+  Protocol getProtocol();
 
   /**
-   * @param client true if this is the HTTP client's reader, reading frames from
-   *     a peer SPDY or HTTP/2 server.
+   * @param client true if this is the HTTP client's reader, reading frames from a server.
    */
-  FrameReader newReader(InputStream in, boolean client);
+  FrameReader newReader(BufferedSource source, boolean client);
 
   /**
-   * @param client true if this is the HTTP client's writer, writing frames to a
-   *     peer SPDY or HTTP/2 server.
+   * @param client true if this is the HTTP client's writer, writing frames to a server.
    */
-  FrameWriter newWriter(OutputStream out, boolean client);
+  FrameWriter newWriter(BufferedSink sink, boolean client);
+
+  int maxFrameSize();
 }
diff --git a/okhttp/src/test/java/com/squareup/okhttp/AsyncApiTest.java b/okhttp/src/test/java/com/squareup/okhttp/AsyncApiTest.java
deleted file mode 100644
index 6636ca7..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/AsyncApiTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import org.junit.After;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-public final class AsyncApiTest {
-  private MockWebServer server = new MockWebServer();
-  private OkHttpClient client = new OkHttpClient();
-  private RecordingReceiver receiver = new RecordingReceiver();
-
-  @After public void tearDown() throws Exception {
-    server.shutdown();
-  }
-
-  @Test public void get() throws Exception {
-    server.enqueue(new MockResponse()
-        .setBody("abc")
-        .addHeader("Content-Type: text/plain"));
-    server.play();
-
-    Request request = new Request.Builder(server.getUrl("/"))
-        .header("User-Agent", "AsyncApiTest")
-        .build();
-    client.enqueue(request, receiver);
-
-    receiver.await(request)
-        .assertCode(200)
-        .assertContainsHeaders("Content-Type: text/plain")
-        .assertBody("abc");
-
-    assertTrue(server.takeRequest().getHeaders().contains("User-Agent: AsyncApiTest"));
-  }
-
-  @Test public void post() throws Exception {
-    server.enqueue(new MockResponse().setBody("abc"));
-    server.play();
-
-    Request request = new Request.Builder(server.getUrl("/"))
-        .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
-        .build();
-    client.enqueue(request, receiver);
-
-    receiver.await(request)
-        .assertCode(200)
-        .assertBody("abc");
-
-    RecordedRequest recordedRequest = server.takeRequest();
-    assertEquals("def", recordedRequest.getUtf8Body());
-    assertEquals("3", recordedRequest.getHeader("Content-Length"));
-  }
-}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/RecordingReceiver.java b/okhttp/src/test/java/com/squareup/okhttp/RecordingReceiver.java
deleted file mode 100644
index ee0db12..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/RecordingReceiver.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Records received HTTP responses so they can be later retrieved by tests.
- */
-public class RecordingReceiver implements Response.Receiver {
-  public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
-
-  private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
-
-  @Override public synchronized void onFailure(Failure failure) {
-    responses.add(new RecordedResponse(failure.request(), null, null, failure));
-    notifyAll();
-  }
-
-  @Override public synchronized void onResponse(Response response) throws IOException {
-    responses.add(new RecordedResponse(
-        response.request(), response, response.body().string(), null));
-    notifyAll();
-  }
-
-  /**
-   * Returns the recorded response triggered by {@code request}. Throws if the
-   * response isn't enqueued before the timeout.
-   */
-  public synchronized RecordedResponse await(Request request) throws Exception {
-    long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
-    while (true) {
-      for (RecordedResponse recordedResponse : responses) {
-        if (recordedResponse.request == request) {
-          return recordedResponse;
-        }
-      }
-
-      long nowMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
-      if (nowMillis >= timeoutMillis) break;
-      wait(timeoutMillis - nowMillis);
-    }
-
-    throw new AssertionError("Timed out waiting for response to " + request);
-  }
-}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
deleted file mode 100644
index e933c17..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/FaultRecoveringOutputStreamTest.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Deque;
-import java.util.List;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class FaultRecoveringOutputStreamTest {
-  @Test public void noRecoveryWithoutReplacement() throws Exception {
-    FaultingOutputStream faulting = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting);
-
-    recovering.write('a');
-    faulting.nextFault = "system on fire";
-    try {
-      recovering.write('b');
-      fail();
-    } catch (IOException e) {
-      assertEquals(Arrays.asList("system on fire"), recovering.exceptionMessages);
-      assertEquals("ab", faulting.receivedUtf8);
-      assertFalse(faulting.closed);
-    }
-  }
-
-  @Test public void successfulRecoveryOnWriteFault() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-
-    recovering.write('a');
-    assertEquals("a", faulting1.receivedUtf8);
-    assertEquals("", faulting2.receivedUtf8);
-    faulting1.nextFault = "system under water";
-    recovering.write('b');
-    assertEquals(Arrays.asList("system under water"), recovering.exceptionMessages);
-    assertEquals("ab", faulting1.receivedUtf8);
-    assertEquals("ab", faulting2.receivedUtf8);
-    assertTrue(faulting1.closed);
-    assertFalse(faulting2.closed);
-
-    // Confirm that new data goes to the new stream.
-    recovering.write('c');
-    assertEquals("ab", faulting1.receivedUtf8);
-    assertEquals("abc", faulting2.receivedUtf8);
-  }
-
-  @Test public void successfulRecoveryOnFlushFault() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-
-    recovering.write('a');
-    faulting1.nextFault = "bad weather";
-    recovering.flush();
-    assertEquals(Arrays.asList("bad weather"), recovering.exceptionMessages);
-    assertEquals("a", faulting1.receivedUtf8);
-    assertEquals("a", faulting2.receivedUtf8);
-    assertTrue(faulting1.closed);
-    assertFalse(faulting2.closed);
-    assertEquals("a", faulting2.flushedUtf8);
-
-    // Confirm that new data goes to the new stream.
-    recovering.write('b');
-    assertEquals("a", faulting1.receivedUtf8);
-    assertEquals("ab", faulting2.receivedUtf8);
-    assertEquals("a", faulting2.flushedUtf8);
-  }
-
-  @Test public void successfulRecoveryOnCloseFault() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-
-    recovering.write('a');
-    faulting1.nextFault = "termites";
-    recovering.close();
-    assertEquals(Arrays.asList("termites"), recovering.exceptionMessages);
-    assertEquals("a", faulting1.receivedUtf8);
-    assertEquals("a", faulting2.receivedUtf8);
-    assertTrue(faulting1.closed);
-    assertTrue(faulting2.closed);
-  }
-
-  @Test public void replacementStreamFaultsImmediately() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    FaultingOutputStream faulting3 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-    recovering.replacements.addLast(faulting3);
-
-    recovering.write('a');
-    assertEquals("a", faulting1.receivedUtf8);
-    assertEquals("", faulting2.receivedUtf8);
-    assertEquals("", faulting3.receivedUtf8);
-    faulting1.nextFault = "offline";
-    faulting2.nextFault = "slow";
-    recovering.write('b');
-    assertEquals(Arrays.asList("offline", "slow"), recovering.exceptionMessages);
-    assertEquals("ab", faulting1.receivedUtf8);
-    assertEquals("a", faulting2.receivedUtf8);
-    assertEquals("ab", faulting3.receivedUtf8);
-    assertTrue(faulting1.closed);
-    assertTrue(faulting2.closed);
-    assertFalse(faulting3.closed);
-
-    // Confirm that new data goes to the new stream.
-    recovering.write('c');
-    assertEquals("ab", faulting1.receivedUtf8);
-    assertEquals("a", faulting2.receivedUtf8);
-    assertEquals("abc", faulting3.receivedUtf8);
-  }
-
-  @Test public void recoverWithFullBuffer() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-
-    recovering.write("abcdefghij".getBytes(UTF_8)); // 10 bytes.
-    faulting1.nextFault = "unlucky";
-    recovering.write('k');
-    assertEquals("abcdefghijk", faulting1.receivedUtf8);
-    assertEquals("abcdefghijk", faulting2.receivedUtf8);
-    assertEquals(Arrays.asList("unlucky"), recovering.exceptionMessages);
-    assertTrue(faulting1.closed);
-    assertFalse(faulting2.closed);
-
-    // Confirm that new data goes to the new stream.
-    recovering.write('l');
-    assertEquals("abcdefghijk", faulting1.receivedUtf8);
-    assertEquals("abcdefghijkl", faulting2.receivedUtf8);
-  }
-
-  @Test public void noRecoveryWithOverfullBuffer() throws Exception {
-    FaultingOutputStream faulting1 = new FaultingOutputStream();
-    FaultingOutputStream faulting2 = new FaultingOutputStream();
-    TestFaultRecoveringOutputStream recovering = new TestFaultRecoveringOutputStream(10, faulting1);
-    recovering.replacements.addLast(faulting2);
-
-    recovering.write("abcdefghijk".getBytes(UTF_8)); // 11 bytes.
-    faulting1.nextFault = "out to lunch";
-    try {
-      recovering.write('l');
-      fail();
-    } catch (IOException expected) {
-      assertEquals("out to lunch", expected.getMessage());
-    }
-
-    assertEquals(Arrays.<String>asList(), recovering.exceptionMessages);
-    assertEquals("abcdefghijkl", faulting1.receivedUtf8);
-    assertEquals("", faulting2.receivedUtf8);
-    assertFalse(faulting1.closed);
-    assertFalse(faulting2.closed);
-  }
-
-  static class FaultingOutputStream extends OutputStream {
-    String receivedUtf8 = "";
-    String flushedUtf8 = null;
-    String nextFault;
-    boolean closed;
-
-    @Override public final void write(int data) throws IOException {
-      write(new byte[] { (byte) data });
-    }
-
-    @Override public void write(byte[] buffer, int offset, int count) throws IOException {
-      receivedUtf8 += new String(buffer, offset, count, UTF_8);
-      if (nextFault != null) throw new IOException(nextFault);
-    }
-
-    @Override public void flush() throws IOException {
-      flushedUtf8 = receivedUtf8;
-      if (nextFault != null) throw new IOException(nextFault);
-    }
-
-    @Override public void close() throws IOException {
-      closed = true;
-      if (nextFault != null) throw new IOException(nextFault);
-    }
-  }
-
-  static class TestFaultRecoveringOutputStream extends FaultRecoveringOutputStream {
-    final List<String> exceptionMessages = new ArrayList<String>();
-    final Deque<OutputStream> replacements = new ArrayDeque<OutputStream>();
-
-    TestFaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream first) {
-      super(maxReplayBufferLength, first);
-    }
-
-    @Override protected OutputStream replacementStream(IOException e) {
-      exceptionMessages.add(e.getMessage());
-      return replacements.poll();
-    }
-  }
-}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java
deleted file mode 100644
index 252f6ac..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java
+++ /dev/null
@@ -1,75 +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;
-
-import java.io.ByteArrayInputStream;
-import java.io.EOFException;
-import java.io.InputStream;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.Util.US_ASCII;
-import static org.junit.Assert.fail;
-
-public final class StrictLineReaderTest {
-  @Test public void lineReaderConsistencyWithReadAsciiLine() throws Exception {
-    // Testing with LineReader buffer capacity 32 to check some corner cases.
-    StrictLineReader lineReader = new StrictLineReader(createTestInputStream(), 32, US_ASCII);
-    InputStream refStream = createTestInputStream();
-    while (true) {
-      try {
-        String refLine = Util.readAsciiLine(refStream);
-        try {
-          String line = lineReader.readLine();
-          if (!refLine.equals(line)) {
-            fail("line (\"" + line + "\") differs from expected (\"" + refLine + "\").");
-          }
-        } catch (EOFException eof) {
-          fail("line reader threw EOFException too early.");
-        }
-      } catch (EOFException refEof) {
-        try {
-          lineReader.readLine();
-          fail("line reader didn't throw the expected EOFException.");
-        } catch (EOFException eof) {
-          // OK
-          break;
-        }
-      }
-    }
-    refStream.close();
-    lineReader.close();
-  }
-
-  private InputStream createTestInputStream() {
-    return new ByteArrayInputStream((
-                /* each source lines below should represent 32 bytes, until the next comment */
-        "12 byte line\n18 byte line......\n" +
-            "pad\nline spanning two 32-byte bu" +
-            "ffers\npad......................\n" +
-            "pad\nline spanning three 32-byte " +
-            "buffers and ending with LF at th" +
-            "e end of a 32 byte buffer......\n" +
-            "pad\nLine ending with CRLF split" +
-            " at the end of a 32-byte buffer\r" +
-            "\npad...........................\n" +
-                        /* end of 32-byte lines */
-            "line ending with CRLF\r\n" +
-            "this is a long line with embedded CR \r ending with CRLF and having more than " +
-            "32 characters\r\n" +
-            "unterminated line - should be dropped").getBytes());
-  }
-}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
deleted file mode 100644
index 7720c5b..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.HttpResponseCache;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.internal.RecordingAuthenticator;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.Authenticator;
-import java.net.CookieManager;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.zip.GZIPOutputStream;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-/** Test how SPDY interacts with HTTP features. */
-public final class HttpOverSpdyTest {
-  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
-    public boolean verify(String hostname, SSLSession session) {
-      return true;
-    }
-  };
-
-  private static final SSLContext sslContext;
-  static {
-    try {
-      sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-    } catch (GeneralSecurityException e) {
-      throw new RuntimeException(e);
-    } catch (UnknownHostException e) {
-      throw new RuntimeException(e);
-    }
-  }
-  private final MockWebServer server = new MockWebServer();
-  private final String hostName = server.getHostName();
-  private final OkHttpClient client = new OkHttpClient();
-  private HttpResponseCache cache;
-
-  @Before public void setUp() throws Exception {
-    server.useHttps(sslContext.getSocketFactory(), false);
-    client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-    String systemTmpDir = System.getProperty("java.io.tmpdir");
-    File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID());
-    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
-  }
-
-  @After public void tearDown() throws Exception {
-    Authenticator.setDefault(null);
-    server.shutdown();
-  }
-
-  @Test public void get() throws Exception {
-    MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet");
-    server.enqueue(response);
-    server.play();
-
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
-    assertContent("ABCDE", connection, Integer.MAX_VALUE);
-    assertEquals(200, connection.getResponseCode());
-    assertEquals("Sweet", connection.getResponseMessage());
-
-    RecordedRequest request = server.takeRequest();
-    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
-    assertContains(request.getHeaders(), ":scheme: https");
-    assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort());
-  }
-
-  @Test public void emptyResponse() throws IOException {
-    server.enqueue(new MockResponse());
-    server.play();
-
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
-    assertEquals(-1, connection.getInputStream().read());
-  }
-
-  @Test public void post() throws Exception {
-    MockResponse response = new MockResponse().setBody("ABCDE");
-    server.enqueue(response);
-    server.play();
-
-    HttpURLConnection connection = client.open(server.getUrl("/foo"));
-    connection.setDoOutput(true);
-    connection.getOutputStream().write("FGHIJ".getBytes(Util.UTF_8));
-    assertContent("ABCDE", connection, Integer.MAX_VALUE);
-
-    RecordedRequest request = server.takeRequest();
-    assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
-    assertEquals("FGHIJ", request.getUtf8Body());
-  }
-
-  @Test public void spdyConnectionReuse() throws Exception {
-    server.enqueue(new MockResponse().setBody("ABCDEF"));
-    server.enqueue(new MockResponse().setBody("GHIJKL"));
-    server.play();
-
-    HttpURLConnection connection1 = client.open(server.getUrl("/r1"));
-    HttpURLConnection connection2 = client.open(server.getUrl("/r2"));
-    assertEquals("ABC", readAscii(connection1.getInputStream(), 3));
-    assertEquals("GHI", readAscii(connection2.getInputStream(), 3));
-    assertEquals("DEF", readAscii(connection1.getInputStream(), 3));
-    assertEquals("JKL", readAscii(connection2.getInputStream(), 3));
-    assertEquals(0, server.takeRequest().getSequenceNumber());
-    assertEquals(1, server.takeRequest().getSequenceNumber());
-  }
-
-  @Test public void gzippedResponseBody() throws Exception {
-    server.enqueue(new MockResponse().addHeader("Content-Encoding: gzip")
-        .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8))));
-    server.play();
-    assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE);
-  }
-
-  @Test public void authenticate() throws Exception {
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED)
-        .addHeader("www-authenticate: Basic realm=\"protected area\"")
-        .setBody("Please authenticate."));
-    server.enqueue(new MockResponse().setBody("Successful auth!"));
-    server.play();
-
-    Authenticator.setDefault(new RecordingAuthenticator());
-    HttpURLConnection connection = client.open(server.getUrl("/"));
-    assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-
-    RecordedRequest denied = server.takeRequest();
-    assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*");
-    RecordedRequest accepted = server.takeRequest();
-    assertEquals("GET / HTTP/1.1", accepted.getRequestLine());
-    assertContains(accepted.getHeaders(),
-        "authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
-  }
-
-  @Test public void redirect() throws Exception {
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-        .addHeader("Location: /foo")
-        .setBody("This page has moved!"));
-    server.enqueue(new MockResponse().setBody("This is the new location!"));
-    server.play();
-
-    HttpURLConnection connection = client.open(server.getUrl("/"));
-    assertContent("This is the new location!", connection, Integer.MAX_VALUE);
-
-    RecordedRequest request1 = server.takeRequest();
-    assertEquals("/", request1.getPath());
-    RecordedRequest request2 = server.takeRequest();
-    assertEquals("/foo", request2.getPath());
-  }
-
-  @Test public void readAfterLastByte() throws Exception {
-    server.enqueue(new MockResponse().setBody("ABC"));
-    server.play();
-
-    HttpURLConnection connection = client.open(server.getUrl("/"));
-    InputStream in = connection.getInputStream();
-    assertEquals("ABC", readAscii(in, 3));
-    assertEquals(-1, in.read());
-    assertEquals(-1, in.read());
-  }
-
-  @Test public void responsesAreCached() throws IOException {
-    client.setResponseCache(cache);
-
-    server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A"));
-    server.play();
-
-    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
-    assertEquals(1, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
-    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
-    assertEquals(3, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(2, cache.getHitCount());
-  }
-
-  @Test public void conditionalCache() throws IOException {
-    client.setResponseCache(cache);
-
-    server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A"));
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-    server.play();
-
-    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
-    assertEquals(1, cache.getRequestCount());
-    assertEquals(1, cache.getNetworkCount());
-    assertEquals(0, cache.getHitCount());
-    assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
-    assertEquals(2, cache.getRequestCount());
-    assertEquals(2, cache.getNetworkCount());
-    assertEquals(1, cache.getHitCount());
-  }
-
-  @Test public void acceptAndTransmitCookies() throws Exception {
-    CookieManager cookieManager = new CookieManager();
-    client.setCookieHandler(cookieManager);
-    server.enqueue(
-        new MockResponse().addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain())
-            .setBody("A"));
-    server.enqueue(new MockResponse().setBody("B"));
-    server.play();
-
-    URL url = server.getUrl("/");
-    assertContent("A", client.open(url), Integer.MAX_VALUE);
-    Map<String, List<String>> requestHeaders = Collections.emptyMap();
-    assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")),
-        cookieManager.get(url.toURI(), requestHeaders));
-
-    assertContent("B", client.open(url), Integer.MAX_VALUE);
-    RecordedRequest requestA = server.takeRequest();
-    assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*");
-    RecordedRequest requestB = server.takeRequest();
-    assertContains(requestB.getHeaders(), "cookie: c=oreo");
-  }
-
-  private <T> void assertContains(Collection<T> collection, T value) {
-    assertTrue(collection.toString(), collection.contains(value));
-  }
-
-  private void assertContent(String expected, URLConnection connection, int limit)
-      throws IOException {
-    connection.connect();
-    assertEquals(expected, readAscii(connection.getInputStream(), limit));
-    ((HttpURLConnection) connection).disconnect();
-  }
-
-  private void assertContainsNoneMatching(List<String> headers, String pattern) {
-    for (String header : headers) {
-      if (header.matches(pattern)) {
-        fail("Header " + header + " matches " + pattern);
-      }
-    }
-  }
-
-  private String readAscii(InputStream in, int count) throws IOException {
-    StringBuilder result = new StringBuilder();
-    for (int i = 0; i < count; i++) {
-      int value = in.read();
-      if (value == -1) {
-        in.close();
-        break;
-      }
-      result.append((char) value);
-    }
-    return result.toString();
-  }
-
-  public byte[] gzip(byte[] bytes) throws IOException {
-    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-    OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
-    gzippedOut.write(bytes);
-    gzippedOut.close();
-    return bytesOut.toByteArray();
-  }
-}
diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
deleted file mode 100644
index 33b147f..0000000
--- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
+++ /dev/null
@@ -1,104 +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.http;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Test;
-
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertEquals;
-
-public final class RawHeadersTest {
-  @Test public void parseNameValueBlock() throws IOException {
-    List<String> nameValueBlock = Arrays.asList(
-        "cache-control", "no-cache, no-store",
-        "set-cookie", "Cookie1\u0000Cookie2",
-        ":status", "200 OK",
-        ":version", "HTTP/1.1");
-    RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
-    assertEquals(3, rawHeaders.length());
-    assertEquals("HTTP/1.1 200 OK", rawHeaders.getStatusLine());
-    assertEquals("no-cache, no-store", rawHeaders.get("cache-control"));
-    assertEquals("Cookie2", rawHeaders.get("set-cookie"));
-    assertEquals("cache-control", rawHeaders.getFieldName(0));
-    assertEquals("no-cache, no-store", rawHeaders.getValue(0));
-    assertEquals("set-cookie", rawHeaders.getFieldName(1));
-    assertEquals("Cookie1", rawHeaders.getValue(1));
-    assertEquals("set-cookie", rawHeaders.getFieldName(2));
-    assertEquals("Cookie2", rawHeaders.getValue(2));
-    assertNull(rawHeaders.get(":status"));
-    assertNull(rawHeaders.get(":version"));
-  }
-
-  @Test public void toNameValueBlock() {
-    RawHeaders rawHeaders = new RawHeaders();
-    rawHeaders.add("cache-control", "no-cache, no-store");
-    rawHeaders.add("set-cookie", "Cookie1");
-    rawHeaders.add("set-cookie", "Cookie2");
-    rawHeaders.add(":status", "200 OK");
-    // TODO: fromNameValueBlock should take the status line headers
-    List<String> nameValueBlock = rawHeaders.toNameValueBlock();
-    List<String> expected =
-        Arrays.asList("cache-control", "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2",
-            ":status", "200 OK");
-    assertEquals(expected, nameValueBlock);
-  }
-
-  @Test public void toNameValueBlockDropsForbiddenHeaders() {
-    RawHeaders rawHeaders = new RawHeaders();
-    rawHeaders.add("Connection", "close");
-    rawHeaders.add("Transfer-Encoding", "chunked");
-    assertEquals(Arrays.<String>asList(), rawHeaders.toNameValueBlock());
-  }
-
-  @Test public void statusMessage() throws IOException {
-    RawHeaders rawHeaders = new RawHeaders();
-    final String message = "Temporary Redirect";
-    final int version = 1;
-    final int code = 200;
-    rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " " + message);
-    assertEquals(message, rawHeaders.getResponseMessage());
-    assertEquals(version, rawHeaders.getHttpMinorVersion());
-    assertEquals(code, rawHeaders.getResponseCode());
-  }
-
-  @Test public void statusMessageWithEmptyMessage() throws IOException {
-    RawHeaders rawHeaders = new RawHeaders();
-    final int version = 1;
-    final int code = 503;
-    rawHeaders.setStatusLine("HTTP/1." + version + " " + code + " ");
-    assertEquals("", rawHeaders.getResponseMessage());
-    assertEquals(version, rawHeaders.getHttpMinorVersion());
-    assertEquals(code, rawHeaders.getResponseCode());
-  }
-
-  /**
-   * This is not defined in the protocol but some servers won't add the leading
-   * empty space when the message is empty.
-   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
-   */
-  @Test public void statusMessageWithEmptyMessageAndNoLeadingSpace() throws IOException {
-    RawHeaders rawHeaders = new RawHeaders();
-    final int version = 1;
-    final int code = 503;
-    rawHeaders.setStatusLine("HTTP/1." + version + " " + code);
-    assertEquals("", rawHeaders.getResponseMessage());
-    assertEquals(version, rawHeaders.getHttpMinorVersion());
-    assertEquals(code, rawHeaders.getResponseCode());
-  }
-}
diff --git a/okhttp-protocols/pom.xml b/okio/pom.xml
similarity index 70%
rename from okhttp-protocols/pom.xml
rename to okio/pom.xml
index 0ecb915..f1a33b5 100644
--- a/okhttp-protocols/pom.xml
+++ b/okio/pom.xml
@@ -6,22 +6,24 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
-  <artifactId>okhttp-protocols</artifactId>
-  <name>OkHttp SPDY and HTTP/2.0 internals</name>
+  <groupId>com.squareup.okio</groupId>
+  <artifactId>okio</artifactId>
+  <name>Okio</name>
 
   <dependencies>
     <dependency>
-      <groupId>org.mortbay.jetty.npn</groupId>
-      <artifactId>npn-boot</artifactId>
-      <optional>true</optional>
-    </dependency>
-    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.mojo</groupId>
+      <artifactId>animal-sniffer-annotations</artifactId>
+      <version>1.10</version>
+      <optional>true</optional>
+    </dependency>
   </dependencies>
 </project>
diff --git a/okio/src/main/java/okio/Base64.java b/okio/src/main/java/okio/Base64.java
new file mode 100644
index 0000000..087b287
--- /dev/null
+++ b/okio/src/main/java/okio/Base64.java
@@ -0,0 +1,148 @@
+/*
+ *  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.
+ */
+
+/**
+ * @author Alexander Y. Kleymenov
+ */
+
+package okio;
+
+import java.io.UnsupportedEncodingException;
+
+final class Base64 {
+  private Base64() {
+  }
+
+  public static byte[] decode(String in) {
+    // Ignore trailing '=' padding and whitespace from the input.
+    int limit = in.length();
+    for (; limit > 0; limit--) {
+      char c = in.charAt(limit - 1);
+      if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') {
+        break;
+      }
+    }
+
+    // If the input includes whitespace, this output array will be longer than necessary.
+    byte[] out = new byte[(int) (limit * 6L / 8L)];
+    int outCount = 0;
+    int inCount = 0;
+
+    int word = 0;
+    for (int pos = 0; pos < limit; pos++) {
+      char c = in.charAt(pos);
+
+      int bits;
+      if (c >= 'A' && c <= 'Z') {
+        // char ASCII value
+        //  A    65    0
+        //  Z    90    25 (ASCII - 65)
+        bits = c - 65;
+      } else if (c >= 'a' && c <= 'z') {
+        // char ASCII value
+        //  a    97    26
+        //  z    122   51 (ASCII - 71)
+        bits = c - 71;
+      } else if (c >= '0' && c <= '9') {
+        // char ASCII value
+        //  0    48    52
+        //  9    57    61 (ASCII + 4)
+        bits = c + 4;
+      } else if (c == '+') {
+        bits = 62;
+      } else if (c == '/') {
+        bits = 63;
+      } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
+        continue;
+      } else {
+        return null;
+      }
+
+      // Append this char's 6 bits to the word.
+      word = (word << 6) | (byte) bits;
+
+      // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
+      inCount++;
+      if (inCount % 4 == 0) {
+        out[outCount++] = (byte) (word >> 16);
+        out[outCount++] = (byte) (word >> 8);
+        out[outCount++] = (byte) word;
+      }
+    }
+
+    int lastWordChars = inCount % 4;
+    if (lastWordChars == 1) {
+      // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
+      return null;
+    } else if (lastWordChars == 2) {
+      // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
+      word = word << 12;
+      out[outCount++] = (byte) (word >> 16);
+    } else if (lastWordChars == 3) {
+      // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
+      word = word << 6;
+      out[outCount++] = (byte) (word >> 16);
+      out[outCount++] = (byte) (word >> 8);
+    }
+
+    // If we sized our out array perfectly, we're done.
+    if (outCount == out.length) return out;
+
+    // Copy the decoded bytes to a new, right-sized array.
+    byte[] prefix = new byte[outCount];
+    System.arraycopy(out, 0, prefix, 0, outCount);
+    return prefix;
+  }
+
+  private static final byte[] MAP = new byte[] {
+      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+      'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
+      'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
+      '5', '6', '7', '8', '9', '+', '/'
+  };
+
+  public static String encode(byte[] in) {
+    int length = (in.length + 2) * 4 / 3;
+    byte[] out = new byte[length];
+    int index = 0, end = in.length - in.length % 3;
+    for (int i = 0; i < end; i += 3) {
+      out[index++] = MAP[(in[i] & 0xff) >> 2];
+      out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
+      out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
+      out[index++] = MAP[(in[i + 2] & 0x3f)];
+    }
+    switch (in.length % 3) {
+      case 1:
+        out[index++] = MAP[(in[end] & 0xff) >> 2];
+        out[index++] = MAP[(in[end] & 0x03) << 4];
+        out[index++] = '=';
+        out[index++] = '=';
+        break;
+      case 2:
+        out[index++] = MAP[(in[end] & 0xff) >> 2];
+        out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
+        out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
+        out[index++] = '=';
+        break;
+    }
+    try {
+      return new String(out, 0, index, "US-ASCII");
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+}
diff --git a/okio/src/main/java/okio/BufferedSink.java b/okio/src/main/java/okio/BufferedSink.java
new file mode 100644
index 0000000..3066011
--- /dev/null
+++ b/okio/src/main/java/okio/BufferedSink.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A sink that keeps a buffer internally so that callers can do small writes
+ * without a performance penalty.
+ */
+public interface BufferedSink extends Sink {
+  /** Returns this sink's internal buffer. */
+  OkBuffer buffer();
+
+  BufferedSink write(ByteString byteString) throws IOException;
+
+  /**
+   * Like {@link OutputStream#write}, this writes a complete byte array to this
+   * sink.
+   */
+  BufferedSink write(byte[] source) throws IOException;
+
+  /**
+   * Like {@link OutputStream#write}, this writes {@code byteCount} bytes
+   * of {@code source}, starting at {@code offset}.
+   */
+  BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;
+
+  /** Encodes {@code string} in UTF-8 and writes it to this sink. */
+  BufferedSink writeUtf8(String string) throws IOException;
+
+  /** Writes a byte to this sink. */
+  BufferedSink writeByte(int b) throws IOException;
+
+  /** Writes a big-endian short to this sink using two bytes. */
+  BufferedSink writeShort(int s) throws IOException;
+
+  /** Writes a little-endian short to this sink using two bytes. */
+  BufferedSink writeShortLe(int s) throws IOException;
+
+  /** Writes a big-endian int to this sink using four bytes. */
+  BufferedSink writeInt(int i) throws IOException;
+
+  /** Writes a little-endian int to this sink using four bytes. */
+  BufferedSink writeIntLe(int i) throws IOException;
+
+  /** Writes a big-endian long to this sink using eight bytes. */
+  BufferedSink writeLong(long v) throws IOException;
+
+  /** Writes a little-endian long to this sink using eight bytes. */
+  BufferedSink writeLongLe(long v) throws IOException;
+
+  /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */
+  BufferedSink emitCompleteSegments() throws IOException;
+
+  /** Returns an output stream that writes to this sink. */
+  OutputStream outputStream();
+}
diff --git a/okio/src/main/java/okio/BufferedSource.java b/okio/src/main/java/okio/BufferedSource.java
new file mode 100644
index 0000000..2b48823
--- /dev/null
+++ b/okio/src/main/java/okio/BufferedSource.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A source that keeps a buffer internally so that callers can do small reads
+ * without a performance penalty. It also allows clients to read ahead,
+ * buffering as much as necessary before consuming input.
+ */
+public interface BufferedSource extends Source {
+  /** Returns this source's internal buffer. */
+  OkBuffer buffer();
+
+  /**
+   * Returns true if there are no more bytes in this source. This will block
+   * until there are bytes to read or the source is definitely exhausted.
+   */
+  boolean exhausted() throws IOException;
+
+  /**
+   * Returns when the buffer contains at least {@code byteCount} bytes. Throws
+   * an {@link java.io.EOFException} if the source is exhausted before the
+   * required bytes can be read.
+   */
+  void require(long byteCount) throws IOException;
+
+  /** Removes a byte from this source and returns it. */
+  byte readByte() throws IOException;
+
+  /** Removes two bytes from this source and returns a big-endian short. */
+  short readShort() throws IOException;
+
+  /** Removes two bytes from this source and returns a little-endian short. */
+  short readShortLe() throws IOException;
+
+  /** Removes four bytes from this source and returns a big-endian int. */
+  int readInt() throws IOException;
+
+  /** Removes four bytes from this source and returns a little-endian int. */
+  int readIntLe() throws IOException;
+
+  /** Removes eight bytes from this source and returns a big-endian long. */
+  long readLong() throws IOException;
+
+  /** Removes eight bytes from this source and returns a little-endian long. */
+  long readLongLe() throws IOException;
+
+  /**
+   * Reads and discards {@code byteCount} bytes from this source. Throws an
+   * {@link java.io.EOFException} if the source is exhausted before the
+   * requested bytes can be skipped.
+   */
+  void skip(long byteCount) throws IOException;
+
+  /** Removes {@code byteCount} bytes from this and returns them as a byte string. */
+  ByteString readByteString(long byteCount) throws IOException;
+
+  /**
+   * Removes {@code byteCount} bytes from this, decodes them as UTF-8 and
+   * returns the string.
+   */
+  String readUtf8(long byteCount) throws IOException;
+
+  /**
+   * Removes and returns characters up to but not including the next line break.
+   * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
+   * not included in the result.
+   *
+   * <p><strong>On the end of the stream this method returns null,</strong> just
+   * like {@link java.io.BufferedReader}. If the source doesn't end with a line
+   * break then an implicit line break is assumed. Null is returned once the
+   * source is exhausted. Use this for human-generated data, where a trailing
+   * line break is optional.
+   */
+  String readUtf8Line() throws IOException;
+
+  /**
+   * Removes and returns characters up to but not including the next line break.
+   * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
+   * not included in the result.
+   *
+   * <p><strong>On the end of the stream this method throws.</strong> Every call
+   * must consume either '\r\n' or '\n'. If these characters are absent in the
+   * stream, an {@link java.io.EOFException} is thrown. Use this for
+   * machine-generated data where a missing line break implies truncated input.
+   */
+  String readUtf8LineStrict() throws IOException;
+
+  /**
+   * Returns the index of {@code b} in the buffer, refilling it if necessary
+   * until it is found. This reads an unbounded number of bytes into the buffer.
+   * Returns -1 if the stream is exhausted before the requested byte is found.
+   */
+  long indexOf(byte b) throws IOException;
+
+  /** Returns an input stream that reads from this source. */
+  InputStream inputStream();
+}
diff --git a/okio/src/main/java/okio/ByteString.java b/okio/src/main/java/okio/ByteString.java
new file mode 100644
index 0000000..6853adb
--- /dev/null
+++ b/okio/src/main/java/okio/ByteString.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * An immutable sequence of bytes.
+ *
+ * <p><strong>Full disclosure:</strong> this class provides untrusted input and
+ * output streams with raw access to the underlying byte array. A hostile
+ * stream implementation could keep a reference to the mutable byte string,
+ * violating the immutable guarantee of this class. For this reason a byte
+ * string's immutability guarantee cannot be relied upon for security in applets
+ * and other environments that run both trusted and untrusted code in the same
+ * process.
+ */
+public final class ByteString {
+  private static final char[] HEX_DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+  /** A singleton empty {@code ByteString}. */
+  public static final ByteString EMPTY = ByteString.of();
+
+  final byte[] data;
+  private transient int hashCode; // Lazily computed; 0 if unknown.
+  private transient String utf8; // Lazily computed.
+
+  ByteString(byte[] data) {
+    this.data = data; // Trusted internal constructor doesn't clone data.
+  }
+
+  /**
+   * Returns a new byte string containing a clone of the bytes of {@code data}.
+   */
+  public static ByteString of(byte... data) {
+    return new ByteString(data.clone());
+  }
+
+  /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */
+  public static ByteString encodeUtf8(String s) {
+    ByteString byteString = new ByteString(s.getBytes(Util.UTF_8));
+    byteString.utf8 = s;
+    return byteString;
+  }
+
+  /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */
+  public String utf8() {
+    String result = utf8;
+    // We don't care if we double-allocate in racy code.
+    return result != null ? result : (utf8 = new String(data, Util.UTF_8));
+  }
+
+  /**
+   * Returns this byte string encoded as <a
+   * href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a>. In violation of the
+   * RFC, the returned string does not wrap lines at 76 columns.
+   */
+  public String base64() {
+    return Base64.encode(data);
+  }
+
+  /**
+   * Decodes the Base64-encoded bytes and returns their value as a byte string.
+   * Returns null if {@code base64} is not a Base64-encoded sequence of bytes.
+   */
+  public static ByteString decodeBase64(String base64) {
+    byte[] decoded = Base64.decode(base64);
+    return decoded != null ? new ByteString(decoded) : null;
+  }
+
+  /** Returns this byte string encoded in hexadecimal. */
+  public String hex() {
+    char[] result = new char[data.length * 2];
+    int c = 0;
+    for (byte b : data) {
+      result[c++] = HEX_DIGITS[(b >> 4) & 0xf];
+      result[c++] = HEX_DIGITS[b & 0xf];
+    }
+    return new String(result);
+  }
+
+  /** Decodes the hex-encoded bytes and returns their value a byte string. */
+  public static ByteString decodeHex(String hex) {
+    if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex);
+
+    byte[] result = new byte[hex.length() / 2];
+    for (int i = 0; i < result.length; i++) {
+      int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4;
+      int d2 = decodeHexDigit(hex.charAt(i * 2 + 1));
+      result[i] = (byte) (d1 + d2);
+    }
+    return of(result);
+  }
+
+  private static int decodeHexDigit(char c) {
+    if (c >= '0' && c <= '9') return c - '0';
+    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+    throw new IllegalArgumentException("Unexpected hex digit: " + c);
+  }
+
+  /**
+   * Reads {@code count} bytes from {@code in} and returns the result.
+   *
+   * @throws java.io.EOFException if {@code in} has fewer than {@code count}
+   *     bytes to read.
+   */
+  public static ByteString read(InputStream in, int byteCount) throws IOException {
+    byte[] result = new byte[byteCount];
+    for (int offset = 0, read; offset < byteCount; offset += read) {
+      read = in.read(result, offset, byteCount - offset);
+      if (read == -1) throw new EOFException();
+    }
+    return new ByteString(result);
+  }
+
+  /**
+   * Returns a byte string equal to this byte string, but with the bytes 'A'
+   * through 'Z' replaced with the corresponding byte in 'a' through 'z'.
+   * Returns this byte string if it contains no bytes in 'A' through 'Z'.
+   */
+  public ByteString toAsciiLowercase() {
+    // Search for an uppercase character. If we don't find one, return this.
+    for (int i = 0; i < data.length; i++) {
+      byte c = data[i];
+      if (c < 'A' || c > 'Z') continue;
+
+      // If we reach this point, this string is not not lowercase. Create and
+      // return a new byte string.
+      byte[] lowercase = data.clone();
+      lowercase[i++] = (byte) (c - ('A' - 'a'));
+      for (; i < lowercase.length; i++) {
+        c = lowercase[i];
+        if (c < 'A' || c > 'Z') continue;
+        lowercase[i] = (byte) (c - ('A' - 'a'));
+      }
+      return new ByteString(lowercase);
+    }
+    return this;
+  }
+
+  /** Returns the byte at {@code pos}. */
+  public byte getByte(int pos) {
+    return data[pos];
+  }
+
+  /**
+   * Returns the number of bytes in this ByteString.
+   */
+  public int size() {
+    return data.length;
+  }
+
+  /**
+   * Returns a byte array containing a copy of the bytes in this {@code ByteString}.
+   */
+  public byte[] toByteArray() {
+    return data.clone();
+  }
+
+  /** Writes the contents of this byte string to {@code out}. */
+  public void write(OutputStream out) throws IOException {
+    out.write(data);
+  }
+
+  @Override public boolean equals(Object o) {
+    return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data);
+  }
+
+  @Override public int hashCode() {
+    int result = hashCode;
+    return result != 0 ? result : (hashCode = Arrays.hashCode(data));
+  }
+
+  @Override public String toString() {
+    if (data.length == 0) {
+      return "ByteString[size=0]";
+    }
+
+    if (data.length <= 16) {
+      return String.format("ByteString[size=%s data=%s]", data.length, hex());
+    }
+
+    try {
+      return String.format("ByteString[size=%s md5=%s]", data.length,
+          ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex());
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/okio/src/main/java/okio/Deadline.java b/okio/src/main/java/okio/Deadline.java
new file mode 100644
index 0000000..e5f951e
--- /dev/null
+++ b/okio/src/main/java/okio/Deadline.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The time that a requested operation is due. If the deadline is reached before
+ * the operation has completed, the operation should be aborted.
+ */
+public class Deadline {
+  public static final Deadline NONE = new Deadline() {
+    @Override public Deadline start(long timeout, TimeUnit unit) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override public boolean reached() {
+      return false;
+    }
+  };
+
+  private long deadlineNanos;
+
+  public Deadline() {
+  }
+
+  public Deadline start(long timeout, TimeUnit unit) {
+    deadlineNanos = System.nanoTime() + unit.toNanos(timeout);
+    return this;
+  }
+
+  public boolean reached() {
+    return System.nanoTime() - deadlineNanos >= 0; // Subtract to avoid overflow!
+  }
+
+  public final void throwIfReached() throws IOException {
+    // TODO: a more catchable exception type?
+    if (reached()) throw new IOException("Deadline reached");
+
+    // If the thread is interrupted, do not proceed with further I/O.
+    if (Thread.interrupted()) throw new InterruptedIOException();
+  }
+}
diff --git a/okio/src/main/java/okio/DeflaterSink.java b/okio/src/main/java/okio/DeflaterSink.java
new file mode 100644
index 0000000..7249f2d
--- /dev/null
+++ b/okio/src/main/java/okio/DeflaterSink.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.util.zip.Deflater;
+
+import static okio.Util.checkOffsetAndCount;
+
+/**
+ * A sink that uses <a href="http://tools.ietf.org/html/rfc1951">DEFLATE</a> to
+ * compress data written to another source.
+ *
+ * <h3>Sync flush</h3>
+ * Aggressive flushing of this stream may result in reduced compression. Each
+ * call to {@link #flush} immediately compresses all currently-buffered data;
+ * this early compression may be less effective than compression performed
+ * without flushing.
+ *
+ * <p>This is equivalent to using {@link Deflater} with the sync flush option.
+ * This class does not offer any partial flush mechanism. For best performance,
+ * only call {@link #flush} when application behavior requires it.
+ */
+public final class DeflaterSink implements Sink {
+  private final BufferedSink sink;
+  private final Deflater deflater;
+  private boolean closed;
+
+  public DeflaterSink(Sink sink, Deflater deflater) {
+    this.sink = Okio.buffer(sink);
+    this.deflater = deflater;
+  }
+
+  @Override public void write(OkBuffer source, long byteCount)
+      throws IOException {
+    checkOffsetAndCount(source.size, 0, byteCount);
+    while (byteCount > 0) {
+      // Share bytes from the head segment of 'source' with the deflater.
+      Segment head = source.head;
+      int toDeflate = (int) Math.min(byteCount, head.limit - head.pos);
+      deflater.setInput(head.data, head.pos, toDeflate);
+
+      // Deflate those bytes into sink.
+      deflate(false);
+
+      // Mark those bytes as read.
+      source.size -= toDeflate;
+      head.pos += toDeflate;
+      if (head.pos == head.limit) {
+        source.head = head.pop();
+        SegmentPool.INSTANCE.recycle(head);
+      }
+
+      byteCount -= toDeflate;
+    }
+  }
+
+  private void deflate(boolean syncFlush) throws IOException {
+    OkBuffer buffer = sink.buffer();
+    while (true) {
+      Segment s = buffer.writableSegment(1);
+
+      // The 4-parameter overload of deflate() doesn't exist in the RI until
+      // Java 1.7, and is public (although with @hide) on Android since 2.3.
+      // The @hide tag means that this code won't compile against the Android
+      // 2.3 SDK, but it will run fine there.
+      int deflated = syncFlush
+          ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH)
+          : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit);
+
+      if (deflated > 0) {
+        s.limit += deflated;
+        buffer.size += deflated;
+        sink.emitCompleteSegments();
+      } else if (deflater.needsInput()) {
+        return;
+      }
+    }
+  }
+
+  @Override public void flush() throws IOException {
+    deflate(true);
+    sink.flush();
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) return;
+
+    // Emit deflated data to the underlying sink. If this fails, we still need
+    // to close the deflater and the sink; otherwise we risk leaking resources.
+    Throwable thrown = null;
+    try {
+      deflater.finish();
+      deflate(false);
+    } catch (Throwable e) {
+      thrown = e;
+    }
+
+    try {
+      deflater.end();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+
+    try {
+      sink.close();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+    closed = true;
+
+    if (thrown != null) Util.sneakyRethrow(thrown);
+  }
+
+  @Override public Sink deadline(Deadline deadline) {
+    sink.deadline(deadline);
+    return this;
+  }
+
+  @Override public String toString() {
+    return "DeflaterSink(" + sink + ")";
+  }
+}
diff --git a/okio/src/main/java/okio/GzipSource.java b/okio/src/main/java/okio/GzipSource.java
new file mode 100644
index 0000000..eae3a16
--- /dev/null
+++ b/okio/src/main/java/okio/GzipSource.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.zip.CRC32;
+import java.util.zip.Inflater;
+
+public final class GzipSource implements Source {
+  private static final byte FHCRC = 1;
+  private static final byte FEXTRA = 2;
+  private static final byte FNAME = 3;
+  private static final byte FCOMMENT = 4;
+
+  private static final byte SECTION_HEADER = 0;
+  private static final byte SECTION_BODY = 1;
+  private static final byte SECTION_TRAILER = 2;
+  private static final byte SECTION_DONE = 3;
+
+  /** The current section. Always progresses forward. */
+  private int section = SECTION_HEADER;
+
+  /**
+   * Our source should yield a GZIP header (which we consume directly), followed
+   * by deflated bytes (which we consume via an InflaterSource), followed by a
+   * GZIP trailer (which we also consume directly).
+   */
+  private final BufferedSource source;
+
+  /** The inflater used to decompress the deflated body. */
+  private final Inflater inflater;
+
+  /**
+   * The inflater source takes care of moving data between compressed source and
+   * decompressed sink buffers.
+   */
+  private final InflaterSource inflaterSource;
+
+  /** Checksum used to check both the GZIP header and decompressed body. */
+  private final CRC32 crc = new CRC32();
+
+  public GzipSource(Source source) throws IOException {
+    this.inflater = new Inflater(true);
+    this.source = Okio.buffer(source);
+    this.inflaterSource = new InflaterSource(this.source, inflater);
+  }
+
+  @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+    if (byteCount == 0) return 0;
+
+    // If we haven't consumed the header, we must consume it before anything else.
+    if (section == SECTION_HEADER) {
+      consumeHeader();
+      section = SECTION_BODY;
+    }
+
+    // Attempt to read at least a byte of the body. If we do, we're done.
+    if (section == SECTION_BODY) {
+      long offset = sink.size;
+      long result = inflaterSource.read(sink, byteCount);
+      if (result != -1) {
+        updateCrc(sink, offset, result);
+        return result;
+      }
+      section = SECTION_TRAILER;
+    }
+
+    // The body is exhausted; time to read the trailer. We always consume the
+    // trailer before returning a -1 exhausted result; that way if you read to
+    // the end of a GzipSource you guarantee that the CRC has been checked.
+    if (section == SECTION_TRAILER) {
+      consumeTrailer();
+      section = SECTION_DONE;
+
+      // Gzip streams self-terminate: they return -1 before their underlying
+      // source returns -1. Here we attempt to force the underlying stream to
+      // return -1 which may trigger it to release its resources. If it doesn't
+      // return -1, then our Gzip data finished prematurely!
+      if (!source.exhausted()) {
+        throw new IOException("gzip finished without exhausting source");
+      }
+    }
+
+    return -1;
+  }
+
+  private void consumeHeader() throws IOException {
+    // Read the 10-byte header. We peek at the flags byte first so we know if we
+    // need to CRC the entire header. Then we read the magic ID1ID2 sequence.
+    // We can skip everything else in the first 10 bytes.
+    // +---+---+---+---+---+---+---+---+---+---+
+    // |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
+    // +---+---+---+---+---+---+---+---+---+---+
+    source.require(10);
+    byte flags = source.buffer().getByte(3);
+    boolean fhcrc = ((flags >> FHCRC) & 1) == 1;
+    if (fhcrc) updateCrc(source.buffer(), 0, 10);
+
+    short id1id2 = source.readShort();
+    checkEqual("ID1ID2", (short) 0x1f8b, id1id2);
+    source.skip(8);
+
+    // Skip optional extra fields.
+    // +---+---+=================================+
+    // | XLEN  |...XLEN bytes of "extra field"...| (more-->)
+    // +---+---+=================================+
+    if (((flags >> FEXTRA) & 1) == 1) {
+      source.require(2);
+      if (fhcrc) updateCrc(source.buffer(), 0, 2);
+      int xlen = source.buffer().readShortLe();
+      source.require(xlen);
+      if (fhcrc) updateCrc(source.buffer(), 0, xlen);
+      source.skip(xlen);
+    }
+
+    // Skip an optional 0-terminated name.
+    // +=========================================+
+    // |...original file name, zero-terminated...| (more-->)
+    // +=========================================+
+    if (((flags >> FNAME) & 1) == 1) {
+      long index = source.indexOf((byte) 0);
+      if (index == -1) throw new EOFException();
+      if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
+      source.skip(index + 1);
+    }
+
+    // Skip an optional 0-terminated comment.
+    // +===================================+
+    // |...file comment, zero-terminated...| (more-->)
+    // +===================================+
+    if (((flags >> FCOMMENT) & 1) == 1) {
+      long index = source.indexOf((byte) 0);
+      if (index == -1) throw new EOFException();
+      if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
+      source.skip(index + 1);
+    }
+
+    // Confirm the optional header CRC.
+    // +---+---+
+    // | CRC16 |
+    // +---+---+
+    if (fhcrc) {
+      checkEqual("FHCRC", source.readShortLe(), (short) crc.getValue());
+      crc.reset();
+    }
+  }
+
+  private void consumeTrailer() throws IOException {
+    // Read the eight-byte trailer. Confirm the body's CRC and size.
+    // +---+---+---+---+---+---+---+---+
+    // |     CRC32     |     ISIZE     |
+    // +---+---+---+---+---+---+---+---+
+    checkEqual("CRC", source.readIntLe(), (int) crc.getValue());
+    checkEqual("ISIZE", source.readIntLe(), inflater.getTotalOut());
+  }
+
+  @Override public Source deadline(Deadline deadline) {
+    source.deadline(deadline);
+    return this;
+  }
+
+  @Override public void close() throws IOException {
+    inflaterSource.close();
+  }
+
+  /** Updates the CRC with the given bytes. */
+  private void updateCrc(OkBuffer buffer, long offset, long byteCount) {
+    for (Segment s = buffer.head; byteCount > 0; s = s.next) {
+      int segmentByteCount = s.limit - s.pos;
+      if (offset < segmentByteCount) {
+        int toUpdate = (int) Math.min(byteCount, segmentByteCount - offset);
+        crc.update(s.data, (int) (s.pos + offset), toUpdate);
+        byteCount -= toUpdate;
+      }
+      offset -= segmentByteCount; // Track the offset of the current segment.
+    }
+  }
+
+  private void checkEqual(String name, int expected, int actual) throws IOException {
+    if (actual != expected) {
+      throw new IOException(String.format(
+          "%s: actual 0x%08x != expected 0x%08x", name, actual, expected));
+    }
+  }
+}
diff --git a/okio/src/main/java/okio/InflaterSource.java b/okio/src/main/java/okio/InflaterSource.java
new file mode 100644
index 0000000..c86c995
--- /dev/null
+++ b/okio/src/main/java/okio/InflaterSource.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+/**
+ * A source that uses <a href="http://tools.ietf.org/html/rfc1951">DEFLATE</a>
+ * to decompress data read from another source.
+ */
+public final class InflaterSource implements Source {
+  private final BufferedSource source;
+  private final Inflater inflater;
+
+  /**
+   * When we call Inflater.setInput(), the inflater keeps our byte array until
+   * it needs input again. This tracks how many bytes the inflater is currently
+   * holding on to.
+   */
+  private int bufferBytesHeldByInflater;
+  private boolean closed;
+
+  public InflaterSource(Source source, Inflater inflater) {
+    this(Okio.buffer(source), inflater);
+  }
+
+  /**
+   * This package-private constructor shares a buffer with its trusted caller.
+   * In general we can't share a BufferedSource because the inflater holds input
+   * bytes until they are inflated.
+   */
+  InflaterSource(BufferedSource source, Inflater inflater) {
+    if (source == null) throw new IllegalArgumentException("source == null");
+    if (inflater == null) throw new IllegalArgumentException("inflater == null");
+    this.source = source;
+    this.inflater = inflater;
+  }
+
+  @Override public long read(
+      OkBuffer sink, long byteCount) throws IOException {
+    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+    if (closed) throw new IllegalStateException("closed");
+    if (byteCount == 0) return 0;
+
+    while (true) {
+      boolean sourceExhausted = refill();
+
+      // Decompress the inflater's compressed data into the sink.
+      try {
+        Segment tail = sink.writableSegment(1);
+        int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit);
+        if (bytesInflated > 0) {
+          tail.limit += bytesInflated;
+          sink.size += bytesInflated;
+          return bytesInflated;
+        }
+        if (inflater.finished() || inflater.needsDictionary()) {
+          releaseInflatedBytes();
+          return -1;
+        }
+        if (sourceExhausted) throw new EOFException("source exhausted prematurely");
+      } catch (DataFormatException e) {
+        throw new IOException(e);
+      }
+    }
+  }
+
+  /**
+   * Refills the inflater with compressed data if it needs input. (And only if
+   * it needs input). Returns true if the inflater required input but the source
+   * was exhausted.
+   */
+  public boolean refill() throws IOException {
+    if (!inflater.needsInput()) return false;
+
+    releaseInflatedBytes();
+    if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible?
+
+    // If there are compressed bytes in the source, assign them to the inflater.
+    if (source.exhausted()) return true;
+
+    // Assign buffer bytes to the inflater.
+    Segment head = source.buffer().head;
+    bufferBytesHeldByInflater = head.limit - head.pos;
+    inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater);
+    return false;
+  }
+
+  /** When the inflater has processed compressed data, remove it from the buffer. */
+  private void releaseInflatedBytes() throws IOException {
+    if (bufferBytesHeldByInflater == 0) return;
+    int toRelease = bufferBytesHeldByInflater - inflater.getRemaining();
+    bufferBytesHeldByInflater -= toRelease;
+    source.skip(toRelease);
+  }
+
+  @Override public Source deadline(Deadline deadline) {
+    source.deadline(deadline);
+    return this;
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) return;
+    inflater.end();
+    closed = true;
+    source.close();
+  }
+}
diff --git a/okio/src/main/java/okio/OkBuffer.java b/okio/src/main/java/okio/OkBuffer.java
new file mode 100644
index 0000000..8dc4290
--- /dev/null
+++ b/okio/src/main/java/okio/OkBuffer.java
@@ -0,0 +1,745 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static okio.Util.UTF_8;
+import static okio.Util.checkOffsetAndCount;
+import static okio.Util.reverseBytesLong;
+
+/**
+ * A collection of bytes in memory.
+ *
+ * <p><strong>Moving data from one OkBuffer to another is fast.</strong> Instead
+ * of copying bytes from one place in memory to another, this class just changes
+ * ownership of the underlying byte arrays.
+ *
+ * <p><strong>This buffer grows with your data.</strong> Just like ArrayList,
+ * each OkBuffer starts small. It consumes only the memory it needs to.
+ *
+ * <p><strong>This buffer pools its byte arrays.</strong> When you allocate a
+ * byte array in Java, the runtime must zero-fill the requested array before
+ * returning it to you. Even if you're going to write over that space anyway.
+ * This class avoids zero-fill and GC churn by pooling byte arrays.
+ */
+public final class OkBuffer implements BufferedSource, BufferedSink, Cloneable {
+  Segment head;
+  long size;
+
+  public OkBuffer() {
+  }
+
+  /** Returns the number of bytes currently in this buffer. */
+  public long size() {
+    return size;
+  }
+
+  @Override public OkBuffer buffer() {
+    return this;
+  }
+
+  @Override public OutputStream outputStream() {
+    return new OutputStream() {
+      @Override public void write(int b) {
+        writeByte((byte) b);
+      }
+
+      @Override public void write(byte[] data, int offset, int byteCount) {
+        OkBuffer.this.write(data, offset, byteCount);
+      }
+
+      @Override public void flush() {
+      }
+
+      @Override public void close() {
+      }
+
+      @Override public String toString() {
+        return this + ".outputStream()";
+      }
+    };
+  }
+
+  @Override public OkBuffer emitCompleteSegments() {
+    return this; // Nowhere to emit to!
+  }
+
+  @Override public boolean exhausted() {
+    return size == 0;
+  }
+
+  @Override public void require(long byteCount) throws EOFException {
+    if (this.size < byteCount) throw new EOFException();
+  }
+
+  @Override public InputStream inputStream() {
+    return new InputStream() {
+      @Override public int read() {
+        return readByte() & 0xff;
+      }
+
+      @Override public int read(byte[] sink, int offset, int byteCount) {
+        return OkBuffer.this.read(sink, offset, byteCount);
+      }
+
+      @Override public int available() {
+        return (int) Math.min(size, Integer.MAX_VALUE);
+      }
+
+      @Override public void close() {
+      }
+
+      @Override public String toString() {
+        return OkBuffer.this + ".inputStream()";
+      }
+    };
+  }
+
+  /**
+   * Returns the number of bytes in segments that are not writable. This is the
+   * number of bytes that can be flushed immediately to an underlying sink
+   * without harming throughput.
+   */
+  public long completeSegmentByteCount() {
+    long result = size;
+    if (result == 0) return 0;
+
+    // Omit the tail if it's still writable.
+    Segment tail = head.prev;
+    if (tail.limit < Segment.SIZE) {
+      result -= tail.limit - tail.pos;
+    }
+
+    return result;
+  }
+
+  @Override public byte readByte() {
+    if (size == 0) throw new IllegalStateException("size == 0");
+
+    Segment segment = head;
+    int pos = segment.pos;
+    int limit = segment.limit;
+
+    byte[] data = segment.data;
+    byte b = data[pos++];
+    size -= 1;
+
+    if (pos == limit) {
+      head = segment.pop();
+      SegmentPool.INSTANCE.recycle(segment);
+    } else {
+      segment.pos = pos;
+    }
+
+    return b;
+  }
+
+  /** Returns the byte at {@code pos}. */
+  public byte getByte(long pos) {
+    checkOffsetAndCount(size, pos, 1);
+    for (Segment s = head; true; s = s.next) {
+      int segmentByteCount = s.limit - s.pos;
+      if (pos < segmentByteCount) return s.data[s.pos + (int) pos];
+      pos -= segmentByteCount;
+    }
+  }
+
+  @Override public short readShort() {
+    if (size < 2) throw new IllegalStateException("size < 2: " + size);
+
+    Segment segment = head;
+    int pos = segment.pos;
+    int limit = segment.limit;
+
+    // If the short is split across multiple segments, delegate to readByte().
+    if (limit - pos < 2) {
+      int s = (readByte() & 0xff) << 8
+          |   (readByte() & 0xff);
+      return (short) s;
+    }
+
+    byte[] data = segment.data;
+    int s = (data[pos++] & 0xff) << 8
+        |   (data[pos++] & 0xff);
+    size -= 2;
+
+    if (pos == limit) {
+      head = segment.pop();
+      SegmentPool.INSTANCE.recycle(segment);
+    } else {
+      segment.pos = pos;
+    }
+
+    return (short) s;
+  }
+
+  @Override public int readInt() {
+    if (size < 4) throw new IllegalStateException("size < 4: " + size);
+
+    Segment segment = head;
+    int pos = segment.pos;
+    int limit = segment.limit;
+
+    // If the int is split across multiple segments, delegate to readByte().
+    if (limit - pos < 4) {
+      return (readByte() & 0xff) << 24
+          |  (readByte() & 0xff) << 16
+          |  (readByte() & 0xff) <<  8
+          |  (readByte() & 0xff);
+    }
+
+    byte[] data = segment.data;
+    int i = (data[pos++] & 0xff) << 24
+        |   (data[pos++] & 0xff) << 16
+        |   (data[pos++] & 0xff) <<  8
+        |   (data[pos++] & 0xff);
+    size -= 4;
+
+    if (pos == limit) {
+      head = segment.pop();
+      SegmentPool.INSTANCE.recycle(segment);
+    } else {
+      segment.pos = pos;
+    }
+
+    return i;
+  }
+
+  @Override public long readLong() {
+    if (size < 8) throw new IllegalStateException("size < 8: " + size);
+
+    Segment segment = head;
+    int pos = segment.pos;
+    int limit = segment.limit;
+
+    // If the long is split across multiple segments, delegate to readInt().
+    if (limit - pos < 8) {
+      return (readInt() & 0xffffffffL) << 32
+          |  (readInt() & 0xffffffffL);
+    }
+
+    byte[] data = segment.data;
+    long v = (data[pos++] & 0xffL) << 56
+        |    (data[pos++] & 0xffL) << 48
+        |    (data[pos++] & 0xffL) << 40
+        |    (data[pos++] & 0xffL) << 32
+        |    (data[pos++] & 0xffL) << 24
+        |    (data[pos++] & 0xffL) << 16
+        |    (data[pos++] & 0xffL) <<  8
+        |    (data[pos++] & 0xffL);
+    size -= 8;
+
+    if (pos == limit) {
+      head = segment.pop();
+      SegmentPool.INSTANCE.recycle(segment);
+    } else {
+      segment.pos = pos;
+    }
+
+    return v;
+  }
+
+  @Override public short readShortLe() {
+    return Util.reverseBytesShort(readShort());
+  }
+
+  @Override public int readIntLe() {
+    return Util.reverseBytesInt(readInt());
+  }
+
+  @Override public long readLongLe() {
+    return Util.reverseBytesLong(readLong());
+  }
+
+  @Override public ByteString readByteString(long byteCount) {
+    return new ByteString(readBytes(byteCount));
+  }
+
+  @Override public String readUtf8(long byteCount) {
+    checkOffsetAndCount(this.size, 0, byteCount);
+    if (byteCount > Integer.MAX_VALUE) {
+      throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
+    }
+    if (byteCount == 0) return "";
+
+    Segment head = this.head;
+    if (head.pos + byteCount > head.limit) {
+      // If the string spans multiple segments, delegate to readBytes().
+      return new String(readBytes(byteCount), Util.UTF_8);
+    }
+
+    String result = new String(head.data, head.pos, (int) byteCount, UTF_8);
+    head.pos += byteCount;
+    this.size -= byteCount;
+
+    if (head.pos == head.limit) {
+      this.head = head.pop();
+      SegmentPool.INSTANCE.recycle(head);
+    }
+
+    return result;
+  }
+
+  @Override public String readUtf8Line() throws IOException {
+    long newline = indexOf((byte) '\n');
+
+    if (newline == -1) {
+      return size != 0 ? readUtf8(size) : null;
+    }
+
+    return readUtf8Line(newline);
+  }
+
+  @Override public String readUtf8LineStrict() throws IOException {
+    long newline = indexOf((byte) '\n');
+    if (newline == -1) throw new EOFException();
+    return readUtf8Line(newline);
+  }
+
+  String readUtf8Line(long newline) {
+    if (newline > 0 && getByte(newline - 1) == '\r') {
+      // Read everything until '\r\n', then skip the '\r\n'.
+      String result = readUtf8((newline - 1));
+      skip(2);
+      return result;
+
+    } else {
+      // Read everything until '\n', then skip the '\n'.
+      String result = readUtf8(newline);
+      skip(1);
+      return result;
+    }
+  }
+
+  private byte[] readBytes(long byteCount) {
+    checkOffsetAndCount(this.size, 0, byteCount);
+    if (byteCount > Integer.MAX_VALUE) {
+      throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
+    }
+
+    int offset = 0;
+    byte[] result = new byte[(int) byteCount];
+
+    while (offset < byteCount) {
+      int toCopy = (int) Math.min(byteCount - offset, head.limit - head.pos);
+      System.arraycopy(head.data, head.pos, result, offset, toCopy);
+
+      offset += toCopy;
+      head.pos += toCopy;
+
+      if (head.pos == head.limit) {
+        Segment toRecycle = head;
+        head = toRecycle.pop();
+        SegmentPool.INSTANCE.recycle(toRecycle);
+      }
+    }
+
+    this.size -= byteCount;
+    return result;
+  }
+
+  /** Like {@link InputStream#read}. */
+  int read(byte[] sink, int offset, int byteCount) {
+    Segment s = this.head;
+    if (s == null) return -1;
+    int toCopy = Math.min(byteCount, s.limit - s.pos);
+    System.arraycopy(s.data, s.pos, sink, offset, toCopy);
+
+    s.pos += toCopy;
+    this.size -= toCopy;
+
+    if (s.pos == s.limit) {
+      this.head = s.pop();
+      SegmentPool.INSTANCE.recycle(s);
+    }
+
+    return toCopy;
+  }
+
+  /**
+   * Discards all bytes in this buffer. Calling this method when you're done
+   * with a buffer will return its segments to the pool.
+   */
+  public void clear() {
+    skip(size);
+  }
+
+  /** Discards {@code byteCount} bytes from the head of this buffer. */
+  @Override public void skip(long byteCount) {
+    checkOffsetAndCount(this.size, 0, byteCount);
+
+    this.size -= byteCount;
+    while (byteCount > 0) {
+      int toSkip = (int) Math.min(byteCount, head.limit - head.pos);
+      byteCount -= toSkip;
+      head.pos += toSkip;
+
+      if (head.pos == head.limit) {
+        Segment toRecycle = head;
+        head = toRecycle.pop();
+        SegmentPool.INSTANCE.recycle(toRecycle);
+      }
+    }
+  }
+
+  @Override public OkBuffer write(ByteString byteString) {
+    return write(byteString.data, 0, byteString.data.length);
+  }
+
+  @Override public OkBuffer writeUtf8(String string) {
+    // TODO: inline UTF-8 encoding to save allocating a byte[]?
+    byte[] data = string.getBytes(Util.UTF_8);
+    return write(data, 0, data.length);
+  }
+
+  @Override public OkBuffer write(byte[] source) {
+    return write(source, 0, source.length);
+  }
+
+  @Override public OkBuffer write(byte[] source, int offset, int byteCount) {
+    int limit = offset + byteCount;
+    while (offset < limit) {
+      Segment tail = writableSegment(1);
+
+      int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
+      System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
+
+      offset += toCopy;
+      tail.limit += toCopy;
+    }
+
+    this.size += byteCount;
+    return this;
+  }
+
+  @Override public OkBuffer writeByte(int b) {
+    Segment tail = writableSegment(1);
+    tail.data[tail.limit++] = (byte) b;
+    size += 1;
+    return this;
+  }
+
+  @Override public OkBuffer writeShort(int s) {
+    Segment tail = writableSegment(2);
+    byte[] data = tail.data;
+    int limit = tail.limit;
+    data[limit++] = (byte) ((s >>> 8) & 0xff);
+    data[limit++] = (byte)  (s        & 0xff);
+    tail.limit = limit;
+    size += 2;
+    return this;
+  }
+
+  @Override public BufferedSink writeShortLe(int s) {
+    return writeShort(Util.reverseBytesShort((short) s));
+  }
+
+  @Override public OkBuffer writeInt(int i) {
+    Segment tail = writableSegment(4);
+    byte[] data = tail.data;
+    int limit = tail.limit;
+    data[limit++] = (byte) ((i >>> 24) & 0xff);
+    data[limit++] = (byte) ((i >>> 16) & 0xff);
+    data[limit++] = (byte) ((i >>>  8) & 0xff);
+    data[limit++] = (byte)  (i         & 0xff);
+    tail.limit = limit;
+    size += 4;
+    return this;
+  }
+
+  @Override public BufferedSink writeIntLe(int i) {
+    return writeInt(Util.reverseBytesInt(i));
+  }
+
+  @Override public OkBuffer writeLong(long v) {
+    Segment tail = writableSegment(8);
+    byte[] data = tail.data;
+    int limit = tail.limit;
+    data[limit++] = (byte) ((v >>> 56L) & 0xff);
+    data[limit++] = (byte) ((v >>> 48L) & 0xff);
+    data[limit++] = (byte) ((v >>> 40L) & 0xff);
+    data[limit++] = (byte) ((v >>> 32L) & 0xff);
+    data[limit++] = (byte) ((v >>> 24L) & 0xff);
+    data[limit++] = (byte) ((v >>> 16L) & 0xff);
+    data[limit++] = (byte) ((v >>>  8L) & 0xff);
+    data[limit++] = (byte)  (v          & 0xff);
+    tail.limit = limit;
+    size += 8;
+    return this;
+  }
+
+  @Override public BufferedSink writeLongLe(long v) {
+    return writeLong(reverseBytesLong(v));
+  }
+
+  /**
+   * Returns a tail segment that we can write at least {@code minimumCapacity}
+   * bytes to, creating it if necessary.
+   */
+  Segment writableSegment(int minimumCapacity) {
+    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
+
+    if (head == null) {
+      head = SegmentPool.INSTANCE.take(); // Acquire a first segment.
+      return head.next = head.prev = head;
+    }
+
+    Segment tail = head.prev;
+    if (tail.limit + minimumCapacity > Segment.SIZE) {
+      tail = tail.push(SegmentPool.INSTANCE.take()); // Append a new empty segment to fill up.
+    }
+    return tail;
+  }
+
+  @Override public void write(OkBuffer source, long byteCount) {
+    // Move bytes from the head of the source buffer to the tail of this buffer
+    // while balancing two conflicting goals: don't waste CPU and don't waste
+    // memory.
+    //
+    //
+    // Don't waste CPU (ie. don't copy data around).
+    //
+    // Copying large amounts of data is expensive. Instead, we prefer to
+    // reassign entire segments from one OkBuffer to the other.
+    //
+    //
+    // Don't waste memory.
+    //
+    // As an invariant, adjacent pairs of segments in an OkBuffer should be at
+    // least 50% full, except for the head segment and the tail segment.
+    //
+    // The head segment cannot maintain the invariant because the application is
+    // consuming bytes from this segment, decreasing its level.
+    //
+    // The tail segment cannot maintain the invariant because the application is
+    // producing bytes, which may require new nearly-empty tail segments to be
+    // appended.
+    //
+    //
+    // Moving segments between buffers
+    //
+    // When writing one buffer to another, we prefer to reassign entire segments
+    // over copying bytes into their most compact form. Suppose we have a buffer
+    // with these segment levels [91%, 61%]. If we append a buffer with a
+    // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
+    //
+    // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
+    // want to append it to a buffer with these segment levels [99%, 3%]. This
+    // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
+    // is, we do not spend time copying bytes around to achieve more efficient
+    // memory use like [100%, 100%, 4%].
+    //
+    // When combining buffers, we will compact adjacent buffers when their
+    // combined level doesn't exceed 100%. For example, when we start with
+    // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
+    //
+    //
+    // Splitting segments
+    //
+    // Occasionally we write only part of a source buffer to a sink buffer. For
+    // example, given a sink [51%, 91%], we may want to write the first 30% of
+    // a source [92%, 82%] to it. To simplify, we first transform the source to
+    // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
+    // yielding sink [51%, 91%, 30%] and source [62%, 82%].
+
+    if (source == this) {
+      throw new IllegalArgumentException("source == this");
+    }
+    checkOffsetAndCount(source.size, 0, byteCount);
+
+    while (byteCount > 0) {
+      // Is a prefix of the source's head segment all that we need to move?
+      if (byteCount < (source.head.limit - source.head.pos)) {
+        Segment tail = head != null ? head.prev : null;
+        if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) {
+          // We're going to need another segment. Split the source's head
+          // segment in two, then move the first of those two to this buffer.
+          source.head = source.head.split((int) byteCount);
+        } else {
+          // Our existing segments are sufficient. Move bytes from source's head to our tail.
+          source.head.writeTo(tail, (int) byteCount);
+          source.size -= byteCount;
+          this.size += byteCount;
+          return;
+        }
+      }
+
+      // Remove the source's head segment and append it to our tail.
+      Segment segmentToMove = source.head;
+      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
+      source.head = segmentToMove.pop();
+      if (head == null) {
+        head = segmentToMove;
+        head.next = head.prev = head;
+      } else {
+        Segment tail = head.prev;
+        tail = tail.push(segmentToMove);
+        tail.compact();
+      }
+      source.size -= movedByteCount;
+      this.size += movedByteCount;
+      byteCount -= movedByteCount;
+    }
+  }
+
+  @Override public long read(OkBuffer sink, long byteCount) {
+    if (this.size == 0) return -1L;
+    if (byteCount > this.size) byteCount = this.size;
+    sink.write(this, byteCount);
+    return byteCount;
+  }
+
+  @Override public OkBuffer deadline(Deadline deadline) {
+    // All operations are in memory so this class doesn't need to honor deadlines.
+    return this;
+  }
+
+  @Override public long indexOf(byte b) {
+    return indexOf(b, 0);
+  }
+
+  /**
+   * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or
+   * -1 if this buffer does not contain {@code b} in that range.
+   */
+  public long indexOf(byte b, long fromIndex) {
+    Segment s = head;
+    if (s == null) return -1L;
+    long offset = 0L;
+    do {
+      int segmentByteCount = s.limit - s.pos;
+      if (fromIndex > segmentByteCount) {
+        fromIndex -= segmentByteCount;
+      } else {
+        byte[] data = s.data;
+        for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) {
+          if (data[(int) pos] == b) return offset + pos - s.pos;
+        }
+        fromIndex = 0;
+      }
+      offset += segmentByteCount;
+      s = s.next;
+    } while (s != head);
+    return -1L;
+  }
+
+  @Override public void flush() {
+  }
+
+  @Override public void close() {
+  }
+
+  /** For testing. This returns the sizes of the segments in this buffer. */
+  List<Integer> segmentSizes() {
+    if (head == null) return Collections.emptyList();
+    List<Integer> result = new ArrayList<Integer>();
+    result.add(head.limit - head.pos);
+    for (Segment s = head.next; s != head; s = s.next) {
+      result.add(s.limit - s.pos);
+    }
+    return result;
+  }
+
+  @Override public boolean equals(Object o) {
+    if (!(o instanceof OkBuffer)) return false;
+    OkBuffer that = (OkBuffer) o;
+    if (size != that.size) return false;
+    if (size == 0) return true; // Both buffers are empty.
+
+    Segment sa = this.head;
+    Segment sb = that.head;
+    int posA = sa.pos;
+    int posB = sb.pos;
+
+    for (long pos = 0, count; pos < size; pos += count) {
+      count = Math.min(sa.limit - posA, sb.limit - posB);
+
+      for (int i = 0; i < count; i++) {
+        if (sa.data[posA++] != sb.data[posB++]) return false;
+      }
+
+      if (posA == sa.limit) {
+        sa = sa.next;
+        posA = sa.pos;
+      }
+
+      if (posB == sb.limit) {
+        sb = sb.next;
+        posB = sb.pos;
+      }
+    }
+
+    return true;
+  }
+
+  @Override public int hashCode() {
+    Segment s = head;
+    if (s == null) return 0;
+    int result = 1;
+    do {
+      for (int pos = s.pos, limit = s.limit; pos < limit; pos++) {
+        result = 31 * result + s.data[pos];
+      }
+      s = s.next;
+    } while (s != head);
+    return result;
+  }
+
+  @Override public String toString() {
+    if (size == 0) {
+      return "OkBuffer[size=0]";
+    }
+
+    if (size <= 16) {
+      ByteString data = clone().readByteString(size);
+      return String.format("OkBuffer[size=%s data=%s]", size, data.hex());
+    }
+
+    try {
+      MessageDigest md5 = MessageDigest.getInstance("MD5");
+      md5.update(head.data, head.pos, head.limit - head.pos);
+      for (Segment s = head.next; s != head; s = s.next) {
+        md5.update(s.data, s.pos, s.limit - s.pos);
+      }
+      return String.format("OkBuffer[size=%s md5=%s]",
+          size, ByteString.of(md5.digest()).hex());
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError();
+    }
+  }
+
+  /** Returns a deep copy of this buffer. */
+  @Override public OkBuffer clone() {
+    OkBuffer result = new OkBuffer();
+    if (size() == 0) return result;
+
+    result.write(head.data, head.pos, head.limit - head.pos);
+    for (Segment s = head.next; s != head; s = s.next) {
+      result.write(s.data, s.pos, s.limit - s.pos);
+    }
+
+    return result;
+  }
+}
diff --git a/okio/src/main/java/okio/Okio.java b/okio/src/main/java/okio/Okio.java
new file mode 100644
index 0000000..3a9b4f9
--- /dev/null
+++ b/okio/src/main/java/okio/Okio.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static okio.Util.checkOffsetAndCount;
+
+public final class Okio {
+  private Okio() {
+  }
+
+  public static BufferedSource buffer(Source source) {
+    return new RealBufferedSource(source);
+  }
+
+  public static BufferedSink buffer(Sink sink) {
+    return new RealBufferedSink(sink);
+  }
+
+  /** Copies bytes from {@code source} to {@code sink}. */
+  public static void copy(OkBuffer source, long offset, long byteCount, OutputStream sink)
+      throws IOException {
+    checkOffsetAndCount(source.size, offset, byteCount);
+
+    // Skip segments that we aren't copying from.
+    Segment s = source.head;
+    while (offset >= (s.limit - s.pos)) {
+      offset -= (s.limit - s.pos);
+      s = s.next;
+    }
+
+    // Copy from one segment at a time.
+    while (byteCount > 0) {
+      int pos = (int) (s.pos + offset);
+      int toWrite = (int) Math.min(s.limit - pos, byteCount);
+      sink.write(s.data, pos, toWrite);
+      byteCount -= toWrite;
+      offset = 0;
+    }
+  }
+
+  /** Returns a sink that writes to {@code out}. */
+  public static Sink sink(final OutputStream out) {
+    return new Sink() {
+      private Deadline deadline = Deadline.NONE;
+
+      @Override public void write(OkBuffer source, long byteCount)
+          throws IOException {
+        checkOffsetAndCount(source.size, 0, byteCount);
+        while (byteCount > 0) {
+          deadline.throwIfReached();
+          Segment head = source.head;
+          int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
+          out.write(head.data, head.pos, toCopy);
+
+          head.pos += toCopy;
+          byteCount -= toCopy;
+          source.size -= toCopy;
+
+          if (head.pos == head.limit) {
+            source.head = head.pop();
+            SegmentPool.INSTANCE.recycle(head);
+          }
+        }
+      }
+
+      @Override public void flush() throws IOException {
+        out.flush();
+      }
+
+      @Override public void close() throws IOException {
+        out.close();
+      }
+
+      @Override public Sink deadline(Deadline deadline) {
+        if (deadline == null) throw new IllegalArgumentException("deadline == null");
+        this.deadline = deadline;
+        return this;
+      }
+
+      @Override public String toString() {
+        return "sink(" + out + ")";
+      }
+    };
+  }
+
+  /** Returns a source that reads from {@code in}. */
+  public static Source source(final InputStream in) {
+    return new Source() {
+      private Deadline deadline = Deadline.NONE;
+
+      @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+        if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+        deadline.throwIfReached();
+        Segment tail = sink.writableSegment(1);
+        int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
+        int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
+        if (bytesRead == -1) return -1;
+        tail.limit += bytesRead;
+        sink.size += bytesRead;
+        return bytesRead;
+      }
+
+      @Override public void close() throws IOException {
+        in.close();
+      }
+
+      @Override public Source deadline(Deadline deadline) {
+        if (deadline == null) throw new IllegalArgumentException("deadline == null");
+        this.deadline = deadline;
+        return this;
+      }
+
+      @Override public String toString() {
+        return "source(" + in + ")";
+      }
+    };
+  }
+}
diff --git a/okio/src/main/java/okio/RealBufferedSink.java b/okio/src/main/java/okio/RealBufferedSink.java
new file mode 100644
index 0000000..74454c6
--- /dev/null
+++ b/okio/src/main/java/okio/RealBufferedSink.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+final class RealBufferedSink implements BufferedSink {
+  public final OkBuffer buffer;
+  public final Sink sink;
+  private boolean closed;
+
+  public RealBufferedSink(Sink sink, OkBuffer buffer) {
+    if (sink == null) throw new IllegalArgumentException("sink == null");
+    this.buffer = buffer;
+    this.sink = sink;
+  }
+
+  public RealBufferedSink(Sink sink) {
+    this(sink, new OkBuffer());
+  }
+
+  @Override public OkBuffer buffer() {
+    return buffer;
+  }
+
+  @Override public void write(OkBuffer source, long byteCount)
+      throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.write(source, byteCount);
+    emitCompleteSegments();
+  }
+
+  @Override public BufferedSink write(ByteString byteString) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.write(byteString);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeUtf8(String string) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeUtf8(string);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink write(byte[] source) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.write(source);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.write(source, offset, byteCount);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeByte(int b) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeByte(b);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeShort(int s) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeShort(s);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeShortLe(int s) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeShortLe(s);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeInt(int i) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeInt(i);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeIntLe(int i) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeIntLe(i);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeLong(long v) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeLong(v);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeLongLe(long v) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeLongLe(v);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink emitCompleteSegments() throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    long byteCount = buffer.completeSegmentByteCount();
+    if (byteCount > 0) sink.write(buffer, byteCount);
+    return this;
+  }
+
+  @Override public OutputStream outputStream() {
+    return new OutputStream() {
+      @Override public void write(int b) throws IOException {
+        if (closed) throw new IOException("closed");
+        buffer.writeByte((byte) b);
+        emitCompleteSegments();
+      }
+
+      @Override public void write(byte[] data, int offset, int byteCount) throws IOException {
+        if (closed) throw new IOException("closed");
+        buffer.write(data, offset, byteCount);
+        emitCompleteSegments();
+      }
+
+      @Override public void flush() throws IOException {
+        // For backwards compatibility, a flush() on a closed stream is a no-op.
+        if (!closed) {
+          RealBufferedSink.this.flush();
+        }
+      }
+
+      @Override public void close() throws IOException {
+        RealBufferedSink.this.close();
+      }
+
+      @Override public String toString() {
+        return RealBufferedSink.this + ".outputStream()";
+      }
+    };
+  }
+
+  @Override public void flush() throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    if (buffer.size > 0) {
+      sink.write(buffer, buffer.size);
+    }
+    sink.flush();
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) return;
+
+    // Emit buffered data to the underlying sink. If this fails, we still need
+    // to close the sink; otherwise we risk leaking resources.
+    Throwable thrown = null;
+    try {
+      if (buffer.size > 0) {
+        sink.write(buffer, buffer.size);
+      }
+    } catch (Throwable e) {
+      thrown = e;
+    }
+
+    try {
+      sink.close();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+    closed = true;
+
+    if (thrown != null) Util.sneakyRethrow(thrown);
+  }
+
+  @Override public Sink deadline(Deadline deadline) {
+    sink.deadline(deadline);
+    return this;
+  }
+
+  @Override public String toString() {
+    return "buffer(" + sink + ")";
+  }
+}
diff --git a/okio/src/main/java/okio/RealBufferedSource.java b/okio/src/main/java/okio/RealBufferedSource.java
new file mode 100644
index 0000000..0189d0f
--- /dev/null
+++ b/okio/src/main/java/okio/RealBufferedSource.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static okio.Util.checkOffsetAndCount;
+
+final class RealBufferedSource implements BufferedSource {
+  public final OkBuffer buffer;
+  public final Source source;
+  private boolean closed;
+
+  public RealBufferedSource(Source source, OkBuffer buffer) {
+    if (source == null) throw new IllegalArgumentException("source == null");
+    this.buffer = buffer;
+    this.source = source;
+  }
+
+  public RealBufferedSource(Source source) {
+    this(source, new OkBuffer());
+  }
+
+  @Override public OkBuffer buffer() {
+    return buffer;
+  }
+
+  @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+    if (closed) throw new IllegalStateException("closed");
+
+    if (buffer.size == 0) {
+      long read = source.read(buffer, Segment.SIZE);
+      if (read == -1) return -1;
+    }
+
+    long toRead = Math.min(byteCount, buffer.size);
+    return buffer.read(sink, toRead);
+  }
+
+  @Override public boolean exhausted() throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1;
+  }
+
+  @Override public void require(long byteCount) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    while (buffer.size < byteCount) {
+      if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException();
+    }
+  }
+
+  @Override public byte readByte() throws IOException {
+    require(1);
+    return buffer.readByte();
+  }
+
+  @Override public ByteString readByteString(long byteCount) throws IOException {
+    require(byteCount);
+    return buffer.readByteString(byteCount);
+  }
+
+  @Override public String readUtf8(long byteCount) throws IOException {
+    require(byteCount);
+    return buffer.readUtf8(byteCount);
+  }
+
+  @Override public String readUtf8Line() throws IOException {
+    long newline = indexOf((byte) '\n');
+
+    if (newline == -1) {
+      return buffer.size != 0 ? readUtf8(buffer.size) : null;
+    }
+
+    return buffer.readUtf8Line(newline);
+  }
+
+  @Override public String readUtf8LineStrict() throws IOException {
+    long newline = indexOf((byte) '\n');
+    if (newline == -1L) throw new EOFException();
+    return buffer.readUtf8Line(newline);
+  }
+
+  @Override public short readShort() throws IOException {
+    require(2);
+    return buffer.readShort();
+  }
+
+  @Override public short readShortLe() throws IOException {
+    require(2);
+    return buffer.readShortLe();
+  }
+
+  @Override public int readInt() throws IOException {
+    require(4);
+    return buffer.readInt();
+  }
+
+  @Override public int readIntLe() throws IOException {
+    require(4);
+    return buffer.readIntLe();
+  }
+
+  @Override public long readLong() throws IOException {
+    require(8);
+    return buffer.readLong();
+  }
+
+  @Override public long readLongLe() throws IOException {
+    require(8);
+    return buffer.readLongLe();
+  }
+
+  @Override public void skip(long byteCount) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    while (byteCount > 0) {
+      if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
+        throw new EOFException();
+      }
+      long toSkip = Math.min(byteCount, buffer.size());
+      buffer.skip(toSkip);
+      byteCount -= toSkip;
+    }
+  }
+
+  @Override public long indexOf(byte b) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    long start = 0;
+    long index;
+    while ((index = buffer.indexOf(b, start)) == -1) {
+      start = buffer.size;
+      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
+    }
+    return index;
+  }
+
+  @Override public InputStream inputStream() {
+    return new InputStream() {
+      @Override public int read() throws IOException {
+        if (closed) throw new IOException("closed");
+        if (buffer.size == 0) {
+          long count = source.read(buffer, Segment.SIZE);
+          if (count == -1) return -1;
+        }
+        return buffer.readByte() & 0xff;
+      }
+
+      @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
+        if (closed) throw new IOException("closed");
+        checkOffsetAndCount(data.length, offset, byteCount);
+
+        if (buffer.size == 0) {
+          long count = source.read(buffer, Segment.SIZE);
+          if (count == -1) return -1;
+        }
+
+        return buffer.read(data, offset, byteCount);
+      }
+
+      @Override public int available() throws IOException {
+        if (closed) throw new IOException("closed");
+        return (int) Math.min(buffer.size, Integer.MAX_VALUE);
+      }
+
+      @Override public void close() throws IOException {
+        RealBufferedSource.this.close();
+      }
+
+      @Override public String toString() {
+        return RealBufferedSource.this + ".inputStream()";
+      }
+    };
+  }
+
+  @Override public Source deadline(Deadline deadline) {
+    source.deadline(deadline);
+    return this;
+  }
+
+  @Override public void close() throws IOException {
+    if (closed) return;
+    closed = true;
+    source.close();
+    buffer.clear();
+  }
+
+  @Override public String toString() {
+    return "buffer(" + source + ")";
+  }
+}
diff --git a/okio/src/main/java/okio/Segment.java b/okio/src/main/java/okio/Segment.java
new file mode 100644
index 0000000..77dbee1
--- /dev/null
+++ b/okio/src/main/java/okio/Segment.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+/**
+ * A segment of an OkBuffer.
+ *
+ * <p>Each segment in an OkBuffer is a circularly-linked list node referencing
+ * the following and preceding segments in the buffer.
+ *
+ * <p>Each segment in the pool is a singly-linked list node referencing the rest
+ * of segments in the pool.
+ */
+final class Segment {
+  /** The size of all segments in bytes. */
+  // TODO: Using fixed-size segments makes pooling easier. But it harms memory
+  //       efficiency and encourages copying. Try variable sized segments?
+  // TODO: Is 2 KiB a good default segment size?
+  static final int SIZE = 2048;
+
+  final byte[] data = new byte[SIZE];
+
+  /** The next byte of application data byte to read in this segment. */
+  int pos;
+
+  /** The first byte of available data ready to be written to. */
+  int limit;
+
+  /** Next segment in a linked or circularly-linked list. */
+  Segment next;
+
+  /** Previous segment in a circularly-linked list. */
+  Segment prev;
+
+  /**
+   * Removes this segment of a circularly-linked list and returns its successor.
+   * Returns null if the list is now empty.
+   */
+  public Segment pop() {
+    Segment result = next != this ? next : null;
+    prev.next = next;
+    next.prev = prev;
+    next = null;
+    prev = null;
+    return result;
+  }
+
+  /**
+   * Appends {@code segment} after this segment in the circularly-linked list.
+   * Returns the pushed segment.
+   */
+  public Segment push(Segment segment) {
+    segment.prev = this;
+    segment.next = next;
+    next.prev = segment;
+    next = segment;
+    return segment;
+  }
+
+  /**
+   * Splits this head of a circularly-linked list into two segments. The first
+   * segment contains the data in {@code [pos..pos+byteCount)}. The second
+   * segment contains the data in {@code [pos+byteCount..limit)}. This can be
+   * useful when moving partial segments from one OkBuffer to another.
+   *
+   * <p>Returns the new head of the circularly-linked list.
+   */
+  public Segment split(int byteCount) {
+    int aSize = byteCount;
+    int bSize = (limit - pos) - byteCount;
+    if (aSize <= 0 || bSize <= 0) throw new IllegalArgumentException();
+
+    // Which side of the split is larger? We want to copy as few bytes as possible.
+    if (aSize < bSize) {
+      // Create a segment of size 'aSize' before this segment.
+      Segment before = SegmentPool.INSTANCE.take();
+      System.arraycopy(data, pos, before.data, before.pos, aSize);
+      pos += aSize;
+      before.limit += aSize;
+      prev.push(before);
+      return before;
+    } else {
+      // Create a new segment of size 'bSize' after this segment.
+      Segment after = SegmentPool.INSTANCE.take();
+      System.arraycopy(data, pos + aSize, after.data, after.pos, bSize);
+      limit -= bSize;
+      after.limit += bSize;
+      push(after);
+      return this;
+    }
+  }
+
+  /**
+   * Call this when the tail and its predecessor may both be less than half
+   * full. This will copy data so that segments can be recycled.
+   */
+  public void compact() {
+    if (prev == this) throw new IllegalStateException();
+    if ((prev.limit - prev.pos) + (limit - pos) > SIZE) return; // Cannot compact.
+    writeTo(prev, limit - pos);
+    pop();
+    SegmentPool.INSTANCE.recycle(this);
+  }
+
+  /** Moves {@code byteCount} bytes from {@code sink} to this segment. */
+  // TODO: if sink has fewer bytes than this, it may be cheaper to reverse the
+  //       direction of the copy and swap the segments!
+  public void writeTo(Segment sink, int byteCount) {
+    if (byteCount + (sink.limit - sink.pos) > SIZE) throw new IllegalArgumentException();
+
+    if (sink.limit + byteCount > SIZE) {
+      // We can't fit byteCount bytes at the sink's current position. Compact sink first.
+      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
+      sink.limit -= sink.pos;
+      sink.pos = 0;
+    }
+
+    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
+    sink.limit += byteCount;
+    pos += byteCount;
+  }
+}
diff --git a/okio/src/main/java/okio/SegmentPool.java b/okio/src/main/java/okio/SegmentPool.java
new file mode 100644
index 0000000..c132f24
--- /dev/null
+++ b/okio/src/main/java/okio/SegmentPool.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+/**
+ * A collection of unused segments, necessary to avoid GC churn and zero-fill.
+ * This pool is a thread-safe static singleton.
+ */
+final class SegmentPool {
+  static final SegmentPool INSTANCE = new SegmentPool();
+
+  /** The maximum number of bytes to pool. */
+  // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
+  static final long MAX_SIZE = 64 * 1024; // 64 KiB.
+
+  /** Singly-linked list of segments. */
+  private Segment next;
+
+  /** Total bytes in this pool. */
+  long byteCount;
+
+  private SegmentPool() {
+  }
+
+  Segment take() {
+    synchronized (this) {
+      if (next != null) {
+        Segment result = next;
+        next = result.next;
+        result.next = null;
+        byteCount -= Segment.SIZE;
+        return result;
+      }
+    }
+    return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
+  }
+
+  void recycle(Segment segment) {
+    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
+    synchronized (this) {
+      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
+      byteCount += Segment.SIZE;
+      segment.next = next;
+      segment.pos = segment.limit = 0;
+      next = segment;
+    }
+  }
+}
diff --git a/okio/src/main/java/okio/Sink.java b/okio/src/main/java/okio/Sink.java
new file mode 100644
index 0000000..402aa0f
--- /dev/null
+++ b/okio/src/main/java/okio/Sink.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Receives a stream of bytes. Use this interface to write data wherever it's
+ * needed: to the network, storage, or a buffer in memory. Sinks may be layered
+ * to transform received data, such as to compress, encrypt, throttle, or add
+ * protocol framing.
+ *
+ * <p>Most application code shouldn't operate on a sink directly, but rather
+ * {@link BufferedSink} which is both more efficient and more convenient. Use
+ * {@link Okio#buffer(Sink)} to wrap any sink with a buffer.
+ *
+ * <p>Sinks are easy to test: just use an {@link OkBuffer} in your tests, and
+ * read from it to confirm it received the data that was expected.
+ *
+ * <h3>Comparison with OutputStream</h3>
+ * This interface is functionally equivalent to {@link java.io.OutputStream}.
+ *
+ * <p>{@code OutputStream} requires multiple layers when emitted data is
+ * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code
+ * BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for
+ * charset encoding. This class uses {@code BufferedSink} for all of the above.
+ *
+ * <p>Sink is also easier to layer: there is no {@link
+ * java.io.OutputStream#write(int) single-byte write} method that is awkward to
+ * implement efficiently.
+ *
+ * <h3>Interop with OutputStream</h3>
+ * Use {@link Okio#sink} to adapt an {@code OutputStream} to a sink. Use {@link
+ * BufferedSink#outputStream} to adapt a sink to an {@code OutputStream}.
+ */
+public interface Sink extends Closeable {
+  /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
+  void write(OkBuffer source, long byteCount) throws IOException;
+
+  /** Pushes all buffered bytes to their final destination. */
+  void flush() throws IOException;
+
+  /**
+   * Sets the deadline for all operations on this sink.
+   * @return this sink.
+   */
+  Sink deadline(Deadline deadline);
+
+  /**
+   * Pushes all buffered bytes to their final destination and releases the
+   * resources held by this sink. It is an error to write a closed sink. It is
+   * safe to close a sink more than once.
+   */
+  @Override void close() throws IOException;
+}
diff --git a/okio/src/main/java/okio/Source.java b/okio/src/main/java/okio/Source.java
new file mode 100644
index 0000000..d402bee
--- /dev/null
+++ b/okio/src/main/java/okio/Source.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Supplies a stream of bytes. Use this interface to read data from wherever
+ * it's located: from the network, storage, or a buffer in memory. Sources may
+ * be layered to transform supplied data, such as to decompress, decrypt, or
+ * remove protocol framing.
+ *
+ * <p>Most applications shouldn't operate on a source directly, but rather
+ * {@link BufferedSource} which is both more efficient and more convenient. Use
+ * {@link Okio#buffer(Source)} to wrap any source with a buffer.
+ *
+ * <p>Sources are easy to test: just use an {@link OkBuffer} in your tests, and
+ * fill it with the data your application is to read.
+ *
+ * <h3>Comparison with InputStream</h3>
+ * This interface is functionally equivalent to {@link java.io.InputStream}.
+ *
+ * <p>{@code InputStream} requires multiple layers when consumed data is
+ * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code
+ * BufferedInputStream} for buffering, and {@code InputStreamReader} for
+ * strings. This class uses {@code BufferedSource} for all of the above.
+ *
+ * <p>Source avoids the impossible-to-implement {@link
+ * java.io.InputStream#available available()} method. Instead callers specify
+ * how many bytes they {@link BufferedSource#require require}.
+ *
+ * <p>Source omits the unsafe-to-compose {@link java.io.InputStream#mark mark
+ * and reset} state that's tracked by {@code InputStream}; callers instead just
+ * buffer what they need.
+ *
+ * <p>When implementing a source, you need not worry about the {@link
+ * java.io.InputStream#read single-byte read} method that is awkward to
+ * implement efficiently and that returns one of 257 possible values.
+ *
+ * <p>And source has a stronger {@code skip} method: {@link BufferedSource#skip}
+ * won't return prematurely.
+ *
+ * <h3>Interop with InputStream</h3>
+ * Use {@link Okio#source} to adapt an {@code InputStream} to a source. Use
+ * {@link BufferedSource#inputStream} to adapt a source to an {@code
+ * InputStream}.
+ */
+public interface Source extends Closeable {
+  /**
+   * Removes at least 1, and up to {@code byteCount} bytes from this and appends
+   * them to {@code sink}. Returns the number of bytes read, or -1 if this
+   * source is exhausted.
+   */
+  long read(OkBuffer sink, long byteCount) throws IOException;
+
+  /**
+   * Sets the deadline for all operations on this source.
+   * @return this source.
+   */
+  Source deadline(Deadline deadline);
+
+  /**
+   * Closes this source and releases the resources held by this source. It is an
+   * error to read a closed source. It is safe to close a source more than once.
+   */
+  @Override void close() throws IOException;
+}
diff --git a/okio/src/main/java/okio/Util.java b/okio/src/main/java/okio/Util.java
new file mode 100644
index 0000000..4759488
--- /dev/null
+++ b/okio/src/main/java/okio/Util.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.nio.charset.Charset;
+
+final class Util {
+  /** A cheap and type-safe constant for the UTF-8 Charset. */
+  public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private Util() {
+  }
+
+  public static void checkOffsetAndCount(long arrayLength, long offset, long count) {
+    if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
+      throw new ArrayIndexOutOfBoundsException();
+    }
+  }
+
+  public static short reverseBytesShort(short s) {
+    int i = s & 0xffff;
+    int reversed = (i & 0xff00) >>> 8
+        |          (i & 0x00ff)  << 8;
+    return (short) reversed;
+  }
+
+  public static int reverseBytesInt(int i) {
+    return (i & 0xff000000) >>> 24
+        |  (i & 0x00ff0000) >>>  8
+        |  (i & 0x0000ff00)  <<  8
+        |  (i & 0x000000ff)  << 24;
+  }
+
+  public static long reverseBytesLong(long v) {
+    return (v & 0xff00000000000000L) >>> 56
+        |  (v & 0x00ff000000000000L) >>> 40
+        |  (v & 0x0000ff0000000000L) >>> 24
+        |  (v & 0x000000ff00000000L) >>>  8
+        |  (v & 0x00000000ff000000L)  <<  8
+        |  (v & 0x0000000000ff0000L)  << 24
+        |  (v & 0x000000000000ff00L)  << 40
+        |  (v & 0x00000000000000ffL)  << 56;
+  }
+
+  /**
+   * Throws {@code t}, even if the declared throws clause doesn't permit it.
+   * This is a terrible – but terribly convenient – hack that makes it easy to
+   * catch and rethrow exceptions after cleanup. See Java Puzzlers #43.
+   */
+  public static void sneakyRethrow(Throwable t) {
+    Util.<Error>sneakyThrow2(t);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T extends Throwable> void sneakyThrow2(Throwable t) throws T {
+    throw (T) t;
+  }
+}
diff --git a/okio/src/test/java/okio/ByteStringTest.java b/okio/src/test/java/okio/ByteStringTest.java
new file mode 100644
index 0000000..16b8e2d
--- /dev/null
+++ b/okio/src/test/java/okio/ByteStringTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ByteStringTest {
+
+  @Test public void getByte() throws Exception {
+    ByteString byteString = ByteString.decodeHex("ab12");
+    assertEquals(-85, byteString.getByte(0));
+    assertEquals(18, byteString.getByte(1));
+  }
+
+  @Test public void getByteOutOfBounds() throws Exception {
+    ByteString byteString = ByteString.decodeHex("ab12");
+    try {
+      byteString.getByte(2);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void equals() throws Exception {
+    ByteString byteString = ByteString.decodeHex("000102");
+    assertTrue(byteString.equals(byteString));
+    assertTrue(byteString.equals(ByteString.decodeHex("000102")));
+    assertTrue(ByteString.of().equals(ByteString.EMPTY));
+    assertTrue(ByteString.EMPTY.equals(ByteString.of()));
+    assertFalse(byteString.equals(new Object()));
+    assertFalse(byteString.equals(ByteString.decodeHex("000201")));
+  }
+
+  private final String bronzeHorseman = "На берегу пустынных волн";
+
+  @Test public void utf8() throws Exception {
+    ByteString byteString = ByteString.encodeUtf8(bronzeHorseman);
+    assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(Util.UTF_8));
+    assertTrue(byteString.equals(ByteString.of(bronzeHorseman.getBytes(Util.UTF_8))));
+    assertEquals(byteString.utf8(), bronzeHorseman);
+  }
+
+  @Test public void testHashCode() throws Exception {
+    ByteString byteString = ByteString.decodeHex("0102");
+    assertEquals(byteString.hashCode(), byteString.hashCode());
+    assertEquals(byteString.hashCode(), ByteString.decodeHex("0102").hashCode());
+  }
+
+  @Test public void read() throws Exception {
+    InputStream in = new ByteArrayInputStream("abc".getBytes(Util.UTF_8));
+    assertEquals(ByteString.decodeHex("6162"), ByteString.read(in, 2));
+    assertEquals(ByteString.decodeHex("63"), ByteString.read(in, 1));
+    assertEquals(ByteString.of(), ByteString.read(in, 0));
+  }
+
+  @Test public void readLowerCase() throws Exception {
+    InputStream in = new ByteArrayInputStream("ABC".getBytes(Util.UTF_8));
+    assertEquals(ByteString.encodeUtf8("ab"), ByteString.read(in, 2).toAsciiLowercase());
+    assertEquals(ByteString.encodeUtf8("c"), ByteString.read(in, 1).toAsciiLowercase());
+    assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiLowercase());
+  }
+
+  @Test public void toAsciiLowerCaseNoUppercase() throws Exception {
+    ByteString s = ByteString.encodeUtf8("a1_+");
+    assertSame(s, s.toAsciiLowercase());
+  }
+
+  @Test public void toAsciiAllUppercase() throws Exception {
+    assertEquals(ByteString.encodeUtf8("ab"), ByteString.encodeUtf8("AB").toAsciiLowercase());
+  }
+
+  @Test public void toAsciiStartsLowercaseEndsUppercase() throws Exception {
+    assertEquals(ByteString.encodeUtf8("abcd"), ByteString.encodeUtf8("abCD").toAsciiLowercase());
+  }
+
+  @Test public void write() throws Exception {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    ByteString.decodeHex("616263").write(out);
+    assertByteArraysEquals(new byte[] { 0x61, 0x62, 0x63 }, out.toByteArray());
+  }
+
+  @Test public void encodeBase64() {
+    assertEquals("", ByteString.encodeUtf8("").base64());
+    assertEquals("AA==", ByteString.encodeUtf8("\u0000").base64());
+    assertEquals("AAA=", ByteString.encodeUtf8("\u0000\u0000").base64());
+    assertEquals("AAAA", ByteString.encodeUtf8("\u0000\u0000\u0000").base64());
+    assertEquals("V2UncmUgZ29ubmEgbWFrZSBhIGZvcnR1bmUgd2l0aCB0aGlzIHBsYWNlLg==",
+        ByteString.encodeUtf8("We're gonna make a fortune with this place.").base64());
+  }
+
+  @Test public void ignoreUnnecessaryPadding() {
+    assertEquals("", ByteString.decodeBase64("====").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AAAA====").utf8());
+  }
+
+  @Test public void decodeBase64() {
+    assertEquals("", ByteString.decodeBase64("").utf8());
+    assertEquals(null, ByteString.decodeBase64("/===")); // Can't do anything with 6 bits!
+    assertEquals(ByteString.decodeHex("ff"), ByteString.decodeBase64("//=="));
+    assertEquals(ByteString.decodeHex("ffff"), ByteString.decodeBase64("///="));
+    assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("////"));
+    assertEquals(ByteString.decodeHex("ffffffffffff"), ByteString.decodeBase64("////////"));
+    assertEquals("What's to be scared about? It's just a little hiccup in the power...",
+        ByteString.decodeBase64("V2hhdCdzIHRvIGJlIHNjYXJlZCBhYm91dD8gSXQncyBqdXN0IGEgbGl0dGxlIGhpY2"
+            + "N1cCBpbiB0aGUgcG93ZXIuLi4=").utf8());
+  }
+
+  @Test public void decodeBase64WithWhitespace() {
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AA AA").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8());
+    assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("A    AAA").utf8());
+    assertEquals("", ByteString.decodeBase64("    ").utf8());
+  }
+
+  @Test public void encodeHex() throws Exception {
+    assertEquals("000102", ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2).hex());
+  }
+
+  @Test public void decodeHex() throws Exception {
+    assertEquals(ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2), ByteString.decodeHex("000102"));
+  }
+
+  @Test public void decodeHexOddNumberOfChars() throws Exception {
+    try {
+      ByteString.decodeHex("aaa");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void decodeHexInvalidChar() throws Exception {
+    try {
+      ByteString.decodeHex("a\u0000");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void toStringOnEmptyByteString() {
+    assertEquals("ByteString[size=0]", ByteString.of().toString());
+  }
+
+  @Test public void toStringOnSmallByteStringIncludesContents() {
+    assertEquals("ByteString[size=16 data=a1b2c3d4e5f61a2b3c4d5e6f10203040]",
+        ByteString.decodeHex("a1b2c3d4e5f61a2b3c4d5e6f10203040").toString());
+  }
+
+  @Test public void toStringOnLargeByteStringIncludesMd5() {
+    assertEquals("ByteString[size=17 md5=2c9728a2138b2f25e9f89f99bdccf8db]",
+        ByteString.encodeUtf8("12345678901234567").toString());
+  }
+
+  private static void assertByteArraysEquals(byte[] a, byte[] b) {
+    assertEquals(Arrays.toString(a), Arrays.toString(b));
+  }
+}
diff --git a/okio/src/test/java/okio/DeflaterSinkTest.java b/okio/src/test/java/okio/DeflaterSinkTest.java
new file mode 100644
index 0000000..0f6b8c2
--- /dev/null
+++ b/okio/src/test/java/okio/DeflaterSinkTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class DeflaterSinkTest {
+  @Test public void deflateWithClose() throws Exception {
+    OkBuffer data = new OkBuffer();
+    String original = "They're moving in herds. They do move in herds.";
+    data.writeUtf8(original);
+    OkBuffer sink = new OkBuffer();
+    DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+    deflaterSink.write(data, data.size());
+    deflaterSink.close();
+    OkBuffer inflated = inflate(sink);
+    assertEquals(original, inflated.readUtf8(inflated.size()));
+  }
+
+  @Test public void deflateWithSyncFlush() throws Exception {
+    String original = "Yes, yes, yes. That's why we're taking extreme precautions.";
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(original);
+    OkBuffer sink = new OkBuffer();
+    DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+    deflaterSink.write(data, data.size());
+    deflaterSink.flush();
+    OkBuffer inflated = inflate(sink);
+    assertEquals(original, inflated.readUtf8(inflated.size()));
+  }
+
+  @Test public void deflateWellCompressed() throws IOException {
+    String original = repeat('a', 1024 * 1024);
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(original);
+    OkBuffer sink = new OkBuffer();
+    DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+    deflaterSink.write(data, data.size());
+    deflaterSink.close();
+    OkBuffer inflated = inflate(sink);
+    assertEquals(original, inflated.readUtf8(inflated.size()));
+  }
+
+  @Test public void deflatePoorlyCompressed() throws IOException {
+    ByteString original = randomBytes(1024 * 1024);
+    OkBuffer data = new OkBuffer();
+    data.write(original);
+    OkBuffer sink = new OkBuffer();
+    DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+    deflaterSink.write(data, data.size());
+    deflaterSink.close();
+    OkBuffer inflated = inflate(sink);
+    assertEquals(original, inflated.readByteString(inflated.size()));
+  }
+
+  @Test public void multipleSegmentsWithoutCompression() throws IOException {
+    OkBuffer buffer = new OkBuffer();
+    Deflater deflater = new Deflater();
+    deflater.setLevel(Deflater.NO_COMPRESSION);
+    DeflaterSink deflaterSink = new DeflaterSink(buffer, deflater);
+    int byteCount = Segment.SIZE * 4;
+    deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', byteCount)), byteCount);
+    deflaterSink.close();
+    assertEquals(repeat('a', byteCount), inflate(buffer).readUtf8(byteCount));
+  }
+
+  /**
+   * This test deflates a single segment of without compression because that's
+   * the easiest way to force close() to emit a large amount of data to the
+   * underlying sink.
+   */
+  @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+    MockSink mockSink = new MockSink();
+    mockSink.scheduleThrow(0, new IOException("first"));
+    mockSink.scheduleThrow(1, new IOException("second"));
+    Deflater deflater = new Deflater();
+    deflater.setLevel(Deflater.NO_COMPRESSION);
+    DeflaterSink deflaterSink = new DeflaterSink(mockSink, deflater);
+    deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', Segment.SIZE)), Segment.SIZE);
+    try {
+      deflaterSink.close();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("first", expected.getMessage());
+    }
+    mockSink.assertLogContains("close()");
+  }
+
+  /**
+   * Uses streaming decompression to inflate {@code deflated}. The input must
+   * either be finished or have a trailing sync flush.
+   */
+  private OkBuffer inflate(OkBuffer deflated) throws IOException {
+    InputStream deflatedIn = deflated.inputStream();
+    Inflater inflater = new Inflater();
+    InputStream inflatedIn = new InflaterInputStream(deflatedIn, inflater);
+    OkBuffer result = new OkBuffer();
+    byte[] buffer = new byte[8192];
+    while (!inflater.needsInput() || deflated.size() > 0 || deflatedIn.available() > 0) {
+      int count = inflatedIn.read(buffer, 0, buffer.length);
+      result.write(buffer, 0, count);
+    }
+    return result;
+  }
+
+  private ByteString randomBytes(int length) {
+    Random random = new Random(0);
+    byte[] randomBytes = new byte[length];
+    random.nextBytes(randomBytes);
+    return ByteString.of(randomBytes);
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/okio/src/test/java/okio/GzipSourceTest.java b/okio/src/test/java/okio/GzipSourceTest.java
new file mode 100644
index 0000000..f14b999
--- /dev/null
+++ b/okio/src/test/java/okio/GzipSourceTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.util.zip.CRC32;
+import org.junit.Test;
+
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class GzipSourceTest {
+
+  @Test public void gunzip() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeader);
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  @Test public void gunzip_withHCRC() throws Exception {
+    CRC32 hcrc = new CRC32();
+    ByteString gzipHeader = gzipHeaderWithFlags((byte) 0x02);
+    hcrc.update(gzipHeader.toByteArray());
+
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeader);
+    gzipped.writeShort(Util.reverseBytesShort((short) hcrc.getValue())); // little endian
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  @Test public void gunzip_withExtra() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeaderWithFlags((byte) 0x04));
+    gzipped.writeShort(Util.reverseBytesShort((short) 7)); // little endian extra length
+    gzipped.write("blubber".getBytes(UTF_8), 0, 7);
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  @Test public void gunzip_withName() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeaderWithFlags((byte) 0x08));
+    gzipped.write("foo.txt".getBytes(UTF_8), 0, 7);
+    gzipped.writeByte(0); // zero-terminated
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  @Test public void gunzip_withComment() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeaderWithFlags((byte) 0x10));
+    gzipped.write("rubbish".getBytes(UTF_8), 0, 7);
+    gzipped.writeByte(0); // zero-terminated
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  /**
+   * For portability, it is a good idea to export the gzipped bytes and try running gzip.  Ex.
+   * {@code echo gzipped | base64 --decode | gzip -l -v}
+   */
+  @Test public void gunzip_withAll() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeaderWithFlags((byte) 0x1c));
+    gzipped.writeShort(Util.reverseBytesShort((short) 7)); // little endian extra length
+    gzipped.write("blubber".getBytes(UTF_8), 0, 7);
+    gzipped.write("foo.txt".getBytes(UTF_8), 0, 7);
+    gzipped.writeByte(0); // zero-terminated
+    gzipped.write("rubbish".getBytes(UTF_8), 0, 7);
+    gzipped.writeByte(0); // zero-terminated
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+    assertGzipped(gzipped);
+  }
+
+  private void assertGzipped(OkBuffer gzipped) throws IOException {
+    OkBuffer gunzipped = gunzip(gzipped);
+    assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8(gunzipped.size()));
+  }
+
+  /**
+   * Note that you cannot test this with old versions of gzip, as they interpret flag bit 1 as
+   * CONTINUATION, not HCRC. For example, this is the case with the default gzip on osx.
+   */
+  @Test public void gunzipWhenHeaderCRCIncorrect() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeaderWithFlags((byte) 0x02));
+    gzipped.writeShort((short) 0); // wrong HCRC!
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer);
+
+    try {
+      gunzip(gzipped);
+      fail();
+    } catch (IOException e) {
+      assertEquals("FHCRC: actual 0x0000261d != expected 0x00000000", e.getMessage());
+    }
+  }
+
+  @Test public void gunzipWhenCRCIncorrect() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeader);
+    gzipped.write(deflated);
+    gzipped.writeInt(Util.reverseBytesInt(0x1234567)); // wrong CRC
+    gzipped.write(gzipTrailer.toByteArray(), 3, 4);
+
+    try {
+      gunzip(gzipped);
+      fail();
+    } catch (IOException e) {
+      assertEquals("CRC: actual 0x37ad8f8d != expected 0x01234567", e.getMessage());
+    }
+  }
+
+  @Test public void gunzipWhenLengthIncorrect() throws Exception {
+    OkBuffer gzipped = new OkBuffer();
+    gzipped.write(gzipHeader);
+    gzipped.write(deflated);
+    gzipped.write(gzipTrailer.toByteArray(), 0, 4);
+    gzipped.writeInt(Util.reverseBytesInt(0x123456)); // wrong length
+
+    try {
+      gunzip(gzipped);
+      fail();
+    } catch (IOException e) {
+      assertEquals("ISIZE: actual 0x00000020 != expected 0x00123456", e.getMessage());
+    }
+  }
+
+  @Test public void gunzipExhaustsSource() throws Exception {
+    OkBuffer gzippedSource = new OkBuffer()
+        .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
+
+    ExhaustableSource exhaustableSource = new ExhaustableSource(gzippedSource);
+    BufferedSource gunzippedSource = Okio.buffer(new GzipSource(exhaustableSource));
+
+    assertEquals('a', gunzippedSource.readByte());
+    assertEquals('b', gunzippedSource.readByte());
+    assertEquals('c', gunzippedSource.readByte());
+    assertFalse(exhaustableSource.exhausted);
+    assertEquals(-1, gunzippedSource.read(new OkBuffer(), 1));
+    assertTrue(exhaustableSource.exhausted);
+  }
+
+  @Test public void gunzipThrowsIfSourceIsNotExhausted() throws Exception {
+    OkBuffer gzippedSource = new OkBuffer()
+        .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
+    gzippedSource.writeByte('d'); // This byte shouldn't be here!
+
+    BufferedSource gunzippedSource = Okio.buffer(new GzipSource(gzippedSource));
+
+    assertEquals('a', gunzippedSource.readByte());
+    assertEquals('b', gunzippedSource.readByte());
+    assertEquals('c', gunzippedSource.readByte());
+    try {
+      gunzippedSource.readByte();
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  private ByteString gzipHeaderWithFlags(byte flags) {
+    byte[] result = gzipHeader.toByteArray();
+    result[3] = flags;
+    return ByteString.of(result);
+  }
+
+  private final ByteString gzipHeader = ByteString.decodeHex("1f8b0800000000000000");
+
+  // Deflated "It's a UNIX system! I know this!"
+  private final ByteString deflated = ByteString.decodeHex(
+      "f32c512f56485408f5f38c5028ae2c2e49cd5554f054c8cecb2f5728c9c82c560400");
+
+  private final ByteString gzipTrailer = ByteString.decodeHex(""
+      + "8d8fad37" // Checksum of deflated.
+      + "20000000" // 32 in little endian.
+  );
+
+  private OkBuffer gunzip(OkBuffer gzipped) throws IOException {
+    OkBuffer result = new OkBuffer();
+    GzipSource source = new GzipSource(gzipped);
+    while (source.read(result, Integer.MAX_VALUE) != -1) {
+    }
+    return result;
+  }
+
+  /** This source keeps track of whether its read have returned -1. */
+  static class ExhaustableSource implements Source {
+    private final Source source;
+    private boolean exhausted;
+
+    ExhaustableSource(Source source) {
+      this.source = source;
+    }
+
+    @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+      long result = source.read(sink, byteCount);
+      if (result == -1) exhausted = true;
+      return result;
+    }
+
+    @Override public Source deadline(Deadline deadline) {
+      source.deadline(deadline);
+      return this;
+    }
+
+    @Override public void close() throws IOException {
+      source.close();
+    }
+  }
+}
diff --git a/okio/src/test/java/okio/InflaterSourceTest.java b/okio/src/test/java/okio/InflaterSourceTest.java
new file mode 100644
index 0000000..e6f2bc6
--- /dev/null
+++ b/okio/src/test/java/okio/InflaterSourceTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class InflaterSourceTest {
+  @Test public void inflate() throws Exception {
+    OkBuffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+        + "tYDAF6CD5s=");
+    OkBuffer inflated = inflate(deflated);
+    assertEquals("God help us, we're in the hands of engineers.", readUtf8(inflated));
+  }
+
+  @Test public void inflateTruncated() throws Exception {
+    OkBuffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+        + "tYDAF6CDw==");
+    try {
+      inflate(deflated);
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test public void inflateWellCompressed() throws Exception {
+    OkBuffer deflated = decodeBase64("eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+        + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8B"
+        + "tFeWvE=\n");
+    String original = repeat('a', 1024 * 1024);
+    OkBuffer inflated = inflate(deflated);
+    assertEquals(original, readUtf8(inflated));
+  }
+
+  @Test public void inflatePoorlyCompressed() throws Exception {
+    ByteString original = randomBytes(1024 * 1024);
+    OkBuffer deflated = deflate(original);
+    OkBuffer inflated = inflate(deflated);
+    assertEquals(original, inflated.readByteString(inflated.size()));
+  }
+
+  private OkBuffer decodeBase64(String s) {
+    return new OkBuffer().write(ByteString.decodeBase64(s));
+  }
+
+  private String readUtf8(OkBuffer buffer) {
+    return buffer.readUtf8(buffer.size());
+  }
+
+  /** Use DeflaterOutputStream to deflate source. */
+  private OkBuffer deflate(ByteString source) throws IOException {
+    OkBuffer result = new OkBuffer();
+    Sink sink = Okio.sink(new DeflaterOutputStream(result.outputStream()));
+    sink.write(new OkBuffer().write(source), source.size());
+    sink.close();
+    return result;
+  }
+
+  /** Returns a new buffer containing the inflated contents of {@code deflated}. */
+  private OkBuffer inflate(OkBuffer deflated) throws IOException {
+    OkBuffer result = new OkBuffer();
+    InflaterSource source = new InflaterSource(deflated, new Inflater());
+    while (source.read(result, Integer.MAX_VALUE) != -1) {
+    }
+    return result;
+  }
+
+  private ByteString randomBytes(int length) {
+    Random random = new Random(0);
+    byte[] randomBytes = new byte[length];
+    random.nextBytes(randomBytes);
+    return ByteString.of(randomBytes);
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/okio/src/test/java/okio/MockSink.java b/okio/src/test/java/okio/MockSink.java
new file mode 100644
index 0000000..bae3259
--- /dev/null
+++ b/okio/src/test/java/okio/MockSink.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/** A scriptable sink. Like Mockito, but worse and requiring less configuration. */
+class MockSink implements Sink {
+  private final List<String> log = new ArrayList<String>();
+  private final Map<Integer, IOException> callThrows = new LinkedHashMap<Integer, IOException>();
+
+  public void assertLog(String... messages) {
+    assertEquals(Arrays.asList(messages), log);
+  }
+
+  public void assertLogContains(String message) {
+    assertTrue(log.contains(message));
+  }
+
+  public void scheduleThrow(int call, IOException e) {
+    callThrows.put(call, e);
+  }
+
+  private void throwIfScheduled() throws IOException {
+    IOException exception = callThrows.get(log.size() - 1);
+    if (exception != null) throw exception;
+  }
+
+  @Override public void write(OkBuffer source, long byteCount) throws IOException {
+    log.add("write(" + source + ", " + byteCount + ")");
+    source.skip(byteCount);
+    throwIfScheduled();
+  }
+
+  @Override public void flush() throws IOException {
+    log.add("flush()");
+    throwIfScheduled();
+  }
+
+  @Override public Sink deadline(Deadline deadline) {
+    log.add("deadline()");
+    return this;
+  }
+
+  @Override public void close() throws IOException {
+    log.add("close()");
+    throwIfScheduled();
+  }
+}
diff --git a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java b/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
new file mode 100644
index 0000000..ac3de72
--- /dev/null
+++ b/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+public final class OkBufferReadUtf8LineTest extends ReadUtf8LineTest {
+  @Override protected BufferedSource newSource(String s) {
+    return new OkBuffer().writeUtf8(s);
+  }
+}
diff --git a/okio/src/test/java/okio/OkBufferTest.java b/okio/src/test/java/okio/OkBufferTest.java
new file mode 100644
index 0000000..f69613a
--- /dev/null
+++ b/okio/src/test/java/okio/OkBufferTest.java
@@ -0,0 +1,682 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class OkBufferTest {
+  @Test public void readAndWriteUtf8() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8("ab");
+    assertEquals(2, buffer.size());
+    buffer.writeUtf8("cdef");
+    assertEquals(6, buffer.size());
+    assertEquals("abcd", buffer.readUtf8(4));
+    assertEquals(2, buffer.size());
+    assertEquals("ef", buffer.readUtf8(2));
+    assertEquals(0, buffer.size());
+    try {
+      buffer.readUtf8(1);
+      fail();
+    } catch (ArrayIndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void completeSegmentByteCountOnEmptyBuffer() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    assertEquals(0, buffer.completeSegmentByteCount());
+  }
+
+  @Test public void completeSegmentByteCountOnBufferWithFullSegments() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a', Segment.SIZE * 4));
+    assertEquals(Segment.SIZE * 4, buffer.completeSegmentByteCount());
+  }
+
+  @Test public void completeSegmentByteCountOnBufferWithIncompleteTailSegment() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a', Segment.SIZE * 4 - 10));
+    assertEquals(Segment.SIZE * 3, buffer.completeSegmentByteCount());
+  }
+
+  @Test public void readUtf8SpansSegments() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a', Segment.SIZE * 2));
+    buffer.readUtf8(Segment.SIZE - 1);
+    assertEquals("aa", buffer.readUtf8(2));
+  }
+
+  @Test public void readUtf8EntireBuffer() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a', Segment.SIZE));
+    assertEquals(repeat('a', Segment.SIZE), buffer.readUtf8(Segment.SIZE));
+  }
+
+  @Test public void toStringOnEmptyBuffer() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    assertEquals("OkBuffer[size=0]", buffer.toString());
+  }
+
+  @Test public void toStringOnSmallBufferIncludesContents() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.write(ByteString.decodeHex("a1b2c3d4e5f61a2b3c4d5e6f10203040"));
+    assertEquals("OkBuffer[size=16 data=a1b2c3d4e5f61a2b3c4d5e6f10203040]", buffer.toString());
+  }
+
+  @Test public void toStringOnLargeBufferIncludesMd5() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.write(ByteString.encodeUtf8("12345678901234567"));
+    assertEquals("OkBuffer[size=17 md5=2c9728a2138b2f25e9f89f99bdccf8db]", buffer.toString());
+  }
+
+  @Test public void toStringOnMultipleSegmentBuffer() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a', 6144));
+    assertEquals("OkBuffer[size=6144 md5=d890021f28522533c1cc1b9b1f83ce73]", buffer.toString());
+  }
+
+  @Test public void multipleSegmentBuffers() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8(repeat('a',  1000));
+    buffer.writeUtf8(repeat('b', 2500));
+    buffer.writeUtf8(repeat('c', 5000));
+    buffer.writeUtf8(repeat('d', 10000));
+    buffer.writeUtf8(repeat('e', 25000));
+    buffer.writeUtf8(repeat('f', 50000));
+
+    assertEquals(repeat('a', 999), buffer.readUtf8(999)); // a...a
+    assertEquals("a" + repeat('b', 2500) + "c", buffer.readUtf8(2502)); // ab...bc
+    assertEquals(repeat('c', 4998), buffer.readUtf8(4998)); // c...c
+    assertEquals("c" + repeat('d', 10000) + "e", buffer.readUtf8(10002)); // cd...de
+    assertEquals(repeat('e', 24998), buffer.readUtf8(24998)); // e...e
+    assertEquals("e" + repeat('f', 50000), buffer.readUtf8(50001)); // ef...f
+    assertEquals(0, buffer.size());
+  }
+
+  @Test public void fillAndDrainPool() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+
+    // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
+    buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+    buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+    assertEquals(0, SegmentPool.INSTANCE.byteCount);
+
+    // Recycle MAX_SIZE segments. They're all in the pool.
+    buffer.readByteString(SegmentPool.MAX_SIZE);
+    assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
+
+    // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
+    buffer.readByteString(SegmentPool.MAX_SIZE);
+    assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
+
+    // Take MAX_SIZE segments to drain the pool.
+    buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+    assertEquals(0, SegmentPool.INSTANCE.byteCount);
+
+    // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
+    buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+    assertEquals(0, SegmentPool.INSTANCE.byteCount);
+  }
+
+  @Test public void moveBytesBetweenBuffersShareSegment() throws Exception {
+    int size = (Segment.SIZE / 2) - 1;
+    List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+    assertEquals(asList(size * 2), segmentSizes);
+  }
+
+  @Test public void moveBytesBetweenBuffersReassignSegment() throws Exception {
+    int size = (Segment.SIZE / 2) + 1;
+    List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+    assertEquals(asList(size, size), segmentSizes);
+  }
+
+  @Test public void moveBytesBetweenBuffersMultipleSegments() throws Exception {
+    int size = 3 * Segment.SIZE + 1;
+    List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+    assertEquals(asList(Segment.SIZE, Segment.SIZE, Segment.SIZE, 1,
+        Segment.SIZE, Segment.SIZE, Segment.SIZE, 1), segmentSizes);
+  }
+
+  private List<Integer> moveBytesBetweenBuffers(String... contents) {
+    StringBuilder expected = new StringBuilder();
+    OkBuffer buffer = new OkBuffer();
+    for (String s : contents) {
+      OkBuffer source = new OkBuffer();
+      source.writeUtf8(s);
+      buffer.write(source, source.size());
+      expected.append(s);
+    }
+    List<Integer> segmentSizes = buffer.segmentSizes();
+    assertEquals(expected.toString(), buffer.readUtf8(expected.length()));
+    return segmentSizes;
+  }
+
+  /** The big part of source's first segment is being moved. */
+  @Test public void writeSplitSourceBufferLeft() throws Exception {
+    int writeSize = Segment.SIZE / 2 + 1;
+
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('b', Segment.SIZE - 10));
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE * 2));
+    sink.write(source, writeSize);
+
+    assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
+    assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
+  }
+
+  /** The big part of source's first segment is staying put. */
+  @Test public void writeSplitSourceBufferRight() throws Exception {
+    int writeSize = Segment.SIZE / 2 - 1;
+
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('b', Segment.SIZE - 10));
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE * 2));
+    sink.write(source, writeSize);
+
+    assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
+    assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
+  }
+
+  @Test public void writePrefixDoesntSplit() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('b', 10));
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE * 2));
+    sink.write(source, 20);
+
+    assertEquals(asList(30), sink.segmentSizes());
+    assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
+    assertEquals(30, sink.size());
+    assertEquals(Segment.SIZE * 2 - 20, source.size());
+  }
+
+  @Test public void writePrefixDoesntSplitButRequiresCompact() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('b', Segment.SIZE - 10)); // limit = size - 10
+    sink.readUtf8(Segment.SIZE - 20); // pos = size = 20
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE * 2));
+    sink.write(source, 20);
+
+    assertEquals(asList(30), sink.segmentSizes());
+    assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
+    assertEquals(30, sink.size());
+    assertEquals(Segment.SIZE * 2 - 20, source.size());
+  }
+
+  @Test public void readExhaustedSource() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('a', 10));
+
+    OkBuffer source = new OkBuffer();
+
+    assertEquals(-1, source.read(sink, 10));
+    assertEquals(10, sink.size());
+    assertEquals(0, source.size());
+  }
+
+  @Test public void readZeroBytesFromSource() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('a', 10));
+
+    OkBuffer source = new OkBuffer();
+
+    // Either 0 or -1 is reasonable here. For consistency with Android's
+    // ByteArrayInputStream we return 0.
+    assertEquals(-1, source.read(sink, 0));
+    assertEquals(10, sink.size());
+    assertEquals(0, source.size());
+  }
+
+  @Test public void moveAllRequestedBytesWithRead() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('a', 10));
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('b', 15));
+
+    assertEquals(10, source.read(sink, 10));
+    assertEquals(20, sink.size());
+    assertEquals(5, source.size());
+    assertEquals(repeat('a', 10) + repeat('b', 10), sink.readUtf8(20));
+  }
+
+  @Test public void moveFewerThanRequestedBytesWithRead() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    sink.writeUtf8(repeat('a', 10));
+
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('b', 20));
+
+    assertEquals(20, source.read(sink, 25));
+    assertEquals(30, sink.size());
+    assertEquals(0, source.size());
+    assertEquals(repeat('a', 10) + repeat('b', 20), sink.readUtf8(30));
+  }
+
+  @Test public void indexOf() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+
+    // The segment is empty.
+    assertEquals(-1, buffer.indexOf((byte) 'a'));
+
+    // The segment has one value.
+    buffer.writeUtf8("a"); // a
+    assertEquals(0, buffer.indexOf((byte) 'a'));
+    assertEquals(-1, buffer.indexOf((byte) 'b'));
+
+    // The segment has lots of data.
+    buffer.writeUtf8(repeat('b', Segment.SIZE - 2)); // ab...b
+    assertEquals(0, buffer.indexOf((byte) 'a'));
+    assertEquals(1, buffer.indexOf((byte) 'b'));
+    assertEquals(-1, buffer.indexOf((byte) 'c'));
+
+    // The segment doesn't start at 0, it starts at 2.
+    buffer.readUtf8(2); // b...b
+    assertEquals(-1, buffer.indexOf((byte) 'a'));
+    assertEquals(0, buffer.indexOf((byte) 'b'));
+    assertEquals(-1, buffer.indexOf((byte) 'c'));
+
+    // The segment is full.
+    buffer.writeUtf8("c"); // b...bc
+    assertEquals(-1, buffer.indexOf((byte) 'a'));
+    assertEquals(0, buffer.indexOf((byte) 'b'));
+    assertEquals(Segment.SIZE - 3, buffer.indexOf((byte) 'c'));
+
+    // The segment doesn't start at 2, it starts at 4.
+    buffer.readUtf8(2); // b...bc
+    assertEquals(-1, buffer.indexOf((byte) 'a'));
+    assertEquals(0, buffer.indexOf((byte) 'b'));
+    assertEquals(Segment.SIZE - 5, buffer.indexOf((byte) 'c'));
+
+    // Two segments.
+    buffer.writeUtf8("d"); // b...bcd, d is in the 2nd segment.
+    assertEquals(asList(Segment.SIZE - 4, 1), buffer.segmentSizes());
+    assertEquals(Segment.SIZE - 4, buffer.indexOf((byte) 'd'));
+    assertEquals(-1, buffer.indexOf((byte) 'e'));
+  }
+
+  @Test public void indexOfWithOffset() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    int halfSegment = Segment.SIZE / 2;
+    buffer.writeUtf8(repeat('a', halfSegment));
+    buffer.writeUtf8(repeat('b', halfSegment));
+    buffer.writeUtf8(repeat('c', halfSegment));
+    buffer.writeUtf8(repeat('d', halfSegment));
+    assertEquals(0, buffer.indexOf((byte) 'a', 0));
+    assertEquals(halfSegment - 1, buffer.indexOf((byte) 'a', halfSegment - 1));
+    assertEquals(halfSegment, buffer.indexOf((byte) 'b', halfSegment - 1));
+    assertEquals(halfSegment * 2, buffer.indexOf((byte) 'c', halfSegment - 1));
+    assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment - 1));
+    assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 2));
+    assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 3));
+    assertEquals(halfSegment * 4 - 1, buffer.indexOf((byte) 'd', halfSegment * 4 - 1));
+  }
+
+  @Test public void writeBytes() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeByte(0xab);
+    data.writeByte(0xcd);
+    assertEquals("OkBuffer[size=2 data=abcd]", data.toString());
+  }
+
+  @Test public void writeLastByteInSegment() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 1));
+    data.writeByte(0x20);
+    data.writeByte(0x21);
+    assertEquals(asList(Segment.SIZE, 1), data.segmentSizes());
+    assertEquals(repeat('a', Segment.SIZE - 1), data.readUtf8(Segment.SIZE - 1));
+    assertEquals("OkBuffer[size=2 data=2021]", data.toString());
+  }
+
+  @Test public void writeShort() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeShort(0xabcd);
+    data.writeShort(0x4321);
+    assertEquals("OkBuffer[size=4 data=abcd4321]", data.toString());
+  }
+
+  @Test public void writeShortLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeShortLe(0xabcd);
+    data.writeShortLe(0x4321);
+    assertEquals("OkBuffer[size=4 data=cdab2143]", data.toString());
+  }
+
+  @Test public void writeInt() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeInt(0xabcdef01);
+    data.writeInt(0x87654321);
+    assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
+  }
+
+  @Test public void writeLastIntegerInSegment() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 4));
+    data.writeInt(0xabcdef01);
+    data.writeInt(0x87654321);
+    assertEquals(asList(Segment.SIZE, 4), data.segmentSizes());
+    assertEquals(repeat('a', Segment.SIZE - 4), data.readUtf8(Segment.SIZE - 4));
+    assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
+  }
+
+  @Test public void writeIntegerDoesntQuiteFitInSegment() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 3));
+    data.writeInt(0xabcdef01);
+    data.writeInt(0x87654321);
+    assertEquals(asList(Segment.SIZE - 3, 8), data.segmentSizes());
+    assertEquals(repeat('a', Segment.SIZE - 3), data.readUtf8(Segment.SIZE - 3));
+    assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
+  }
+
+  @Test public void writeIntLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeIntLe(0xabcdef01);
+    data.writeIntLe(0x87654321);
+    assertEquals("OkBuffer[size=8 data=01efcdab21436587]", data.toString());
+  }
+
+  @Test public void writeLong() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeLong(0xabcdef0187654321L);
+    data.writeLong(0xcafebabeb0b15c00L);
+    assertEquals("OkBuffer[size=16 data=abcdef0187654321cafebabeb0b15c00]", data.toString());
+  }
+
+  @Test public void writeLongLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeLongLe(0xabcdef0187654321L);
+    data.writeLongLe(0xcafebabeb0b15c00L);
+    assertEquals("OkBuffer[size=16 data=2143658701efcdab005cb1b0bebafeca]", data.toString());
+  }
+
+  @Test public void readByte() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+    assertEquals(0xab, data.readByte() & 0xff);
+    assertEquals(0xcd, data.readByte() & 0xff);
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readShort() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+    });
+    assertEquals((short) 0xabcd, data.readShort());
+    assertEquals((short) 0xef01, data.readShort());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readShortLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10
+    });
+    assertEquals((short) 0xcdab, data.readShortLe());
+    assertEquals((short) 0x10ef, data.readShortLe());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readShortSplitAcrossMultipleSegments() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 1));
+    data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+    data.readUtf8(Segment.SIZE - 1);
+    assertEquals((short) 0xabcd, data.readShort());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readInt() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21
+    });
+    assertEquals(0xabcdef01, data.readInt());
+    assertEquals(0x87654321, data.readInt());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readIntLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21
+    });
+    assertEquals(0x10efcdab, data.readIntLe());
+    assertEquals(0x21436587, data.readIntLe());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readIntSplitAcrossMultipleSegments() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 3));
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+    });
+    data.readUtf8(Segment.SIZE - 3);
+    assertEquals(0xabcdef01, data.readInt());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readLong() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+        (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
+        (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
+    });
+    assertEquals(0xabcdef1087654321L, data.readLong());
+    assertEquals(0x3647586912233445L, data.readLong());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readLongLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+        (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
+        (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
+    });
+    assertEquals(0x2143658710efcdabL, data.readLongLe());
+    assertEquals(0x4534231269584736L, data.readLongLe());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readLongSplitAcrossMultipleSegments() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 7));
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+    });
+    data.readUtf8(Segment.SIZE - 7);
+    assertEquals(0xabcdef0187654321L, data.readLong());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void byteAt() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8("a");
+    buffer.writeUtf8(repeat('b', Segment.SIZE));
+    buffer.writeUtf8("c");
+    assertEquals('a', buffer.getByte(0));
+    assertEquals('a', buffer.getByte(0)); // getByte doesn't mutate!
+    assertEquals('c', buffer.getByte(buffer.size - 1));
+    assertEquals('b', buffer.getByte(buffer.size - 2));
+    assertEquals('b', buffer.getByte(buffer.size - 3));
+  }
+
+  @Test public void getByteOfEmptyBuffer() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    try {
+      buffer.getByte(0);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void skip() throws Exception {
+    OkBuffer buffer = new OkBuffer();
+    buffer.writeUtf8("a");
+    buffer.writeUtf8(repeat('b', Segment.SIZE));
+    buffer.writeUtf8("c");
+    buffer.skip(1);
+    assertEquals('b', buffer.readByte() & 0xff);
+    buffer.skip(Segment.SIZE - 2);
+    assertEquals('b', buffer.readByte() & 0xff);
+    buffer.skip(1);
+    assertEquals(0, buffer.size());
+  }
+
+  @Test public void testWritePrefixToEmptyBuffer() {
+    OkBuffer sink = new OkBuffer();
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("abcd");
+    sink.write(source, 2);
+    assertEquals("ab", sink.readUtf8(2));
+  }
+
+  @Test public void cloneDoesNotObserveWritesToOriginal() throws Exception {
+    OkBuffer original = new OkBuffer();
+    OkBuffer clone = original.clone();
+    original.writeUtf8("abc");
+    assertEquals(0, clone.size());
+  }
+
+  @Test public void cloneDoesNotObserveReadsFromOriginal() throws Exception {
+    OkBuffer original = new OkBuffer();
+    original.writeUtf8("abc");
+    OkBuffer clone = original.clone();
+    assertEquals("abc", original.readUtf8(3));
+    assertEquals(3, clone.size());
+    assertEquals("ab", clone.readUtf8(2));
+  }
+
+  @Test public void originalDoesNotObserveWritesToClone() throws Exception {
+    OkBuffer original = new OkBuffer();
+    OkBuffer clone = original.clone();
+    clone.writeUtf8("abc");
+    assertEquals(0, original.size());
+  }
+
+  @Test public void originalDoesNotObserveReadsFromClone() throws Exception {
+    OkBuffer original = new OkBuffer();
+    original.writeUtf8("abc");
+    OkBuffer clone = original.clone();
+    assertEquals("abc", clone.readUtf8(3));
+    assertEquals(3, original.size());
+    assertEquals("ab", original.readUtf8(2));
+  }
+
+  @Test public void cloneMultipleSegments() throws Exception {
+    OkBuffer original = new OkBuffer();
+    original.writeUtf8(repeat('a', Segment.SIZE * 3));
+    OkBuffer clone = original.clone();
+    original.writeUtf8(repeat('b', Segment.SIZE * 3));
+    clone.writeUtf8(repeat('c', Segment.SIZE * 3));
+
+    assertEquals(repeat('a', Segment.SIZE * 3) + repeat('b', Segment.SIZE * 3),
+        original.readUtf8(Segment.SIZE * 6));
+    assertEquals(repeat('a', Segment.SIZE * 3) + repeat('c', Segment.SIZE * 3),
+        clone.readUtf8(Segment.SIZE * 6));
+  }
+
+  @Test public void testEqualsAndHashCodeEmpty() throws Exception {
+    OkBuffer a = new OkBuffer();
+    OkBuffer b = new OkBuffer();
+    assertTrue(a.equals(b));
+    assertTrue(a.hashCode() == b.hashCode());
+  }
+
+  @Test public void testEqualsAndHashCode() throws Exception {
+    OkBuffer a = new OkBuffer().writeUtf8("dog");
+    OkBuffer b = new OkBuffer().writeUtf8("hotdog");
+    assertFalse(a.equals(b));
+    assertFalse(a.hashCode() == b.hashCode());
+
+    b.readUtf8(3); // Leaves b containing 'dog'.
+    assertTrue(a.equals(b));
+    assertTrue(a.hashCode() == b.hashCode());
+  }
+
+  @Test public void testEqualsAndHashCodeSpanningSegments() throws Exception {
+    byte[] data = new byte[1024 * 1024];
+    Random dice = new Random(0);
+    dice.nextBytes(data);
+
+    OkBuffer a = bufferWithRandomSegmentLayout(dice, data);
+    OkBuffer b = bufferWithRandomSegmentLayout(dice, data);
+    assertTrue(a.equals(b));
+    assertTrue(a.hashCode() == b.hashCode());
+
+    data[data.length / 2]++; // Change a single byte.
+    OkBuffer c = bufferWithRandomSegmentLayout(dice, data);
+    assertFalse(a.equals(c));
+    assertFalse(a.hashCode() == c.hashCode());
+  }
+
+  /**
+   * Returns a new buffer containing the data in {@code data}, and a segment
+   * layout determined by {@code dice}.
+   */
+  private OkBuffer bufferWithRandomSegmentLayout(Random dice, byte[] data) {
+    OkBuffer result = new OkBuffer();
+
+    // Writing to result directly will yield packed segments. Instead, write to
+    // other buffers, then write those buffers to result.
+    for (int pos = 0, byteCount; pos < data.length; pos += byteCount) {
+      byteCount = (Segment.SIZE / 2) + dice.nextInt(Segment.SIZE / 2);
+      if (byteCount > data.length - pos) byteCount = data.length - pos;
+      int offset = dice.nextInt(Segment.SIZE - byteCount);
+
+      OkBuffer segment = new OkBuffer();
+      segment.write(new byte[offset]);
+      segment.write(data, pos, byteCount);
+      segment.skip(offset);
+
+      result.write(segment, byteCount);
+    }
+
+    return result;
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/okio/src/test/java/okio/OkioTest.java b/okio/src/test/java/okio/OkioTest.java
new file mode 100644
index 0000000..e56979f
--- /dev/null
+++ b/okio/src/test/java/okio/OkioTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+import org.junit.Test;
+
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class OkioTest {
+  @Test public void sinkFromOutputStream() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8("a");
+    data.writeUtf8(repeat('b', 9998));
+    data.writeUtf8("c");
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    Sink sink = Okio.sink(out);
+    sink.write(data, 3);
+    assertEquals("abb", out.toString("UTF-8"));
+    sink.write(data, data.size());
+    assertEquals("a" + repeat('b', 9998) + "c", out.toString("UTF-8"));
+  }
+
+  @Test public void sourceFromInputStream() throws Exception {
+    InputStream in = new ByteArrayInputStream(
+        ("a" + repeat('b', Segment.SIZE * 2) + "c").getBytes(UTF_8));
+
+    // Source: ab...bc
+    Source source = Okio.source(in);
+    OkBuffer sink = new OkBuffer();
+
+    // Source: b...bc. Sink: abb.
+    assertEquals(3, source.read(sink, 3));
+    assertEquals("abb", sink.readUtf8(3));
+
+    // Source: b...bc. Sink: b...b.
+    assertEquals(Segment.SIZE, source.read(sink, 20000));
+    assertEquals(repeat('b', Segment.SIZE), sink.readUtf8(sink.size()));
+
+    // Source: b...bc. Sink: b...bc.
+    assertEquals(Segment.SIZE - 1, source.read(sink, 20000));
+    assertEquals(repeat('b', Segment.SIZE - 2) + "c", sink.readUtf8(sink.size()));
+
+    // Source and sink are empty.
+    assertEquals(-1, source.read(sink, 1));
+  }
+
+  @Test public void sourceFromInputStreamBounds() throws Exception {
+    Source source = Okio.source(new ByteArrayInputStream(new byte[100]));
+    try {
+      source.read(new OkBuffer(), -1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/src/test/java/okio/ReadUtf8LineTest.java
new file mode 100644
index 0000000..79b4c8a
--- /dev/null
+++ b/okio/src/test/java/okio/ReadUtf8LineTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class ReadUtf8LineTest {
+  protected abstract BufferedSource newSource(String s);
+
+  @Test public void readLines() throws IOException {
+    BufferedSource source = newSource("abc\ndef\n");
+    assertEquals("abc", source.readUtf8LineStrict());
+    assertEquals("def", source.readUtf8LineStrict());
+    try {
+      source.readUtf8LineStrict();
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test public void emptyLines() throws IOException {
+    BufferedSource source = newSource("\n\n\n");
+    assertEquals("", source.readUtf8LineStrict());
+    assertEquals("", source.readUtf8LineStrict());
+    assertEquals("", source.readUtf8LineStrict());
+    assertTrue(source.exhausted());
+  }
+
+  @Test public void crDroppedPrecedingLf() throws IOException {
+    BufferedSource source = newSource("abc\r\ndef\r\nghi\rjkl\r\n");
+    assertEquals("abc", source.readUtf8LineStrict());
+    assertEquals("def", source.readUtf8LineStrict());
+    assertEquals("ghi\rjkl", source.readUtf8LineStrict());
+  }
+
+  @Test public void bufferedReaderCompatible() throws IOException {
+    BufferedSource source = newSource("abc\ndef");
+    assertEquals("abc", source.readUtf8Line());
+    assertEquals("def", source.readUtf8Line());
+    assertEquals(null, source.readUtf8Line());
+  }
+
+  @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException {
+    BufferedSource source = newSource("abc\ndef\n");
+    assertEquals("abc", source.readUtf8Line());
+    assertEquals("def", source.readUtf8Line());
+    assertEquals(null, source.readUtf8Line());
+  }
+}
diff --git a/okio/src/test/java/okio/RealBufferedSinkTest.java b/okio/src/test/java/okio/RealBufferedSinkTest.java
new file mode 100644
index 0000000..80a1317
--- /dev/null
+++ b/okio/src/test/java/okio/RealBufferedSinkTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import org.junit.Test;
+
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class RealBufferedSinkTest {
+  @Test public void outputStreamFromSink() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    OutputStream out = new RealBufferedSink(sink).outputStream();
+    out.write('a');
+    out.write(repeat('b', 9998).getBytes(UTF_8));
+    out.write('c');
+    out.flush();
+    assertEquals("a" + repeat('b', 9998) + "c", sink.readUtf8(10000));
+  }
+
+  @Test public void outputStreamFromSinkBounds() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    OutputStream out = new RealBufferedSink(sink).outputStream();
+    try {
+      out.write(new byte[100], 50, 51);
+      fail();
+    } catch (ArrayIndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void bufferedSinkEmitsTailWhenItIsComplete() throws IOException {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8(repeat('a', Segment.SIZE - 1));
+    assertEquals(0, sink.size());
+    bufferedSink.writeByte(0);
+    assertEquals(Segment.SIZE, sink.size());
+    assertEquals(0, bufferedSink.buffer().size());
+  }
+
+  @Test public void bufferedSinkEmitZero() throws IOException {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8("");
+    assertEquals(0, sink.size());
+  }
+
+  @Test public void bufferedSinkEmitMultipleSegments() throws IOException {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 4 - 1));
+    assertEquals(Segment.SIZE * 3, sink.size());
+    assertEquals(Segment.SIZE - 1, bufferedSink.buffer().size());
+  }
+
+  @Test public void bufferedSinkFlush() throws IOException {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeByte('a');
+    assertEquals(0, sink.size());
+    bufferedSink.flush();
+    assertEquals(0, bufferedSink.buffer().size());
+    assertEquals(1, sink.size());
+  }
+
+  @Test public void bytesEmittedToSinkWithFlush() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8("abc");
+    bufferedSink.flush();
+    assertEquals(3, sink.size());
+  }
+
+  @Test public void bytesNotEmittedToSinkWithoutFlush() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8("abc");
+    assertEquals(0, sink.size());
+  }
+
+  @Test public void completeSegmentsEmitted() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 3));
+    assertEquals(Segment.SIZE * 3, sink.size());
+  }
+
+  @Test public void incompleteSegmentsNotEmitted() throws Exception {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 3 - 1));
+    assertEquals(Segment.SIZE * 2, sink.size());
+  }
+
+  @Test public void closeEmitsBufferedBytes() throws IOException {
+    OkBuffer sink = new OkBuffer();
+    BufferedSink bufferedSink = new RealBufferedSink(sink);
+    bufferedSink.writeByte('a');
+    bufferedSink.close();
+    assertEquals('a', sink.readByte());
+  }
+
+  @Test public void closeWithExceptionWhenWriting() throws IOException {
+    MockSink mockSink = new MockSink();
+    mockSink.scheduleThrow(0, new IOException());
+    BufferedSink bufferedSink = new RealBufferedSink(mockSink);
+    bufferedSink.writeByte('a');
+    try {
+      bufferedSink.close();
+      fail();
+    } catch (IOException expected) {
+    }
+    mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+  }
+
+  @Test public void closeWithExceptionWhenClosing() throws IOException {
+    MockSink mockSink = new MockSink();
+    mockSink.scheduleThrow(1, new IOException());
+    BufferedSink bufferedSink = new RealBufferedSink(mockSink);
+    bufferedSink.writeByte('a');
+    try {
+      bufferedSink.close();
+      fail();
+    } catch (IOException expected) {
+    }
+    mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+  }
+
+  @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+    MockSink mockSink = new MockSink();
+    mockSink.scheduleThrow(0, new IOException("first"));
+    mockSink.scheduleThrow(1, new IOException("second"));
+    BufferedSink bufferedSink = new RealBufferedSink(mockSink);
+    bufferedSink.writeByte('a');
+    try {
+      bufferedSink.close();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("first", expected.getMessage());
+    }
+    mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+  }
+
+  @Test public void operationsAfterClose() throws IOException {
+    MockSink mockSink = new MockSink();
+    BufferedSink bufferedSink = new RealBufferedSink(mockSink);
+    bufferedSink.writeByte('a');
+    bufferedSink.close();
+
+    // Test a sample set of methods.
+    try {
+      bufferedSink.writeByte('a');
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSink.write(new byte[10]);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSink.emitCompleteSegments();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSink.flush();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    // Test a sample set of methods on the OutputStream.
+    OutputStream os = bufferedSink.outputStream();
+    try {
+      os.write('a');
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      os.write(new byte[10]);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    // Permitted
+    os.flush();
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java b/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java
new file mode 100644
index 0000000..8793640
--- /dev/null
+++ b/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+
+public final class RealBufferedSourceReadUtf8LineTest extends ReadUtf8LineTest {
+  /** Returns a buffered source that gets bytes of {@code data} one at a time. */
+  @Override protected BufferedSource newSource(String s) {
+    final OkBuffer buffer = new OkBuffer().writeUtf8(s);
+
+    Source slowSource = new Source() {
+      @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+        return buffer.read(sink, Math.min(1, byteCount));
+      }
+
+      @Override public Source deadline(Deadline deadline) {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override public void close() throws IOException {
+        throw new UnsupportedOperationException();
+      }
+    };
+
+    return Okio.buffer(slowSource);
+  }
+}
diff --git a/okio/src/test/java/okio/RealBufferedSourceTest.java b/okio/src/test/java/okio/RealBufferedSourceTest.java
new file mode 100644
index 0000000..a77eaf2
--- /dev/null
+++ b/okio/src/test/java/okio/RealBufferedSourceTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import org.junit.Test;
+
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class RealBufferedSourceTest {
+  @Test public void inputStreamFromSource() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("a");
+    source.writeUtf8(repeat('b', Segment.SIZE));
+    source.writeUtf8("c");
+
+    InputStream in = new RealBufferedSource(source).inputStream();
+    assertEquals(0, in.available());
+    assertEquals(Segment.SIZE + 2, source.size());
+
+    // Reading one byte buffers a full segment.
+    assertEquals('a', in.read());
+    assertEquals(Segment.SIZE - 1, in.available());
+    assertEquals(2, source.size());
+
+    // Reading as much as possible reads the rest of that buffered segment.
+    byte[] data = new byte[Segment.SIZE * 2];
+    assertEquals(Segment.SIZE - 1, in.read(data, 0, data.length));
+    assertEquals(repeat('b', Segment.SIZE - 1), new String(data, 0, Segment.SIZE - 1, UTF_8));
+    assertEquals(2, source.size());
+
+    // Continuing to read buffers the next segment.
+    assertEquals('b', in.read());
+    assertEquals(1, in.available());
+    assertEquals(0, source.size());
+
+    // Continuing to read reads from the buffer.
+    assertEquals('c', in.read());
+    assertEquals(0, in.available());
+    assertEquals(0, source.size());
+
+    // Once we've exhausted the source, we're done.
+    assertEquals(-1, in.read());
+    assertEquals(0, source.size());
+  }
+
+  @Test public void inputStreamFromSourceBounds() throws IOException {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', 100));
+    InputStream in = new RealBufferedSource(source).inputStream();
+    try {
+      in.read(new byte[100], 50, 51);
+      fail();
+    } catch (ArrayIndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void requireTracksBufferFirst() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("bb");
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    bufferedSource.buffer().writeUtf8("aa");
+
+    bufferedSource.require(2);
+    assertEquals(2, bufferedSource.buffer().size());
+    assertEquals(2, source.size());
+  }
+
+  @Test public void requireIncludesBufferBytes() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("b");
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    bufferedSource.buffer().writeUtf8("a");
+
+    bufferedSource.require(2);
+    assertEquals("ab", bufferedSource.buffer().readUtf8(2));
+  }
+
+  @Test public void requireInsufficientData() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("a");
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+
+    try {
+      bufferedSource.require(2);
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test public void requireReadsOneSegmentAtATime() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE));
+    source.writeUtf8(repeat('b', Segment.SIZE));
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+
+    bufferedSource.require(2);
+    assertEquals(Segment.SIZE, source.size());
+    assertEquals(Segment.SIZE, bufferedSource.buffer().size());
+  }
+
+  @Test public void skipInsufficientData() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("a");
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    try {
+      bufferedSource.skip(2);
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test public void skipReadsOneSegmentAtATime() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8(repeat('a', Segment.SIZE));
+    source.writeUtf8(repeat('b', Segment.SIZE));
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    bufferedSource.skip(2);
+    assertEquals(Segment.SIZE, source.size());
+    assertEquals(Segment.SIZE - 2, bufferedSource.buffer().size());
+  }
+
+  @Test public void skipTracksBufferFirst() throws Exception {
+    OkBuffer source = new OkBuffer();
+    source.writeUtf8("bb");
+
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    bufferedSource.buffer().writeUtf8("aa");
+
+    bufferedSource.skip(2);
+    assertEquals(0, bufferedSource.buffer().size());
+    assertEquals(2, source.size());
+  }
+
+  @Test public void operationsAfterClose() throws IOException {
+    OkBuffer source = new OkBuffer();
+    BufferedSource bufferedSource = new RealBufferedSource(source);
+    bufferedSource.close();
+
+    // Test a sample set of methods.
+    try {
+      bufferedSource.indexOf((byte) 1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSource.skip(1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSource.readByte();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      bufferedSource.readByteString(10);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+
+    // Test a sample set of methods on the InputStream.
+    InputStream is = bufferedSource.inputStream();
+    try {
+      is.read();
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      is.read(new byte[10]);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  private String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+}
diff --git a/parent.iws b/parent.iws
deleted file mode 100644
index 54653fd..0000000
--- a/parent.iws
+++ /dev/null
@@ -1,746 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="CCaseConfig">
-    <option name="checkoutReserved" value="false" />
-    <option name="markExternalChangeAsUpToDate" value="true" />
-    <option name="checkInUseHijack" value="true" />
-    <option name="useUcmModel" value="true" />
-    <option name="synchOutside" value="false" />
-    <option name="isHistoryResticted" value="true" />
-    <option name="useIdenticalSwitch" value="true" />
-    <option name="synchActivitiesOnRefresh" value="true" />
-    <option name="lastScr" value="" />
-    <option name="scrTextFileName" value="" />
-    <option name="historyRevisionsNumber" value="4" />
-  </component>
-  <component name="ChangeListManager">
-    <list default="true" id="94459e15-aabd-4fe7-abb5-9cc51fb49e33" name="Default" comment="">
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/.gitignore" afterPath="$PROJECT_DIR$/.gitignore" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java" afterPath="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java" />
-    </list>
-    <ignored path="parent.iws" />
-    <ignored path=".idea/workspace.xml" />
-    <option name="TRACKING_ENABLED" value="true" />
-    <option name="SHOW_DIALOG" value="false" />
-    <option name="HIGHLIGHT_CONFLICTS" value="true" />
-    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
-    <option name="LAST_RESOLUTION" value="IGNORE" />
-  </component>
-  <component name="ChangesViewManager" flattened_view="true" show_ignored="false" />
-  <component name="CreatePatchCommitExecutor">
-    <option name="PATCH_PATH" value="" />
-    <option name="REVERSE_PATCH" value="false" />
-  </component>
-  <component name="DaemonCodeAnalyzer">
-    <disable_hints />
-  </component>
-  <component name="DebuggerManager">
-    <breakpoint_any>
-      <breakpoint>
-        <option name="NOTIFY_CAUGHT" value="true" />
-        <option name="NOTIFY_UNCAUGHT" value="true" />
-        <option name="ENABLED" value="false" />
-        <option name="LOG_ENABLED" value="false" />
-        <option name="LOG_EXPRESSION_ENABLED" value="false" />
-        <option name="SUSPEND_POLICY" value="SuspendAll" />
-        <option name="COUNT_FILTER_ENABLED" value="false" />
-        <option name="COUNT_FILTER" value="0" />
-        <option name="CONDITION_ENABLED" value="false" />
-        <option name="CLASS_FILTERS_ENABLED" value="false" />
-        <option name="INSTANCE_FILTERS_ENABLED" value="false" />
-        <option name="CONDITION" value="" />
-        <option name="LOG_MESSAGE" value="" />
-      </breakpoint>
-      <breakpoint>
-        <option name="NOTIFY_CAUGHT" value="true" />
-        <option name="NOTIFY_UNCAUGHT" value="true" />
-        <option name="ENABLED" value="false" />
-        <option name="LOG_ENABLED" value="false" />
-        <option name="LOG_EXPRESSION_ENABLED" value="false" />
-        <option name="SUSPEND_POLICY" value="SuspendAll" />
-        <option name="COUNT_FILTER_ENABLED" value="false" />
-        <option name="COUNT_FILTER" value="0" />
-        <option name="CONDITION_ENABLED" value="false" />
-        <option name="CLASS_FILTERS_ENABLED" value="false" />
-        <option name="INSTANCE_FILTERS_ENABLED" value="false" />
-        <option name="CONDITION" value="" />
-        <option name="LOG_MESSAGE" value="" />
-      </breakpoint>
-    </breakpoint_any>
-    <breakpoint_rules />
-    <ui_properties />
-  </component>
-  <component name="FavoritesManager">
-    <favorites_list name="parent" />
-  </component>
-  <component name="FileEditorManager">
-    <leaf>
-      <file leaf-file-name="Util.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="332" column="28" selection-start="10444" selection-end="10444" vertical-scroll-proportion="-21.076923">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="Address.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/Address.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="37" column="107" selection-start="1441" selection-end="1441" vertical-scroll-proportion="0.0">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="Collections.class" pinned="false" current="false" current-in-tab="false">
-        <entry file="jar://$PROJECT_DIR$/../../../sdk/platforms/android-16/android.jar!/java/util/Collections.class">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="108" column="40" selection-start="6176" selection-end="6176" vertical-scroll-proportion="-29.48">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="OkHttpClient.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="244" column="30" selection-start="8360" selection-end="8360" vertical-scroll-proportion="-19.038462">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="HttpURLConnectionImpl.java" pinned="false" current="true" current-in-tab="true">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="614" column="75" selection-start="21903" selection-end="21903" vertical-scroll-proportion="0.81121284">
-              <folding>
-                <element signature="imports" expanded="true" />
-              </folding>
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="Transport.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="42" column="72" selection-start="1758" selection-end="1758" vertical-scroll-proportion="0.0">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="ResponseHeaders.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="462" column="81" selection-start="15285" selection-end="15285" vertical-scroll-proportion="-4.923077">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="HttpEngine.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="651" column="106" selection-start="22531" selection-end="22531" vertical-scroll-proportion="-21.653847">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="SpdyTransport.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="77" column="19" selection-start="3014" selection-end="3014" vertical-scroll-proportion="0.0">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="HttpTransport.java" pinned="false" current="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java">
-          <provider selected="true" editor-type-id="text-editor">
-            <state line="141" column="94" selection-start="5240" selection-end="5240" vertical-scroll-proportion="0.0">
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-    </leaf>
-  </component>
-  <component name="FindManager">
-    <FindUsagesManager>
-      <setting name="OPEN_NEW_TAB" value="false" />
-    </FindUsagesManager>
-  </component>
-  <component name="Git.Settings">
-    <option name="CHECKOUT_INCLUDE_TAGS" value="false" />
-    <option name="UPDATE_CHANGES_POLICY" value="STASH" />
-    <option name="LINE_SEPARATORS_CONVERSION" value="ASK" />
-  </component>
-  <component name="IdeDocumentHistory">
-    <option name="changedFiles">
-      <list>
-        <option value="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java" />
-        <option value="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java" />
-        <option value="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java" />
-        <option value="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java" />
-        <option value="$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java" />
-      </list>
-    </option>
-  </component>
-  <component name="MavenImportPreferences">
-    <option name="importingSettings">
-      <MavenImportingSettings>
-        <option name="keepSourceFolders" value="false" />
-      </MavenImportingSettings>
-    </option>
-  </component>
-  <component name="ModuleEditorState">
-    <option name="LAST_EDITED_MODULE_NAME" />
-    <option name="LAST_EDITED_TAB_NAME" />
-  </component>
-  <component name="ProjectLevelVcsManager" settingsEditedManually="false">
-    <OptionsSetting value="true" id="Add" />
-    <OptionsSetting value="true" id="Remove" />
-    <OptionsSetting value="true" id="Checkout" />
-    <OptionsSetting value="true" id="Update" />
-    <OptionsSetting value="true" id="Status" />
-    <OptionsSetting value="true" id="Edit" />
-    <OptionsSetting value="true" id="Undo Check Out" />
-    <OptionsSetting value="true" id="Get Latest Version" />
-    <ConfirmationsSetting value="0" id="Add" />
-    <ConfirmationsSetting value="0" id="Remove" />
-  </component>
-  <component name="ProjectReloadState">
-    <option name="STATE" value="0" />
-  </component>
-  <component name="ProjectView">
-    <navigator currentView="ProjectPane" proportions="" version="1" splitterProportion="0.5">
-      <flattenPackages />
-      <showMembers />
-      <showModules />
-      <showLibraryContents />
-      <hideEmptyPackages />
-      <abbreviatePackageNames />
-      <autoscrollToSource />
-      <autoscrollFromSource />
-      <sortByType />
-    </navigator>
-    <panes>
-      <pane id="Scope" />
-      <pane id="PackagesPane" />
-      <pane id="Favorites" />
-      <pane id="ProjectPane">
-        <subPane>
-          <PATH>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="parent" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
-            </PATH_ELEMENT>
-          </PATH>
-          <PATH>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="parent" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-          </PATH>
-          <PATH>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="parent" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-          </PATH>
-          <PATH>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="parent" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="src" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-          </PATH>
-          <PATH>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="parent" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="src" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="main" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="java" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-            <PATH_ELEMENT>
-              <option name="myItemId" value="okhttp" />
-              <option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
-            </PATH_ELEMENT>
-          </PATH>
-        </subPane>
-      </pane>
-    </panes>
-  </component>
-  <component name="PropertiesComponent">
-    <property name="GoToFile.includeJavaFiles" value="false" />
-    <property name="project.structure.last.edited" value="Libraries" />
-    <property name="project.structure.proportion" value="0.15" />
-    <property name="options.splitter.main.proportions" value="0.3" />
-    <property name="MemberChooser.sorted" value="false" />
-    <property name="options.lastSelected" value="preferences.sourceCode" />
-    <property name="project.structure.side.proportion" value="0.34837964" />
-    <property name="MemberChooser.copyJavadoc" value="false" />
-    <property name="GoToClass.toSaveIncludeLibraries" value="false" />
-    <property name="WebServerToolWindowFactoryState" value="false" />
-    <property name="MemberChooser.showClasses" value="true" />
-    <property name="GoToClass.includeLibraries" value="false" />
-    <property name="options.searchVisible" value="true" />
-    <property name="options.splitter.details.proportions" value="0.2" />
-  </component>
-  <component name="RunManager">
-    <configuration default="true" type="Remote" factoryName="Remote">
-      <option name="USE_SOCKET_TRANSPORT" value="true" />
-      <option name="SERVER_MODE" value="false" />
-      <option name="SHMEM_ADDRESS" value="javadebug" />
-      <option name="HOST" value="localhost" />
-      <option name="PORT" value="5005" />
-      <method>
-        <option name="AntTarget" enabled="false" />
-        <option name="BuildArtifacts" enabled="false" />
-        <option name="Maven.BeforeRunTask" enabled="false" />
-        <option name="PhingTarget" enabled="false" />
-      </method>
-    </configuration>
-    <configuration default="true" type="Applet" factoryName="Applet">
-      <module name="" />
-      <option name="MAIN_CLASS_NAME" />
-      <option name="HTML_FILE_NAME" />
-      <option name="HTML_USED" value="false" />
-      <option name="WIDTH" value="400" />
-      <option name="HEIGHT" value="300" />
-      <option name="POLICY_FILE" value="$APPLICATION_HOME_DIR$/bin/appletviewer.policy" />
-      <option name="VM_PARAMETERS" />
-      <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
-      <option name="ALTERNATIVE_JRE_PATH" />
-      <method>
-        <option name="AntTarget" enabled="false" />
-        <option name="BuildArtifacts" enabled="false" />
-        <option name="Make" enabled="true" />
-        <option name="Maven.BeforeRunTask" enabled="false" />
-        <option name="PhingTarget" enabled="false" />
-      </method>
-    </configuration>
-    <configuration default="true" type="Application" factoryName="Application">
-      <extension name="coverage" enabled="false" merge="false" />
-      <option name="MAIN_CLASS_NAME" />
-      <option name="VM_PARAMETERS" />
-      <option name="PROGRAM_PARAMETERS" />
-      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
-      <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
-      <option name="ALTERNATIVE_JRE_PATH" />
-      <option name="ENABLE_SWING_INSPECTOR" value="false" />
-      <option name="ENV_VARIABLES" />
-      <option name="PASS_PARENT_ENVS" value="true" />
-      <module name="" />
-      <envs />
-      <method>
-        <option name="AntTarget" enabled="false" />
-        <option name="BuildArtifacts" enabled="false" />
-        <option name="Make" enabled="true" />
-        <option name="Maven.BeforeRunTask" enabled="false" />
-        <option name="PhingTarget" enabled="false" />
-      </method>
-    </configuration>
-    <configuration default="true" type="JUnit" factoryName="JUnit">
-      <extension name="coverage" enabled="false" merge="false" />
-      <module name="" />
-      <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
-      <option name="ALTERNATIVE_JRE_PATH" />
-      <option name="PACKAGE_NAME" />
-      <option name="MAIN_CLASS_NAME" />
-      <option name="METHOD_NAME" />
-      <option name="TEST_OBJECT" value="class" />
-      <option name="VM_PARAMETERS" />
-      <option name="PARAMETERS" />
-      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
-      <option name="ENV_VARIABLES" />
-      <option name="PASS_PARENT_ENVS" value="true" />
-      <option name="TEST_SEARCH_SCOPE">
-        <value defaultName="moduleWithDependencies" />
-      </option>
-      <envs />
-      <patterns />
-      <method>
-        <option name="AntTarget" enabled="false" />
-        <option name="BuildArtifacts" enabled="false" />
-        <option name="Make" enabled="true" />
-        <option name="Maven.BeforeRunTask" enabled="false" />
-        <option name="PhingTarget" enabled="false" />
-      </method>
-    </configuration>
-    <list size="0" />
-    <configuration name="&lt;template&gt;" type="WebApp" default="true" selected="false">
-      <Host>localhost</Host>
-      <Port>5050</Port>
-    </configuration>
-  </component>
-  <component name="ShelveChangesManager" show_recycled="false" />
-  <component name="SvnConfiguration" maxAnnotateRevisions="500">
-    <option name="USER" value="" />
-    <option name="PASSWORD" value="" />
-    <option name="mySSHConnectionTimeout" value="30000" />
-    <option name="mySSHReadTimeout" value="30000" />
-    <option name="LAST_MERGED_REVISION" />
-    <option name="MERGE_DRY_RUN" value="false" />
-    <option name="MERGE_DIFF_USE_ANCESTRY" value="true" />
-    <option name="UPDATE_LOCK_ON_DEMAND" value="false" />
-    <option name="IGNORE_SPACES_IN_MERGE" value="false" />
-    <option name="DETECT_NESTED_COPIES" value="true" />
-    <option name="CHECK_NESTED_FOR_QUICK_MERGE" value="false" />
-    <option name="IGNORE_SPACES_IN_ANNOTATE" value="true" />
-    <option name="SHOW_MERGE_SOURCES_IN_ANNOTATE" value="true" />
-    <option name="FORCE_UPDATE" value="false" />
-    <configuration useDefault="true">$USER_HOME$/.subversion_IDEA</configuration>
-    <myIsUseDefaultProxy>false</myIsUseDefaultProxy>
-  </component>
-  <component name="TaskManager">
-    <task active="true" id="Default" summary="Default task">
-      <created>1370025380149</created>
-      <updated>1370025380149</updated>
-    </task>
-    <servers />
-  </component>
-  <component name="ToolWindowManager">
-    <frame x="641" y="44" width="1352" height="1503" extended-state="1" />
-    <editor active="false" />
-    <layout>
-      <window_info id="Changes" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Phing Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Palette" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" weight="0.32970226" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
-      <window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
-      <window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
-      <window_info id="IDEtalk Messages" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="IDEtalk" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
-      <window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="1" side_tool="true" content_ui="tabs" />
-      <window_info id="Maven Projects" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
-      <window_info id="Dependency Viewer" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="-1" side_tool="false" content_ui="tabs" />
-      <window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" weight="0.25" sideWeight="0.67029774" order="0" side_tool="false" content_ui="tabs" />
-      <window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
-      <window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
-      <window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
-      <window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
-      <window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
-    </layout>
-  </component>
-  <component name="VcsManagerConfiguration">
-    <option name="OFFER_MOVE_TO_ANOTHER_CHANGELIST_ON_PARTIAL_COMMIT" value="true" />
-    <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="true" />
-    <option name="CHECK_NEW_TODO" value="true" />
-    <option name="myTodoPanelSettings">
-      <value>
-        <are-packages-shown value="false" />
-        <are-modules-shown value="false" />
-        <flatten-packages value="false" />
-        <is-autoscroll-to-source value="false" />
-      </value>
-    </option>
-    <option name="PERFORM_UPDATE_IN_BACKGROUND" value="true" />
-    <option name="PERFORM_COMMIT_IN_BACKGROUND" value="true" />
-    <option name="PERFORM_EDIT_IN_BACKGROUND" value="true" />
-    <option name="PERFORM_CHECKOUT_IN_BACKGROUND" value="true" />
-    <option name="PERFORM_ADD_REMOVE_IN_BACKGROUND" value="true" />
-    <option name="PERFORM_ROLLBACK_IN_BACKGROUND" value="false" />
-    <option name="CHECK_LOCALLY_CHANGED_CONFLICTS_IN_BACKGROUND" value="false" />
-    <option name="ENABLE_BACKGROUND_PROCESSES" value="false" />
-    <option name="CHANGED_ON_SERVER_INTERVAL" value="60" />
-    <option name="SHOW_ONLY_CHANGED_IN_SELECTION_DIFF" value="true" />
-    <option name="CHECK_COMMIT_MESSAGE_SPELLING" value="true" />
-    <option name="DEFAULT_PATCH_EXTENSION" value="patch" />
-    <option name="FORCE_NON_EMPTY_COMMENT" value="false" />
-    <option name="LAST_COMMIT_MESSAGE" />
-    <option name="MAKE_NEW_CHANGELIST_ACTIVE" value="true" />
-    <option name="OPTIMIZE_IMPORTS_BEFORE_PROJECT_COMMIT" value="false" />
-    <option name="CHECK_FILES_UP_TO_DATE_BEFORE_COMMIT" value="false" />
-    <option name="REFORMAT_BEFORE_PROJECT_COMMIT" value="false" />
-    <option name="REFORMAT_BEFORE_FILE_COMMIT" value="false" />
-    <option name="FILE_HISTORY_DIALOG_COMMENTS_SPLITTER_PROPORTION" value="0.8" />
-    <option name="FILE_HISTORY_DIALOG_SPLITTER_PROPORTION" value="0.5" />
-    <option name="ACTIVE_VCS_NAME" />
-    <option name="UPDATE_GROUP_BY_PACKAGES" value="false" />
-    <option name="UPDATE_GROUP_BY_CHANGELIST" value="false" />
-    <option name="SHOW_FILE_HISTORY_AS_TREE" value="false" />
-    <option name="FILE_HISTORY_SPLITTER_PROPORTION" value="0.6" />
-  </component>
-  <component name="VssConfiguration">
-    <option name="CLIENT_PATH" value="" />
-    <option name="SRCSAFEINI_PATH" value="" />
-    <option name="USER_NAME" value="" />
-    <option name="PWD" value="" />
-    <CheckoutOptions>
-      <option name="COMMENT" value="" />
-      <option name="DO_NOT_GET_LATEST_VERSION" value="false" />
-      <option name="REPLACE_WRITABLE" value="false" />
-      <option name="RECURSIVE" value="false" />
-    </CheckoutOptions>
-    <CheckinOptions>
-      <option name="COMMENT" value="" />
-      <option name="KEEP_CHECKED_OUT" value="false" />
-      <option name="RECURSIVE" value="false" />
-    </CheckinOptions>
-    <AddOptions>
-      <option name="STORE_ONLY_LATEST_VERSION" value="false" />
-      <option name="CHECK_OUT_IMMEDIATELY" value="false" />
-    </AddOptions>
-    <UndocheckoutOptions>
-      <option name="MAKE_WRITABLE" value="false" />
-      <option name="REPLACE_LOCAL_COPY" value="2" />
-      <option name="RECURSIVE" value="false" />
-    </UndocheckoutOptions>
-    <GetOptions>
-      <option name="REPLACE_WRITABLE" value="0" />
-      <option name="MAKE_WRITABLE" value="false" />
-      <option name="ANSWER_NEGATIVELY" value="false" />
-      <option name="ANSWER_POSITIVELY" value="false" />
-      <option name="RECURSIVE" value="false" />
-      <option name="VERSION" />
-    </GetOptions>
-  </component>
-  <component name="XDebuggerManager">
-    <breakpoint-manager />
-  </component>
-  <component name="antWorkspaceConfiguration">
-    <option name="IS_AUTOSCROLL_TO_SOURCE" value="false" />
-    <option name="FILTER_TARGETS" value="false" />
-  </component>
-  <component name="editorHistoryManager">
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="79" column="18" selection-start="2756" selection-end="2756" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="42" column="61" selection-start="1690" selection-end="1690" vertical-scroll-proportion="-8.038462">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/Connection.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="203" column="57" selection-start="7474" selection-end="7474" vertical-scroll-proportion="-17.76923">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="399" column="14" selection-start="12625" selection-end="12627" vertical-scroll-proportion="157.88">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="414" column="55" selection-start="13008" selection-end="13008" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="301" column="42" selection-start="12286" selection-end="12286" vertical-scroll-proportion="-14.730769">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="42" column="72" selection-start="1758" selection-end="1758" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="77" column="19" selection-start="3014" selection-end="3014" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="jar://$PROJECT_DIR$/../../../sdk/platforms/android-16/android.jar!/java/util/Collections.class">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="108" column="40" selection-start="6176" selection-end="6176" vertical-scroll-proportion="-29.48">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/Address.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="37" column="107" selection-start="1441" selection-end="1441" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="244" column="30" selection-start="8360" selection-end="8360" vertical-scroll-proportion="-19.038462">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="332" column="28" selection-start="10444" selection-end="10444" vertical-scroll-proportion="-21.076923">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="141" column="94" selection-start="5240" selection-end="5240" vertical-scroll-proportion="0.0">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="462" column="81" selection-start="15285" selection-end="15285" vertical-scroll-proportion="-4.923077">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="651" column="106" selection-start="22531" selection-end="22531" vertical-scroll-proportion="-21.653847">
-          <folding />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java">
-      <provider selected="true" editor-type-id="text-editor">
-        <state line="614" column="75" selection-start="21903" selection-end="21903" vertical-scroll-proportion="0.81121284">
-          <folding>
-            <element signature="imports" expanded="true" />
-          </folding>
-        </state>
-      </provider>
-    </entry>
-  </component>
-  <component name="masterDetails">
-    <states>
-      <state key="ArtifactsStructureConfigurable.UI">
-        <settings>
-          <artifact-editor />
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.2" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-      <state key="FacetStructureConfigurable.UI">
-        <settings>
-          <last-edited>Android</last-edited>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.2" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-      <state key="GlobalLibrariesConfigurable.UI">
-        <settings>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.2" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-      <state key="JdkListConfigurable.UI">
-        <settings>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.2" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-      <state key="ModuleStructureConfigurable.UI">
-        <settings>
-          <last-edited>okhttp</last-edited>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.34837964" />
-                <option value="0.5" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-      <state key="ProjectLibrariesConfigurable.UI">
-        <settings>
-          <last-edited>Unnamed</last-edited>
-          <splitter-proportions>
-            <option name="proportions">
-              <list>
-                <option value="0.34837964" />
-              </list>
-            </option>
-          </splitter-proportions>
-        </settings>
-      </state>
-    </states>
-  </component>
-</project>
-
diff --git a/pom.xml b/pom.xml
index a24a0ef..c534a1d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
 
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>parent</artifactId>
-  <version>1.2.2-SNAPSHOT</version>
+  <version>2.0.0-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>OkHttp (Parent)</name>
@@ -21,9 +21,12 @@
   <modules>
     <module>okhttp</module>
     <module>okhttp-apache</module>
-    <module>okhttp-protocols</module>
+    <module>okhttp-tests</module>
+    <module>okcurl</module>
+    <module>okio</module>
     <module>mockwebserver</module>
     <module>samples</module>
+    <module>benchmarks</module>
   </modules>
 
   <properties>
@@ -35,9 +38,11 @@
     <bouncycastle.version>1.48</bouncycastle.version>
     <gson.version>2.2.3</gson.version>
     <apache.http.version>4.2.2</apache.http.version>
+    <airlift.version>0.6</airlift.version>
+    <guava.version>16.0</guava.version>
 
     <!-- Test Dependencies -->
-    <junit.version>4.10</junit.version>
+    <junit.version>4.11</junit.version>
   </properties>
 
   <scm>
@@ -86,6 +91,16 @@
         <artifactId>httpclient</artifactId>
         <version>${apache.http.version}</version>
       </dependency>
+      <dependency>
+        <groupId>io.airlift</groupId>
+        <artifactId>airline</artifactId>
+        <version>${airlift.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>${guava.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -105,10 +120,17 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
-          <version>2.9</version>
+          <version>2.16</version>
           <configuration>
             <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
           </configuration>
+          <dependencies>
+            <dependency>
+              <groupId>org.apache.maven.surefire</groupId>
+              <artifactId>surefire-junit47</artifactId>
+              <version>2.16</version>
+            </dependency>
+          </dependencies>
         </plugin>
 
         <plugin>
@@ -116,21 +138,28 @@
           <artifactId>maven-javadoc-plugin</artifactId>
           <version>2.9</version>
         </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-release-plugin</artifactId>
-          <version>2.3.2</version>
-          <configuration>
-            <autoVersionSubmodules>true</autoVersionSubmodules>
-          </configuration>
-        </plugin>
       </plugins>
     </pluginManagement>
 
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-release-plugin</artifactId>
+        <version>2.4.2</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.apache.maven.scm</groupId>
+            <artifactId>maven-scm-provider-gitexe</artifactId>
+            <version>1.9</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <autoVersionSubmodules>true</autoVersionSubmodules>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
         <version>2.10</version>
         <configuration>
@@ -150,7 +179,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>animal-sniffer-maven-plugin</artifactId>
-        <version>1.9</version>
+        <version>1.10</version>
         <executions>
           <execution>
             <phase>test</phase>
@@ -162,8 +191,8 @@
         <configuration>
           <signature>
             <groupId>org.codehaus.mojo.signature</groupId>
-            <artifactId>java15</artifactId>
-            <version>1.0</version>
+            <artifactId>java16</artifactId>
+            <version>1.1</version>
           </signature>
         </configuration>
       </plugin>
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
new file mode 100644
index 0000000..0015f7c
--- /dev/null
+++ b/samples/crawler/pom.xml
@@ -0,0 +1,27 @@
+<?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.sample</groupId>
+    <artifactId>sample-parent</artifactId>
+    <version>2.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>crawler</artifactId>
+  <name>Sample: Crawler</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+      <version>1.7.3</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
new file mode 100644
index 0000000..d80c13f
--- /dev/null
+++ b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.sample;
+
+import com.squareup.okhttp.HttpResponseCache;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+/**
+ * Fetches HTML from a requested URL, follows the links, and repeats.
+ */
+public final class Crawler {
+  public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private final OkHttpClient client;
+  private final Set<URL> fetchedUrls = Collections.synchronizedSet(new LinkedHashSet<URL>());
+  private final LinkedBlockingQueue<URL> queue = new LinkedBlockingQueue<URL>();
+
+  public Crawler(OkHttpClient client) {
+    this.client = client;
+  }
+
+  private void parallelDrainQueue(int threadCount) {
+    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+    for (int i = 0; i < threadCount; i++) {
+      executor.execute(new Runnable() {
+        @Override public void run() {
+          try {
+            drainQueue();
+          } catch (Exception e) {
+            e.printStackTrace();
+          }
+        }
+      });
+    }
+    executor.shutdown();
+  }
+
+  private void drainQueue() throws Exception {
+    for (URL url; (url = queue.take()) != null; ) {
+      if (!fetchedUrls.add(url)) {
+        continue;
+      }
+
+      try {
+        fetch(url);
+      } catch (IOException e) {
+        System.out.printf("XXX: %s %s%n", url, e);
+      }
+    }
+  }
+
+  public void fetch(URL url) throws IOException {
+    HttpURLConnection connection = client.open(url);
+    String responseSource = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    String contentType = connection.getHeaderField("Content-Type");
+    int responseCode = connection.getResponseCode();
+
+    System.out.printf("%03d: %s %s%n", responseCode, url, responseSource);
+
+    if (responseCode >= 400) {
+      connection.getErrorStream().close();
+      return;
+    }
+
+    InputStream in = connection.getInputStream();
+    if (responseCode != 200 || contentType == null) {
+      in.close();
+      return;
+    }
+
+    MediaType mediaType = MediaType.parse(contentType);
+    Document document = Jsoup.parse(in, mediaType.charset(UTF_8).name(), url.toString());
+    for (Element element : document.select("a[href]")) {
+      String href = element.attr("href");
+      URL link = parseUrl(url, href);
+      if (link != null) queue.add(link);
+    }
+
+    in.close();
+  }
+
+  private URL parseUrl(URL url, String href) {
+    try {
+      URL result = new URL(url, href);
+      return result.getProtocol().equals("http") || result.getProtocol().equals("https")
+          ? result
+          : null;
+    } catch (MalformedURLException e) {
+      return null;
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    if (args.length != 2) {
+      System.out.println("Usage: Crawler <cache dir> <root>");
+      return;
+    }
+
+    int threadCount = 20;
+    long cacheByteCount = 1024L * 1024L * 100L;
+
+    OkHttpClient client = new OkHttpClient();
+    HttpResponseCache httpResponseCache = new HttpResponseCache(new File(args[0]), cacheByteCount);
+    client.setOkResponseCache(httpResponseCache);
+
+    Crawler crawler = new Crawler(client);
+    crawler.queue.add(new URL(args[1]));
+    crawler.parallelDrainQueue(threadCount);
+  }
+}
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index dc0633c..61e5875 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>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>guide</artifactId>
diff --git a/samples/pom.xml b/samples/pom.xml
index 2462fd0..62d1240 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <groupId>com.squareup.okhttp.sample</groupId>
@@ -16,6 +16,7 @@
 
   <modules>
     <module>guide</module>
+    <module>crawler</module>
     <module>simple-client</module>
     <module>static-server</module>
   </modules>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index 990152b..93657e2 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>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</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 8969f47..c6424e2 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
@@ -18,7 +18,7 @@
       new TypeToken<List<Contributor>>() {
       };
 
-  class Contributor {
+  static class Contributor {
     String login;
     int contributions;
   }
diff --git a/samples/static-server/pom.xml b/samples/static-server/pom.xml
index f98e5b5..70188c7 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>1.2.2-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>static-server</artifactId>
diff --git a/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java b/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
index 274bf9d..cb0e24e 100644
--- a/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
+++ b/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
@@ -9,6 +9,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.SecureRandom;
@@ -116,8 +117,12 @@
   private static SSLContext sslContext(String keystoreFile, String password)
       throws GeneralSecurityException, IOException {
     KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
-    keystore.load(new FileInputStream(keystoreFile), password.toCharArray());
-
+    InputStream in = new FileInputStream(keystoreFile);
+    try {
+      keystore.load(in, password.toCharArray());
+    } finally {
+      Util.closeQuietly(in);
+    }
     KeyManagerFactory keyManagerFactory =
         KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
     keyManagerFactory.init(keystore, password.toCharArray());
diff --git a/website/index.html b/website/index.html
index 34234ed..095102d 100644
--- a/website/index.html
+++ b/website/index.html
@@ -127,7 +127,7 @@
                 -->
 
             <h3 id="download">Download</h3>
-            <p><a href="http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&c=jar-with-dependencies&v=LATEST" class="dl version-href">&darr; <span class="version-tag">Latest</span> JAR</a></p>
+            <p><a href="http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST" class="dl version-href">&darr; <span class="version-tag">Latest</span> JAR</a></p>
             <p>The source code to the OkHttp, its samples, and this website is <a href="http://github.com/square/okhttp">available on GitHub</a>.</p>
 
             <h4>Maven</h4>
@@ -168,7 +168,7 @@
               </ul>
               <ul class="nav nav-pills nav-stacked secondary">
                 <li><a href="javadoc/index.html">Javadoc</a></li>
-                <li><a href="https://plus.google.com/communities/109244258569782858265/stream/b6d99838-775f-45a6-a259-af04d42d8639">Google+ Community</a></li>
+                <li><a href="http://stackoverflow.com/questions/tagged/okhttp?sort=active">StackOverflow</a></li>
               </ul>
             </div>
           </div>
@@ -210,8 +210,7 @@
         // Look up the latest version of the library.
         $.fn.artifactVersion({
           'groupId': 'com.squareup.okhttp',
-          'artifactId': 'okhttp',
-          'classifier': 'jar-with-dependencies'
+          'artifactId': 'okhttp'
         }, function(version, url) {
           $('.version').text(version);
           $('.version-tag').text('v' + version);