Upgrade to the latest OkHttp, with changes through 2013-Feb-01.

This is OkHttp commit 2137b7a72518cfac088757ef6d0fb3d085bf6e3d
Most notable changes are improvements to ConnectionPool (fixing
infinite loops!) and upgrading to SPDY/3.

Change-Id: Icd9e5e3857a45f9b68a164b920d33f48cdddb780
diff --git a/README.md b/README.md
index 9793fcf..c3a44ee 100644
--- a/README.md
+++ b/README.md
@@ -23,16 +23,33 @@
 Known Issues
 ------------
 
-The SPDY implementation is incomplete:
-
-* Settings frames are not honored. Flow control is not implemented.
-* It assumes a well-behaved peer. If the peer sends an invalid frame, OkHttp's SPDY client will not respond with the required `RST` frame.
-
 OkHttp uses the platform's [ProxySelector][2]. Prior to Android 4.0, `ProxySelector` didn't honor the `proxyHost` and `proxyPort` system properties for HTTPS connections. Work around this by specifying the `https.proxyHost` and `https.proxyPort` system properties when using a proxy with HTTPS.
 
 OkHttp's test suite creates an in-process HTTPS server. Prior to Android 2.3, SSL server sockets were broken, and so HTTPS tests will time out when run on such devices.
 
 
+Building
+--------
+
+### On the Desktop
+Run OkHttp tests on the desktop with Maven. Running SPDY tests on the desktop uses [Jetty-NPN](http://wiki.eclipse.org/Jetty/Feature/NPN) which requires OpenJDK 7+.
+```
+mvn clean test
+```
+
+### On a Device
+Test on a USB-attached Android using [Vogar](https://code.google.com/p/vogar/). Unfortunately `dx` requires that you build with Java 6, otherwise the test class will be silently omitted from the `.dex` file.
+```
+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
+```
+
+
 License
 -------
 
diff --git a/checkstyle.xml b/checkstyle.xml
index b54793c..2079edc 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -1,120 +1,132 @@
-<?xml version="1.0"?>

-<!DOCTYPE module PUBLIC

-    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"

-    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">

-

-<module name="Checker">

-    <module name="NewlineAtEndOfFile"/>

-    <module name="FileLength"/>

-    <module name="FileTabCharacter"/>

-

-    <!-- Trailing spaces -->

-    <module name="RegexpSingleline">

-        <property name="format" value="\s+$"/>

-        <property name="message" value="Line has trailing spaces."/>

-    </module>

-

-    <module name="TreeWalker">

-        <property name="cacheFile" value="${checkstyle.cache.file}"/>

-

-        <!-- Checks for Javadoc comments.                     -->

-        <!-- See http://checkstyle.sf.net/config_javadoc.html -->

-        <!--module name="JavadocMethod"/-->

-        <!--module name="JavadocType"/-->

-        <!--module name="JavadocVariable"/-->

-        <module name="JavadocStyle"/>

-

-

-        <!-- Checks for Naming Conventions.                  -->

-        <!-- See http://checkstyle.sf.net/config_naming.html -->

-        <module name="ConstantName"/>

-        <module name="LocalFinalVariableName"/>

-        <module name="LocalVariableName"/>

-        <module name="MemberName"/>

-        <module name="MethodName"/>

-        <module name="PackageName"/>

-        <module name="ParameterName"/>

-        <module name="StaticVariableName"/>

-        <module name="TypeName"/>

-

-

-        <!-- Checks for imports                              -->

-        <!-- See http://checkstyle.sf.net/config_import.html -->

-        <module name="AvoidStarImport"/>

-        <module name="IllegalImport"/> <!-- defaults to sun.* packages -->

-        <module name="RedundantImport"/>

-        <module name="UnusedImports"/>

-

-

-        <!-- Checks for Size Violations.                    -->

-        <!-- See http://checkstyle.sf.net/config_sizes.html -->

-        <module name="LineLength">

-            <property name="max" value="120"/>

-        </module>

-        <module name="MethodLength"/>

-        <module name="ParameterNumber"/>

-

-

-        <!-- Checks for whitespace                               -->

-        <!-- See http://checkstyle.sf.net/config_whitespace.html -->

-        <module name="GenericWhitespace"/>

-        <module name="EmptyForIteratorPad"/>

-        <module name="MethodParamPad"/>

-        <module name="NoWhitespaceAfter"/>

-        <module name="NoWhitespaceBefore"/>

-        <module name="OperatorWrap"/>

-        <module name="ParenPad"/>

-        <module name="TypecastParenPad"/>

-        <module name="WhitespaceAfter"/>

-        <module name="WhitespaceAround"/>

-

-

-        <!-- Modifier Checks                                    -->

-        <!-- See http://checkstyle.sf.net/config_modifiers.html -->

-        <!--module name="ModifierOrder"/-->

-        <module name="RedundantModifier"/>

-

-

-        <!-- Checks for blocks. You know, those {}'s         -->

-        <!-- See http://checkstyle.sf.net/config_blocks.html -->

-        <module name="AvoidNestedBlocks"/>

-        <!--module name="EmptyBlock"/-->

-        <module name="LeftCurly"/>

-        <!--<module name="NeedBraces"/>-->

-        <module name="RightCurly"/>

-

-

-        <!-- Checks for common coding problems               -->

-        <!-- See http://checkstyle.sf.net/config_coding.html -->

-        <!--module name="AvoidInlineConditionals"/-->

-        <module name="CovariantEquals"/>

-        <module name="DoubleCheckedLocking"/>

-        <module name="EmptyStatement"/>

-        <!--<module name="EqualsAvoidNull"/>-->

-        <module name="EqualsHashCode"/>

-        <!--module name="HiddenField"/-->

-        <module name="IllegalInstantiation"/>

-        <!--module name="InnerAssignment"/-->

-        <!--module name="MagicNumber"/-->

-        <!--module name="MissingSwitchDefault"/-->

-        <module name="RedundantThrows"/>

-        <module name="SimplifyBooleanExpression"/>

-        <module name="SimplifyBooleanReturn"/>

-

-        <!-- Checks for class design                         -->

-        <!-- See http://checkstyle.sf.net/config_design.html -->

-        <!--module name="DesignForExtension"/-->

-        <module name="FinalClass"/>

-        <module name="HideUtilityClassConstructor"/>

-        <module name="InterfaceIsType"/>

-        <!--s/module name="VisibilityModifier"/-->

-

-

-        <!-- Miscellaneous other checks.                   -->

-        <!-- See http://checkstyle.sf.net/config_misc.html -->

-        <module name="ArrayTypeStyle"/>

-        <!--module name="FinalParameters"/-->

-        <!--module name="TodoComment"/-->

-        <module name="UpperEll"/>

-    </module>

-</module>

+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+    <module name="NewlineAtEndOfFile"/>
+    <module name="FileLength"/>
+    <module name="FileTabCharacter"/>
+
+    <!-- Trailing spaces -->
+    <module name="RegexpSingleline">
+        <property name="format" value="\s+$"/>
+        <property name="message" value="Line has trailing spaces."/>
+    </module>
+
+    <!-- Space after 'for' and 'if' -->
+    <module name="RegexpSingleline">
+        <property name="format" value="^\s*(for|if)\b[^ ]"/>
+        <property name="message" value="Space needed before opening parenthesis."/>
+    </module>
+
+    <!-- For each spacing -->
+    <module name="RegexpSingleline">
+        <property name="format" value="^\s*for \(.*?([^ ]:|:[^ ])"/>
+        <property name="message" value="Space needed around ':' character."/>
+    </module>
+
+    <module name="TreeWalker">
+        <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+        <!-- Checks for Javadoc comments.                     -->
+        <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+        <!--module name="JavadocMethod"/-->
+        <!--module name="JavadocType"/-->
+        <!--module name="JavadocVariable"/-->
+        <module name="JavadocStyle"/>
+
+
+        <!-- Checks for Naming Conventions.                  -->
+        <!-- See http://checkstyle.sf.net/config_naming.html -->
+        <!--<module name="ConstantName"/>-->
+        <module name="LocalFinalVariableName"/>
+        <module name="LocalVariableName"/>
+        <module name="MemberName"/>
+        <module name="MethodName"/>
+        <module name="PackageName"/>
+        <module name="ParameterName"/>
+        <module name="StaticVariableName"/>
+        <module name="TypeName"/>
+
+
+        <!-- Checks for imports                              -->
+        <!-- See http://checkstyle.sf.net/config_import.html -->
+        <module name="AvoidStarImport"/>
+        <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+        <module name="RedundantImport"/>
+        <module name="UnusedImports"/>
+
+
+        <!-- Checks for Size Violations.                    -->
+        <!-- See http://checkstyle.sf.net/config_sizes.html -->
+        <module name="LineLength">
+            <property name="max" value="100"/>
+        </module>
+        <module name="MethodLength"/>
+        <module name="ParameterNumber"/>
+
+
+        <!-- Checks for whitespace                               -->
+        <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+        <module name="GenericWhitespace"/>
+        <!--<module name="EmptyForIteratorPad"/>-->
+        <module name="MethodParamPad"/>
+        <!--<module name="NoWhitespaceAfter"/>-->
+        <!--<module name="NoWhitespaceBefore"/>-->
+        <module name="OperatorWrap"/>
+        <module name="ParenPad"/>
+        <module name="TypecastParenPad"/>
+        <module name="WhitespaceAfter"/>
+        <module name="WhitespaceAround"/>
+
+
+        <!-- Modifier Checks                                    -->
+        <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+        <module name="ModifierOrder"/>
+        <module name="RedundantModifier"/>
+
+
+        <!-- Checks for blocks. You know, those {}'s         -->
+        <!-- See http://checkstyle.sf.net/config_blocks.html -->
+        <module name="AvoidNestedBlocks"/>
+        <!--module name="EmptyBlock"/-->
+        <module name="LeftCurly"/>
+        <!--<module name="NeedBraces"/>-->
+        <module name="RightCurly"/>
+
+
+        <!-- Checks for common coding problems               -->
+        <!-- See http://checkstyle.sf.net/config_coding.html -->
+        <!--module name="AvoidInlineConditionals"/-->
+        <module name="CovariantEquals"/>
+        <module name="DoubleCheckedLocking"/>
+        <module name="EmptyStatement"/>
+        <!--<module name="EqualsAvoidNull"/>-->
+        <module name="EqualsHashCode"/>
+        <!--module name="HiddenField"/-->
+        <module name="IllegalInstantiation"/>
+        <!--module name="InnerAssignment"/-->
+        <!--module name="MagicNumber"/-->
+        <!--module name="MissingSwitchDefault"/-->
+        <module name="RedundantThrows"/>
+        <module name="SimplifyBooleanExpression"/>
+        <module name="SimplifyBooleanReturn"/>
+
+        <!-- Checks for class design                         -->
+        <!-- See http://checkstyle.sf.net/config_design.html -->
+        <!--module name="DesignForExtension"/-->
+        <!--<module name="FinalClass"/>-->
+        <module name="HideUtilityClassConstructor"/>
+        <module name="InterfaceIsType"/>
+        <!--module name="VisibilityModifier"/-->
+
+
+        <!-- Miscellaneous other checks.                   -->
+        <!-- See http://checkstyle.sf.net/config_misc.html -->
+        <module name="ArrayTypeStyle"/>
+        <!--module name="FinalParameters"/-->
+        <!--module name="TodoComment"/-->
+        <module name="UpperEll"/>
+    </module>
+</module>
diff --git a/pom.xml b/pom.xml
index b95912d..0afcaa6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
     </parent>
     <groupId>com.squareup</groupId>
     <artifactId>okhttp</artifactId>
-    <version>0.8-SNAPSHOT</version>
+    <version>0.9-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>okhttp</name>
@@ -38,11 +38,11 @@
         <!-- Compilation -->
         <java.version>1.6</java.version>
         <npn.version>8.1.2.v20120308</npn.version>
-        <mockwebserver.version>20120905</mockwebserver.version>
+        <mockwebserver.version>20130122</mockwebserver.version>
         <bouncycastle.version>1.47</bouncycastle.version>
 
         <!-- Test Dependencies -->
-        <junit.version>3.8.2</junit.version>
+        <junit.version>4.10</junit.version>
     </properties>
 
     <scm>
@@ -68,6 +68,7 @@
             <groupId>org.mortbay.jetty.npn</groupId>
             <artifactId>npn-boot</artifactId>
             <version>${npn.version}</version>
+            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>com.google.mockwebserver</groupId>
@@ -101,70 +102,6 @@
                 </configuration>
             </plugin>
             <plugin>
-                <groupId>org.sonatype.plugins</groupId>
-                <artifactId>jarjar-maven-plugin</artifactId>
-                <version>1.5</version>
-                <executions>
-                    <execution>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>jarjar</goal>
-                        </goals>
-                        <configuration>
-                            <includes>
-                                <include>asm:asm</include>
-                                <include>org.sonatype.sisu.inject:cglib</include>
-                            </includes>
-                            <rules>
-                                <rule>
-                                    <pattern>libcore.**</pattern>
-                                    <result>com.squareup.okhttp.libcore.@1</result>
-                                </rule>
-                            </rules>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.9</version>
-                <configuration>
-                    <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
-                </configuration>
-            </plugin>
-            <plugin>
-                <!--
-                  OkHttp requires with javac >= 1.7 for syncFlush on DeflaterOutputStream.
-                  Its language version must be <= 1.6 for dx.
-
-                  Running this code on Java 6 or earlier will fail at runtime due to the missing
-                  syncFlush API.
-
-                  Dalvik's core library includes syncFlush, but with an @hide tag so that it doesn't
-                  show up in the documentation or the android.jar stubs. This code works fine on
-                  Dalvik.
-                -->
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-enforcer-plugin</artifactId>
-                <version>1.1</version>
-                <executions>
-                    <execution>
-                        <id>enforce-java</id>
-                        <goals>
-                            <goal>enforce</goal>
-                        </goals>
-                        <configuration>
-                            <rules>
-                                <requireJavaVersion>
-                                    <version>[1.7.0,)</version>
-                                </requireJavaVersion>
-                            </rules>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
                 <version>2.9.1</version>
@@ -183,6 +120,14 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.9</version>
+                <configuration>
+                    <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/src/main/java/com/squareup/okhttp/Address.java b/src/main/java/com/squareup/okhttp/Address.java
index 430eff5..cd41ac9 100644
--- a/src/main/java/com/squareup/okhttp/Address.java
+++ b/src/main/java/com/squareup/okhttp/Address.java
@@ -15,12 +15,13 @@
  */
 package com.squareup.okhttp;
 
-import static com.squareup.okhttp.internal.Util.equal;
 import java.net.Proxy;
 import java.net.UnknownHostException;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLSocketFactory;
 
+import static com.squareup.okhttp.internal.Util.equal;
+
 /**
  * A specification for a connection to an origin server. For simple connections,
  * this is the server's hostname and port. If an explicit proxy is requested (or
@@ -32,81 +33,79 @@
  * {@link Connection}.
  */
 public final class Address {
-    final Proxy proxy;
-    final String uriHost;
-    final int uriPort;
-    final SSLSocketFactory sslSocketFactory;
-    final HostnameVerifier hostnameVerifier;
+  final Proxy proxy;
+  final String uriHost;
+  final int uriPort;
+  final SSLSocketFactory sslSocketFactory;
+  final HostnameVerifier hostnameVerifier;
 
-    public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
-            HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
-        if (uriHost == null) throw new NullPointerException("uriHost == null");
-        if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
-        this.proxy = proxy;
-        this.uriHost = uriHost;
-        this.uriPort = uriPort;
-        this.sslSocketFactory = sslSocketFactory;
-        this.hostnameVerifier = hostnameVerifier;
-    }
+  public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory,
+      HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException {
+    if (uriHost == null) throw new NullPointerException("uriHost == null");
+    if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
+    this.proxy = proxy;
+    this.uriHost = uriHost;
+    this.uriPort = uriPort;
+    this.sslSocketFactory = sslSocketFactory;
+    this.hostnameVerifier = hostnameVerifier;
+  }
 
-    /**
-     * Returns the hostname of the origin server.
-     */
-    public String getUriHost() {
-        return uriHost;
-    }
+  /** Returns the hostname of the origin server. */
+  public String getUriHost() {
+    return uriHost;
+  }
 
-    /**
-     * Returns the port of the origin server; typically 80 or 443. Unlike
-     * may {@code getPort()} accessors, this method never returns -1.
-     */
-    public int getUriPort() {
-        return uriPort;
-    }
+  /**
+   * Returns the port of the origin server; typically 80 or 443. Unlike
+   * may {@code getPort()} accessors, this method never returns -1.
+   */
+  public int getUriPort() {
+    return uriPort;
+  }
 
-    /**
-     * Returns the SSL socket factory, or null if this is not an HTTPS
-     * address.
-     */
-    public SSLSocketFactory getSslSocketFactory() {
-        return sslSocketFactory;
-    }
+  /**
+   * Returns the SSL socket factory, or null if this is not an HTTPS
+   * address.
+   */
+  public SSLSocketFactory getSslSocketFactory() {
+    return sslSocketFactory;
+  }
 
-    /**
-     * Returns the hostname verifier, or null if this is not an HTTPS
-     * address.
-     */
-    public HostnameVerifier getHostnameVerifier() {
-        return hostnameVerifier;
-    }
+  /**
+   * Returns the hostname verifier, or null if this is not an HTTPS
+   * address.
+   */
+  public HostnameVerifier getHostnameVerifier() {
+    return hostnameVerifier;
+  }
 
-    /**
-     * Returns this address's explicitly-specified HTTP proxy, or null to
-     * delegate to the HTTP client's proxy selector.
-     */
-    public Proxy getProxy() {
-        return proxy;
-    }
+  /**
+   * Returns this address's explicitly-specified HTTP proxy, or null to
+   * delegate to the HTTP client's proxy selector.
+   */
+  public Proxy getProxy() {
+    return proxy;
+  }
 
-    @Override public boolean equals(Object other) {
-        if (other instanceof Address) {
-            Address that = (Address) other;
-            return equal(this.proxy, that.proxy)
-                    && this.uriHost.equals(that.uriHost)
-                    && this.uriPort == that.uriPort
-                    && equal(this.sslSocketFactory, that.sslSocketFactory)
-                    && equal(this.hostnameVerifier, that.hostnameVerifier);
-        }
-        return false;
+  @Override public boolean equals(Object other) {
+    if (other instanceof Address) {
+      Address that = (Address) other;
+      return equal(this.proxy, that.proxy)
+          && this.uriHost.equals(that.uriHost)
+          && this.uriPort == that.uriPort
+          && equal(this.sslSocketFactory, that.sslSocketFactory)
+          && equal(this.hostnameVerifier, that.hostnameVerifier);
     }
+    return false;
+  }
 
-    @Override public int hashCode() {
-        int result = 17;
-        result = 31 * result + uriHost.hashCode();
-        result = 31 * result + uriPort;
-        result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
-        result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
-        result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
-        return result;
-    }
+  @Override public int hashCode() {
+    int result = 17;
+    result = 31 * result + uriHost.hashCode();
+    result = 31 * result + uriPort;
+    result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
+    result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+    result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+    return result;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java
index 61f74d1..1679cd5 100644
--- a/src/main/java/com/squareup/okhttp/Connection.java
+++ b/src/main/java/com/squareup/okhttp/Connection.java
@@ -28,8 +28,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
 import java.net.Socket;
@@ -37,6 +35,9 @@
 import java.util.Arrays;
 import javax.net.ssl.SSLSocket;
 
+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
@@ -53,10 +54,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 transports.
  * </ul>
  * Unfortunately, older HTTPS servers refuse to connect when such options are
  * presented. Rather than avoiding these options entirely, this class allows a
@@ -64,241 +65,257 @@
  * should the attempt fail.
  */
 public final class Connection implements Closeable {
-    private static final byte[] NPN_PROTOCOLS = new byte[] {
-            6, 's', 'p', 'd', 'y', '/', '2',
-            8, 'h', 't', 't', 'p', '/', '1', '.', '1',
-    };
-    private static final byte[] SPDY2 = new byte[] {
-            's', 'p', 'd', 'y', '/', '2',
-    };
-    private static final byte[] HTTP_11 = new byte[] {
-            'h', 't', 't', 'p', '/', '1', '.', '1',
-    };
+  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 Address address;
-    private final Proxy proxy;
-    private final InetSocketAddress inetSocketAddress;
-    private final boolean modernTls;
+  private final Address address;
+  private final Proxy proxy;
+  private final InetSocketAddress inetSocketAddress;
+  private final boolean modernTls;
 
-    private Socket socket;
-    private InputStream in;
-    private OutputStream out;
-    private boolean recycled = false;
-    private SpdyConnection spdyConnection;
-    private int httpMinorVersion = 1; // Assume HTTP/1.1
+  private Socket socket;
+  private InputStream in;
+  private OutputStream out;
+  private boolean connected = false;
+  private SpdyConnection spdyConnection;
+  private int httpMinorVersion = 1; // Assume HTTP/1.1
+  private long idleStartTimeNs;
 
-    public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
-            boolean modernTls) {
-        if (address == null) throw new NullPointerException("address == null");
-        if (proxy == null) throw new NullPointerException("proxy == null");
-        if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
-        this.address = address;
-        this.proxy = proxy;
-        this.inetSocketAddress = inetSocketAddress;
-        this.modernTls = modernTls;
+  public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+      boolean modernTls) {
+    if (address == null) throw new NullPointerException("address == null");
+    if (proxy == null) throw new NullPointerException("proxy == null");
+    if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+    this.address = address;
+    this.proxy = proxy;
+    this.inetSocketAddress = inetSocketAddress;
+    this.modernTls = modernTls;
+  }
+
+  public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
+      throws IOException {
+    if (connected) {
+      throw new IllegalStateException("already connected");
+    }
+    connected = true;
+    socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket();
+    socket.connect(inetSocketAddress, connectTimeout);
+    socket.setSoTimeout(readTimeout);
+    in = socket.getInputStream();
+    out = socket.getOutputStream();
+
+    if (address.sslSocketFactory != null) {
+      upgradeToTls(tunnelRequest);
     }
 
-    public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
-            throws IOException {
-        socket = (proxy.type() != Proxy.Type.HTTP)
-                ? new Socket(proxy)
-                : new Socket();
-        socket.connect(inetSocketAddress, connectTimeout);
-        socket.setSoTimeout(readTimeout);
-        in = socket.getInputStream();
-        out = socket.getOutputStream();
+    // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
+    if (!isSpdy()) {
+      int bufferSize = 128;
+      in = new BufferedInputStream(in, bufferSize);
+    }
+  }
 
-        if (address.sslSocketFactory != null) {
-            upgradeToTls(tunnelRequest);
-        }
+  /**
+   * Create an {@code SSLSocket} and perform the TLS handshake and certificate
+   * validation.
+   */
+  private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
+    Platform platform = Platform.get();
 
-        // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
-        if (!isSpdy()) {
-            int bufferSize = 128;
-            in = new BufferedInputStream(in, bufferSize);
-        }
+    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+    if (requiresTunnel()) {
+      makeTunnel(tunnelRequest);
     }
 
-    /**
-     * Create an {@code SSLSocket} and perform the TLS handshake and certificate
-     * validation.
-     */
-    private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
-        Platform platform = Platform.get();
-
-        // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
-        if (requiresTunnel()) {
-            makeTunnel(tunnelRequest);
-        }
-
-        // Create the wrapper over connected socket.
-        socket = address.sslSocketFactory.createSocket(
-                socket, address.uriHost, address.uriPort, true /* autoClose */);
-        SSLSocket sslSocket = (SSLSocket) socket;
-        if (modernTls) {
-            platform.enableTlsExtensions(sslSocket, address.uriHost);
-        } else {
-            platform.supportTlsIntolerantServer(sslSocket);
-        }
-
-        if (modernTls) {
-            platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
-        }
-
-        // Force handshake. This can throw!
-        sslSocket.startHandshake();
-
-        // Verify that the socket's certificates are acceptable for the target host.
-        if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
-            throw new IOException("Hostname '" + address.uriHost + "' was not verified");
-        }
-
-        out = sslSocket.getOutputStream();
-        in = sslSocket.getInputStream();
-
-        byte[] selectedProtocol;
-        if (modernTls
-                && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
-            if (Arrays.equals(selectedProtocol, SPDY2)) {
-                sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
-                spdyConnection = new SpdyConnection.Builder(true, in, out).build();
-            } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
-                throw new IOException("Unexpected NPN transport "
-                        + new String(selectedProtocol, "ISO-8859-1"));
-            }
-        }
+    // Create the wrapper over connected socket.
+    socket = address.sslSocketFactory
+        .createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */);
+    SSLSocket sslSocket = (SSLSocket) socket;
+    if (modernTls) {
+      platform.enableTlsExtensions(sslSocket, address.uriHost);
+    } else {
+      platform.supportTlsIntolerantServer(sslSocket);
     }
 
-    @Override public void close() throws IOException {
-        socket.close();
+    if (modernTls) {
+      platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
     }
 
-    /**
-     * Returns the proxy that this connection is using.
-     *
-     * <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.
-     */
-    public Proxy getProxy() {
-        return proxy;
+    // Force handshake. This can throw!
+    sslSocket.startHandshake();
+
+    // Verify that the socket's certificates are acceptable for the target host.
+    if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) {
+      throw new IOException("Hostname '" + address.uriHost + "' was not verified");
     }
 
-    public Address getAddress() {
-        return address;
-    }
+    out = sslSocket.getOutputStream();
+    in = sslSocket.getInputStream();
 
-    public InetSocketAddress getSocketAddress() {
-        return inetSocketAddress;
+    byte[] selectedProtocol;
+    if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+      if (Arrays.equals(selectedProtocol, SPDY3)) {
+        sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
+        spdyConnection = new SpdyConnection.Builder(true, in, out).build();
+      } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
+        throw new IOException(
+            "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
+      }
     }
+  }
 
-    public boolean isModernTls() {
-        return modernTls;
+  /** Returns true if {@link #connect} has been attempted on this connection. */
+  public boolean isConnected() {
+    return connected;
+  }
+
+  @Override public void close() throws IOException {
+    socket.close();
+  }
+
+  /**
+   * Returns the proxy that this connection is using.
+   *
+   * <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.
+   */
+  public Proxy getProxy() {
+    return proxy;
+  }
+
+  public Address getAddress() {
+    return address;
+  }
+
+  public InetSocketAddress getSocketAddress() {
+    return inetSocketAddress;
+  }
+
+  public boolean isModernTls() {
+    return modernTls;
+  }
+
+  /**
+   * Returns the socket that this connection uses, or null if the connection
+   * is not currently connected.
+   */
+  public Socket getSocket() {
+    return socket;
+  }
+
+  /** Returns true if this connection is alive. */
+  public boolean isAlive() {
+    return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
+  }
+
+  public void resetIdleStartTime() {
+    if (spdyConnection != null) {
+      throw new IllegalStateException("spdyConnection != null");
     }
+    this.idleStartTimeNs = System.nanoTime();
+  }
 
-    /**
-     * Returns the socket that this connection uses, or null if the connection
-     * is not currently connected.
-     */
-    public Socket getSocket() {
-        return socket;
+  /** Returns true if this connection is idle. */
+  public boolean isIdle() {
+    return spdyConnection == null || spdyConnection.isIdle();
+  }
+
+  /**
+   * Returns true if this connection has been idle for longer than
+   * {@code keepAliveDurationNs}.
+   */
+  public boolean isExpired(long keepAliveDurationNs) {
+    return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
+  }
+
+  /**
+   * Returns the time in ns when this connection became idle. Undefined if
+   * this connection is not idle.
+   */
+  public long getIdleStartTimeNs() {
+    return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
+  }
+
+  /** 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);
+  }
+
+  /**
+   * Returns true if this is a SPDY connection. Such connections can be used
+   * in multiple HTTP requests simultaneously.
+   */
+  public boolean isSpdy() {
+    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
+   * value is 1 for new connections.
+   */
+  public int getHttpMinorVersion() {
+    return httpMinorVersion;
+  }
+
+  public void setHttpMinorVersion(int httpMinorVersion) {
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  /**
+   * Returns true if the HTTP connection needs to tunnel one protocol over
+   * another, such as when using HTTPS through an HTTP proxy. When doing so,
+   * we must avoid buffering bytes intended for the higher-level protocol.
+   */
+  public boolean requiresTunnel() {
+    return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP;
+  }
+
+  /**
+   * 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();
+    while (true) {
+      out.write(requestHeaders.toBytes());
+      RawHeaders responseHeaders = RawHeaders.fromBytes(in);
+
+      switch (responseHeaders.getResponseCode()) {
+        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(HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
+                  proxy, url);
+          if (credentialsFound) {
+            continue;
+          } else {
+            throw new IOException("Failed to authenticate with proxy");
+          }
+        default:
+          throw new IOException(
+              "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
+      }
     }
-
-    /**
-     * Returns true if this connection has been used to satisfy an earlier
-     * HTTP request/response pair.
-     *
-     * <p>The HTTP client treats recycled and non-recycled connections
-     * differently. I/O failures on recycled connections are often temporary:
-     * the remote peer may have closed the socket because it was idle for an
-     * extended period of time. When fresh connections suffer similar failures
-     * the problem is fatal and the request is not retried.
-     */
-    public boolean isRecycled() {
-        return recycled;
-    }
-
-    public void setRecycled() {
-        this.recycled = true;
-    }
-
-    /**
-     * Returns true if this connection is eligible to be reused for another
-     * request/response pair.
-     */
-    protected boolean isEligibleForRecycling() {
-        return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
-    }
-
-    /**
-     * 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);
-    }
-
-    /**
-     * Returns true if this is a SPDY connection. Such connections can be used
-     * in multiple HTTP requests simultaneously.
-     */
-    public boolean isSpdy() {
-        return spdyConnection != null;
-    }
-
-    /**
-     * 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
-     * value is 1 for new connections.
-     */
-    public int getHttpMinorVersion() {
-        return httpMinorVersion;
-    }
-
-    public void setHttpMinorVersion(int httpMinorVersion) {
-        this.httpMinorVersion = httpMinorVersion;
-    }
-
-    /**
-     * Returns true if the HTTP connection needs to tunnel one protocol over
-     * another, such as when using HTTPS through an HTTP proxy. When doing so,
-     * we must avoid buffering bytes intended for the higher-level protocol.
-     */
-    public boolean requiresTunnel() {
-        return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP;
-    }
-
-    /**
-     * 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();
-        while (true) {
-            out.write(requestHeaders.toBytes());
-            RawHeaders responseHeaders = RawHeaders.fromBytes(in);
-
-            switch (responseHeaders.getResponseCode()) {
-            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(HTTP_PROXY_AUTH,
-                        responseHeaders, requestHeaders, proxy, url);
-                if (credentialsFound) {
-                    continue;
-                } else {
-                    throw new IOException("Failed to authenticate with proxy");
-                }
-            default:
-                throw new IOException("Unexpected response code for CONNECT: "
-                        + responseHeaders.getResponseCode());
-            }
-        }
-    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/ConnectionPool.java b/src/main/java/com/squareup/okhttp/ConnectionPool.java
index afb0e58..be4eb1d 100644
--- a/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -1,41 +1,34 @@
-/*
- *  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 com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.Util;
 import java.net.SocketException;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
- * requests that share the same {@link Address} may share a {@link Connection}.
- * This class implements the policy of which connections to keep open for future
- * use.
+ * requests that share the same {@link com.squareup.okhttp.Address} may share a
+ * {@link com.squareup.okhttp.Connection}. This class implements the policy of
+ * which connections to keep open for future use.
  *
  * <p>The {@link #getDefault() system-wide default} uses system properties for
  * tuning parameters:
  * <ul>
- *   <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
- *       pooled at all. Default is true.
- *   <li>{@code http.maxConnections} maximum number of connections to each
- *       address. Default is 5.
+ *     <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
+ *         pooled at all. Default is true.
+ *     <li>{@code http.maxConnections} maximum number of idle connections to
+ *         each to keep in the pool. Default is 5.
+ *     <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
+ *         connection alive in the pool before closing it. Default is 5 minutes.
+ *         This property isn't used by {@code HttpURLConnection}.
  * </ul>
  *
  * <p>The default instance <i>doesn't</i> adjust its configuration as system
@@ -43,127 +36,207 @@
  * parameters do so before making HTTP connections, and that this class is
  * initialized lazily.
  */
-public final class ConnectionPool {
-    private static final ConnectionPool systemDefault;
-    static {
-        String keepAlive = System.getProperty("http.keepAlive");
-        String maxConnections = System.getProperty("http.maxConnections");
-        if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
-            systemDefault = new ConnectionPool(0);
-        } else if (maxConnections != null) {
-            systemDefault = new ConnectionPool(Integer.parseInt(maxConnections));
-        } else {
-            systemDefault = new ConnectionPool(5);
-        }
+public class ConnectionPool {
+  private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
+  private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
+
+  private static final ConnectionPool systemDefault;
+
+  static {
+    String keepAlive = System.getProperty("http.keepAlive");
+    String keepAliveDuration = System.getProperty("http.keepAliveDuration");
+    String maxIdleConnections = System.getProperty("http.maxConnections");
+    long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
+        : DEFAULT_KEEP_ALIVE_DURATION_MS;
+    if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
+      systemDefault = new ConnectionPool(0, keepAliveDurationMs);
+    } else if (maxIdleConnections != null) {
+      systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
+    } else {
+      systemDefault = new ConnectionPool(5, keepAliveDurationMs);
     }
+  }
 
-    /** The maximum number of idle connections for each address. */
-    private final int maxConnections;
-    private final HashMap<Address, List<Connection>> connectionPool
-            = new HashMap<Address, List<Connection>>();
+  /** The maximum number of idle connections for each address. */
+  private final int maxIdleConnections;
+  private final long keepAliveDurationNs;
 
-    public ConnectionPool(int maxConnections) {
-        this.maxConnections = maxConnections;
-    }
+  private final LinkedList<Connection> connections = new LinkedList<Connection>();
 
-    public static ConnectionPool getDefault() {
-        return systemDefault;
-    }
-
-    /**
-     * Returns a recycled connection to {@code address}, or null if no such
-     * connection exists.
-     */
-    public Connection get(Address address) {
-        // First try to reuse an existing HTTP connection.
-        synchronized (connectionPool) {
-            List<Connection> connections = connectionPool.get(address);
-            while (connections != null) {
-                Connection connection = connections.get(connections.size() - 1);
-                if (!connection.isSpdy()) {
-                    connections.remove(connections.size() - 1);
-                }
-                if (connections.isEmpty()) {
-                    connectionPool.remove(address);
-                    connections = null;
-                }
-                if (!connection.isEligibleForRecycling()) {
-                    Util.closeQuietly(connection);
-                    continue;
-                }
-                try {
-                    Platform.get().tagSocket(connection.getSocket());
-                } catch (SocketException e) {
-                    // When unable to tag, skip recycling and close
-                    Platform.get().logW("Unable to tagSocket(): " + e);
-                    Util.closeQuietly(connection);
-                    continue;
-                }
-                return connection;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Gives {@code connection} to the pool. The pool may store the connection,
-     * or close it, as its policy describes.
-     *
-     * <p>It is an error to use {@code connection} after calling this method.
-     */
-    public void recycle(Connection connection) {
-        if (connection.isSpdy()) {
-            return;
+  /** We use a single background thread to cleanup expired connections. */
+  private final ExecutorService executorService =
+      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
+    @Override public Void call() throws Exception {
+      List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
+      int idleConnectionCount = 0;
+      synchronized (ConnectionPool.this) {
+        for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
+          Connection connection = i.next();
+          if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
+            i.remove();
+            expiredConnections.add(connection);
+            if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
+          } else if (connection.isIdle()) {
+            idleConnectionCount++;
+          }
         }
 
+        for (Iterator<Connection> i = connections.descendingIterator();
+            i.hasNext() && idleConnectionCount > maxIdleConnections; ) {
+          Connection connection = i.next();
+          if (connection.isIdle()) {
+            expiredConnections.add(connection);
+            i.remove();
+            --idleConnectionCount;
+          }
+        }
+      }
+      for (Connection expiredConnection : expiredConnections) {
+        Util.closeQuietly(expiredConnection);
+      }
+      return null;
+    }
+  };
+
+  public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+    this.maxIdleConnections = maxIdleConnections;
+    this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+  }
+
+  /**
+   * Returns a snapshot of the connections in this pool, ordered from newest to
+   * oldest. Waits for the cleanup callable to run if it is currently scheduled.
+   */
+  List<Connection> getConnections() {
+    waitForCleanupCallableToRun();
+    synchronized (this) {
+      return new ArrayList<Connection>(connections);
+    }
+  }
+
+  /**
+   * Blocks until the executor service has processed all currently enqueued
+   * jobs.
+   */
+  private void waitForCleanupCallableToRun() {
+    try {
+      executorService.submit(new Runnable() {
+        @Override public void run() {
+        }
+      }).get();
+    } catch (Exception e) {
+      throw new AssertionError();
+    }
+  }
+
+  public static ConnectionPool getDefault() {
+    return systemDefault;
+  }
+
+  /** Returns total number of connections in the pool. */
+  public synchronized int getConnectionCount() {
+    return connections.size();
+  }
+
+  /** Returns total number of spdy connections in the pool. */
+  public synchronized int getSpdyConnectionCount() {
+    int total = 0;
+    for (Connection connection : connections) {
+      if (connection.isSpdy()) total++;
+    }
+    return total;
+  }
+
+  /** Returns total number of http connections in the pool. */
+  public synchronized int getHttpConnectionCount() {
+    int total = 0;
+    for (Connection connection : connections) {
+      if (!connection.isSpdy()) total++;
+    }
+    return total;
+  }
+
+  /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
+  public synchronized Connection get(Address address) {
+    Connection foundConnection = null;
+    for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
+      Connection connection = i.next();
+      if (!connection.getAddress().equals(address)
+          || !connection.isAlive()
+          || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
+        continue;
+      }
+      i.remove();
+      if (!connection.isSpdy()) {
         try {
-            Platform.get().untagSocket(connection.getSocket());
+          Platform.get().tagSocket(connection.getSocket());
         } catch (SocketException e) {
-            // When unable to remove tagging, skip recycling and close
-            Platform.get().logW("Unable to untagSocket(): " + e);
-            Util.closeQuietly(connection);
-            return;
+          Util.closeQuietly(connection);
+          // When unable to tag, skip recycling and close
+          Platform.get().logW("Unable to tagSocket(): " + e);
+          continue;
         }
-
-        if (maxConnections > 0 && connection.isEligibleForRecycling()) {
-            Address address = connection.getAddress();
-            synchronized (connectionPool) {
-                List<Connection> connections = connectionPool.get(address);
-                if (connections == null) {
-                    connections = new ArrayList<Connection>();
-                    connectionPool.put(address, connections);
-                }
-                if (connections.size() < maxConnections) {
-                    connection.setRecycled();
-                    connections.add(connection);
-                    return; // keep the connection open
-                }
-            }
-        }
-
-        // don't close streams while holding a lock!
-        Util.closeQuietly(connection);
+      }
+      foundConnection = connection;
+      break;
     }
 
-    /**
-     * Shares the SPDY connection with the pool. Callers to this method may
-     * continue to use {@code connection}.
-     */
-    public void share(Connection connection) {
-        if (!connection.isSpdy()) {
-            throw new IllegalArgumentException();
-        }
-        if (maxConnections <= 0 || !connection.isEligibleForRecycling()) {
-            return;
-        }
-        Address address = connection.getAddress();
-        synchronized (connectionPool) {
-            List<Connection> connections = connectionPool.get(address);
-            if (connections == null) {
-                connections = new ArrayList<Connection>(1);
-                connections.add(connection);
-                connectionPool.put(address, connections);
-            }
-        }
+    if (foundConnection != null && foundConnection.isSpdy()) {
+      connections.addFirst(foundConnection); // Add it back after iteration.
     }
+
+    executorService.submit(connectionsCleanupCallable);
+    return foundConnection;
+  }
+
+  /**
+   * Gives {@code connection} to the pool. The pool may store the connection,
+   * or close it, as its policy describes.
+   *
+   * <p>It is an error to use {@code connection} after calling this method.
+   */
+  public void recycle(Connection connection) {
+    executorService.submit(connectionsCleanupCallable);
+
+    if (connection.isSpdy()) {
+      return;
+    }
+
+    if (!connection.isAlive()) {
+      Util.closeQuietly(connection);
+      return;
+    }
+
+    try {
+      Platform.get().untagSocket(connection.getSocket());
+    } catch (SocketException e) {
+      // When unable to remove tagging, skip recycling and close.
+      Platform.get().logW("Unable to untagSocket(): " + e);
+      Util.closeQuietly(connection);
+      return;
+    }
+
+    synchronized (this) {
+      connections.addFirst(connection);
+      connection.resetIdleStartTime();
+    }
+  }
+
+  /**
+   * 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;
+    }
+    if (connection.isAlive()) {
+      synchronized (this) {
+        connections.addFirst(connection);
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 2205487..4cc5ec6 100644
--- a/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -27,130 +27,123 @@
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
 
-/**
- * Configures and creates HTTP connections.
- */
+/** Configures and creates HTTP connections. */
 public final class OkHttpClient {
-    private Proxy proxy;
-    private ProxySelector proxySelector;
-    private CookieHandler cookieHandler;
-    private ResponseCache responseCache;
-    private SSLSocketFactory sslSocketFactory;
-    private HostnameVerifier hostnameVerifier;
-    private ConnectionPool connectionPool;
+  private Proxy proxy;
+  private ProxySelector proxySelector;
+  private CookieHandler cookieHandler;
+  private ResponseCache responseCache;
+  private SSLSocketFactory sslSocketFactory;
+  private HostnameVerifier hostnameVerifier;
+  private ConnectionPool connectionPool;
 
-    /**
-     * Sets the HTTP proxy that will be used by connections created by this
-     * client. This takes precedence over {@link #setProxySelector}, which is
-     * only honored when this proxy is null (which it is by default). To disable
-     * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
-     */
-    public OkHttpClient setProxy(Proxy proxy) {
-        this.proxy = proxy;
-        return this;
+  /**
+   * Sets the HTTP proxy that will be used by connections created by this
+   * client. This takes precedence over {@link #setProxySelector}, which is
+   * only honored when this proxy is null (which it is by default). To disable
+   * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
+   */
+  public OkHttpClient setProxy(Proxy proxy) {
+    this.proxy = proxy;
+    return this;
+  }
+
+  /**
+   * Sets the proxy selection policy to be used if no {@link #setProxy proxy}
+   * is specified explicitly. The proxy selector may return multiple proxies;
+   * in that case they will be tried in sequence until a successful connection
+   * is established.
+   *
+   * <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
+   * proxy selector will be used.
+   */
+  public OkHttpClient setProxySelector(ProxySelector proxySelector) {
+    this.proxySelector = proxySelector;
+    return this;
+  }
+
+  /**
+   * Sets the cookie handler to be used to read outgoing cookies and write
+   * incoming cookies.
+   *
+   * <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
+   * cookie handler will be used.
+   */
+  public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
+    this.cookieHandler = cookieHandler;
+    return this;
+  }
+
+  /**
+   * 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) {
+    this.responseCache = responseCache;
+    return this;
+  }
+
+  /**
+   * 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.
+   */
+  public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+    this.sslSocketFactory = sslSocketFactory;
+    return this;
+  }
+
+  /**
+   * Sets the verifier used to confirm that response certificates apply to
+   * requested hostnames for HTTPS connections.
+   *
+   * <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
+   * system-wide default} hostname verifier will be used.
+   */
+  public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+    this.hostnameVerifier = hostnameVerifier;
+    return this;
+  }
+
+  /**
+   * Sets the connection pool used to recycle HTTP and HTTPS connections.
+   *
+   * <p>If unset, the {@link ConnectionPool#getDefault() system-wide
+   * default} connection pool will be used.
+   */
+  public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
+    this.connectionPool = connectionPool;
+    return this;
+  }
+
+  public HttpURLConnection open(URL url) {
+    ProxySelector proxySelector =
+        this.proxySelector != null ? this.proxySelector : ProxySelector.getDefault();
+    CookieHandler cookieHandler =
+        this.cookieHandler != null ? this.cookieHandler : CookieHandler.getDefault();
+    ResponseCache responseCache =
+        this.responseCache != null ? this.responseCache : ResponseCache.getDefault();
+    ConnectionPool connectionPool =
+        this.connectionPool != null ? this.connectionPool : ConnectionPool.getDefault();
+
+    String protocol = url.getProtocol();
+    if (protocol.equals("http")) {
+      return new HttpURLConnectionImpl(url, 80, proxy, proxySelector, cookieHandler, responseCache,
+          connectionPool);
+    } else if (protocol.equals("https")) {
+      HttpsURLConnectionImpl result =
+          new HttpsURLConnectionImpl(url, 443, proxy, proxySelector, cookieHandler, responseCache,
+              connectionPool);
+      result.setSSLSocketFactory(this.sslSocketFactory != null ? this.sslSocketFactory
+          : HttpsURLConnection.getDefaultSSLSocketFactory());
+      result.setHostnameVerifier(this.hostnameVerifier != null ? this.hostnameVerifier
+          : HttpsURLConnection.getDefaultHostnameVerifier());
+      return result;
+    } else {
+      throw new IllegalArgumentException("Unexpected protocol: " + protocol);
     }
-
-    /**
-     * Sets the proxy selection policy to be used if no {@link #setProxy proxy}
-     * is specified explicitly. The proxy selector may return multiple proxies;
-     * in that case they will be tried in sequence until a successful connection
-     * is established.
-     *
-     * <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
-     * proxy selector will be used.
-     */
-    public OkHttpClient setProxySelector(ProxySelector proxySelector) {
-        this.proxySelector = proxySelector;
-        return this;
-    }
-
-    /**
-     * Sets the cookie handler to be used to read outgoing cookies and write
-     * incoming cookies.
-     *
-     * <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
-     * cookie handler will be used.
-     */
-    public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
-        this.cookieHandler = cookieHandler;
-        return this;
-    }
-
-    /**
-     * 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) {
-        this.responseCache = responseCache;
-        return this;
-    }
-
-    /**
-     * 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.
-     */
-    public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
-        this.sslSocketFactory = sslSocketFactory;
-        return this;
-    }
-
-    /**
-     * Sets the verifier used to confirm that response certificates apply to
-     * requested hostnames for HTTPS connections.
-     *
-     * <p>If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier()
-     * system-wide default} hostname verifier will be used.
-     */
-    public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
-        this.hostnameVerifier = hostnameVerifier;
-        return this;
-    }
-
-    /**
-     * Sets the connection pool used to recycle HTTP and HTTPS connections.
-     *
-     * <p>If unset, the {@link ConnectionPool#getDefault() system-wide
-     * default} connection pool will be used.
-     */
-    public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
-        this.connectionPool = connectionPool;
-        return this;
-    }
-
-    public HttpURLConnection open(URL url) {
-        ProxySelector proxySelector = this.proxySelector != null
-                ? this.proxySelector
-                : ProxySelector.getDefault();
-        CookieHandler cookieHandler = this.cookieHandler != null
-                ? this.cookieHandler
-                : CookieHandler.getDefault();
-        ResponseCache responseCache = this.responseCache != null
-                ? this.responseCache
-                : ResponseCache.getDefault();
-        ConnectionPool connectionPool = this.connectionPool != null
-                ? this.connectionPool
-                : ConnectionPool.getDefault();
-
-        String protocol = url.getProtocol();
-        if (protocol.equals("http")) {
-            return new HttpURLConnectionImpl(
-                    url, 80, proxy, proxySelector, cookieHandler, responseCache, connectionPool);
-        } else if (protocol.equals("https")) {
-            HttpsURLConnectionImpl result = new HttpsURLConnectionImpl(
-                    url, 443, proxy, proxySelector, cookieHandler, responseCache, connectionPool);
-            result.setSSLSocketFactory(this.sslSocketFactory != null
-                    ? this.sslSocketFactory
-                    : HttpsURLConnection.getDefaultSSLSocketFactory());
-            result.setHostnameVerifier(this.hostnameVerifier != null
-                    ? this.hostnameVerifier
-                    : HttpsURLConnection.getDefaultHostnameVerifier());
-            return result;
-        } else {
-            throw new IllegalArgumentException("Unexpected protocol: " + protocol);
-        }
-    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/OkResponseCache.java b/src/main/java/com/squareup/okhttp/OkResponseCache.java
index c0f7c56..b7e3801 100644
--- a/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/OkResponseCache.java
@@ -26,19 +26,13 @@
  */
 public interface OkResponseCache {
 
-    /**
-     * Track an HTTP response being satisfied by {@code source}.
-     */
-    void trackResponse(ResponseSource source);
+  /** Track an HTTP response being satisfied by {@code source}. */
+  void trackResponse(ResponseSource source);
 
-    /**
-     * Track an conditional GET that was satisfied by this cache.
-     */
-    void trackConditionalCacheHit();
+  /** Track an conditional GET that was satisfied by this cache. */
+  void trackConditionalCacheHit();
 
-    /**
-     * Updates stored HTTP headers using a hit on a conditional GET.
-     */
-    void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
-            throws IOException;
+  /** Updates stored HTTP headers using a hit on a conditional GET. */
+  void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+      throws IOException;
 }
diff --git a/src/main/java/com/squareup/okhttp/ResponseSource.java b/src/main/java/com/squareup/okhttp/ResponseSource.java
index 83388d6..4eca172 100644
--- a/src/main/java/com/squareup/okhttp/ResponseSource.java
+++ b/src/main/java/com/squareup/okhttp/ResponseSource.java
@@ -15,29 +15,23 @@
  */
 package com.squareup.okhttp;
 
-/**
- * The source of an HTTP response.
- */
+/** The source of an HTTP response. */
 public enum ResponseSource {
 
-    /**
-     * The response was returned from the local cache.
-     */
-    CACHE,
+  /** The response was returned from the local cache. */
+  CACHE,
 
-    /**
-     * The response is available in the cache but must be validated with the
-     * network. The cache result will be used if it is still valid; otherwise
-     * the network's response will be used.
-     */
-    CONDITIONAL_CACHE,
+  /**
+   * The response is available in the cache but must be validated with the
+   * network. The cache result will be used if it is still valid; otherwise
+   * the network's response will be used.
+   */
+  CONDITIONAL_CACHE,
 
-    /**
-     * The response was returned from the network.
-     */
-    NETWORK;
+  /** The response was returned from the network. */
+  NETWORK;
 
-    public boolean requiresConnection() {
-        return this == CONDITIONAL_CACHE || this == NETWORK;
-    }
+  public boolean requiresConnection() {
+    return this == CONDITIONAL_CACHE || this == NETWORK;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/TunnelRequest.java b/src/main/java/com/squareup/okhttp/TunnelRequest.java
index 39a820c..5260b87 100644
--- a/src/main/java/com/squareup/okhttp/TunnelRequest.java
+++ b/src/main/java/com/squareup/okhttp/TunnelRequest.java
@@ -15,9 +15,10 @@
  */
 package com.squareup.okhttp;
 
-import static com.squareup.okhttp.internal.Util.getDefaultPort;
 import com.squareup.okhttp.internal.http.RawHeaders;
 
+import static com.squareup.okhttp.internal.Util.getDefaultPort;
+
 /**
  * Routing and authentication information sent to an HTTP proxy to create a
  * HTTPS to an origin server. Everything in the tunnel request is sent
@@ -27,48 +28,48 @@
  * 5.2</a>.
  */
 public final class TunnelRequest {
-    final String host;
-    final int port;
-    final String userAgent;
-    final String proxyAuthorization;
+  final String host;
+  final int port;
+  final String userAgent;
+  final String proxyAuthorization;
 
-    /**
-     * @param host the origin server's hostname. Not null.
-     * @param port the origin server's port, like 80 or 443.
-     * @param userAgent the client's user-agent. Not null.
-     * @param proxyAuthorization proxy authorization, or null if the proxy is
-     *     used without an authorization header.
-     */
-    public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
-        if (host == null) throw new NullPointerException("host == null");
-        if (userAgent == null) throw new NullPointerException("userAgent == null");
-        this.host = host;
-        this.port = port;
-        this.userAgent = userAgent;
-        this.proxyAuthorization = proxyAuthorization;
+  /**
+   * @param host the origin server's hostname. Not null.
+   * @param port the origin server's port, like 80 or 443.
+   * @param userAgent the client's user-agent. Not null.
+   * @param proxyAuthorization proxy authorization, or null if the proxy is
+   * used without an authorization header.
+   */
+  public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
+    if (host == null) throw new NullPointerException("host == null");
+    if (userAgent == null) throw new NullPointerException("userAgent == null");
+    this.host = host;
+    this.port = port;
+    this.userAgent = userAgent;
+    this.proxyAuthorization = proxyAuthorization;
+  }
+
+  /**
+   * 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");
+
+    // Always set Host and User-Agent.
+    result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
+    result.set("User-Agent", userAgent);
+
+    // Copy over the Proxy-Authorization header if it exists.
+    if (proxyAuthorization != null) {
+      result.set("Proxy-Authorization", proxyAuthorization);
     }
 
-    /**
-     * 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");
-
-        // Always set Host and User-Agent.
-        result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
-        result.set("User-Agent", userAgent);
-
-        // Copy over the Proxy-Authorization header if it exists.
-        if (proxyAuthorization != null) {
-            result.set("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;
-    }
+    // 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;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/Base64.java b/src/main/java/com/squareup/okhttp/internal/Base64.java
index 458e536..79cd020 100644
--- a/src/main/java/com/squareup/okhttp/internal/Base64.java
+++ b/src/main/java/com/squareup/okhttp/internal/Base64.java
@@ -16,150 +16,149 @@
  */
 
 /**
-* @author Alexander Y. Kleymenov
-*/
+ * @author Alexander Y. Kleymenov
+ */
 
 package com.squareup.okhttp.internal;
 
-import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
 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() {
-    }
+  private Base64() {
+  }
 
-    public static byte[] decode(byte[] in) {
-        return decode(in, in.length);
-    }
+  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;
+  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;
     }
-
-    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);
-        }
+    // 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/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index 96f6d96..00fe2f1 100644
--- a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -16,7 +16,6 @@
 
 package com.squareup.okhttp.internal;
 
-import static com.squareup.okhttp.internal.Util.UTF_8;
 import java.io.BufferedWriter;
 import java.io.Closeable;
 import java.io.EOFException;
@@ -43,6 +42,8 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
 /**
  * A cache that uses a bounded amount of space on a filesystem. Each cache
  * entry has a string key and a fixed number of values. Values are byte
@@ -65,12 +66,12 @@
  * entry may have only one editor at one time; if a value is not available to be
  * edited then {@link #edit} will return null.
  * <ul>
- *     <li>When an entry is being <strong>created</strong> it is necessary to
- *         supply a full set of values; the empty value should be used as a
- *         placeholder if necessary.
- *     <li>When an entry is being <strong>edited</strong>, it is not necessary
- *         to supply data for every value; values default to their previous
- *         value.
+ * <li>When an entry is being <strong>created</strong> it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
  * </ul>
  * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
  * or {@link Editor#abort}. Committing is atomic: a read observes the full set
@@ -87,759 +88,739 @@
  * responding appropriately.
  */
 public final class DiskLruCache implements Closeable {
-    static final String JOURNAL_FILE = "journal";
-    static final String JOURNAL_FILE_TMP = "journal.tmp";
-    static final String MAGIC = "libcore.io.DiskLruCache";
-    static final String VERSION_1 = "1";
-    static final long ANY_SEQUENCE_NUMBER = -1;
-    private static final String CLEAN = "CLEAN";
-    private static final String DIRTY = "DIRTY";
-    private static final String REMOVE = "REMOVE";
-    private static final String READ = "READ";
+  static final String JOURNAL_FILE = "journal";
+  static final String JOURNAL_FILE_TMP = "journal.tmp";
+  static final String MAGIC = "libcore.io.DiskLruCache";
+  static final String VERSION_1 = "1";
+  static final long ANY_SEQUENCE_NUMBER = -1;
+  private static final String CLEAN = "CLEAN";
+  private static final String DIRTY = "DIRTY";
+  private static final String REMOVE = "REMOVE";
+  private static final String READ = "READ";
 
-    /*
-     * This cache uses a journal file named "journal". A typical journal file
-     * looks like this:
-     *     libcore.io.DiskLruCache
-     *     1
-     *     100
-     *     2
-     *
-     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
-     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
-     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
-     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
-     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
-     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
-     *     READ 335c4c6028171cfddfbaae1a9c313c52
-     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
-     *
-     * The first five lines of the journal form its header. They are the
-     * constant string "libcore.io.DiskLruCache", the disk cache's version,
-     * the application's version, the value count, and a blank line.
-     *
-     * Each of the subsequent lines in the file is a record of the state of a
-     * cache entry. Each line contains space-separated values: a state, a key,
-     * and optional state-specific values.
-     *   o DIRTY lines track that an entry is actively being created or updated.
-     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
-     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
-     *     temporary files may need to be deleted.
-     *   o CLEAN lines track a cache entry that has been successfully published
-     *     and may be read. A publish line is followed by the lengths of each of
-     *     its values.
-     *   o READ lines track accesses for LRU.
-     *   o REMOVE lines track entries that have been deleted.
-     *
-     * The journal file is appended to as cache operations occur. The journal may
-     * occasionally be compacted by dropping redundant lines. A temporary file named
-     * "journal.tmp" will be used during compaction; that file should be deleted if
-     * it exists when the cache is opened.
-     */
+  // This cache uses a journal file named "journal". A typical journal file
+  // looks like this:
+  //     libcore.io.DiskLruCache
+  //     1
+  //     100
+  //     2
+  //
+  //     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+  //     DIRTY 335c4c6028171cfddfbaae1a9c313c52
+  //     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+  //     REMOVE 335c4c6028171cfddfbaae1a9c313c52
+  //     DIRTY 1ab96a171faeeee38496d8b330771a7a
+  //     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+  //     READ 335c4c6028171cfddfbaae1a9c313c52
+  //     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+  //
+  // The first five lines of the journal form its header. They are the
+  // constant string "libcore.io.DiskLruCache", the disk cache's version,
+  // the application's version, the value count, and a blank line.
+  //
+  // Each of the subsequent lines in the file is a record of the state of a
+  // cache entry. Each line contains space-separated values: a state, a key,
+  // and optional state-specific values.
+  //   o DIRTY lines track that an entry is actively being created or updated.
+  //     Every successful DIRTY action should be followed by a CLEAN or REMOVE
+  //     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+  //     temporary files may need to be deleted.
+  //   o CLEAN lines track a cache entry that has been successfully published
+  //     and may be read. A publish line is followed by the lengths of each of
+  //     its values.
+  //   o READ lines track accesses for LRU.
+  //   o REMOVE lines track entries that have been deleted.
+  //
+  // The journal file is appended to as cache operations occur. The journal may
+  // occasionally be compacted by dropping redundant lines. A temporary file named
+  // "journal.tmp" will be used during compaction; that file should be deleted if
+  // it exists when the cache is opened.
 
-    private final File directory;
-    private final File journalFile;
-    private final File journalFileTmp;
-    private final int appVersion;
-    private final long maxSize;
-    private final int valueCount;
-    private long size = 0;
-    private Writer journalWriter;
-    private final LinkedHashMap<String, Entry> lruEntries
-            = new LinkedHashMap<String, Entry>(0, 0.75f, true);
-    private int redundantOpCount;
+  private final File directory;
+  private final File journalFile;
+  private final File journalFileTmp;
+  private final int appVersion;
+  private final long maxSize;
+  private final int valueCount;
+  private long size = 0;
+  private Writer journalWriter;
+  private final LinkedHashMap<String, Entry> lruEntries =
+      new LinkedHashMap<String, Entry>(0, 0.75f, true);
+  private int redundantOpCount;
 
-    /**
-     * To differentiate between old and current snapshots, each entry is given
-     * a sequence number each time an edit is committed. A snapshot is stale if
-     * its sequence number is not equal to its entry's sequence number.
-     */
-    private long nextSequenceNumber = 0;
+  /**
+   * To differentiate between old and current snapshots, each entry is given
+   * a sequence number each time an edit is committed. A snapshot is stale if
+   * its sequence number is not equal to its entry's sequence number.
+   */
+  private long nextSequenceNumber = 0;
 
-    /** This cache uses a single background thread to evict entries. */
-    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
-            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
-    private final Callable<Void> cleanupCallable = new Callable<Void>() {
-        @Override public Void call() throws Exception {
-            synchronized (DiskLruCache.this) {
-                if (journalWriter == null) {
-                    return null; // closed
-                }
-                trimToSize();
-                if (journalRebuildRequired()) {
-                    rebuildJournal();
-                    redundantOpCount = 0;
-                }
-            }
-            return null;
+  /** This cache uses a single background thread to evict entries. */
+  private final ExecutorService executorService =
+      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+  private final Callable<Void> cleanupCallable = new Callable<Void>() {
+    @Override public Void call() throws Exception {
+      synchronized (DiskLruCache.this) {
+        if (journalWriter == null) {
+          return null; // closed
         }
-    };
+        trimToSize();
+        if (journalRebuildRequired()) {
+          rebuildJournal();
+          redundantOpCount = 0;
+        }
+      }
+      return null;
+    }
+  };
 
-    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
-        this.directory = directory;
-        this.appVersion = appVersion;
-        this.journalFile = new File(directory, JOURNAL_FILE);
-        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
-        this.valueCount = valueCount;
-        this.maxSize = maxSize;
+  private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+    this.directory = directory;
+    this.appVersion = appVersion;
+    this.journalFile = new File(directory, JOURNAL_FILE);
+    this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+    this.valueCount = valueCount;
+    this.maxSize = maxSize;
+  }
+
+  /**
+   * Opens the cache in {@code directory}, creating a cache if none exists
+   * there.
+   *
+   * @param directory a writable directory
+   * @param valueCount the number of values per cache entry. Must be positive.
+   * @param maxSize the maximum number of bytes this cache should use to store
+   * @throws IOException if reading or writing the cache directory fails
+   */
+  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+      throws IOException {
+    if (maxSize <= 0) {
+      throw new IllegalArgumentException("maxSize <= 0");
+    }
+    if (valueCount <= 0) {
+      throw new IllegalArgumentException("valueCount <= 0");
     }
 
-    /**
-     * Opens the cache in {@code directory}, creating a cache if none exists
-     * there.
-     *
-     * @param directory a writable directory
-     * @param appVersion
-     * @param valueCount the number of values per cache entry. Must be positive.
-     * @param maxSize the maximum number of bytes this cache should use to store
-     * @throws IOException if reading or writing the cache directory fails
-     */
-    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
-            throws IOException {
-        if (maxSize <= 0) {
-            throw new IllegalArgumentException("maxSize <= 0");
-        }
-        if (valueCount <= 0) {
-            throw new IllegalArgumentException("valueCount <= 0");
-        }
-
-        // prefer to pick up where we left off
-        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
-        if (cache.journalFile.exists()) {
-            try {
-                cache.readJournal();
-                cache.processJournal();
-                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
-                return cache;
-            } catch (IOException journalIsCorrupt) {
-                Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
-                        + journalIsCorrupt.getMessage() + ", removing");
-                cache.delete();
-            }
-        }
-
-        // create a new empty cache
-        directory.mkdirs();
-        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
-        cache.rebuildJournal();
+    // prefer to pick up where we left off
+    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+    if (cache.journalFile.exists()) {
+      try {
+        cache.readJournal();
+        cache.processJournal();
+        cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
         return cache;
+      } catch (IOException journalIsCorrupt) {
+        Platform.get()
+            .logW("DiskLruCache "
+                + directory
+                + " is corrupt: "
+                + journalIsCorrupt.getMessage()
+                + ", removing");
+        cache.delete();
+      }
     }
 
-    private void readJournal() throws IOException {
-        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile),
-                Util.US_ASCII);
+    // create a new empty cache
+    directory.mkdirs();
+    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+    cache.rebuildJournal();
+    return cache;
+  }
+
+  private void readJournal() throws IOException {
+    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+    try {
+      String magic = reader.readLine();
+      String version = reader.readLine();
+      String appVersionString = reader.readLine();
+      String valueCountString = reader.readLine();
+      String blank = reader.readLine();
+      if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
+          .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
+          .equals(blank)) {
+        throw new IOException("unexpected journal header: ["
+            + magic
+            + ", "
+            + version
+            + ", "
+            + valueCountString
+            + ", "
+            + blank
+            + "]");
+      }
+
+      while (true) {
         try {
-            String magic = reader.readLine();
-            String version = reader.readLine();
-            String appVersionString = reader.readLine();
-            String valueCountString = reader.readLine();
-            String blank = reader.readLine();
-            if (!MAGIC.equals(magic)
-                    || !VERSION_1.equals(version)
-                    || !Integer.toString(appVersion).equals(appVersionString)
-                    || !Integer.toString(valueCount).equals(valueCountString)
-                    || !"".equals(blank)) {
-                throw new IOException("unexpected journal header: ["
-                        + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
-            }
-
-            while (true) {
-                try {
-                    readJournalLine(reader.readLine());
-                } catch (EOFException endOfJournal) {
-                    break;
-                }
-            }
-        } finally {
-            Util.closeQuietly(reader);
+          readJournalLine(reader.readLine());
+        } catch (EOFException endOfJournal) {
+          break;
         }
+      }
+    } finally {
+      Util.closeQuietly(reader);
+    }
+  }
+
+  private void readJournalLine(String line) throws IOException {
+    String[] parts = line.split(" ");
+    if (parts.length < 2) {
+      throw new IOException("unexpected journal line: " + line);
     }
 
-    private void readJournalLine(String line) throws IOException {
-        String[] parts = line.split(" ");
-        if (parts.length < 2) {
-            throw new IOException("unexpected journal line: " + line);
-        }
-
-        String key = parts[1];
-        if (parts[0].equals(REMOVE) && parts.length == 2) {
-            lruEntries.remove(key);
-            return;
-        }
-
-        Entry entry = lruEntries.get(key);
-        if (entry == null) {
-            entry = new Entry(key);
-            lruEntries.put(key, entry);
-        }
-
-        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
-            entry.readable = true;
-            entry.currentEditor = null;
-            entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
-        } else if (parts[0].equals(DIRTY) && parts.length == 2) {
-            entry.currentEditor = new Editor(entry);
-        } else if (parts[0].equals(READ) && parts.length == 2) {
-            // this work was already done by calling lruEntries.get()
-        } else {
-            throw new IOException("unexpected journal line: " + line);
-        }
+    String key = parts[1];
+    if (parts[0].equals(REMOVE) && parts.length == 2) {
+      lruEntries.remove(key);
+      return;
     }
 
-    /**
-     * Computes the initial size and collects garbage as a part of opening the
-     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
-     */
-    private void processJournal() throws IOException {
-        deleteIfExists(journalFileTmp);
-        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();) {
-            Entry entry = i.next();
-            if (entry.currentEditor == null) {
-                for (int t = 0; t < valueCount; t++) {
-                    size += entry.lengths[t];
-                }
-            } else {
-                entry.currentEditor = null;
-                for (int t = 0; t < valueCount; t++) {
-                    deleteIfExists(entry.getCleanFile(t));
-                    deleteIfExists(entry.getDirtyFile(t));
-                }
-                i.remove();
-            }
-        }
+    Entry entry = lruEntries.get(key);
+    if (entry == null) {
+      entry = new Entry(key);
+      lruEntries.put(key, entry);
     }
 
-    /**
-     * Creates a new journal that omits redundant information. This replaces the
-     * current journal if it exists.
-     */
-    private synchronized void rebuildJournal() throws IOException {
-        if (journalWriter != null) {
-            journalWriter.close();
-        }
-
-        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
-        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");
-
-        for (Entry entry : lruEntries.values()) {
-            if (entry.currentEditor != null) {
-                writer.write(DIRTY + ' ' + entry.key + '\n');
-            } else {
-                writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
-            }
-        }
-
-        writer.close();
-        journalFileTmp.renameTo(journalFile);
-        journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+    if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+      entry.readable = true;
+      entry.currentEditor = null;
+      entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
+    } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+      entry.currentEditor = new Editor(entry);
+    } else if (parts[0].equals(READ) && parts.length == 2) {
+      // this work was already done by calling lruEntries.get()
+    } else {
+      throw new IOException("unexpected journal line: " + line);
     }
+  }
 
-    private static void deleteIfExists(File file) throws IOException {
-        file.delete();
-    }
-
-    /**
-     * Returns a snapshot of the entry named {@code key}, or null if it doesn't
-     * exist is not currently readable. If a value is returned, it is moved to
-     * the head of the LRU queue.
-     */
-    public synchronized Snapshot get(String key) throws IOException {
-        checkNotClosed();
-        validateKey(key);
-        Entry entry = lruEntries.get(key);
-        if (entry == null) {
-            return null;
+  /**
+   * Computes the initial size and collects garbage as a part of opening the
+   * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+   */
+  private void processJournal() throws IOException {
+    deleteIfExists(journalFileTmp);
+    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
+      Entry entry = i.next();
+      if (entry.currentEditor == null) {
+        for (int t = 0; t < valueCount; t++) {
+          size += entry.lengths[t];
         }
-
-        if (!entry.readable) {
-            return null;
-        }
-
-        /*
-         * Open all streams eagerly to guarantee that we see a single published
-         * snapshot. If we opened streams lazily then the streams could come
-         * from different edits.
-         */
-        InputStream[] ins = new InputStream[valueCount];
-        try {
-            for (int i = 0; i < valueCount; i++) {
-                ins[i] = new FileInputStream(entry.getCleanFile(i));
-            }
-        } catch (FileNotFoundException e) {
-            // a file must have been deleted manually!
-            return null;
-        }
-
-        redundantOpCount++;
-        journalWriter.append(READ + ' ' + key + '\n');
-        if (journalRebuildRequired()) {
-            executorService.submit(cleanupCallable);
-        }
-
-        return new Snapshot(key, entry.sequenceNumber, ins);
-    }
-
-    /**
-     * Returns an editor for the entry named {@code key}, or null if another
-     * edit is in progress.
-     */
-    public Editor edit(String key) throws IOException {
-        return edit(key, ANY_SEQUENCE_NUMBER);
-    }
-
-    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
-        checkNotClosed();
-        validateKey(key);
-        Entry entry = lruEntries.get(key);
-        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
-                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
-            return null; // snapshot is stale
-        }
-        if (entry == null) {
-            entry = new Entry(key);
-            lruEntries.put(key, entry);
-        } else if (entry.currentEditor != null) {
-            return null; // another edit is in progress
-        }
-
-        Editor editor = new Editor(entry);
-        entry.currentEditor = editor;
-
-        // flush the journal before creating files to prevent file leaks
-        journalWriter.write(DIRTY + ' ' + key + '\n');
-        journalWriter.flush();
-        return editor;
-    }
-
-    /**
-     * Returns the directory where this cache stores its data.
-     */
-    public File getDirectory() {
-        return directory;
-    }
-
-    /**
-     * Returns the maximum number of bytes that this cache should use to store
-     * its data.
-     */
-    public long maxSize() {
-        return maxSize;
-    }
-
-    /**
-     * Returns the number of bytes currently being used to store the values in
-     * this cache. This may be greater than the max size if a background
-     * deletion is pending.
-     */
-    public synchronized long size() {
-        return size;
-    }
-
-    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
-        Entry entry = editor.entry;
-        if (entry.currentEditor != editor) {
-            throw new IllegalStateException();
-        }
-
-        // if this edit is creating the entry for the first time, every index must have a value
-        if (success && !entry.readable) {
-            for (int i = 0; i < valueCount; i++) {
-                if (!editor.written[i]) {
-                    editor.abort();
-                    throw new IllegalStateException(
-                            "Newly created entry didn't create value for index " + i);
-                }
-                if (!entry.getDirtyFile(i).exists()) {
-                    editor.abort();
-                    Platform.get().logW(
-                            "DiskLruCache: Newly created entry doesn't have file for index " + i);
-                    return;
-                }
-            }
-        }
-
-        for (int i = 0; i < valueCount; i++) {
-            File dirty = entry.getDirtyFile(i);
-            if (success) {
-                if (dirty.exists()) {
-                    File clean = entry.getCleanFile(i);
-                    dirty.renameTo(clean);
-                    long oldLength = entry.lengths[i];
-                    long newLength = clean.length();
-                    entry.lengths[i] = newLength;
-                    size = size - oldLength + newLength;
-                }
-            } else {
-                deleteIfExists(dirty);
-            }
-        }
-
-        redundantOpCount++;
+      } else {
         entry.currentEditor = null;
-        if (entry.readable | success) {
-            entry.readable = true;
-            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
-            if (success) {
-                entry.sequenceNumber = nextSequenceNumber++;
-            }
-        } else {
-            lruEntries.remove(entry.key);
-            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+        for (int t = 0; t < valueCount; t++) {
+          deleteIfExists(entry.getCleanFile(t));
+          deleteIfExists(entry.getDirtyFile(t));
         }
+        i.remove();
+      }
+    }
+  }
 
-        if (size > maxSize || journalRebuildRequired()) {
-            executorService.submit(cleanupCallable);
+  /**
+   * Creates a new journal that omits redundant information. This replaces the
+   * current journal if it exists.
+   */
+  private synchronized void rebuildJournal() throws IOException {
+    if (journalWriter != null) {
+      journalWriter.close();
+    }
+
+    Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
+    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");
+
+    for (Entry entry : lruEntries.values()) {
+      if (entry.currentEditor != null) {
+        writer.write(DIRTY + ' ' + entry.key + '\n');
+      } else {
+        writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+      }
+    }
+
+    writer.close();
+    journalFileTmp.renameTo(journalFile);
+    journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+  }
+
+  private static void deleteIfExists(File file) throws IOException {
+    file.delete();
+  }
+
+  /**
+   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
+   * exist is not currently readable. If a value is returned, it is moved to
+   * the head of the LRU queue.
+   */
+  public synchronized Snapshot get(String key) throws IOException {
+    checkNotClosed();
+    validateKey(key);
+    Entry entry = lruEntries.get(key);
+    if (entry == null) {
+      return null;
+    }
+
+    if (!entry.readable) {
+      return null;
+    }
+
+    // Open all streams eagerly to guarantee that we see a single published
+    // snapshot. If we opened streams lazily then the streams could come
+    // from different edits.
+    InputStream[] ins = new InputStream[valueCount];
+    try {
+      for (int i = 0; i < valueCount; i++) {
+        ins[i] = new FileInputStream(entry.getCleanFile(i));
+      }
+    } catch (FileNotFoundException e) {
+      // a file must have been deleted manually!
+      return null;
+    }
+
+    redundantOpCount++;
+    journalWriter.append(READ + ' ' + key + '\n');
+    if (journalRebuildRequired()) {
+      executorService.submit(cleanupCallable);
+    }
+
+    return new Snapshot(key, entry.sequenceNumber, ins);
+  }
+
+  /**
+   * Returns an editor for the entry named {@code key}, or null if another
+   * edit is in progress.
+   */
+  public Editor edit(String key) throws IOException {
+    return edit(key, ANY_SEQUENCE_NUMBER);
+  }
+
+  private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+    checkNotClosed();
+    validateKey(key);
+    Entry entry = lruEntries.get(key);
+    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
+        || entry.sequenceNumber != expectedSequenceNumber)) {
+      return null; // snapshot is stale
+    }
+    if (entry == null) {
+      entry = new Entry(key);
+      lruEntries.put(key, entry);
+    } else if (entry.currentEditor != null) {
+      return null; // another edit is in progress
+    }
+
+    Editor editor = new Editor(entry);
+    entry.currentEditor = editor;
+
+    // flush the journal before creating files to prevent file leaks
+    journalWriter.write(DIRTY + ' ' + key + '\n');
+    journalWriter.flush();
+    return editor;
+  }
+
+  /** Returns the directory where this cache stores its data. */
+  public File getDirectory() {
+    return directory;
+  }
+
+  /**
+   * Returns the maximum number of bytes that this cache should use to store
+   * its data.
+   */
+  public long maxSize() {
+    return maxSize;
+  }
+
+  /**
+   * Returns the number of bytes currently being used to store the values in
+   * this cache. This may be greater than the max size if a background
+   * deletion is pending.
+   */
+  public synchronized long size() {
+    return size;
+  }
+
+  private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
+    Entry entry = editor.entry;
+    if (entry.currentEditor != editor) {
+      throw new IllegalStateException();
+    }
+
+    // if this edit is creating the entry for the first time, every index must have a value
+    if (success && !entry.readable) {
+      for (int i = 0; i < valueCount; i++) {
+        if (!editor.written[i]) {
+          editor.abort();
+          throw new IllegalStateException("Newly created entry didn't create value for index " + i);
         }
+        if (!entry.getDirtyFile(i).exists()) {
+          editor.abort();
+          Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
+          return;
+        }
+      }
+    }
+
+    for (int i = 0; i < valueCount; i++) {
+      File dirty = entry.getDirtyFile(i);
+      if (success) {
+        if (dirty.exists()) {
+          File clean = entry.getCleanFile(i);
+          dirty.renameTo(clean);
+          long oldLength = entry.lengths[i];
+          long newLength = clean.length();
+          entry.lengths[i] = newLength;
+          size = size - oldLength + newLength;
+        }
+      } else {
+        deleteIfExists(dirty);
+      }
+    }
+
+    redundantOpCount++;
+    entry.currentEditor = null;
+    if (entry.readable | success) {
+      entry.readable = true;
+      journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+      if (success) {
+        entry.sequenceNumber = nextSequenceNumber++;
+      }
+    } else {
+      lruEntries.remove(entry.key);
+      journalWriter.write(REMOVE + ' ' + entry.key + '\n');
+    }
+
+    if (size > maxSize || journalRebuildRequired()) {
+      executorService.submit(cleanupCallable);
+    }
+  }
+
+  /**
+   * We only rebuild the journal when it will halve the size of the journal
+   * and eliminate at least 2000 ops.
+   */
+  private boolean journalRebuildRequired() {
+    final int redundantOpCompactThreshold = 2000;
+    return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
+  }
+
+  /**
+   * Drops the entry for {@code key} if it exists and can be removed. Entries
+   * actively being edited cannot be removed.
+   *
+   * @return true if an entry was removed.
+   */
+  public synchronized boolean remove(String key) throws IOException {
+    checkNotClosed();
+    validateKey(key);
+    Entry entry = lruEntries.get(key);
+    if (entry == null || entry.currentEditor != null) {
+      return false;
+    }
+
+    for (int i = 0; i < valueCount; i++) {
+      File file = entry.getCleanFile(i);
+      if (!file.delete()) {
+        throw new IOException("failed to delete " + file);
+      }
+      size -= entry.lengths[i];
+      entry.lengths[i] = 0;
+    }
+
+    redundantOpCount++;
+    journalWriter.append(REMOVE + ' ' + key + '\n');
+    lruEntries.remove(key);
+
+    if (journalRebuildRequired()) {
+      executorService.submit(cleanupCallable);
+    }
+
+    return true;
+  }
+
+  /** Returns true if this cache has been closed. */
+  public boolean isClosed() {
+    return journalWriter == null;
+  }
+
+  private void checkNotClosed() {
+    if (journalWriter == null) {
+      throw new IllegalStateException("cache is closed");
+    }
+  }
+
+  /** Force buffered operations to the filesystem. */
+  public synchronized void flush() throws IOException {
+    checkNotClosed();
+    trimToSize();
+    journalWriter.flush();
+  }
+
+  /** Closes this cache. Stored values will remain on the filesystem. */
+  public synchronized void close() throws IOException {
+    if (journalWriter == null) {
+      return; // already closed
+    }
+    for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
+      if (entry.currentEditor != null) {
+        entry.currentEditor.abort();
+      }
+    }
+    trimToSize();
+    journalWriter.close();
+    journalWriter = null;
+  }
+
+  private void trimToSize() throws IOException {
+    while (size > maxSize) {
+      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
+      remove(toEvict.getKey());
+    }
+  }
+
+  /**
+   * 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
+   * the cache.
+   */
+  public void delete() throws IOException {
+    close();
+    Util.deleteContents(directory);
+  }
+
+  private void validateKey(String key) {
+    if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
+      throw new IllegalArgumentException(
+          "keys must not contain spaces or newlines: \"" + key + "\"");
+    }
+  }
+
+  private static String inputStreamToString(InputStream in) throws IOException {
+    return Util.readFully(new InputStreamReader(in, UTF_8));
+  }
+
+  /** A snapshot of the values for an entry. */
+  public final class Snapshot implements Closeable {
+    private final String key;
+    private final long sequenceNumber;
+    private final InputStream[] ins;
+
+    private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+      this.key = key;
+      this.sequenceNumber = sequenceNumber;
+      this.ins = ins;
     }
 
     /**
-     * We only rebuild the journal when it will halve the size of the journal
-     * and eliminate at least 2000 ops.
+     * Returns an editor for this snapshot's entry, or null if either the
+     * entry has changed since this snapshot was created or if another edit
+     * is in progress.
      */
-    private boolean journalRebuildRequired() {
-        final int redundantOpCompactThreshold = 2000;
-        return redundantOpCount >= redundantOpCompactThreshold
-                && redundantOpCount >= lruEntries.size();
+    public Editor edit() throws IOException {
+      return DiskLruCache.this.edit(key, sequenceNumber);
+    }
+
+    /** Returns the unbuffered stream with the value for {@code index}. */
+    public InputStream getInputStream(int index) {
+      return ins[index];
+    }
+
+    /** Returns the string value for {@code index}. */
+    public String getString(int index) throws IOException {
+      return inputStreamToString(getInputStream(index));
+    }
+
+    @Override public void close() {
+      for (InputStream in : ins) {
+        Util.closeQuietly(in);
+      }
+    }
+  }
+
+  /** Edits the values for an entry. */
+  public final class Editor {
+    private final Entry entry;
+    private final boolean[] written;
+    private boolean hasErrors;
+
+    private Editor(Entry entry) {
+      this.entry = entry;
+      this.written = (entry.readable) ? null : new boolean[valueCount];
     }
 
     /**
-     * Drops the entry for {@code key} if it exists and can be removed. Entries
-     * actively being edited cannot be removed.
-     *
-     * @return true if an entry was removed.
+     * Returns an unbuffered input stream to read the last committed value,
+     * or null if no value has been committed.
      */
-    public synchronized boolean remove(String key) throws IOException {
-        checkNotClosed();
-        validateKey(key);
-        Entry entry = lruEntries.get(key);
-        if (entry == null || entry.currentEditor != null) {
-            return false;
+    public InputStream newInputStream(int index) throws IOException {
+      synchronized (DiskLruCache.this) {
+        if (entry.currentEditor != this) {
+          throw new IllegalStateException();
         }
-
-        for (int i = 0; i < valueCount; i++) {
-            File file = entry.getCleanFile(i);
-            if (!file.delete()) {
-                throw new IOException("failed to delete " + file);
-            }
-            size -= entry.lengths[i];
-            entry.lengths[i] = 0;
+        if (!entry.readable) {
+          return null;
         }
-
-        redundantOpCount++;
-        journalWriter.append(REMOVE + ' ' + key + '\n');
-        lruEntries.remove(key);
-
-        if (journalRebuildRequired()) {
-            executorService.submit(cleanupCallable);
-        }
-
-        return true;
+        return new FileInputStream(entry.getCleanFile(index));
+      }
     }
 
     /**
-     * Returns true if this cache has been closed.
+     * Returns the last committed value as a string, or null if no value
+     * has been committed.
      */
-    public boolean isClosed() {
-        return journalWriter == null;
-    }
-
-    private void checkNotClosed() {
-        if (journalWriter == null) {
-            throw new IllegalStateException("cache is closed");
-        }
+    public String getString(int index) throws IOException {
+      InputStream in = newInputStream(index);
+      return in != null ? inputStreamToString(in) : null;
     }
 
     /**
-     * Force buffered operations to the filesystem.
+     * Returns a new unbuffered output stream to write the value at
+     * {@code index}. If the underlying output stream encounters errors
+     * when writing to the filesystem, this edit will be aborted when
+     * {@link #commit} is called. The returned output stream does not throw
+     * IOExceptions.
      */
-    public synchronized void flush() throws IOException {
-        checkNotClosed();
-        trimToSize();
-        journalWriter.flush();
+    public OutputStream newOutputStream(int index) throws IOException {
+      synchronized (DiskLruCache.this) {
+        if (entry.currentEditor != this) {
+          throw new IllegalStateException();
+        }
+        if (!entry.readable) {
+          written[index] = true;
+        }
+        return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+      }
+    }
+
+    /** 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), UTF_8);
+        writer.write(value);
+      } finally {
+        Util.closeQuietly(writer);
+      }
     }
 
     /**
-     * Closes this cache. Stored values will remain on the filesystem.
+     * Commits this edit so it is visible to readers.  This releases the
+     * edit lock so another edit may be started on the same key.
      */
-    public synchronized void close() throws IOException {
-        if (journalWriter == null) {
-            return; // already closed
-        }
-        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
-            if (entry.currentEditor != null) {
-                entry.currentEditor.abort();
-            }
-        }
-        trimToSize();
-        journalWriter.close();
-        journalWriter = null;
-    }
-
-    private void trimToSize() throws IOException {
-        while (size > maxSize) {
-            Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
-            remove(toEvict.getKey());
-        }
+    public void commit() throws IOException {
+      if (hasErrors) {
+        completeEdit(this, false);
+        remove(entry.key); // the previous entry is stale
+      } else {
+        completeEdit(this, true);
+      }
     }
 
     /**
-     * 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
-     * the cache.
+     * Aborts this edit. This releases the edit lock so another edit may be
+     * started on the same key.
      */
-    public void delete() throws IOException {
-        close();
-        Util.deleteContents(directory);
+    public void abort() throws IOException {
+      completeEdit(this, false);
     }
 
-    private void validateKey(String key) {
-        if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
-            throw new IllegalArgumentException(
-                    "keys must not contain spaces or newlines: \"" + key + "\"");
+    private final class FaultHidingOutputStream extends FilterOutputStream {
+      private FaultHidingOutputStream(OutputStream out) {
+        super(out);
+      }
+
+      @Override public void write(int oneByte) {
+        try {
+          out.write(oneByte);
+        } catch (IOException e) {
+          hasErrors = true;
         }
+      }
+
+      @Override public void write(byte[] buffer, int offset, int length) {
+        try {
+          out.write(buffer, offset, length);
+        } catch (IOException e) {
+          hasErrors = true;
+        }
+      }
+
+      @Override public void close() {
+        try {
+          out.close();
+        } catch (IOException e) {
+          hasErrors = true;
+        }
+      }
+
+      @Override public void flush() {
+        try {
+          out.flush();
+        } catch (IOException e) {
+          hasErrors = true;
+        }
+      }
+    }
+  }
+
+  private final class Entry {
+    private final String key;
+
+    /** Lengths of this entry's files. */
+    private final long[] lengths;
+
+    /** True if this entry has ever been published. */
+    private boolean readable;
+
+    /** The ongoing edit or null if this entry is not being edited. */
+    private Editor currentEditor;
+
+    /** The sequence number of the most recently committed edit to this entry. */
+    private long sequenceNumber;
+
+    private Entry(String key) {
+      this.key = key;
+      this.lengths = new long[valueCount];
     }
 
-    private static String inputStreamToString(InputStream in) throws IOException {
-        return Util.readFully(new InputStreamReader(in, UTF_8));
+    public String getLengths() throws IOException {
+      StringBuilder result = new StringBuilder();
+      for (long size : lengths) {
+        result.append(' ').append(size);
+      }
+      return result.toString();
     }
 
-    /**
-     * A snapshot of the values for an entry.
-     */
-    public final class Snapshot implements Closeable {
-        private final String key;
-        private final long sequenceNumber;
-        private final InputStream[] ins;
+    /** Set lengths using decimal numbers like "10123". */
+    private void setLengths(String[] strings) throws IOException {
+      if (strings.length != valueCount) {
+        throw invalidLengths(strings);
+      }
 
-        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
-            this.key = key;
-            this.sequenceNumber = sequenceNumber;
-            this.ins = ins;
+      try {
+        for (int i = 0; i < strings.length; i++) {
+          lengths[i] = Long.parseLong(strings[i]);
         }
-
-        /**
-         * Returns an editor for this snapshot's entry, or null if either the
-         * entry has changed since this snapshot was created or if another edit
-         * is in progress.
-         */
-        public Editor edit() throws IOException {
-            return DiskLruCache.this.edit(key, sequenceNumber);
-        }
-
-        /**
-         * Returns the unbuffered stream with the value for {@code index}.
-         */
-        public InputStream getInputStream(int index) {
-            return ins[index];
-        }
-
-        /**
-         * Returns the string value for {@code index}.
-         */
-        public String getString(int index) throws IOException {
-            return inputStreamToString(getInputStream(index));
-        }
-
-        @Override public void close() {
-            for (InputStream in : ins) {
-                Util.closeQuietly(in);
-            }
-        }
+      } catch (NumberFormatException e) {
+        throw invalidLengths(strings);
+      }
     }
 
-    /**
-     * Edits the values for an entry.
-     */
-    public final class Editor {
-        private final Entry entry;
-        private final boolean[] written;
-        private boolean hasErrors;
-
-        private Editor(Entry entry) {
-            this.entry = entry;
-            this.written = (entry.readable) ? null : new boolean[valueCount];
-        }
-
-        /**
-         * Returns an unbuffered input stream to read the last committed value,
-         * or null if no value has been committed.
-         */
-        public InputStream newInputStream(int index) throws IOException {
-            synchronized (DiskLruCache.this) {
-                if (entry.currentEditor != this) {
-                    throw new IllegalStateException();
-                }
-                if (!entry.readable) {
-                    return null;
-                }
-                return new FileInputStream(entry.getCleanFile(index));
-            }
-        }
-
-        /**
-         * Returns the last committed value as a string, or null if no value
-         * has been committed.
-         */
-        public String getString(int index) throws IOException {
-            InputStream in = newInputStream(index);
-            return in != null ? inputStreamToString(in) : null;
-        }
-
-        /**
-         * Returns a new unbuffered output stream to write the value at
-         * {@code index}. If the underlying output stream encounters errors
-         * when writing to the filesystem, this edit will be aborted when
-         * {@link #commit} is called. The returned output stream does not throw
-         * IOExceptions.
-         */
-        public OutputStream newOutputStream(int index) throws IOException {
-            synchronized (DiskLruCache.this) {
-                if (entry.currentEditor != this) {
-                    throw new IllegalStateException();
-                }
-                if (!entry.readable) {
-                    written[index] = true;
-                }
-                return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
-            }
-        }
-
-        /**
-         * 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), UTF_8);
-                writer.write(value);
-            } finally {
-                Util.closeQuietly(writer);
-            }
-        }
-
-        /**
-         * Commits this edit so it is visible to readers.  This releases the
-         * edit lock so another edit may be started on the same key.
-         */
-        public void commit() throws IOException {
-            if (hasErrors) {
-                completeEdit(this, false);
-                remove(entry.key); // the previous entry is stale
-            } else {
-                completeEdit(this, true);
-            }
-        }
-
-        /**
-         * Aborts this edit. This releases the edit lock so another edit may be
-         * started on the same key.
-         */
-        public void abort() throws IOException {
-            completeEdit(this, false);
-        }
-
-        private final class FaultHidingOutputStream extends FilterOutputStream {
-            private FaultHidingOutputStream(OutputStream out) {
-                super(out);
-            }
-
-            @Override public void write(int oneByte) {
-                try {
-                    out.write(oneByte);
-                } catch (IOException e) {
-                    hasErrors = true;
-                }
-            }
-
-            @Override public void write(byte[] buffer, int offset, int length) {
-                try {
-                    out.write(buffer, offset, length);
-                } catch (IOException e) {
-                    hasErrors = true;
-                }
-            }
-
-            @Override public void close() {
-                try {
-                    out.close();
-                } catch (IOException e) {
-                    hasErrors = true;
-                }
-            }
-
-            @Override public void flush() {
-                try {
-                    out.flush();
-                } catch (IOException e) {
-                    hasErrors = true;
-                }
-            }
-        }
+    private IOException invalidLengths(String[] strings) throws IOException {
+      throw new IOException("unexpected journal line: " + Arrays.toString(strings));
     }
 
-    private final class Entry {
-        private final String key;
-
-        /** Lengths of this entry's files. */
-        private final long[] lengths;
-
-        /** True if this entry has ever been published. */
-        private boolean readable;
-
-        /** The ongoing edit or null if this entry is not being edited. */
-        private Editor currentEditor;
-
-        /** The sequence number of the most recently committed edit to this entry. */
-        private long sequenceNumber;
-
-        private Entry(String key) {
-            this.key = key;
-            this.lengths = new long[valueCount];
-        }
-
-        public String getLengths() throws IOException {
-            StringBuilder result = new StringBuilder();
-            for (long size : lengths) {
-                result.append(' ').append(size);
-            }
-            return result.toString();
-        }
-
-        /**
-         * Set lengths using decimal numbers like "10123".
-         */
-        private void setLengths(String[] strings) throws IOException {
-            if (strings.length != valueCount) {
-                throw invalidLengths(strings);
-            }
-
-            try {
-                for (int i = 0; i < strings.length; i++) {
-                    lengths[i] = Long.parseLong(strings[i]);
-                }
-            } catch (NumberFormatException e) {
-                throw invalidLengths(strings);
-            }
-        }
-
-        private IOException invalidLengths(String[] strings) throws IOException {
-            throw new IOException("unexpected journal line: " + Arrays.toString(strings));
-        }
-
-        public File getCleanFile(int i) {
-            return new File(directory, key + "." + i);
-        }
-
-        public File getDirtyFile(int i) {
-            return new File(directory, key + "." + i + ".tmp");
-        }
+    public File getCleanFile(int i) {
+      return new File(directory, key + "." + i);
     }
+
+    public File getDirtyFile(int i) {
+      return new File(directory, key + "." + i + ".tmp");
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/Dns.java b/src/main/java/com/squareup/okhttp/internal/Dns.java
index 37fa609..69b2d37 100644
--- a/src/main/java/com/squareup/okhttp/internal/Dns.java
+++ b/src/main/java/com/squareup/okhttp/internal/Dns.java
@@ -23,11 +23,11 @@
  * make code more testable.
  */
 public interface Dns {
-    Dns DEFAULT = new Dns() {
-        @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
-            return InetAddress.getAllByName(host);
-        }
-    };
+  Dns DEFAULT = new Dns() {
+    @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+      return InetAddress.getAllByName(host);
+    }
+  };
 
-    InetAddress[] getAllByName(String host) throws UnknownHostException;
+  InetAddress[] getAllByName(String host) throws UnknownHostException;
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/Platform.java b/src/main/java/com/squareup/okhttp/internal/Platform.java
index ab71c62..75cd66f 100644
--- a/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -47,297 +47,297 @@
  * public API in Java 7 and callable via reflection in Android 4.1+.
  */
 public class Platform {
-    private static final Platform PLATFORM = findPlatform();
+  private static final Platform PLATFORM = findPlatform();
 
-    private Constructor<DeflaterOutputStream> deflaterConstructor;
+  private Constructor<DeflaterOutputStream> deflaterConstructor;
 
-    public static Platform get() {
-        return PLATFORM;
+  public static Platform get() {
+    return PLATFORM;
+  }
+
+  public void logW(String warning) {
+    System.out.println(warning);
+  }
+
+  public void tagSocket(Socket socket) throws SocketException {
+  }
+
+  public void untagSocket(Socket socket) throws SocketException {
+  }
+
+  public URI toUriLenient(URL url) throws URISyntaxException {
+    return url.toURI(); // this isn't as good as the built-in toUriLenient
+  }
+
+  /**
+   * Attempt a TLS connection with useful extensions enabled. This mode
+   * supports more features, but is less likely to be compatible with older
+   * HTTPS servers.
+   */
+  public void enableTlsExtensions(SSLSocket socket, String uriHost) {
+  }
+
+  /**
+   * Attempt a secure connection with basic functionality to maximize
+   * compatibility. Currently this uses SSL 3.0.
+   */
+  public void supportTlsIntolerantServer(SSLSocket socket) {
+    socket.setEnabledProtocols(new String[] {"SSLv3"});
+  }
+
+  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
+  public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+    return null;
+  }
+
+  /**
+   * 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) {
+  }
+
+  /**
+   * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
+   * value blocks. This throws an {@link UnsupportedOperationException} on
+   * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
+   */
+  public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
+      boolean syncFlush) {
+    try {
+      Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
+      if (constructor == null) {
+        constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
+            OutputStream.class, Deflater.class, boolean.class);
+      }
+      return constructor.newInstance(out, deflater, syncFlush);
+    } catch (NoSuchMethodException e) {
+      throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
+    } catch (InvocationTargetException e) {
+      throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
+          : new RuntimeException(e.getCause());
+    } catch (InstantiationException e) {
+      throw new RuntimeException(e);
+    } catch (IllegalAccessException e) {
+      throw new AssertionError();
+    }
+  }
+
+  /** Attempt to match the host runtime to a capable Platform implementation. */
+  private static Platform findPlatform() {
+    // Attempt to find Android 2.3+ APIs.
+    Class<?> openSslSocketClass;
+    Method setUseSessionTickets;
+    Method setHostname;
+    try {
+      openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+      setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
+      setHostname = openSslSocketClass.getMethod("setHostname", String.class);
+
+      // Attempt to find Android 4.1+ APIs.
+      try {
+        Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
+        Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
+        return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
+            getNpnSelectedProtocol);
+      } catch (NoSuchMethodException ignored) {
+        return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
+      }
+    } catch (ClassNotFoundException ignored) {
+      // This isn't an Android runtime.
+    } catch (NoSuchMethodException ignored) {
+      // This isn't Android 2.3 or better.
     }
 
-    public void logW(String warning) {
-        System.out.println(warning);
+    // Attempt to find the Jetty's NPN extension for OpenJDK.
+    try {
+      String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
+      Class<?> nextProtoNegoClass = Class.forName(npnClassName);
+      Class<?> providerClass = Class.forName(npnClassName + "$Provider");
+      Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
+      Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
+      Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
+      Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
+      return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
+          serverProviderClass);
+    } catch (ClassNotFoundException ignored) {
+      return new Platform(); // NPN isn't on the classpath.
+    } catch (NoSuchMethodException ignored) {
+      return new Platform(); // The NPN version isn't what we expect.
+    }
+  }
+
+  /**
+   * Android version 2.3 and newer support TLS session tickets and server name
+   * indication (SNI).
+   */
+  private static class Android23 extends Platform {
+    protected final Class<?> openSslSocketClass;
+    private final Method setUseSessionTickets;
+    private final Method setHostname;
+
+    private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
+        Method setHostname) {
+      this.openSslSocketClass = openSslSocketClass;
+      this.setUseSessionTickets = setUseSessionTickets;
+      this.setHostname = setHostname;
     }
 
-    public void tagSocket(Socket socket) throws SocketException {
-    }
-
-    public void untagSocket(Socket socket) throws SocketException {
-    }
-
-    public URI toUriLenient(URL url) throws URISyntaxException {
-        return url.toURI(); // this isn't as good as the built-in toUriLenient
-    }
-
-    /**
-     * Attempt a TLS connection with useful extensions enabled. This mode
-     * supports more features, but is less likely to be compatible with older
-     * HTTPS servers.
-     */
-    public void enableTlsExtensions(SSLSocket socket, String uriHost) {
-    }
-
-    /**
-     * Attempt a secure connection with basic functionality to maximize
-     * compatibility. Currently this uses SSL 3.0.
-     */
-    public void supportTlsIntolerantServer(SSLSocket socket) {
-        socket.setEnabledProtocols(new String[]{"SSLv3"});
-    }
-
-    /**
-     * Returns the negotiated protocol, or null if no protocol was negotiated.
-     */
-    public byte[] getNpnSelectedProtocol(SSLSocket socket) {
-        return null;
-    }
-
-    /**
-     * 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) {
-    }
-
-    /**
-     * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
-     * value blocks. This throws an {@link UnsupportedOperationException} on
-     * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
-     */
-    public OutputStream newDeflaterOutputStream(
-            OutputStream out, Deflater deflater, boolean syncFlush) {
+    @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
+      super.enableTlsExtensions(socket, uriHost);
+      if (openSslSocketClass.isInstance(socket)) {
+        // This is Android: use reflection on OpenSslSocketImpl.
         try {
-            Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
-            if (constructor == null) {
-                constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
-                        OutputStream.class, Deflater.class, boolean.class);
-            }
-            return constructor.newInstance(out, deflater, syncFlush);
-        } catch (NoSuchMethodException e) {
-            throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
+          setUseSessionTickets.invoke(socket, true);
+          setHostname.invoke(socket, uriHost);
         } catch (InvocationTargetException e) {
-            throw e.getCause() instanceof RuntimeException
-                    ? (RuntimeException) e.getCause()
-                    : new RuntimeException(e.getCause());
-        } catch (InstantiationException e) {
-            throw new RuntimeException(e);
+          throw new RuntimeException(e);
         } catch (IllegalAccessException e) {
-            throw new AssertionError();
+          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(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
+        Method setNpnProtocols, Method getNpnSelectedProtocol) {
+      super(openSslSocketClass, setUseSessionTickets, setHostname);
+      this.setNpnProtocols = setNpnProtocols;
+      this.getNpnSelectedProtocol = getNpnSelectedProtocol;
     }
 
-    /**
-     * Attempt to match the host runtime to a capable Platform implementation.
-     */
-    private static Platform findPlatform() {
-        // Attempt to find Android 2.3+ APIs.
-        Class<?> openSslSocketClass;
-        Method setUseSessionTickets;
-        Method setHostname;
-        try {
-            openSslSocketClass = Class.forName(
-                    "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
-            setUseSessionTickets = openSslSocketClass.getMethod(
-                    "setUseSessionTickets", boolean.class);
-            setHostname = openSslSocketClass.getMethod("setHostname", String.class);
-
-            // Attempt to find Android 4.1+ APIs.
-            try {
-                Method setNpnProtocols = openSslSocketClass.getMethod(
-                        "setNpnProtocols", byte[].class);
-                Method getNpnSelectedProtocol = openSslSocketClass.getMethod(
-                        "getNpnSelectedProtocol");
-                return new Android41(openSslSocketClass, setUseSessionTickets, setHostname,
-                        setNpnProtocols, getNpnSelectedProtocol);
-            } catch (NoSuchMethodException ignored) {
-                return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
-            }
-        } catch (ClassNotFoundException ignored) {
-            // This isn't an Android runtime.
-        } catch (NoSuchMethodException ignored) {
-            // This isn't Android 2.3 or better.
-        }
-
-        // Attempt to find the Jetty's NPN extension for OpenJDK.
-        try {
-            String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
-            Class<?> nextProtoNegoClass = Class.forName(npnClassName);
-            Class<?> providerClass = Class.forName(npnClassName + "$Provider");
-            Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
-            Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
-            Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
-            return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass);
-        } catch (ClassNotFoundException ignored) {
-            return new Platform(); // NPN isn't on the classpath.
-        } catch (NoSuchMethodException ignored) {
-            return new Platform(); // The NPN version isn't what we expect.
-        }
+    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
+      if (!openSslSocketClass.isInstance(socket)) {
+        return;
+      }
+      try {
+        setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
     }
 
-    /**
-     * Android version 2.3 and newer support TLS session tickets and server name
-     * indication (SNI).
-     */
-    private static class Android23 extends Platform {
-        protected final Class<?> openSslSocketClass;
-        private final Method setUseSessionTickets;
-        private final Method setHostname;
+    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+      if (!openSslSocketClass.isInstance(socket)) {
+        return null;
+      }
+      try {
+        return (byte[]) getNpnSelectedProtocol.invoke(socket);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    }
+  }
 
-        private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
-                Method setHostname) {
-            this.openSslSocketClass = openSslSocketClass;
-            this.setUseSessionTickets = setUseSessionTickets;
-            this.setHostname = setHostname;
-        }
+  /**
+   * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
+   * path.
+   */
+  private static class JdkWithJettyNpnPlatform extends Platform {
+    private final Method getMethod;
+    private final Method putMethod;
+    private final Class<?> clientProviderClass;
+    private final Class<?> serverProviderClass;
 
-        @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);
-                }
-            }
-        }
+    public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
+        Class<?> serverProviderClass) {
+      this.putMethod = putMethod;
+      this.getMethod = getMethod;
+      this.clientProviderClass = clientProviderClass;
+      this.serverProviderClass = serverProviderClass;
     }
 
-    /**
-     * Android version 4.1 and newer support NPN.
-     */
-    private static class Android41 extends Android23 {
-        private final Method setNpnProtocols;
-        private final Method getNpnSelectedProtocol;
-
-        private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets,
-                Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
-            super(openSslSocketClass, setUseSessionTickets, setHostname);
-            this.setNpnProtocols = setNpnProtocols;
-            this.getNpnSelectedProtocol = getNpnSelectedProtocol;
+    @Override public void setNpnProtocols(SSLSocket socket, byte[] 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;
         }
-
-        @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
-            if (!openSslSocketClass.isInstance(socket)) {
-                return;
-            }
-            try {
-                setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
-            } catch (IllegalAccessException e) {
-                throw new AssertionError(e);
-            } catch (InvocationTargetException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
-            if (!openSslSocketClass.isInstance(socket)) {
-                return null;
-            }
-            try {
-                return (byte[]) getNpnSelectedProtocol.invoke(socket);
-            } catch (InvocationTargetException e) {
-                throw new RuntimeException(e);
-            } catch (IllegalAccessException e) {
-                throw new AssertionError(e);
-            }
-        }
+        Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
+            new Class[] {clientProviderClass, serverProviderClass},
+            new JettyNpnProvider(strings));
+        putMethod.invoke(null, socket, provider);
+      } catch (UnsupportedEncodingException e) {
+        throw new AssertionError(e);
+      } catch (InvocationTargetException e) {
+        throw new AssertionError(e);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
     }
 
-    /**
-     * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
-     * path.
-     */
-    private static class JdkWithJettyNpnPlatform extends Platform {
-        private final Method getMethod;
-        private final Method putMethod;
-        private final Class<?> clientProviderClass;
-
-        public JdkWithJettyNpnPlatform(
-                Method putMethod, Method getMethod, Class<?> clientProviderClass) {
-            this.putMethod = putMethod;
-            this.getMethod = getMethod;
-            this.clientProviderClass = clientProviderClass;
+    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
+      try {
+        JettyNpnProvider provider =
+            (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
+        if (!provider.unsupported && provider.selected == null) {
+          Logger logger = Logger.getLogger(OkHttpClient.class.getName());
+          logger.log(Level.INFO,
+              "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();
+      } catch (InvocationTargetException e) {
+        throw new AssertionError();
+      } catch (IllegalAccessException e) {
+        throw new AssertionError();
+      }
+    }
+  }
 
-        @Override public void setNpnProtocols(SSLSocket socket, byte[] 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;
-                }
-                Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
-                        new Class[] {clientProviderClass}, new JettyNpnProvider(strings));
-                putMethod.invoke(null, socket, provider);
-            } catch (UnsupportedEncodingException e) {
-                throw new AssertionError(e);
-            } catch (InvocationTargetException e) {
-                throw new AssertionError(e);
-            } catch (IllegalAccessException e) {
-                throw new AssertionError(e);
-            }
-        }
+  /**
+   * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
+   * without a compile-time dependency on those interfaces.
+   */
+  private static class JettyNpnProvider implements InvocationHandler {
+    private final List<String> protocols;
+    private boolean unsupported;
+    private String selected;
 
-        @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
-            try {
-                JettyNpnProvider provider = (JettyNpnProvider) Proxy.getInvocationHandler(
-                        getMethod.invoke(null, socket));
-                if (!provider.unsupported && provider.selected == null) {
-                    Logger logger = Logger.getLogger(OkHttpClient.class.getName());
-                    logger.log(Level.INFO, "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();
-            } catch (InvocationTargetException e) {
-                throw new AssertionError();
-            } catch (IllegalAccessException e) {
-                throw new AssertionError();
-            }
-        }
+    public JettyNpnProvider(List<String> protocols) {
+      this.protocols = protocols;
     }
 
-    /**
-     * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
-     * without a compile-time dependency on those interfaces.
-     */
-    private static class JettyNpnProvider implements InvocationHandler {
-        private final List<String> clientProtocols;
-        private boolean unsupported;
-        private String selected;
-
-        public JettyNpnProvider(List<String> clientProtocols) {
-            this.clientProtocols = clientProtocols;
-        }
-
-        @Override public Object invoke(Object proxy, Method method, Object[] args)
-                throws Throwable {
-            String methodName = method.getName();
-            Class<?> returnType = method.getReturnType();
-            if (methodName.equals("supports") && boolean.class == returnType) {
-                return true;
-            } else if (methodName.equals("unsupported") && void.class == returnType) {
-                this.unsupported = true;
-                return null;
-            } else if (methodName.equals("selectProtocol") && 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];
-                System.out.println("CLIENT PROTOCOLS: " + clientProtocols + " SERVER PROTOCOLS: " + serverProtocols);
-                this.selected = clientProtocols.get(0);
-                return selected;
-            } else {
-                return method.invoke(this, args);
-            }
-        }
+    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+      String methodName = method.getName();
+      Class<?> returnType = method.getReturnType();
+      if (args == null) {
+        args = Util.EMPTY_STRING_ARRAY;
+      }
+      if (methodName.equals("supports") && boolean.class == returnType) {
+        return true;
+      } else if (methodName.equals("unsupported") && void.class == returnType) {
+        this.unsupported = true;
+        return null;
+      } else if (methodName.equals("protocols") && args.length == 0) {
+        return protocols;
+      } else if (methodName.equals("selectProtocol")
+          && 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;
+      } else if (methodName.equals("protocolSelected") && args.length == 1) {
+        this.selected = (String) args[0];
+        return null;
+      } else {
+        return method.invoke(this, args);
+      }
     }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
index 9011b2c..93f1754 100644
--- a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java
@@ -16,9 +16,6 @@
 
 package com.squareup.okhttp.internal;
 
-import static com.squareup.okhttp.internal.Util.ISO_8859_1;
-import static com.squareup.okhttp.internal.Util.US_ASCII;
-import static com.squareup.okhttp.internal.Util.UTF_8;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.EOFException;
@@ -26,6 +23,10 @@
 import java.io.InputStream;
 import java.nio.charset.Charset;
 
+import static com.squareup.okhttp.internal.Util.ISO_8859_1;
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
 /**
  * Buffers input from an {@link InputStream} for reading lines.
  *
@@ -45,188 +46,186 @@
  * 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 static final byte CR = (byte) '\r';
+  private static final byte LF = (byte) '\n';
 
-    private final InputStream in;
-    private final Charset charset;
+  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;
+  // 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 StrictLineReader} with the default capacity and charset.
-     *
-     * @param in the {@code InputStream} to read data from.
-     * @throws NullPointerException if {@code in} is null.
-     */
-    public StrictLineReader(InputStream in) {
-        this(in, 8192);
+  /**
+   * Constructs a new {@code StrictLineReader} with the default capacity and charset.
+   *
+   * @param in the {@code InputStream} to read data from.
+   * @throws NullPointerException if {@code in} is null.
+   */
+  public StrictLineReader(InputStream in) {
+    this(in, 8192);
+  }
+
+  /**
+   * Constructs a new {@code LineReader} with the specified capacity and the default charset.
+   *
+   * @param in the {@code InputStream} to read data from.
+   * @param capacity the capacity of the buffer.
+   * @throws NullPointerException if {@code in} is null.
+   * @throws IllegalArgumentException for negative or zero {@code capacity}.
+   */
+  public StrictLineReader(InputStream in, int capacity) {
+    this(in, capacity, US_ASCII);
+  }
+
+  /**
+   * 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 is 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 is 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(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) {
+      throw new IllegalArgumentException("Unsupported encoding");
     }
 
-    /**
-     * Constructs a new {@code LineReader} with the specified capacity and the default charset.
-     *
-     * @param in the {@code InputStream} to read data from.
-     * @param capacity the capacity of the buffer.
-     * @throws NullPointerException if {@code in} is null.
-     * @throws IllegalArgumentException for negative or zero {@code capacity}.
-     */
-    public StrictLineReader(InputStream in, int capacity) {
-        this(in, capacity, US_ASCII);
-    }
+    this.in = in;
+    this.charset = charset;
+    buf = new byte[capacity];
+  }
 
-    /**
-     * 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 is 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);
+  /**
+   * 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}.
+   */
+  @Override
+  public void close() throws IOException {
+    synchronized (in) {
+      if (buf != null) {
+        buf = null;
+        in.close();
+      }
     }
+  }
 
-    /**
-     * 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 is 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();
+  /**
+   * 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);
+          pos = i + 1;
+          return res;
         }
-        if (capacity < 0) {
-            throw new IllegalArgumentException("capacity <= 0");
-        }
-        if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) {
-            throw new IllegalArgumentException("Unsupported encoding");
-        }
+      }
 
-        this.in = in;
-        this.charset = charset;
-        buf = new byte[capacity];
-    }
+      // 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;
+          return new String(buf, 0, length, charset);
+        }
+      };
 
-    /**
-     * 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}.
-     */
-    @Override
-    public void close() throws IOException {
-        synchronized (in) {
-            if (buf != null) {
-                buf = null;
-                in.close();
+      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();
+          }
         }
+      }
     }
+  }
 
-    /**
-     * 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);
-                    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;
-                    return new String(buf, 0, length, charset);
-                }
-            };
-
-            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 + "\"");
     }
+  }
 
-    /**
-     * 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.
+   *
+   * @throws IOException for underlying {@code InputStream} errors.
+   * @throws EOFException for the end of source stream.
+   */
+  private void fillBuf() throws IOException {
+    int result = in.read(buf, 0, buf.length);
+    if (result == -1) {
+      throw new EOFException();
     }
-
-    /**
-     * Reads new input data into the buffer. Call only with pos == end or end == -1,
-     * depending on the desired outcome if the function throws.
-     *
-     * @throws IOException for underlying {@code InputStream} errors.
-     * @throws EOFException for the end of source stream.
-     */
-    private void fillBuf() throws IOException {
-        int result = in.read(buf, 0, buf.length);
-        if (result == -1) {
-            throw new EOFException();
-        }
-        pos = 0;
-        end = result;
-    }
+    pos = 0;
+    end = result;
+  }
 }
 
diff --git a/src/main/java/com/squareup/okhttp/internal/Util.java b/src/main/java/com/squareup/okhttp/internal/Util.java
index f47362c..994a646 100644
--- a/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -24,293 +24,313 @@
 import java.io.OutputStream;
 import java.io.Reader;
 import java.io.StringWriter;
+import java.net.Socket;
 import java.net.URI;
 import java.net.URL;
 import java.nio.ByteOrder;
 import java.nio.charset.Charset;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicReference;
 
-/**
- * Junk drawer of utility methods.
- */
+/** Junk drawer of utility methods. */
 public final class Util {
-    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+  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 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 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[]>();
+  /** 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 Util() {
+  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 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);
+  }
+
+  /** Recursively delete everything in {@code dir}. */
+  // TODO: this should specify paths as Strings rather than as Files
+  public static void deleteContents(File dir) throws IOException {
+    File[] files = dir.listFiles();
+    if (files == null) {
+      throw new IllegalArgumentException("not a 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 {
+    // acquire the shared skip buffer.
+    byte[] buffer = skipBuffer.getAndSet(null);
+    if (buffer == null) {
+      buffer = new byte[4096];
     }
 
-    public static int getEffectivePort(URI uri) {
-        return getEffectivePort(uri.getScheme(), uri.getPort());
+    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;
+      }
     }
 
-    public static int getEffectivePort(URL url) {
-        return getEffectivePort(url.getProtocol(), url.getPort());
+    // 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;
+  }
 
-    private static int getEffectivePort(String scheme, int specifiedPort) {
-        return specifiedPort != -1
-                ? specifiedPort
-                : getDefaultPort(scheme);
+  /**
+   * 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);
     }
-
-    public static int getDefaultPort(String scheme) {
-        if ("http".equalsIgnoreCase(scheme)) {
-            return 80;
-        } else if ("https".equalsIgnoreCase(scheme)) {
-            return 443;
-        } else {
-            return -1;
-        }
+    int length = result.length();
+    if (length > 0 && result.charAt(length - 1) == '\r') {
+      result.setLength(length - 1);
     }
+    return result.toString();
+  }
 
-    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 'closeable', ignoring any checked exceptions. Does nothing if '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 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);
-    }
-
-    /**
-     * Recursively delete everything in {@code dir}.
-     */
-    // TODO: this should specify paths as Strings rather than as Files
-    public static void deleteContents(File dir) throws IOException {
-        File[] files = dir.listFiles();
-        if (files == null) {
-            throw new IllegalArgumentException("not a 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 {
-        // 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();
-    }
+  public static ThreadFactory newThreadFactory(final String name, final boolean daemon) {
+    return new ThreadFactory() {
+      @Override public Thread newThread(Runnable r) {
+        Thread result = new Thread(r, name);
+        result.setDaemon(daemon);
+        return result;
+      }
+    };
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
index d915552..187f3b6 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java
@@ -33,75 +33,75 @@
  * 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;
+  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;
+  AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, CacheRequest cacheRequest)
+      throws IOException {
+    this.in = in;
+    this.httpEngine = httpEngine;
 
-        OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
+    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;
+    // some apps return a null body; for compatibility we treat that like a null cache request
+    if (cacheBody == null) {
+      cacheRequest = null;
     }
 
-    /**
-     * 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);
-    }
+    this.cacheBody = cacheBody;
+    this.cacheRequest = cacheRequest;
+  }
 
-    protected final void checkNotClosed() throws IOException {
-        if (closed) {
-            throw new IOException("stream closed");
-        }
-    }
+  /**
+   * 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 cacheWrite(byte[] buffer, int offset, int count) throws IOException {
-        if (cacheBody != null) {
-            cacheBody.write(buffer, offset, count);
-        }
+  protected final void checkNotClosed() throws IOException {
+    if (closed) {
+      throw new IOException("stream closed");
     }
+  }
 
-    /**
-     * 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 reuseSocket) throws IOException {
-        if (cacheRequest != null) {
-            cacheBody.close();
-        }
-        httpEngine.release(reuseSocket);
+  protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException {
+    if (cacheBody != null) {
+      cacheBody.write(buffer, offset, count);
     }
+  }
 
-    /**
-     * 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(false);
+  /**
+   * 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 streamCancelled) throws IOException {
+    if (cacheRequest != null) {
+      cacheBody.close();
     }
+    httpEngine.release(streamCancelled);
+  }
+
+  /**
+   * 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/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
index 5d835cc..90675b0 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java
@@ -26,15 +26,15 @@
  * requests to the same server, subclasses should not close the socket stream.
  */
 abstract class AbstractHttpOutputStream extends OutputStream {
-    protected boolean closed;
+  protected boolean closed;
 
-    @Override public final void write(int data) throws IOException {
-        write(new byte[] {(byte) data});
-    }
+  @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");
-        }
+  protected final void checkNotClosed() throws IOException {
+    if (closed) {
+      throw new IOException("stream closed");
     }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java b/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
index 0dd096e..12e6409 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
@@ -18,97 +18,95 @@
 
 final class HeaderParser {
 
-    public interface CacheControlHandler {
-        void handle(String directive, String parameter);
+  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);
     }
+  }
 
-    /**
-     * 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);
-        }
+  /**
+   * Returns the next index in {@code input} at or after {@code pos} that
+   * contains a character from {@code characters}. Returns the input length if
+   * none of the requested characters can be found.
+   */
+  public static int skipUntil(String input, int pos, String characters) {
+    for (; pos < input.length(); pos++) {
+      if (characters.indexOf(input.charAt(pos)) != -1) {
+        break;
+      }
     }
+    return pos;
+  }
 
-    /**
-     * Returns the next index in {@code input} at or after {@code pos} that
-     * contains a character from {@code characters}. Returns the input length if
-     * none of the requested characters can be found.
-     */
-    public static int skipUntil(String input, int pos, String characters) {
-        for (; pos < input.length(); pos++) {
-            if (characters.indexOf(input.charAt(pos)) != -1) {
-                break;
-            }
-        }
-        return pos;
+  /**
+   * Returns the next non-whitespace character in {@code input} that is white
+   * space. Result is undefined if input contains newline characters.
+   */
+  public static int skipWhitespace(String input, int pos) {
+    for (; pos < input.length(); pos++) {
+      char c = input.charAt(pos);
+      if (c != ' ' && c != '\t') {
+        break;
+      }
     }
+    return pos;
+  }
 
-    /**
-     * Returns the next non-whitespace character in {@code input} that is white
-     * space. Result is undefined if input contains newline characters.
-     */
-    public static int skipWhitespace(String input, int pos) {
-        for (; pos < input.length(); pos++) {
-            char c = input.charAt(pos);
-            if (c != ' ' && c != '\t') {
-                break;
-            }
-        }
-        return pos;
+  /**
+   * Returns {@code value} as a positive integer, or 0 if it is negative, or
+   * -1 if it cannot be parsed.
+   */
+  public static int parseSeconds(String value) {
+    try {
+      long seconds = Long.parseLong(value);
+      if (seconds > Integer.MAX_VALUE) {
+        return Integer.MAX_VALUE;
+      } else if (seconds < 0) {
+        return 0;
+      } else {
+        return (int) seconds;
+      }
+    } catch (NumberFormatException e) {
+      return -1;
     }
+  }
 
-    /**
-     * Returns {@code value} as a positive integer, or 0 if it is negative, or
-     * -1 if it cannot be parsed.
-     */
-    public static int parseSeconds(String value) {
-        try {
-            long seconds = Long.parseLong(value);
-            if (seconds > Integer.MAX_VALUE) {
-                return Integer.MAX_VALUE;
-            } else if (seconds < 0) {
-                return 0;
-            } else {
-                return (int) seconds;
-            }
-        } catch (NumberFormatException e) {
-            return -1;
-        }
-    }
-
-    private HeaderParser() {
-    }
+  private HeaderParser() {
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
index 70104a5..4ccd12a 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
@@ -19,8 +19,6 @@
 import com.squareup.okhttp.internal.Base64;
 import java.io.IOException;
 import java.net.Authenticator;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.PasswordAuthentication;
@@ -29,158 +27,149 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/**
- * Handles HTTP authentication headers from origin and proxy servers.
- */
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+
+/** Handles HTTP authentication headers from origin and proxy servers. */
 public final class HttpAuthenticator {
-    private HttpAuthenticator() {
+  private HttpAuthenticator() {
+  }
+
+  /**
+   * 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.
+   */
+  public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
+      RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
+    if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
+      throw new IllegalArgumentException();
     }
 
-    /**
-     * 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.
-     */
-    public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders,
-            RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException {
-        if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) {
-            throw new IllegalArgumentException();
-        }
-
-        // Keep asking for username/password until authorized.
-        String challengeHeader = responseCode == HTTP_PROXY_AUTH
-                ? "Proxy-Authenticate"
-                : "WWW-Authenticate";
-        String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
-        if (credentials == null) {
-            return false; // Could not find credentials so end the request cycle.
-        }
-
-        // Add authorization credentials, bypassing the already-connected check.
-        String fieldName = responseCode == HTTP_PROXY_AUTH
-                ? "Proxy-Authorization"
-                : "Authorization";
-        successorRequestHeaders.set(fieldName, credentials);
-        return true;
+    // Keep asking for username/password until authorized.
+    String challengeHeader =
+        responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate";
+    String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url);
+    if (credentials == null) {
+      return false; // Could not find credentials so end the request cycle.
     }
 
-    /**
-     * Returns the authorization credentials that may satisfy the challenge.
-     * Returns null if a challenge header was not provided or if credentials
-     * were not available.
-     */
-    private static String getCredentials(RawHeaders responseHeaders,
-            String challengeHeader, Proxy proxy, URL url) throws IOException {
-        List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
-        if (challenges.isEmpty()) {
-            return null;
-        }
+    // Add authorization credentials, bypassing the already-connected check.
+    String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization";
+    successorRequestHeaders.set(fieldName, credentials);
+    return true;
+  }
 
-        for (Challenge challenge : challenges) {
-            // Use the global authenticator to get the password.
-            PasswordAuthentication auth;
-            if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
-                InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
-                auth = Authenticator.requestPasswordAuthentication(
-                        proxyAddress.getHostName(), getConnectToInetAddress(proxy, url),
-                        proxyAddress.getPort(), url.getProtocol(), challenge.realm,
-                        challenge.scheme, url, Authenticator.RequestorType.PROXY);
-            } else {
-                auth = Authenticator.requestPasswordAuthentication(
-                        url.getHost(), getConnectToInetAddress(proxy, url), url.getPort(),
-                        url.getProtocol(), challenge.realm, challenge.scheme, url,
-                        Authenticator.RequestorType.SERVER);
-            }
-            if (auth == null) {
-                continue;
-            }
-
-            // Use base64 to encode the username and password.
-            String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
-            byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
-            String encoded = Base64.encode(bytes);
-            return challenge.scheme + " " + encoded;
-        }
-
-        return null;
+  /**
+   * Returns the authorization credentials that may satisfy the challenge.
+   * Returns null if a challenge header was not provided or if credentials
+   * were not available.
+   */
+  private static String getCredentials(RawHeaders responseHeaders, String challengeHeader,
+      Proxy proxy, URL url) throws IOException {
+    List<Challenge> challenges = parseChallenges(responseHeaders, challengeHeader);
+    if (challenges.isEmpty()) {
+      return null;
     }
 
-    private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
-        return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
-                ? ((InetSocketAddress) proxy.address()).getAddress()
-                : InetAddress.getByName(url.getHost());
+    for (Challenge challenge : challenges) {
+      // Use the global authenticator to get the password.
+      PasswordAuthentication auth;
+      if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) {
+        InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+        auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(),
+            getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(),
+            challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY);
+      } else {
+        auth = Authenticator.requestPasswordAuthentication(url.getHost(),
+            getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm,
+            challenge.scheme, url, Authenticator.RequestorType.SERVER);
+      }
+      if (auth == null) {
+        continue;
+      }
+
+      // Use base64 to encode the username and password.
+      String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword());
+      byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
+      String encoded = Base64.encode(bytes);
+      return challenge.scheme + " " + encoded;
     }
 
-    /**
-     * Parse RFC 2617 challenges. This API is only interested in the scheme
-     * name and realm.
-     */
-    private static List<Challenge> parseChallenges(
-            RawHeaders responseHeaders, String challengeHeader) {
-        /*
-         * auth-scheme = token
-         * auth-param  = token "=" ( token | quoted-string )
-         * challenge   = auth-scheme 1*SP 1#auth-param
-         * 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))) {
-                continue;
-            }
-            String value = responseHeaders.getValue(h);
-            int pos = 0;
-            while (pos < value.length()) {
-                int tokenStart = pos;
-                pos = HeaderParser.skipUntil(value, pos, " ");
+    return null;
+  }
 
-                String scheme = value.substring(tokenStart, pos).trim();
-                pos = HeaderParser.skipWhitespace(value, pos);
+  private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+    return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
+        ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost());
+  }
 
-                // TODO: This currently only handles schemes with a 'realm' parameter;
-                //       It needs to be fixed to handle any scheme and any parameters
-                //       http://code.google.com/p/android/issues/detail?id=11140
+  /**
+   * Parse RFC 2617 challenges. This API is only interested in the scheme
+   * name and realm.
+   */
+  private static List<Challenge> parseChallenges(RawHeaders responseHeaders,
+      String challengeHeader) {
+    // auth-scheme = token
+    // auth-param  = token "=" ( token | quoted-string )
+    // challenge   = auth-scheme 1*SP 1#auth-param
+    // 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))) {
+        continue;
+      }
+      String value = responseHeaders.getValue(h);
+      int pos = 0;
+      while (pos < value.length()) {
+        int tokenStart = pos;
+        pos = HeaderParser.skipUntil(value, pos, " ");
 
-                if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
-                    break; // Unexpected challenge parameter; give up!
-                }
+        String scheme = value.substring(tokenStart, pos).trim();
+        pos = HeaderParser.skipWhitespace(value, pos);
 
-                pos += "realm=\"".length();
-                int realmStart = pos;
-                pos = HeaderParser.skipUntil(value, pos, "\"");
-                String realm = value.substring(realmStart, pos);
-                pos++; // Consume '"' close quote.
-                pos = HeaderParser.skipUntil(value, pos, ",");
-                pos++; // Consume ',' comma.
-                pos = HeaderParser.skipWhitespace(value, pos);
-                result.add(new Challenge(scheme, realm));
-            }
+        // TODO: This currently only handles schemes with a 'realm' parameter;
+        //       It needs to be fixed to handle any scheme and any parameters
+        //       http://code.google.com/p/android/issues/detail?id=11140
+
+        if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
+          break; // Unexpected challenge parameter; give up!
         }
-        return result;
+
+        pos += "realm=\"".length();
+        int realmStart = pos;
+        pos = HeaderParser.skipUntil(value, pos, "\"");
+        String realm = value.substring(realmStart, pos);
+        pos++; // Consume '"' close quote.
+        pos = HeaderParser.skipUntil(value, pos, ",");
+        pos++; // Consume ',' comma.
+        pos = HeaderParser.skipWhitespace(value, pos);
+        result.add(new Challenge(scheme, realm));
+      }
+    }
+    return result;
+  }
+
+  /** An RFC 2617 challenge. */
+  private static final class Challenge {
+    final String scheme;
+    final String realm;
+
+    Challenge(String scheme, String realm) {
+      this.scheme = scheme;
+      this.realm = realm;
     }
 
-    /**
-     * An RFC 2617 challenge.
-     */
-    private static final class Challenge {
-        final String scheme;
-        final String realm;
-
-        Challenge(String scheme, String realm) {
-            this.scheme = scheme;
-            this.realm = realm;
-        }
-
-        @Override public boolean equals(Object o) {
-            return o instanceof Challenge
-                    && ((Challenge) o).scheme.equals(scheme)
-                    && ((Challenge) o).realm.equals(realm);
-        }
-
-        @Override public int hashCode() {
-            return scheme.hashCode() + 31 * realm.hashCode();
-        }
+    @Override public boolean equals(Object o) {
+      return o instanceof Challenge
+          && ((Challenge) o).scheme.equals(scheme)
+          && ((Challenge) o).realm.equals(realm);
     }
+
+    @Override public int hashCode() {
+      return scheme.hashCode() + 31 * realm.hashCode();
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
index 41f03fa..acb5fda 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java
@@ -28,67 +28,55 @@
  */
 final class HttpDate {
 
-    /**
-     * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
-     * cookies are on the fast path.
-     */
-    private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT
-            = new ThreadLocal<DateFormat>() {
+  /**
+   * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such
+   * cookies are on the fast path.
+   */
+  private static final ThreadLocal<DateFormat> STANDARD_DATE_FORMAT =
+      new ThreadLocal<DateFormat>() {
         @Override protected DateFormat initialValue() {
-            DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
-            rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
-            return rfc1123;
+          DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+          rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
+          return rfc1123;
         }
-    };
+      };
 
-    /**
-     * If we fail to parse a date in a non-standard format, try each of these formats in sequence.
-     */
-    private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
+  /** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */
+  private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] {
             /* This list comes from  {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */
-            "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",
+      "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",
 
             /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */
-            "EEE MMM d yyyy HH:mm:ss z",
-    };
+      "EEE MMM d yyyy HH:mm:ss z", };
 
-    /**
-     * Returns the date for {@code value}. Returns null if the value couldn't be
-     * parsed.
-     */
-    public static Date parse(String value) {
-        try {
-            return STANDARD_DATE_FORMAT.get().parse(value);
-        } catch (ParseException ignore) {
-        }
-        for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
-            try {
-                return new SimpleDateFormat(formatString, Locale.US).parse(value);
-            } catch (ParseException ignore) {
-            }
-        }
-        return null;
+  /**
+   * Returns the date for {@code value}. Returns null if the value couldn't be
+   * parsed.
+   */
+  public static Date parse(String value) {
+    try {
+      return STANDARD_DATE_FORMAT.get().parse(value);
+    } catch (ParseException ignore) {
     }
+    for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) {
+      try {
+        return new SimpleDateFormat(formatString, Locale.US).parse(value);
+      } catch (ParseException ignore) {
+      }
+    }
+    return null;
+  }
 
-    /**
-     * Returns the string for {@code value}.
-     */
-    public static String format(Date value) {
-        return STANDARD_DATE_FORMAT.get().format(value);
-    }
+  /** Returns the string for {@code value}. */
+  public static String format(Date value) {
+    return STANDARD_DATE_FORMAT.get().format(value);
+  }
 
-    private HttpDate() {
-    }
+  private HttpDate() {
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 22483ac..8d40705 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -22,12 +22,9 @@
 import com.squareup.okhttp.OkResponseCache;
 import com.squareup.okhttp.ResponseSource;
 import com.squareup.okhttp.TunnelRequest;
+import com.squareup.okhttp.internal.Dns;
 import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.Util;
-import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
-import static com.squareup.okhttp.internal.Util.getDefaultPort;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-import com.squareup.okhttp.internal.Dns;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -50,19 +47,23 @@
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLSocketFactory;
 
+import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
+import static com.squareup.okhttp.internal.Util.getDefaultPort;
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
 /**
  * Handles a single HTTP request/response pair. Each HTTP engine follows this
  * lifecycle:
  * <ol>
- *     <li>It is created.
- *     <li>The HTTP request message is sent with sendRequest(). Once the request
- *         is sent it is an error to modify the request headers. After
- *         sendRequest() has been called the request body can be written to if
- *         it exists.
- *     <li>The HTTP response message is read with readResponse(). After the
- *         response has been read the response headers and body can be read.
- *         All responses have a response body input stream, though in some
- *         instances this stream is empty.
+ * <li>It is created.
+ * <li>The HTTP request message is sent with sendRequest(). Once the request
+ * is sent it is an error to modify the request headers. After
+ * sendRequest() has been called the request body can be written to if
+ * it exists.
+ * <li>The HTTP response message is read with readResponse(). After the
+ * response has been read the response headers and body can be read.
+ * All responses have a response body input stream, though in some
+ * instances this stream is empty.
  * </ol>
  *
  * <p>The request and response may be served by the HTTP response cache, by the
@@ -74,602 +75,606 @@
  * 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;
+  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;
 
-    protected final HttpURLConnectionImpl policy;
+  protected final HttpURLConnectionImpl policy;
 
-    protected final String method;
+  protected final String method;
 
-    private ResponseSource responseSource;
+  private ResponseSource responseSource;
 
-    protected Connection connection;
-    protected RouteSelector routeSelector;
-    private OutputStream requestBodyOut;
+  protected Connection connection;
+  protected RouteSelector routeSelector;
+  private OutputStream requestBodyOut;
 
-    private Transport transport;
+  private Transport transport;
 
-    private InputStream responseTransferIn;
-    private InputStream responseBodyIn;
+  private InputStream responseTransferIn;
+  private InputStream responseBodyIn;
 
-    private CacheResponse cacheResponse;
-    private CacheRequest cacheRequest;
+  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;
+  /** The time when the request headers were written, or -1 if they haven't been written yet. */
+  long sentRequestMillis = -1;
 
-    /**
-     * True if this client added an "Accept-Encoding: gzip" header field and is
-     * therefore responsible for also decompressing the transfer stream.
-     */
-    private boolean transparentGzip;
+  /**
+   * True if 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;
+  final URI uri;
 
-    final RequestHeaders requestHeaders;
+  final RequestHeaders requestHeaders;
 
-    /** Null until a response is received from the network or the cache. */
-    ResponseHeaders responseHeaders;
+  /** Null until a response is received from the network or the cache. */
+  ResponseHeaders responseHeaders;
 
-    /*
-     * 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 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;
 
-    /**
-     * 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 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;
+  /** 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 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.
-     */
-    public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
-            Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
-        this.policy = policy;
-        this.method = method;
-        this.connection = connection;
-        this.requestBodyOut = requestBodyOut;
+  /**
+   * @param requestHeaders the client's supplied request headers. This class
+   * creates a private copy that it can mutate.
+   * @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.
+   */
+  public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+      Connection connection, RetryableOutputStream requestBodyOut) throws IOException {
+    this.policy = policy;
+    this.method = method;
+    this.connection = connection;
+    this.requestBodyOut = requestBodyOut;
 
-        try {
-            uri = Platform.get().toUriLenient(policy.getURL());
-        } catch (URISyntaxException e) {
-            throw new IOException(e);
-        }
-
-        this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
+    try {
+      uri = Platform.get().toUriLenient(policy.getURL());
+    } catch (URISyntaxException e) {
+      throw new IOException(e);
     }
 
-    public URI getUri() {
-        return uri;
+    this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
+  }
+
+  public URI getUri() {
+    return uri;
+  }
+
+  /**
+   * Figures out what the response source will be, and opens a socket to that
+   * source if necessary. Prepares the request headers and gets ready to start
+   * writing the request body if it exists.
+   */
+  public final void sendRequest() throws IOException {
+    if (responseSource != null) {
+      return;
     }
 
-    /**
-     * Figures out what the response source will be, and opens a socket to that
-     * source if necessary. Prepares the request headers and gets ready to start
-     * writing the request body if it exists.
-     */
-    public final void sendRequest() throws IOException {
-        if (responseSource != null) {
-            return;
-        }
+    prepareRawRequestHeaders();
+    initResponseSource();
+    if (policy.responseCache instanceof OkResponseCache) {
+      ((OkResponseCache) policy.responseCache).trackResponse(responseSource);
+    }
 
-        prepareRawRequestHeaders();
-        initResponseSource();
+    // 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);
+      }
+      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) {
+      policy.connectionPool.recycle(connection);
+      connection = null;
+    }
+  }
+
+  /**
+   * 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() || policy.responseCache == null) {
+      return;
+    }
+
+    CacheResponse candidate =
+        policy.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();
+    }
+  }
+
+  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();
+    }
+  }
+
+  /** Connect to the origin server either directly or via a proxy. */
+  protected final void connect() throws IOException {
+    if (connection != null) {
+      return;
+    }
+    if (routeSelector == null) {
+      String uriHost = uri.getHost();
+      if (uriHost == null) {
+        throw new UnknownHostException(uri.toString());
+      }
+      Address address =
+          new Address(uriHost, getEffectivePort(uri), getSslSocketFactory(), getHostnameVerifier(),
+              policy.getProxy());
+      routeSelector =
+          new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
+    }
+    connection = routeSelector.next();
+    if (!connection.isConnected()) {
+      connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
+      policy.connectionPool.maybeShare(connection);
+    }
+    connected(connection);
+    Proxy proxy = connection.getProxy();
+    if (proxy != null) {
+      policy.setProxy(proxy);
+      // Add the authority to the request line when we're using a proxy.
+      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) {
+  }
+
+  /**
+   * Called immediately before the transport transmits HTTP request headers.
+   * This is used to observe the sent time should the request be cached.
+   */
+  public void writingRequestHeaders() {
+    if (sentRequestMillis != -1) {
+      throw new IllegalStateException();
+    }
+    sentRequestMillis = System.currentTimeMillis();
+  }
+
+  /**
+   * @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");
+  }
+
+  /** Returns the request body or null if this request doesn't have a body. */
+  public final OutputStream getRequestBody() {
+    if (responseSource == null) {
+      throw new IllegalStateException();
+    }
+    return requestBodyOut;
+  }
+
+  public final boolean hasResponse() {
+    return responseHeaders != null;
+  }
+
+  public final RequestHeaders getRequestHeaders() {
+    return requestHeaders;
+  }
+
+  public final ResponseHeaders getResponseHeaders() {
+    if (responseHeaders == null) {
+      throw new IllegalStateException();
+    }
+    return responseHeaders;
+  }
+
+  public final int getResponseCode() {
+    if (responseHeaders == null) {
+      throw new IllegalStateException();
+    }
+    return responseHeaders.getHeaders().getResponseCode();
+  }
+
+  public final InputStream getResponseBody() {
+    if (responseHeaders == null) {
+      throw new IllegalStateException();
+    }
+    return responseBodyIn;
+  }
+
+  public final CacheResponse getCacheResponse() {
+    return cacheResponse;
+  }
+
+  public final Connection getConnection() {
+    return connection;
+  }
+
+  /**
+   * Returns true if {@code cacheResponse} is of the right type. This
+   * condition is necessary but not sufficient for the cached response to
+   * be used.
+   */
+  protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+    return true;
+  }
+
+  private void maybeCache() throws IOException {
+    // Are we caching at all?
+    if (!policy.getUseCaches() || policy.responseCache == null) {
+      return;
+    }
+
+    // Should we cache this response for this request?
+    if (!responseHeaders.isCacheable(requestHeaders)) {
+      return;
+    }
+
+    // Offer this request to the cache.
+    cacheRequest = policy.responseCache.put(uri, getHttpConnectionToCache());
+  }
+
+  protected HttpURLConnection getHttpConnectionToCache() {
+    return policy;
+  }
+
+  /**
+   * 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.
+   */
+  public final void automaticallyReleaseConnectionToPool() {
+    automaticallyReleaseConnectionToPool = true;
+    if (connection != null && connectionReleased) {
+      policy.connectionPool.recycle(connection);
+      connection = null;
+    }
+  }
+
+  /**
+   * 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.
+   */
+  public final void release(boolean streamCancelled) {
+    // 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(streamCancelled, requestBodyOut,
+          responseTransferIn)) {
+        Util.closeQuietly(connection);
+        connection = null;
+      } else if (automaticallyReleaseConnectionToPool) {
+        policy.connectionPool.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);
+    } else {
+      responseBodyIn = transferStream;
+    }
+  }
+
+  /**
+   * Returns true if the response must have a (possibly 0-length) body.
+   * 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")) {
+      return false;
+    }
+
+    if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
+        && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
+        && responseCode != HttpURLConnectionImpl.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()) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Populates requestHeaders 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());
+
+    if (requestHeaders.getUserAgent() == null) {
+      requestHeaders.setUserAgent(getDefaultUserAgent());
+    }
+
+    if (requestHeaders.getHost() == null) {
+      requestHeaders.setHost(getOriginAddress(policy.getURL()));
+    }
+
+    if ((connection == null || connection.getHttpMinorVersion() != 0)
+        && requestHeaders.getConnection() == null) {
+      requestHeaders.setConnection("Keep-Alive");
+    }
+
+    if (requestHeaders.getAcceptEncoding() == null) {
+      transparentGzip = true;
+      requestHeaders.setAcceptEncoding("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));
+    }
+
+    CookieHandler cookieHandler = policy.cookieHandler;
+    if (cookieHandler != null) {
+      requestHeaders.addCookies(
+          cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
+    }
+  }
+
+  /**
+   * 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 policy.usingProxy();
+  }
+
+  /**
+   * Returns the SSL configuration for connections created by this engine.
+   * We cannot reuse HTTPS connections if the socket factory has changed.
+   */
+  protected SSLSocketFactory getSslSocketFactory() {
+    return null;
+  }
+
+  /**
+   * Returns the hostname verifier for connections created by this engine. We
+   * cannot reuse HTTPS connections if the hostname verifier has changed.
+   */
+  protected HostnameVerifier getHostnameVerifier() {
+    return null;
+  }
+
+  public static String getDefaultUserAgent() {
+    String agent = System.getProperty("http.agent");
+    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;
+  }
+
+  /**
+   * Flushes the remaining request header and body, parses the HTTP response
+   * headers and starts reading the HTTP response body if it exists.
+   */
+  public final void readResponse() throws IOException {
+    if (hasResponse()) {
+      return;
+    }
+
+    if (responseSource == null) {
+      throw new IllegalStateException("readResponse() without sendRequest()");
+    }
+
+    if (!responseSource.requiresConnection()) {
+      return;
+    }
+
+    if (sentRequestMillis == -1) {
+      if (requestBodyOut instanceof RetryableOutputStream) {
+        int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
+        requestHeaders.setContentLength(contentLength);
+      }
+      transport.writeRequestHeaders();
+    }
+
+    if (requestBodyOut != null) {
+      requestBodyOut.close();
+      if (requestBodyOut instanceof RetryableOutputStream) {
+        transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
+      }
+    }
+
+    transport.flushRequest();
+
+    responseHeaders = transport.readResponseHeaders();
+    responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
+
+    if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
+      if (cachedResponseHeaders.validate(responseHeaders)) {
+        release(false);
+        ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
+        setResponse(combinedHeaders, cachedResponseBody);
         if (policy.responseCache instanceof OkResponseCache) {
-            ((OkResponseCache) policy.responseCache).trackResponse(responseSource);
+          OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache;
+          httpResponseCache.trackConditionalCacheHit();
+          httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
         }
-
-        /*
-         * 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);
-            }
-            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) {
-            policy.connectionPool.recycle(connection);
-            connection = null;
-        }
+        return;
+      } else {
+        Util.closeQuietly(cachedResponseBody);
+      }
     }
 
-    /**
-     * 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() || policy.responseCache == null) {
-            return;
-        }
-
-        CacheResponse candidate = policy.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();
-        }
+    if (hasResponseBody()) {
+      maybeCache(); // reentrant. this calls into user code which may call back into this!
     }
 
-    private void sendSocketRequest() throws IOException {
-        if (connection == null) {
-            connect();
-        }
+    initContentStream(transport.getTransferStream(cacheRequest));
+  }
 
-        if (transport != null) {
-            throw new IllegalStateException();
-        }
+  protected TunnelRequest getTunnelConfig() {
+    return null;
+  }
 
-        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();
-        }
+  public void receiveHeaders(RawHeaders headers) throws IOException {
+    CookieHandler cookieHandler = policy.cookieHandler;
+    if (cookieHandler != null) {
+      cookieHandler.put(uri, headers.toMultimap(true));
     }
-
-    /**
-     * Connect to the origin server either directly or via a proxy.
-     */
-    protected final void connect() throws IOException {
-        if (connection != null) {
-            return;
-        }
-        if (routeSelector == null) {
-            String uriHost = uri.getHost();
-            if (uriHost == null) {
-                throw new UnknownHostException(uri.toString());
-            }
-            Address address = new Address(uriHost, getEffectivePort(uri),
-                    getSslSocketFactory(), getHostnameVerifier(), policy.getProxy());
-            routeSelector = new RouteSelector(
-                    address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
-        }
-        connection = routeSelector.next();
-        if (!connection.isRecycled()) {
-            connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(),
-                    getTunnelConfig());
-            if (connection.isSpdy()) {
-                policy.connectionPool.share(connection);
-            }
-        }
-        connected(connection);
-        Proxy proxy = connection.getProxy();
-        if (proxy != null) {
-            policy.setProxy(proxy);
-            // Add the authority to the request line when we're using a proxy.
-            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) {
-    }
-
-    /**
-     * @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");
-    }
-
-    /**
-     * Returns the request body or null if this request doesn't have a body.
-     */
-    public final OutputStream getRequestBody() {
-        if (responseSource == null) {
-            throw new IllegalStateException();
-        }
-        return requestBodyOut;
-    }
-
-    public final boolean hasResponse() {
-        return responseHeaders != null;
-    }
-
-    public final RequestHeaders getRequestHeaders() {
-        return requestHeaders;
-    }
-
-    public final ResponseHeaders getResponseHeaders() {
-        if (responseHeaders == null) {
-            throw new IllegalStateException();
-        }
-        return responseHeaders;
-    }
-
-    public final int getResponseCode() {
-        if (responseHeaders == null) {
-            throw new IllegalStateException();
-        }
-        return responseHeaders.getHeaders().getResponseCode();
-    }
-
-    public final InputStream getResponseBody() {
-        if (responseHeaders == null) {
-            throw new IllegalStateException();
-        }
-        return responseBodyIn;
-    }
-
-    public final CacheResponse getCacheResponse() {
-        return cacheResponse;
-    }
-
-    public final Connection getConnection() {
-        return connection;
-    }
-
-    public final boolean hasRecycledConnection() {
-        return connection != null && connection.isRecycled();
-    }
-
-    /**
-     * Returns true if {@code cacheResponse} is of the right type. This
-     * condition is necessary but not sufficient for the cached response to
-     * be used.
-     */
-    protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
-        return true;
-    }
-
-    private void maybeCache() throws IOException {
-        // Are we caching at all?
-        if (!policy.getUseCaches() || policy.responseCache == null) {
-            return;
-        }
-
-        // Should we cache this response for this request?
-        if (!responseHeaders.isCacheable(requestHeaders)) {
-            return;
-        }
-
-        // Offer this request to the cache.
-        cacheRequest = policy.responseCache.put(uri, getHttpConnectionToCache());
-    }
-
-    protected HttpURLConnection getHttpConnectionToCache() {
-        return policy;
-    }
-
-    /**
-     * 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.
-     */
-    public final void automaticallyReleaseConnectionToPool() {
-        automaticallyReleaseConnectionToPool = true;
-        if (connection != null && connectionReleased) {
-            policy.connectionPool.recycle(connection);
-            connection = null;
-        }
-    }
-
-    /**
-     * 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.
-     */
-    public final void release(boolean reusable) {
-        // If the response body comes from the cache, close it.
-        if (responseBodyIn == cachedResponseBody) {
-            Util.closeQuietly(responseBodyIn);
-        }
-
-        if (!connectionReleased && connection != null) {
-            connectionReleased = true;
-
-            if (!reusable || !transport.makeReusable(requestBodyOut, responseTransferIn)) {
-                Util.closeQuietly(connection);
-                connection = null;
-            } else if (automaticallyReleaseConnectionToPool) {
-                policy.connectionPool.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
-             */
-            responseHeaders.stripContentEncoding();
-            responseBodyIn = new GZIPInputStream(transferStream);
-        } else {
-            responseBodyIn = transferStream;
-        }
-    }
-
-    /**
-     * Returns true if the response must have a (possibly 0-length) body.
-     * 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")) {
-            return false;
-        }
-
-        if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
-                && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
-                && responseCode != HttpURLConnectionImpl.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()) {
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Populates requestHeaders 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());
-
-        if (requestHeaders.getUserAgent() == null) {
-            requestHeaders.setUserAgent(getDefaultUserAgent());
-        }
-
-        if (requestHeaders.getHost() == null) {
-            requestHeaders.setHost(getOriginAddress(policy.getURL()));
-        }
-
-        if ((connection == null || connection.getHttpMinorVersion() != 0)
-                && requestHeaders.getConnection() == null) {
-            requestHeaders.setConnection("Keep-Alive");
-        }
-
-        if (requestHeaders.getAcceptEncoding() == null) {
-            transparentGzip = true;
-            requestHeaders.setAcceptEncoding("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));
-        }
-
-        CookieHandler cookieHandler = policy.cookieHandler;
-        if (cookieHandler != null) {
-            requestHeaders.addCookies(
-                    cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
-        }
-    }
-
-    /**
-     * 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 policy.usingProxy();
-    }
-
-    /**
-     * Returns the SSL configuration for connections created by this engine.
-     * We cannot reuse HTTPS connections if the socket factory has changed.
-     */
-    protected SSLSocketFactory getSslSocketFactory() {
-        return null;
-    }
-
-    /**
-     * Returns the hostname verifier for connections created by this engine. We
-     * cannot reuse HTTPS connections if the hostname verifier has changed.
-     */
-    protected HostnameVerifier getHostnameVerifier() {
-        return null;
-    }
-
-    public static String getDefaultUserAgent() {
-        String agent = System.getProperty("http.agent");
-        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;
-    }
-
-    /**
-     * Flushes the remaining request header and body, parses the HTTP response
-     * headers and starts reading the HTTP response body if it exists.
-     */
-    public final void readResponse() throws IOException {
-        if (hasResponse()) {
-            return;
-        }
-
-        if (responseSource == null) {
-            throw new IllegalStateException("readResponse() without sendRequest()");
-        }
-
-        if (!responseSource.requiresConnection()) {
-            return;
-        }
-
-        if (sentRequestMillis == -1) {
-            if (requestBodyOut instanceof RetryableOutputStream) {
-                int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
-                requestHeaders.setContentLength(contentLength);
-            }
-            transport.writeRequestHeaders();
-        }
-
-        if (requestBodyOut != null) {
-            requestBodyOut.close();
-            if (requestBodyOut instanceof RetryableOutputStream) {
-                transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
-            }
-        }
-
-        transport.flushRequest();
-
-        responseHeaders = transport.readResponseHeaders();
-        responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
-
-        if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
-            if (cachedResponseHeaders.validate(responseHeaders)) {
-                release(true);
-                ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
-                setResponse(combinedHeaders, cachedResponseBody);
-                if (policy.responseCache instanceof OkResponseCache) {
-                    OkResponseCache httpResponseCache
-                            = (OkResponseCache) policy.responseCache;
-                    httpResponseCache.trackConditionalCacheHit();
-                    httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
-                }
-                return;
-            } else {
-                Util.closeQuietly(cachedResponseBody);
-            }
-        }
-
-        if (hasResponseBody()) {
-            maybeCache(); // reentrant. this calls into user code which may call back into this!
-        }
-
-        initContentStream(transport.getTransferStream(cacheRequest));
-    }
-
-    protected TunnelRequest getTunnelConfig() {
-        return null;
-    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java b/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
index 4bdd8fc..8735166 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java
@@ -18,12 +18,10 @@
 
 import com.squareup.okhttp.OkResponseCache;
 import com.squareup.okhttp.ResponseSource;
-import com.squareup.okhttp.internal.Util;
-import static com.squareup.okhttp.internal.Util.US_ASCII;
-import static com.squareup.okhttp.internal.Util.UTF_8;
 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 java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -56,557 +54,555 @@
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLPeerUnverifiedException;
 
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
 /**
  * Cache responses in a directory on the file system. Most clients should use
  * {@code android.net.HttpResponseCache}, the stable, documented front end for
  * this.
  */
 public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
-    private static final char[] DIGITS = {
-            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
-    };
+  private static final char[] DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
 
-    // TODO: add APIs to iterate the cache?
-    private static final int VERSION = 201105;
-    private static final int ENTRY_METADATA = 0;
-    private static final int ENTRY_BODY = 1;
-    private static final int ENTRY_COUNT = 2;
+  // TODO: add APIs to iterate the cache?
+  private static final int VERSION = 201105;
+  private static final int ENTRY_METADATA = 0;
+  private static final int ENTRY_BODY = 1;
+  private static final int ENTRY_COUNT = 2;
 
-    private final DiskLruCache cache;
+  private final DiskLruCache cache;
 
-    /* read and write statistics, all guarded by 'this' */
-    private int writeSuccessCount;
-    private int writeAbortCount;
-    private int networkCount;
-    private int hitCount;
-    private int requestCount;
+  /* read and write statistics, all guarded by 'this' */
+  private int writeSuccessCount;
+  private int writeAbortCount;
+  private int networkCount;
+  private int hitCount;
+  private int requestCount;
 
-    public HttpResponseCache(File directory, long maxSize) throws IOException {
-        cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  public HttpResponseCache(File directory, long maxSize) throws IOException {
+    cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  }
+
+  private String uriToKey(URI uri) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(uri.toString().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);
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) {
+    String key = uriToKey(uri);
+    DiskLruCache.Snapshot snapshot;
+    Entry entry;
+    try {
+      snapshot = cache.get(key);
+      if (snapshot == null) {
+        return null;
+      }
+      entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
+    } catch (IOException e) {
+      // Give up because the cache cannot be read.
+      return null;
     }
 
-    private String uriToKey(URI uri) {
-        try {
-            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
-            byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
-            return bytesToHexString(md5bytes);
-        } catch (NoSuchAlgorithmException e) {
-            throw new AssertionError(e);
-        } catch (UnsupportedEncodingException e) {
-            throw new AssertionError(e);
-        }
+    if (!entry.matches(uri, requestMethod, requestHeaders)) {
+      snapshot.close();
+      return null;
     }
 
-    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);
+    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
+        : new EntryCacheResponse(entry, snapshot);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    if (!(urlConnection instanceof HttpURLConnection)) {
+      return null;
     }
 
-    @Override public CacheResponse get(URI uri, String requestMethod,
-            Map<String, List<String>> requestHeaders) {
-        String key = uriToKey(uri);
-        DiskLruCache.Snapshot snapshot;
-        Entry entry;
-        try {
-            snapshot = cache.get(key);
-            if (snapshot == null) {
-                return null;
-            }
-            entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
-        } catch (IOException e) {
-            // Give up because the cache cannot be read.
-            return null;
-        }
+    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
+    String requestMethod = httpConnection.getRequestMethod();
+    String key = uriToKey(uri);
 
-        if (!entry.matches(uri, requestMethod, requestHeaders)) {
-            snapshot.close();
-            return null;
-        }
-
-        return entry.isHttps()
-                ? new EntrySecureCacheResponse(entry, snapshot)
-                : new EntryCacheResponse(entry, snapshot);
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+        "DELETE")) {
+      try {
+        cache.remove(key);
+      } catch (IOException ignored) {
+        // The cache cannot be written.
+      }
+      return null;
+    } else if (!requestMethod.equals("GET")) {
+      // Don't cache non-GET responses. We're technically allowed to cache
+      // HEAD requests and some POST requests, but the complexity of doing
+      // so is high and the benefit is low.
+      return null;
     }
 
-    @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
-        if (!(urlConnection instanceof HttpURLConnection)) {
-            return null;
-        }
-
-        HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
-        String requestMethod = httpConnection.getRequestMethod();
-        String key = uriToKey(uri);
-
-        if (requestMethod.equals("POST")
-                || requestMethod.equals("PUT")
-                || requestMethod.equals("DELETE")) {
-            try {
-                cache.remove(key);
-            } catch (IOException ignored) {
-                // The cache cannot be written.
-            }
-            return null;
-        } else if (!requestMethod.equals("GET")) {
-            /*
-             * Don't cache non-GET responses. We're technically allowed to cache
-             * HEAD requests and some POST requests, but the complexity of doing
-             * so is high and the benefit is low.
-             */
-            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);
-        DiskLruCache.Editor editor = null;
-        try {
-            editor = cache.edit(key);
-            if (editor == null) {
-                return null;
-            }
-            entry.writeTo(editor);
-            return new CacheRequestImpl(editor);
-        } catch (IOException e) {
-            abortQuietly(editor);
-            return null;
-        }
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    if (httpEngine == null) {
+      // Don't cache unless the HTTP implementation is ours.
+      return null;
     }
 
-    /**
-     * 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.
-     */
-    @Override public 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;
-        DiskLruCache.Editor editor = null;
-        try {
-            editor = snapshot.edit(); // returns null if snapshot is not current
-            if (editor != null) {
-                entry.writeTo(editor);
-                editor.commit();
-            }
-        } catch (IOException e) {
-            abortQuietly(editor);
-        }
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    if (response.hasVaryAll()) {
+      return null;
     }
 
-    private void abortQuietly(DiskLruCache.Editor editor) {
-        // Give up because the cache cannot be written.
-        try {
-            if (editor != null) {
-                editor.abort();
-            }
-        } catch (IOException ignored) {
-        }
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = cache.edit(key);
+      if (editor == null) {
+        return null;
+      }
+      entry.writeTo(editor);
+      return new CacheRequestImpl(editor);
+    } catch (IOException e) {
+      abortQuietly(editor);
+      return null;
     }
+  }
 
-    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;
-        }
+  /**
+   * 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.
+   */
+  @Override public 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;
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = snapshot.edit(); // returns null if snapshot is not current
+      if (editor != null) {
+        entry.writeTo(editor);
+        editor.commit();
+      }
+    } catch (IOException e) {
+      abortQuietly(editor);
     }
+  }
 
-    public DiskLruCache getCache() {
-        return cache;
+  private void abortQuietly(DiskLruCache.Editor editor) {
+    // Give up because the cache cannot be written.
+    try {
+      if (editor != null) {
+        editor.abort();
+      }
+    } catch (IOException ignored) {
     }
+  }
 
-    public synchronized int getWriteAbortCount() {
-        return writeAbortCount;
+  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;
     }
+  }
 
-    public synchronized int getWriteSuccessCount() {
-        return writeSuccessCount;
-    }
+  public DiskLruCache getCache() {
+    return cache;
+  }
 
-    public synchronized void trackResponse(ResponseSource source) {
-        requestCount++;
+  public synchronized int getWriteAbortCount() {
+    return writeAbortCount;
+  }
 
-        switch (source) {
-        case CACHE:
-            hitCount++;
-            break;
-        case CONDITIONAL_CACHE:
-        case NETWORK:
-            networkCount++;
-            break;
-        }
-    }
+  public synchronized int getWriteSuccessCount() {
+    return writeSuccessCount;
+  }
 
-    public synchronized void trackConditionalCacheHit() {
+  public synchronized void trackResponse(ResponseSource source) {
+    requestCount++;
+
+    switch (source) {
+      case CACHE:
         hitCount++;
+        break;
+      case CONDITIONAL_CACHE:
+      case NETWORK:
+        networkCount++;
+        break;
+    }
+  }
+
+  public synchronized void trackConditionalCacheHit() {
+    hitCount++;
+  }
+
+  public synchronized int getNetworkCount() {
+    return networkCount;
+  }
+
+  public synchronized int getHitCount() {
+    return hitCount;
+  }
+
+  public synchronized int getRequestCount() {
+    return requestCount;
+  }
+
+  private final class CacheRequestImpl extends CacheRequest {
+    private final DiskLruCache.Editor editor;
+    private OutputStream cacheOut;
+    private boolean done;
+    private OutputStream body;
+
+    public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
+      this.editor = editor;
+      this.cacheOut = editor.newOutputStream(ENTRY_BODY);
+      this.body = new FilterOutputStream(cacheOut) {
+        @Override public void close() throws IOException {
+          synchronized (HttpResponseCache.this) {
+            if (done) {
+              return;
+            }
+            done = true;
+            writeSuccessCount++;
+          }
+          super.close();
+          editor.commit();
+        }
+
+        @Override
+        public void write(byte[] buffer, int offset, int length) throws IOException {
+          // Since we don't override "write(int oneByte)", we can write directly to "out"
+          // and avoid the inefficient implementation from the FilterOutputStream.
+          out.write(buffer, offset, length);
+        }
+      };
     }
 
-    public synchronized int getNetworkCount() {
-        return networkCount;
+    @Override public void abort() {
+      synchronized (HttpResponseCache.this) {
+        if (done) {
+          return;
+        }
+        done = true;
+        writeAbortCount++;
+      }
+      Util.closeQuietly(cacheOut);
+      try {
+        editor.abort();
+      } catch (IOException ignored) {
+      }
     }
 
-    public synchronized int getHitCount() {
-        return hitCount;
+    @Override public OutputStream getBody() throws IOException {
+      return body;
     }
+  }
 
-    public synchronized int getRequestCount() {
-        return requestCount;
-    }
-
-    private final class CacheRequestImpl extends CacheRequest {
-        private final DiskLruCache.Editor editor;
-        private OutputStream cacheOut;
-        private boolean done;
-        private OutputStream body;
-
-        public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
-            this.editor = editor;
-            this.cacheOut = editor.newOutputStream(ENTRY_BODY);
-            this.body = new FilterOutputStream(cacheOut) {
-                @Override public void close() throws IOException {
-                    synchronized (HttpResponseCache.this) {
-                        if (done) {
-                            return;
-                        }
-                        done = true;
-                        writeSuccessCount++;
-                    }
-                    super.close();
-                    editor.commit();
-                }
-
-                @Override
-                public void write(byte[] buffer, int offset, int length) throws IOException {
-                    // Since we don't override "write(int oneByte)", we can write directly to "out"
-                    // and avoid the inefficient implementation from the FilterOutputStream.
-                    out.write(buffer, offset, length);
-                }
-            };
-        }
-
-        @Override public void abort() {
-            synchronized (HttpResponseCache.this) {
-                if (done) {
-                    return;
-                }
-                done = true;
-                writeAbortCount++;
-            }
-            Util.closeQuietly(cacheOut);
-            try {
-                editor.abort();
-            } catch (IOException ignored) {
-            }
-        }
-
-        @Override public OutputStream getBody() throws IOException {
-            return body;
-        }
-    }
-
-    private static final class Entry {
-        private final String uri;
-        private final RawHeaders varyHeaders;
-        private final String requestMethod;
-        private final RawHeaders responseHeaders;
-        private final String cipherSuite;
-        private final Certificate[] peerCertificates;
-        private final Certificate[] localCertificates;
-
-        /*
-         * Reads an entry from an input stream. A typical entry looks like this:
-         *   http://google.com/foo
-         *   GET
-         *   2
-         *   Accept-Language: fr-CA
-         *   Accept-Charset: UTF-8
-         *   HTTP/1.1 200 OK
-         *   3
-         *   Content-Type: image/png
-         *   Content-Length: 100
-         *   Cache-Control: max-age=600
-         *
-         * A typical HTTPS file looks like this:
-         *   https://google.com/foo
-         *   GET
-         *   2
-         *   Accept-Language: fr-CA
-         *   Accept-Charset: UTF-8
-         *   HTTP/1.1 200 OK
-         *   3
-         *   Content-Type: image/png
-         *   Content-Length: 100
-         *   Cache-Control: max-age=600
-         *
-         *   AES_256_WITH_MD5
-         *   2
-         *   base64-encoded peerCertificate[0]
-         *   base64-encoded peerCertificate[1]
-         *   -1
-         *
-         * The file is newline separated. The first two lines are the URL and
-         * the request method. Next is the number of HTTP Vary request header
-         * lines, followed by those lines.
-         *
-         * Next is the response status line, followed by the number of HTTP
-         * response header lines, followed by those lines.
-         *
-         * HTTPS responses also contain SSL session information. This begins
-         * with a blank line, and then a line containing the cipher suite. Next
-         * is the length of the peer certificate chain. These certificates are
-         * base64-encoded and appear each on their own line. The next line
-         * contains the length of the local certificate chain. These
-         * certificates are also base64-encoded and appear each on their own
-         * line. A length of -1 is used to encode a null array.
-         */
-        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();
-                for (int i = 0; i < varyRequestHeaderLineCount; i++) {
-                    varyHeaders.addLine(reader.readLine());
-                }
-
-                responseHeaders = new RawHeaders();
-                responseHeaders.setStatusLine(reader.readLine());
-                int responseHeaderLineCount = reader.readInt();
-                for (int i = 0; i < responseHeaderLineCount; i++) {
-                    responseHeaders.addLine(reader.readLine());
-                }
-
-                if (isHttps()) {
-                    String blank = reader.readLine();
-                    if (!blank.isEmpty()) {
-                        throw new IOException("expected \"\" but was \"" + blank + "\"");
-                    }
-                    cipherSuite = reader.readLine();
-                    peerCertificates = readCertArray(reader);
-                    localCertificates = readCertArray(reader);
-                } else {
-                    cipherSuite = null;
-                    peerCertificates = null;
-                    localCertificates = 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);
-
-            if (isHttps()) {
-                HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
-                cipherSuite = httpsConnection.getCipherSuite();
-                Certificate[] peerCertificatesNonFinal = null;
-                try {
-                    peerCertificatesNonFinal = httpsConnection.getServerCertificates();
-                } catch (SSLPeerUnverifiedException ignored) {
-                }
-                peerCertificates = peerCertificatesNonFinal;
-                localCertificates = httpsConnection.getLocalCertificates();
-            } else {
-                cipherSuite = null;
-                peerCertificates = null;
-                localCertificates = null;
-            }
-        }
-
-        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(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(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');
-            }
-
-            if (isHttps()) {
-                writer.write('\n');
-                writer.write(cipherSuite + '\n');
-                writeCertArray(writer, peerCertificates);
-                writeCertArray(writer, localCertificates);
-            }
-            writer.close();
-        }
-
-        private boolean isHttps() {
-            return uri.startsWith("https://");
-        }
-
-        private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
-            int length = reader.readInt();
-            if (length == -1) {
-                return 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));
-                }
-                return result;
-            } catch (CertificateException e) {
-                throw new IOException(e);
-            }
-        }
-
-        private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
-            if (certificates == null) {
-                writer.write("-1\n");
-                return;
-            }
-            try {
-                writer.write(Integer.toString(certificates.length) + '\n');
-                for (Certificate certificate : certificates) {
-                    byte[] bytes = certificate.getEncoded();
-                    String line = Base64.encode(bytes);
-                    writer.write(line + '\n');
-                }
-            } catch (CertificateEncodingException e) {
-                throw new IOException(e);
-            }
-        }
-
-        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);
-        }
-    }
+  private static final class Entry {
+    private final String uri;
+    private final RawHeaders varyHeaders;
+    private final String requestMethod;
+    private final RawHeaders responseHeaders;
+    private final String cipherSuite;
+    private final Certificate[] peerCertificates;
+    private final Certificate[] localCertificates;
 
     /**
-     * Returns an input stream that reads the body of a snapshot, closing the
-     * snapshot when the stream is closed.
+     * Reads an entry from an input stream. A typical entry looks like this:
+     * <pre>{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }</pre>
+     *
+     * <p>A typical HTTPS file looks like this:
+     * <pre>{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }</pre>
+     * The file is newline separated. The first two lines are the URL and
+     * the request method. Next is the number of HTTP Vary request header
+     * lines, followed by those lines.
+     *
+     * <p>Next is the response status line, followed by the number of HTTP
+     * response header lines, followed by those lines.
+     *
+     * <p>HTTPS responses also contain SSL session information. This begins
+     * with a blank line, and then a line containing the cipher suite. Next
+     * is the length of the peer certificate chain. These certificates are
+     * base64-encoded and appear each on their own line. The next line
+     * contains the length of the local certificate chain. These
+     * certificates are also base64-encoded and appear each on their own
+     * line. A length of -1 is used to encode a null array.
      */
-    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();
-            }
-        };
+    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();
+        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+          varyHeaders.addLine(reader.readLine());
+        }
+
+        responseHeaders = new RawHeaders();
+        responseHeaders.setStatusLine(reader.readLine());
+        int responseHeaderLineCount = reader.readInt();
+        for (int i = 0; i < responseHeaderLineCount; i++) {
+          responseHeaders.addLine(reader.readLine());
+        }
+
+        if (isHttps()) {
+          String blank = reader.readLine();
+          if (!blank.isEmpty()) {
+            throw new IOException("expected \"\" but was \"" + blank + "\"");
+          }
+          cipherSuite = reader.readLine();
+          peerCertificates = readCertArray(reader);
+          localCertificates = readCertArray(reader);
+        } else {
+          cipherSuite = null;
+          peerCertificates = null;
+          localCertificates = null;
+        }
+      } finally {
+        in.close();
+      }
     }
 
-    static class EntryCacheResponse extends CacheResponse {
-        private final Entry entry;
-        private final DiskLruCache.Snapshot snapshot;
-        private final InputStream in;
+    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);
 
-        public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
-            this.entry = entry;
-            this.snapshot = snapshot;
-            this.in = newBodyInputStream(snapshot);
+      if (isHttps()) {
+        HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+        cipherSuite = httpsConnection.getCipherSuite();
+        Certificate[] peerCertificatesNonFinal = null;
+        try {
+          peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+        } catch (SSLPeerUnverifiedException ignored) {
         }
-
-        @Override public Map<String, List<String>> getHeaders() {
-            return entry.responseHeaders.toMultimap(true);
-        }
-
-        @Override public InputStream getBody() {
-            return in;
-        }
+        peerCertificates = peerCertificatesNonFinal;
+        localCertificates = httpsConnection.getLocalCertificates();
+      } else {
+        cipherSuite = null;
+        peerCertificates = null;
+        localCertificates = null;
+      }
     }
 
-    static class EntrySecureCacheResponse extends SecureCacheResponse {
-        private final Entry entry;
-        private final DiskLruCache.Snapshot snapshot;
-        private final InputStream in;
+    public void writeTo(DiskLruCache.Editor editor) throws IOException {
+      OutputStream out = editor.newOutputStream(ENTRY_METADATA);
+      Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
 
-        public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
-            this.entry = entry;
-            this.snapshot = snapshot;
-            this.in = newBodyInputStream(snapshot);
-        }
+      writer.write(uri + '\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');
+      }
 
-        @Override public Map<String, List<String>> getHeaders() {
-            return entry.responseHeaders.toMultimap(true);
-        }
+      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');
+      }
 
-        @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);
-            }
-            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();
-        }
+      if (isHttps()) {
+        writer.write('\n');
+        writer.write(cipherSuite + '\n');
+        writeCertArray(writer, peerCertificates);
+        writeCertArray(writer, localCertificates);
+      }
+      writer.close();
     }
+
+    private boolean isHttps() {
+      return uri.startsWith("https://");
+    }
+
+    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
+      int length = reader.readInt();
+      if (length == -1) {
+        return 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));
+        }
+        return result;
+      } catch (CertificateException e) {
+        throw new IOException(e);
+      }
+    }
+
+    private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
+      if (certificates == null) {
+        writer.write("-1\n");
+        return;
+      }
+      try {
+        writer.write(Integer.toString(certificates.length) + '\n');
+        for (Certificate certificate : certificates) {
+          byte[] bytes = certificate.getEncoded();
+          String line = Base64.encode(bytes);
+          writer.write(line + '\n');
+        }
+      } catch (CertificateEncodingException e) {
+        throw new IOException(e);
+      }
+    }
+
+    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);
+    }
+  }
+
+  /**
+   * 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();
+      }
+    };
+  }
+
+  static class EntryCacheResponse extends CacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(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 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);
+      }
+      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();
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index 1c73423..dd7a38d 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -18,542 +18,482 @@
 
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.internal.Util;
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.CacheRequest;
-import java.net.CookieHandler;
 import java.net.ProtocolException;
 import java.net.Socket;
 
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
 public final class HttpTransport implements Transport {
-    /**
-     * The maximum number of bytes to buffer when sending headers and a request
-     * body. When the headers and body can be sent in a single write, the
-     * request completes sooner. In one WiFi benchmark, using a large enough
-     * buffer sped up some uploads by half.
-     */
-    private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
+  /**
+   * The maximum number of bytes to buffer when sending headers and a request
+   * body. When the headers and body can be sent in a single write, the
+   * request completes sooner. In one WiFi benchmark, using a large enough
+   * buffer sped up some uploads by half.
+   */
+  private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
 
-    /**
-     * 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 = 30;
+  /**
+   * 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;
+  public static final int DEFAULT_CHUNK_LENGTH = 1024;
 
-    private final HttpEngine httpEngine;
-    private final InputStream socketIn;
-    private final OutputStream socketOut;
+  private final HttpEngine httpEngine;
+  private final InputStream socketIn;
+  private final OutputStream socketOut;
 
-    /**
-     * 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;
+  /**
+   * 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) {
-        this.httpEngine = httpEngine;
-        this.socketOut = outputStream;
-        this.requestOut = outputStream;
-        this.socketIn = inputStream;
+  public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) {
+    this.httpEngine = httpEngine;
+    this.socketOut = outputStream;
+    this.requestOut = outputStream;
+    this.socketIn = inputStream;
+  }
+
+  @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 OutputStream createRequestBody() throws IOException {
-        boolean chunked = httpEngine.requestHeaders.isChunked();
-        if (!chunked
-                && httpEngine.policy.getChunkLength() > 0
-                && httpEngine.connection.getHttpMinorVersion() != 0) {
-            httpEngine.requestHeaders.setChunked();
-            chunked = true;
-        }
-
-        // Stream a request body of unknown length.
-        if (chunked) {
-            int chunkLength = httpEngine.policy.getChunkLength();
-            if (chunkLength == -1) {
-                chunkLength = DEFAULT_CHUNK_LENGTH;
-            }
-            writeRequestHeaders();
-            return new ChunkedOutputStream(requestOut, chunkLength);
-        }
-
-        // Stream a request body of a known length.
-        int fixedContentLength = httpEngine.policy.getFixedContentLength();
-        if (fixedContentLength != -1) {
-            httpEngine.requestHeaders.setContentLength(fixedContentLength);
-            writeRequestHeaders();
-            return new FixedLengthOutputStream(requestOut, fixedContentLength);
-        }
-
-        // Buffer a request body of a known length.
-        int contentLength = httpEngine.requestHeaders.getContentLength();
-        if (contentLength != -1) {
-            writeRequestHeaders();
-            return new RetryableOutputStream(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();
+    // Stream a request body of unknown length.
+    if (chunked) {
+      int chunkLength = httpEngine.policy.getChunkLength();
+      if (chunkLength == -1) {
+        chunkLength = DEFAULT_CHUNK_LENGTH;
+      }
+      writeRequestHeaders();
+      return new ChunkedOutputStream(requestOut, chunkLength);
     }
 
-    @Override public void flushRequest() throws IOException {
-        requestOut.flush();
-        requestOut = socketOut;
+    // Stream a request body of a known length.
+    int fixedContentLength = httpEngine.policy.getFixedContentLength();
+    if (fixedContentLength != -1) {
+      httpEngine.requestHeaders.setContentLength(fixedContentLength);
+      writeRequestHeaders();
+      return new FixedLengthOutputStream(requestOut, fixedContentLength);
     }
 
-    @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
-        requestBody.writeToSocket(requestOut);
+    // Buffer a request body of a known length.
+    int contentLength = httpEngine.requestHeaders.getContentLength();
+    if (contentLength != -1) {
+      writeRequestHeaders();
+      return new RetryableOutputStream(contentLength);
     }
 
-    /**
-     * Prepares the HTTP headers and sends them to the server.
-     *
-     * <p>For streaming requests with a body, headers must be prepared
-     * <strong>before</strong> the output stream has been written to. Otherwise
-     * the body would need to be buffered!
-     *
-     * <p>For non-streaming requests with a body, headers must be prepared
-     * <strong>after</strong> the output stream has been written to and closed.
-     * This ensures that the {@code Content-Length} header field receives the
-     * proper value.
-     */
-    public void writeRequestHeaders() throws IOException {
-        if (httpEngine.sentRequestMillis != -1) {
-            throw new IllegalStateException();
-        }
-        httpEngine.sentRequestMillis = System.currentTimeMillis();
+    // 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();
+  }
 
-        int contentLength = httpEngine.requestHeaders.getContentLength();
-        RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
-        byte[] bytes = headersToSend.toBytes();
+  @Override public void flushRequest() throws IOException {
+    requestOut.flush();
+    requestOut = socketOut;
+  }
 
-        if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
-            requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
-        }
+  @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+    requestBody.writeToSocket(requestOut);
+  }
 
-        requestOut.write(bytes);
+  /**
+   * Prepares the HTTP headers and sends them to the server.
+   *
+   * <p>For streaming requests with a body, headers must be prepared
+   * <strong>before</strong> the output stream has been written to. Otherwise
+   * the body would need to be buffered!
+   *
+   * <p>For non-streaming requests with a body, headers must be prepared
+   * <strong>after</strong> the output stream has been written to and closed.
+   * This ensures that the {@code Content-Length} header field receives the
+   * proper value.
+   */
+  public void writeRequestHeaders() throws IOException {
+    httpEngine.writingRequestHeaders();
+    int contentLength = httpEngine.requestHeaders.getContentLength();
+    RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
+    byte[] bytes = headersToSend.toBytes();
+
+    if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
+      requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
     }
 
-    @Override public ResponseHeaders readResponseHeaders() throws IOException {
-        RawHeaders headers = RawHeaders.fromBytes(socketIn);
-        httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
-        receiveHeaders(headers);
-        return new ResponseHeaders(httpEngine.uri, headers);
+    requestOut.write(bytes);
+  }
+
+  @Override public ResponseHeaders readResponseHeaders() throws IOException {
+    RawHeaders headers = RawHeaders.fromBytes(socketIn);
+    httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
+    httpEngine.receiveHeaders(headers);
+    return new ResponseHeaders(httpEngine.uri, headers);
+  }
+
+  public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+      InputStream responseBodyIn) {
+    if (streamCancelled) {
+      return false;
     }
 
-    private void receiveHeaders(RawHeaders headers) throws IOException {
-        CookieHandler cookieHandler = httpEngine.policy.cookieHandler;
-        if (cookieHandler != null) {
-            cookieHandler.put(httpEngine.uri, headers.toMultimap(true));
-        }
+    // We cannot reuse sockets that have incomplete output.
+    if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+      return false;
     }
 
-    public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
-        // We cannot reuse sockets that have incomplete output.
-        if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
-            return false;
-        }
+    // If the request specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.requestHeaders.hasConnectionClose()) {
+      return false;
+    }
 
-        // If the request specified that the connection shouldn't be reused, don't reuse it.
-        if (httpEngine.requestHeaders.hasConnectionClose()) {
-            return false;
-        }
+    // If the response specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
+      return false;
+    }
 
-        // If the response specified that the connection shouldn't be reused, don't reuse it.
-        if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
-            return false;
-        }
+    if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
+      return false;
+    }
 
-        if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
-            return false;
-        }
+    if (responseBodyIn != null) {
+      return discardStream(httpEngine, responseBodyIn);
+    }
 
-        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.
+   */
+  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 InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+    if (!httpEngine.hasResponseBody()) {
+      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
+    }
+
+    if (httpEngine.responseHeaders.isChunked()) {
+      return new ChunkedInputStream(socketIn, cacheRequest, this);
+    }
+
+    if (httpEngine.responseHeaders.getContentLength() != -1) {
+      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
+          httpEngine.responseHeaders.getContentLength());
+    }
+
+    // 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 AbstractHttpOutputStream {
+    private final OutputStream socketOut;
+    private int bytesRemaining;
+
+    private FixedLengthOutputStream(OutputStream socketOut, int 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 AbstractHttpOutputStream {
+    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);
     }
 
     /**
-     * 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.
+     * 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 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;
-        }
+    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 InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
-        if (!httpEngine.hasResponseBody()) {
-            return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
-        }
+    @Override public synchronized void write(byte[] buffer, int offset, int count)
+        throws IOException {
+      checkNotClosed();
+      checkOffsetAndCount(buffer.length, offset, count);
 
-        if (httpEngine.responseHeaders.isChunked()) {
-            return new ChunkedInputStream(socketIn, cacheRequest, this);
-        }
+      while (count > 0) {
+        int numBytesWritten;
 
-        if (httpEngine.responseHeaders.getContentLength() != -1) {
-            return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
-                    httpEngine.responseHeaders.getContentLength());
-        }
-
-        /*
-         * 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 AbstractHttpOutputStream {
-        private final OutputStream socketOut;
-        private int bytesRemaining;
-
-        private FixedLengthOutputStream(OutputStream socketOut, int 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 AbstractHttpOutputStream {
-        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
-            }
+        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();
-            socketOut.flush();
+          }
+        } else {
+          // write a single chunk of size maxChunkLength to the stream
+          numBytesWritten = maxChunkLength;
+          writeHex(numBytesWritten);
+          socketOut.write(buffer, offset, numBytesWritten);
+          socketOut.write(CRLF);
         }
 
-        @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);
-        }
+        offset += numBytesWritten;
+        count -= numBytesWritten;
+      }
     }
 
     /**
-     * An HTTP body with a fixed length specified in advance.
+     * Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
+     * followed by CRLF.
      */
-    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(true);
-            }
-        }
-
-        @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(true);
-            }
-            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;
-        }
+    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);
     }
 
-    /**
-     * 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);
-                transport.receiveHeaders(rawResponseHeaders);
-                endOfInput(true);
-            }
-        }
-
-        @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;
-        }
+    @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();
     }
 
-    /**
-     * An HTTP payload terminated by the end of the socket stream.
-     */
-    private static final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
-        private boolean inputExhausted;
-
-        private UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest,
-                HttpEngine httpEngine) throws IOException {
-            super(is, 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(false);
-                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();
-            }
-        }
+    @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(false);
+      }
+    }
+
+    @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(false);
+      }
+      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(false);
+      }
+    }
+
+    @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;
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index aec7b4e..40cd516 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -20,7 +20,6 @@
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.internal.Util;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -41,6 +40,8 @@
 import java.util.Map;
 import javax.net.ssl.SSLHandshakeException;
 
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
 /**
  * This implementation uses HttpEngine to send requests and receive responses.
  * This class may use multiple HttpEngines to follow redirects, authentication
@@ -56,439 +57,427 @@
  * is currently connected to a server.
  */
 public class HttpURLConnectionImpl extends HttpURLConnection {
-    /**
-     * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0
-     * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx
-     */
-    private static final int MAX_REDIRECTS = 5;
+  /**
+   * 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;
 
-    private final int defaultPort;
+  private final int defaultPort;
 
-    private Proxy proxy;
-    final ProxySelector proxySelector;
-    final CookieHandler cookieHandler;
-    final ResponseCache responseCache;
-    final ConnectionPool connectionPool;
+  private Proxy proxy;
+  final ProxySelector proxySelector;
+  final CookieHandler cookieHandler;
+  final ResponseCache responseCache;
+  final ConnectionPool connectionPool;
 
-    private final RawHeaders rawRequestHeaders = new RawHeaders();
+  private final RawHeaders rawRequestHeaders = new RawHeaders();
 
-    private int redirectionCount;
+  private int redirectionCount;
 
-    protected IOException httpEngineFailure;
-    protected HttpEngine httpEngine;
+  protected IOException httpEngineFailure;
+  protected HttpEngine httpEngine;
 
-    public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector,
-            CookieHandler cookieHandler, ResponseCache responseCache,
-            ConnectionPool connectionPool) {
-        super(url);
-        this.defaultPort = defaultPort;
-        this.proxy = proxy;
-        this.proxySelector = proxySelector;
-        this.cookieHandler = cookieHandler;
-        this.responseCache = responseCache;
-        this.connectionPool = connectionPool;
+  public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector,
+      CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) {
+    super(url);
+    this.defaultPort = defaultPort;
+    this.proxy = proxy;
+    this.proxySelector = proxySelector;
+    this.cookieHandler = cookieHandler;
+    this.responseCache = responseCache;
+    this.connectionPool = connectionPool;
+  }
+
+  @Override public final void connect() throws IOException {
+    initHttpEngine();
+    boolean success;
+    do {
+      success = execute(false);
+    } while (!success);
+  }
+
+  @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);
+    }
+  }
+
+  /**
+   * Returns an input stream from the server in the case of error such as the
+   * requested file (txt, htm, html) is not found on the remote server.
+   */
+  @Override public final InputStream getErrorStream() {
+    try {
+      HttpEngine response = getResponse();
+      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
+        return response.getResponseBody();
+      }
+      return null;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field at {@code position}. Returns null if there
+   * are fewer than {@code position} headers.
+   */
+  @Override public final String getHeaderField(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getValue(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field corresponding to the {@code fieldName}, or
+   * null if there is no such field. If the field has multiple values, the
+   * last value is returned.
+   */
+  @Override public final String getHeaderField(String fieldName) {
+    try {
+      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
+      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final String getHeaderFieldKey(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getHeaderFields() {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getRequestProperties() {
+    if (connected) {
+      throw new IllegalStateException(
+          "Cannot access request header fields after connection is set");
+    }
+    return rawRequestHeaders.toMultimap(false);
+  }
+
+  @Override public final InputStream getInputStream() throws IOException {
+    if (!doInput) {
+      throw new ProtocolException("This protocol does not support input");
     }
 
-    @Override public final void connect() throws IOException {
-        initHttpEngine();
-        boolean success;
-        do {
-            success = execute(false);
-        } while (!success);
+    HttpEngine response = getResponse();
+
+    // if the requested file does not exist, throw an exception formerly the
+    // Error page from the server was returned if the requested file was
+    // text/html this has changed to return FileNotFoundException for all
+    // file types
+    if (getResponseCode() >= HTTP_BAD_REQUEST) {
+      throw new FileNotFoundException(url.toString());
     }
 
-    @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(false);
+    InputStream result = response.getResponseBody();
+    if (result == null) {
+      throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
+    }
+    return result;
+  }
+
+  @Override public final OutputStream getOutputStream() throws IOException {
+    connect();
+
+    OutputStream result = httpEngine.getRequestBody();
+    if (result == 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 result;
+  }
+
+  @Override public final Permission getPermission() throws IOException {
+    String connectToAddress = getConnectToHost() + ":" + getConnectToPort();
+    return new SocketPermission(connectToAddress, "connect, resolve");
+  }
+
+  private String getConnectToHost() {
+    return usingProxy() ? ((InetSocketAddress) proxy.address()).getHostName() : getURL().getHost();
+  }
+
+  private int getConnectToPort() {
+    int hostPort =
+        usingProxy() ? ((InetSocketAddress) proxy.address()).getPort() : getURL().getPort();
+    return hostPort < 0 ? getDefaultPort() : hostPort;
+  }
+
+  @Override public final String getRequestProperty(String field) {
+    if (field == null) {
+      return null;
+    }
+    return rawRequestHeaders.get(field);
+  }
+
+  private void initHttpEngine() throws IOException {
+    if (httpEngineFailure != null) {
+      throw httpEngineFailure;
+    } else if (httpEngine != null) {
+      return;
+    }
+
+    connected = true;
+    try {
+      if (doOutput) {
+        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")) {
+          // If the request method is neither POST nor PUT, then you're not writing
+          throw new ProtocolException(method + " does not support writing");
         }
+      }
+      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
+    } catch (IOException e) {
+      httpEngineFailure = e;
+      throw e;
+    }
+  }
+
+  /**
+   * Create a new HTTP engine. This hook method is non-final so it can be
+   * overridden by HttpsURLConnectionImpl.
+   */
+  protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+      Connection connection, RetryableOutputStream requestBody) throws IOException {
+    return new HttpEngine(this, method, requestHeaders, connection, requestBody);
+  }
+
+  /**
+   * Aggressively tries to get the final HTTP response, potentially making
+   * many HTTP requests in the process in order to cope with redirects and
+   * authentication.
+   */
+  private HttpEngine getResponse() throws IOException {
+    initHttpEngine();
+
+    if (httpEngine.hasResponse()) {
+      return httpEngine;
     }
 
-    /**
-     * Returns an input stream from the server in the case of error such as the
-     * requested file (txt, htm, html) is not found on the remote server.
-     */
-    @Override public final InputStream getErrorStream() {
-        try {
-            HttpEngine response = getResponse();
-            if (response.hasResponseBody()
-                    && response.getResponseCode() >= HTTP_BAD_REQUEST) {
-                return response.getResponseBody();
-            }
-            return null;
-        } catch (IOException e) {
-            return null;
-        }
-    }
+    while (true) {
+      if (!execute(true)) {
+        continue;
+      }
 
-    /**
-     * Returns the value of the field at {@code position}. Returns null if there
-     * are fewer than {@code position} headers.
-     */
-    @Override public final String getHeaderField(int position) {
-        try {
-            return getResponse().getResponseHeaders().getHeaders().getValue(position);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    /**
-     * Returns the value of the field corresponding to the {@code fieldName}, or
-     * null if there is no such field. If the field has multiple values, the
-     * last value is returned.
-     */
-    @Override public final String getHeaderField(String fieldName) {
-        try {
-            RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
-            return fieldName == null
-                    ? rawHeaders.getStatusLine()
-                    : rawHeaders.get(fieldName);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    @Override public final String getHeaderFieldKey(int position) {
-        try {
-            return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    @Override public final Map<String, List<String>> getHeaderFields() {
-        try {
-            return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    @Override public final Map<String, List<String>> getRequestProperties() {
-        if (connected) {
-            throw new IllegalStateException(
-                    "Cannot access request header fields after connection is set");
-        }
-        return rawRequestHeaders.toMultimap(false);
-    }
-
-    @Override public final InputStream getInputStream() throws IOException {
-        if (!doInput) {
-            throw new ProtocolException("This protocol does not support input");
-        }
-
-        HttpEngine response = getResponse();
-
-        /*
-         * if the requested file does not exist, throw an exception formerly the
-         * Error page from the server was returned if the requested file was
-         * text/html this has changed to return FileNotFoundException for all
-         * file types
-         */
-        if (getResponseCode() >= HTTP_BAD_REQUEST) {
-            throw new FileNotFoundException(url.toString());
-        }
-
-        InputStream result = response.getResponseBody();
-        if (result == null) {
-            throw new ProtocolException("No response body exists; responseCode="
-                    + getResponseCode());
-        }
-        return result;
-    }
-
-    @Override public final OutputStream getOutputStream() throws IOException {
-        connect();
-
-        OutputStream result = httpEngine.getRequestBody();
-        if (result == 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 result;
-    }
-
-    @Override public final Permission getPermission() throws IOException {
-        String connectToAddress = getConnectToHost() + ":" + getConnectToPort();
-        return new SocketPermission(connectToAddress, "connect, resolve");
-    }
-
-    private String getConnectToHost() {
-        return usingProxy()
-                ? ((InetSocketAddress) proxy.address()).getHostName()
-                : getURL().getHost();
-    }
-
-    private int getConnectToPort() {
-        int hostPort = usingProxy()
-                ? ((InetSocketAddress) proxy.address()).getPort()
-                : getURL().getPort();
-        return hostPort < 0 ? getDefaultPort() : hostPort;
-    }
-
-    @Override public final String getRequestProperty(String field) {
-        if (field == null) {
-            return null;
-        }
-        return rawRequestHeaders.get(field);
-    }
-
-    private void initHttpEngine() throws IOException {
-        if (httpEngineFailure != null) {
-            throw httpEngineFailure;
-        } else if (httpEngine != null) {
-            return;
-        }
-
-        connected = true;
-        try {
-            if (doOutput) {
-                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")) {
-                    // If the request method is neither POST nor PUT, then you're not writing
-                    throw new ProtocolException(method + " does not support writing");
-                }
-            }
-            httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
-        } catch (IOException e) {
-            httpEngineFailure = e;
-            throw e;
-        }
-    }
-
-    /**
-     * Create a new HTTP engine. This hook method is non-final so it can be
-     * overridden by HttpsURLConnectionImpl.
-     */
-    protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
-            Connection connection, RetryableOutputStream requestBody) throws IOException {
-        return new HttpEngine(this, method, requestHeaders, connection, requestBody);
-    }
-
-    /**
-     * Aggressively tries to get the final HTTP response, potentially making
-     * many HTTP requests in the process in order to cope with redirects and
-     * authentication.
-     */
-    private HttpEngine getResponse() throws IOException {
-        initHttpEngine();
-
-        if (httpEngine.hasResponse()) {
-            return httpEngine;
-        }
-
-        while (true) {
-            if (!execute(true)) {
-                continue;
-            }
-
-            Retry retry = processResponseHeaders();
-            if (retry == Retry.NONE) {
-                httpEngine.automaticallyReleaseConnectionToPool();
-                return httpEngine;
-            }
-
-            /*
-             * The first request was insufficient. Prepare for another...
-             */
-            String retryMethod = method;
-            OutputStream 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();
-            if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM
-                    || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) {
-                retryMethod = "GET";
-                requestBody = null;
-            }
-
-            if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
-                throw new HttpRetryException("Cannot retry streamed HTTP body",
-                        httpEngine.getResponseCode());
-            }
-
-            if (retry == Retry.DIFFERENT_CONNECTION) {
-                httpEngine.automaticallyReleaseConnectionToPool();
-            }
-
-            httpEngine.release(true);
-
-            httpEngine = newHttpEngine(retryMethod, rawRequestHeaders,
-                    httpEngine.getConnection(), (RetryableOutputStream) requestBody);
-        }
-    }
-
-    /**
-     * Sends a request and optionally reads a response. Returns true if the
-     * request was successfully executed, and false if the request can be
-     * retried. Throws an exception if the request failed permanently.
-     */
-    private boolean execute(boolean readResponse) throws IOException {
-        try {
-            httpEngine.sendRequest();
-            if (readResponse) {
-                httpEngine.readResponse();
-            }
-            return true;
-        } catch (IOException e) {
-            RouteSelector routeSelector = httpEngine.routeSelector;
-            if (routeSelector == null) {
-                throw e; // Without a route selector, we can't retry.
-            } else if (httpEngine.connection != null) {
-                routeSelector.connectFailed(httpEngine.connection, e);
-            }
-
-            // The connection failure isn't fatal if there's another route to attempt.
-            OutputStream requestBody = httpEngine.getRequestBody();
-            if (routeSelector.hasNext() && isRecoverable(e)
-                    && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
-                httpEngine.release(false);
-                httpEngine = newHttpEngine(method, rawRequestHeaders, null,
-                        (RetryableOutputStream) requestBody);
-                httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
-                return false;
-            }
-            httpEngineFailure = e;
-            throw e;
-        }
-    }
-
-    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;
-    }
-
-    HttpEngine getHttpEngine() {
+      Retry retry = processResponseHeaders();
+      if (retry == Retry.NONE) {
+        httpEngine.automaticallyReleaseConnectionToPool();
         return httpEngine;
+      }
+
+      // The first request was insufficient. Prepare for another...
+      String retryMethod = method;
+      OutputStream 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();
+      if (responseCode == HTTP_MULT_CHOICE
+          || responseCode == HTTP_MOVED_PERM
+          || responseCode == HTTP_MOVED_TEMP
+          || responseCode == HTTP_SEE_OTHER) {
+        retryMethod = "GET";
+        requestBody = null;
+      }
+
+      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
+        throw new HttpRetryException("Cannot retry streamed HTTP body",
+            httpEngine.getResponseCode());
+      }
+
+      if (retry == Retry.DIFFERENT_CONNECTION) {
+        httpEngine.automaticallyReleaseConnectionToPool();
+      }
+
+      httpEngine.release(false);
+
+      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
+          (RetryableOutputStream) requestBody);
     }
+  }
 
-    enum Retry {
-        NONE,
-        SAME_CONNECTION,
-        DIFFERENT_CONNECTION
+  /**
+   * Sends a request and optionally reads a response. Returns true if the
+   * request was successfully executed, and false if the request can be
+   * retried. Throws an exception if the request failed permanently.
+   */
+  private boolean execute(boolean readResponse) throws IOException {
+    try {
+      httpEngine.sendRequest();
+      if (readResponse) {
+        httpEngine.readResponse();
+      }
+      return true;
+    } catch (IOException e) {
+      RouteSelector routeSelector = httpEngine.routeSelector;
+      if (routeSelector != null && httpEngine.connection != null) {
+        routeSelector.connectFailed(httpEngine.connection, e);
+      }
+      if (routeSelector == null && httpEngine.connection == null) {
+        throw e; // If we failed before finding a route or a connection, give up.
+      }
+
+      // The connection failure isn't fatal if there's another route to attempt.
+      OutputStream requestBody = httpEngine.getRequestBody();
+      if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody
+          == null || requestBody instanceof RetryableOutputStream)) {
+        httpEngine.release(true);
+        httpEngine =
+            newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody);
+        httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+        return false;
+      }
+      httpEngineFailure = e;
+      throw e;
     }
+  }
 
-    /**
-     * Returns the retry action to take for the current response headers. The
-     * headers, proxy and target URL or this connection may be adjusted to
-     * prepare for a follow up request.
-     */
-    private Retry processResponseHeaders() throws IOException {
-        switch (getResponseCode()) {
-        case HTTP_PROXY_AUTH:
-            if (!usingProxy()) {
-                throw new ProtocolException(
-                        "Received HTTP_PROXY_AUTH (407) code while not using proxy");
-            }
-            // fall-through
-        case HTTP_UNAUTHORIZED:
-            boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
-                    httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url);
-            return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
+  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;
+  }
 
-        case HTTP_MULT_CHOICE:
-        case HTTP_MOVED_PERM:
-        case HTTP_MOVED_TEMP:
-        case HTTP_SEE_OTHER:
-            if (!getInstanceFollowRedirects()) {
-                return Retry.NONE;
-            }
-            if (++redirectionCount > MAX_REDIRECTS) {
-                throw new ProtocolException("Too many redirects");
-            }
-            String location = getHeaderField("Location");
-            if (location == null) {
-                return Retry.NONE;
-            }
-            URL previousUrl = url;
-            url = new URL(previousUrl, location);
-            if (!previousUrl.getProtocol().equals(url.getProtocol())) {
-                return Retry.NONE; // the scheme changed; don't retry.
-            }
-            if (previousUrl.getHost().equals(url.getHost())
-                    && getEffectivePort(previousUrl) == getEffectivePort(url)) {
-                return Retry.SAME_CONNECTION;
-            } else {
-                return Retry.DIFFERENT_CONNECTION;
-            }
+  HttpEngine getHttpEngine() {
+    return httpEngine;
+  }
 
-        default:
-            return Retry.NONE;
+  enum Retry {
+    NONE,
+    SAME_CONNECTION,
+    DIFFERENT_CONNECTION
+  }
+
+  /**
+   * Returns the retry action to take for the current response headers. The
+   * headers, proxy and target URL or this connection may be adjusted to
+   * prepare for a follow up request.
+   */
+  private Retry processResponseHeaders() throws IOException {
+    switch (getResponseCode()) {
+      case HTTP_PROXY_AUTH:
+        if (!usingProxy()) {
+          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
         }
-    }
+        // fall-through
+      case HTTP_UNAUTHORIZED:
+        boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
+            httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url);
+        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
 
-    final int getDefaultPort() {
-        return defaultPort;
-    }
-
-    /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
-    final int getFixedContentLength() {
-        return fixedContentLength;
-    }
-
-    /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
-    final int getChunkLength() {
-        return chunkLength;
-    }
-
-    final Proxy getProxy() {
-        return proxy;
-    }
-
-    final void setProxy(Proxy proxy) {
-        this.proxy = proxy;
-    }
-
-    @Override public final boolean usingProxy() {
-        return (proxy != null && proxy.type() != Proxy.Type.DIRECT);
-    }
-
-    @Override public String getResponseMessage() throws IOException {
-        return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
-    }
-
-    @Override public final int getResponseCode() throws IOException {
-        return getResponse().getResponseCode();
-    }
-
-    @Override public final void setRequestProperty(String field, String newValue) {
-        if (connected) {
-            throw new IllegalStateException("Cannot set request property after connection is made");
+      case HTTP_MULT_CHOICE:
+      case HTTP_MOVED_PERM:
+      case HTTP_MOVED_TEMP:
+      case HTTP_SEE_OTHER:
+        if (!getInstanceFollowRedirects()) {
+          return Retry.NONE;
         }
-        if (field == null) {
-            throw new NullPointerException("field == null");
+        if (++redirectionCount > MAX_REDIRECTS) {
+          throw new ProtocolException("Too many redirects: " + redirectionCount);
         }
-        rawRequestHeaders.set(field, newValue);
-    }
+        String location = getHeaderField("Location");
+        if (location == null) {
+          return Retry.NONE;
+        }
+        URL previousUrl = url;
+        url = new URL(previousUrl, location);
+        if (!previousUrl.getProtocol().equals(url.getProtocol())) {
+          return Retry.NONE; // the scheme changed; don't retry.
+        }
+        if (previousUrl.getHost().equals(url.getHost())
+            && getEffectivePort(previousUrl) == getEffectivePort(url)) {
+          return Retry.SAME_CONNECTION;
+        } else {
+          return Retry.DIFFERENT_CONNECTION;
+        }
 
-    @Override public final void addRequestProperty(String field, String value) {
-        if (connected) {
-            throw new IllegalStateException("Cannot add request property after connection is made");
-        }
-        if (field == null) {
-            throw new NullPointerException("field == null");
-        }
-        rawRequestHeaders.add(field, value);
+      default:
+        return Retry.NONE;
     }
+  }
+
+  final int getDefaultPort() {
+    return defaultPort;
+  }
+
+  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
+  final int getFixedContentLength() {
+    return fixedContentLength;
+  }
+
+  /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
+  final int getChunkLength() {
+    return chunkLength;
+  }
+
+  final Proxy getProxy() {
+    return proxy;
+  }
+
+  final void setProxy(Proxy proxy) {
+    this.proxy = proxy;
+  }
+
+  @Override public final boolean usingProxy() {
+    return (proxy != null && proxy.type() != Proxy.Type.DIRECT);
+  }
+
+  @Override public String getResponseMessage() throws IOException {
+    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
+  }
+
+  @Override public final int getResponseCode() throws IOException {
+    return getResponse().getResponseCode();
+  }
+
+  @Override public final void setRequestProperty(String field, String newValue) {
+    if (connected) {
+      throw new IllegalStateException("Cannot set request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.set(field, newValue);
+  }
+
+  @Override public final void addRequestProperty(String field, String value) {
+    if (connected) {
+      throw new IllegalStateException("Cannot add request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.add(field, value);
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index b0fc73b..8212903 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -19,7 +19,6 @@
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.TunnelRequest;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -43,418 +42,418 @@
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
 public final class HttpsURLConnectionImpl extends HttpsURLConnection {
 
-    /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
-    private final HttpUrlConnectionDelegate delegate;
+  /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
+  private final HttpUrlConnectionDelegate delegate;
 
-    public HttpsURLConnectionImpl(URL url, int defaultPort, Proxy proxy,
-            ProxySelector proxySelector, CookieHandler cookieHandler, ResponseCache responseCache,
-            ConnectionPool connectionPool) {
-        super(url);
-        delegate = new HttpUrlConnectionDelegate(url, defaultPort, proxy, proxySelector,
-                cookieHandler, responseCache, connectionPool);
+  public HttpsURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector,
+      CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) {
+    super(url);
+    delegate = new HttpUrlConnectionDelegate(url, defaultPort, proxy, proxySelector, cookieHandler,
+        responseCache, connectionPool);
+  }
+
+  private void checkConnected() {
+    if (delegate.getSSLSocket() == null) {
+      throw new IllegalStateException("Connection has not yet been established");
+    }
+  }
+
+  HttpEngine getHttpEngine() {
+    return delegate.getHttpEngine();
+  }
+
+  @Override
+  public String getCipherSuite() {
+    SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getCipherSuite();
+    }
+    checkConnected();
+    return delegate.getSSLSocket().getSession().getCipherSuite();
+  }
+
+  @Override
+  public Certificate[] getLocalCertificates() {
+    SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getLocalCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    checkConnected();
+    return delegate.getSSLSocket().getSession().getLocalCertificates();
+  }
+
+  @Override
+  public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getServerCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    checkConnected();
+    return delegate.getSSLSocket().getSession().getPeerCertificates();
+  }
+
+  @Override
+  public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getPeerPrincipal();
+    }
+    checkConnected();
+    return delegate.getSSLSocket().getSession().getPeerPrincipal();
+  }
+
+  @Override
+  public Principal getLocalPrincipal() {
+    SecureCacheResponse cacheResponse = delegate.getCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getLocalPrincipal();
+    }
+    checkConnected();
+    return delegate.getSSLSocket().getSession().getLocalPrincipal();
+  }
+
+  @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);
+  }
+
+  private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
+    private HttpUrlConnectionDelegate(URL url, int defaultPort, Proxy proxy,
+        ProxySelector proxySelector, CookieHandler cookieHandler, ResponseCache responseCache,
+        ConnectionPool connectionPool) {
+      super(url, defaultPort, proxy, proxySelector, cookieHandler, responseCache, connectionPool);
     }
 
-    private void checkConnected() {
-        if (delegate.getSSLSocket() == null) {
-            throw new IllegalStateException("Connection has not yet been established");
-        }
+    @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+        Connection connection, RetryableOutputStream requestBody) throws IOException {
+      return new HttpsEngine(this, method, requestHeaders, connection, requestBody,
+          HttpsURLConnectionImpl.this);
     }
 
-    HttpEngine getHttpEngine() {
-        return delegate.getHttpEngine();
+    public SecureCacheResponse getCacheResponse() {
+      HttpsEngine engine = (HttpsEngine) httpEngine;
+      return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null;
     }
 
-    @Override
-    public String getCipherSuite() {
-        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
-        if (cacheResponse != null) {
-            return cacheResponse.getCipherSuite();
-        }
-        checkConnected();
-        return delegate.getSSLSocket().getSession().getCipherSuite();
+    public SSLSocket getSSLSocket() {
+      HttpsEngine engine = (HttpsEngine) httpEngine;
+      return engine != null ? engine.sslSocket : null;
+    }
+  }
+
+  private static final class HttpsEngine extends HttpEngine {
+    /**
+     * Stash of HttpsEngine.connection.socket to implement requests like
+     * {@link #getCipherSuite} even after the connection has been recycled.
+     */
+    private SSLSocket sslSocket;
+
+    private final HttpsURLConnectionImpl enclosing;
+
+    /**
+     * @param policy the HttpURLConnectionImpl with connection configuration
+     * @param enclosing the HttpsURLConnection with HTTPS features
+     */
+    private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+        Connection connection, RetryableOutputStream requestBody, HttpsURLConnectionImpl enclosing)
+        throws IOException {
+      super(policy, method, requestHeaders, connection, requestBody);
+      this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
+      this.enclosing = enclosing;
     }
 
-    @Override
-    public Certificate[] getLocalCertificates() {
-        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
-        if (cacheResponse != null) {
-            List<Certificate> result = cacheResponse.getLocalCertificateChain();
-            return result != null ? result.toArray(new Certificate[result.size()]) : null;
-        }
-        checkConnected();
-        return delegate.getSSLSocket().getSession().getLocalCertificates();
+    @Override protected void connected(Connection connection) {
+      this.sslSocket = (SSLSocket) connection.getSocket();
     }
 
-    @Override
-    public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
-        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
-        if (cacheResponse != null) {
-            List<Certificate> result = cacheResponse.getServerCertificateChain();
-            return result != null ? result.toArray(new Certificate[result.size()]) : null;
-        }
-        checkConnected();
-        return delegate.getSSLSocket().getSession().getPeerCertificates();
+    @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+      return cacheResponse instanceof SecureCacheResponse;
     }
 
-    @Override
-    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
-        if (cacheResponse != null) {
-            return cacheResponse.getPeerPrincipal();
-        }
-        checkConnected();
-        return delegate.getSSLSocket().getSession().getPeerPrincipal();
+    @Override protected boolean includeAuthorityInRequestLine() {
+      // Even if there is a proxy, it isn't involved. Always request just the file.
+      return false;
     }
 
-    @Override
-    public Principal getLocalPrincipal() {
-        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
-        if (cacheResponse != null) {
-            return cacheResponse.getLocalPrincipal();
-        }
-        checkConnected();
-        return delegate.getSSLSocket().getSession().getLocalPrincipal();
+    @Override protected SSLSocketFactory getSslSocketFactory() {
+      return enclosing.getSSLSocketFactory();
     }
 
-    @Override
-    public void disconnect() {
-        delegate.disconnect();
+    @Override protected HostnameVerifier getHostnameVerifier() {
+      return enclosing.getHostnameVerifier();
     }
 
-    @Override
-    public InputStream getErrorStream() {
-        return delegate.getErrorStream();
+    @Override protected HttpURLConnection getHttpConnectionToCache() {
+      return enclosing;
     }
 
-    @Override
-    public String getRequestMethod() {
-        return delegate.getRequestMethod();
+    @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());
     }
-
-    @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);
-    }
-
-    private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
-        private HttpUrlConnectionDelegate(URL url, int defaultPort, Proxy proxy,
-                ProxySelector proxySelector, CookieHandler cookieHandler,
-                ResponseCache responseCache, ConnectionPool connectionPool) {
-            super(url, defaultPort, proxy, proxySelector, cookieHandler, responseCache,
-                    connectionPool);
-        }
-
-        @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
-                Connection connection, RetryableOutputStream requestBody) throws IOException {
-            return new HttpsEngine(this, method, requestHeaders, connection, requestBody,
-                    HttpsURLConnectionImpl.this);
-        }
-
-        public SecureCacheResponse getCacheResponse() {
-            HttpsEngine engine = (HttpsEngine) httpEngine;
-            return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null;
-        }
-
-        public SSLSocket getSSLSocket() {
-            HttpsEngine engine = (HttpsEngine) httpEngine;
-            return engine != null ? engine.sslSocket : null;
-        }
-    }
-
-    private static final class HttpsEngine extends HttpEngine {
-        /**
-         * Stash of HttpsEngine.connection.socket to implement requests like
-         * {@link #getCipherSuite} even after the connection has been recycled.
-         */
-        private SSLSocket sslSocket;
-
-        private final HttpsURLConnectionImpl enclosing;
-
-        /**
-         * @param policy the HttpURLConnectionImpl with connection configuration
-         * @param enclosing the HttpsURLConnection with HTTPS features
-         */
-        private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
-                Connection connection, RetryableOutputStream requestBody,
-                HttpsURLConnectionImpl enclosing) throws IOException {
-            super(policy, method, requestHeaders, connection, requestBody);
-            this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
-            this.enclosing = enclosing;
-        }
-
-        @Override protected void connected(Connection connection) {
-            this.sslSocket = (SSLSocket) connection.getSocket();
-        }
-
-        @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 file.
-            return false;
-        }
-
-        @Override protected SSLSocketFactory getSslSocketFactory() {
-            return enclosing.getSSLSocketFactory();
-        }
-
-        @Override protected HostnameVerifier getHostnameVerifier() {
-            return enclosing.getHostnameVerifier();
-        }
-
-        @Override protected HttpURLConnection getHttpConnectionToCache() {
-            return enclosing;
-        }
-
-        @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/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
index edb1436..6799eeb 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -51,389 +51,372 @@
  * 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 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 (!statusLine.startsWith("HTTP/1.")
+        || statusLine.charAt(8) != ' '
+        || 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 = statusLine.substring(13);
+    this.responseCode = responseCode;
+    this.statusLine = statusLine;
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
+    String status = null;
+    String version = null;
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String name = namesAndValues.get(i);
+      if (":status".equals(name)) {
+        status = namesAndValues.get(i + 1);
+      } else if (":version".equals(name)) {
+        version = namesAndValues.get(i + 1);
+      }
+    }
+    if (status == null || version == null) {
+      throw new ProtocolException("Expected ':status' and ':version' headers not present");
+    }
+    setStatusLine(version + " " + status);
+  }
+
+  /**
+   * @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.
+   */
+  public void addLine(String line) {
+    int index = line.indexOf(":");
+    if (index == -1) {
+      add("", line);
+    } else {
+      add(line.substring(0, index), line.substring(index + 1));
+    }
+  }
+
+  /** 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) {
+      // Given null values, the RI sends a malformed field line like
+      // "Accept\r\n". For platform compatibility and HTTP compliance, we
+      // print a warning and ignore null values.
+      Platform.get()
+          .logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
+      return;
+    }
+    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 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;
+  }
+
+  /** @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) {
+        result.addAll(fieldName, values);
+      } 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);
+
+      // TODO: promote this check to where names and values are created
+      if (name.length() == 0
+          || value.length() == 0
+          || name.indexOf('\0') != -1
+          || value.indexOf('\0') != -1) {
+        throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+      }
+
+      // 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;
         }
-    };
-
-    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() {
+      }
     }
+    return result;
+  }
 
-    public RawHeaders(RawHeaders copyFrom) {
-        namesAndValues.addAll(copyFrom.namesAndValues);
-        requestLine = copyFrom.requestLine;
-        statusLine = copyFrom.statusLine;
-        httpMinorVersion = copyFrom.httpMinorVersion;
-        responseCode = copyFrom.responseCode;
-        responseMessage = copyFrom.responseMessage;
+  public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
+    if (nameValueBlock.size() % 2 != 0) {
+      throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
     }
-
-    /**
-     * 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 (!statusLine.startsWith("HTTP/1.")
-                || statusLine.charAt(8) != ' '
-                || statusLine.charAt(12) != ' ') {
-            throw new ProtocolException("Unexpected status line: " + statusLine);
+    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();
         }
-        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 = statusLine.substring(13);
-        this.responseCode = responseCode;
-        this.statusLine = statusLine;
-        this.httpMinorVersion = httpMinorVersion;
+        result.namesAndValues.add(name);
+        result.namesAndValues.add(values.substring(start, end));
+        start = end + 1;
+      }
     }
-
-    public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
-        String status = null;
-        String version = null;
-        for (int i = 0; i < namesAndValues.size(); i += 2) {
-            String name = namesAndValues.get(i);
-            if ("status".equals(name)) {
-                status = namesAndValues.get(i + 1);
-            } else if ("version".equals(name)) {
-                version = namesAndValues.get(i + 1);
-            }
-        }
-        if (status == null || version == null) {
-            throw new ProtocolException("Expected 'status' and 'version' headers not present");
-        }
-        setStatusLine(version + " " + status);
-    }
-
-    /**
-     * @param method like "GET", "POST", "HEAD", etc.
-     * @param scheme like "https"
-     * @param url like "/foo/bar.html"
-     * @param version like "HTTP/1.1"
-     */
-    public void addSpdyRequestHeaders(String method, String scheme, String url, String version) {
-        // TODO: populate the statusLine for the client's benefit?
-        add("method", method);
-        add("scheme", scheme);
-        add("url", url);
-        add("version", version);
-    }
-
-    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.
-     */
-    public void addLine(String line) {
-        int index = line.indexOf(":");
-        if (index == -1) {
-            add("", line);
-        } else {
-            add(line.substring(0, index), line.substring(index + 1));
-        }
-    }
-
-    /**
-     * 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) {
-            /*
-             * Given null values, the RI sends a malformed field line like
-             * "Accept\r\n". For platform compatibility and HTTP compliance, we
-             * print a warning and ignore null values.
-             */
-            Platform.get().logW("Ignoring HTTP header field '"
-                    + fieldName + "' because its value is null");
-            return;
-        }
-        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 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;
-    }
-
-    /**
-     * @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) {
-                result.addAll(fieldName, values);
-            } 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);
-
-            // TODO: promote this check to where names and values are created
-            if (name.length() == 0 || value.length() == 0
-                    || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
-                throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
-            }
-
-            // Drop headers that are ignored when layering HTTP over SPDY.
-            if (name.equals("connection") || name.equals("accept-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;
-    }
-
-    public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
-        if (nameValueBlock.size() % 2 != 0) {
-            throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
-        }
-        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();
-                }
-                result.namesAndValues.add(name);
-                result.namesAndValues.add(values.substring(start, end));
-                start = end + 1;
-            }
-        }
-        return result;
-    }
+    return result;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
index 2c12b95..2544cee 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -21,272 +21,270 @@
 import java.util.List;
 import java.util.Map;
 
-/**
- * Parsed HTTP request headers.
- */
+/** Parsed HTTP request headers. */
 final class RequestHeaders {
-    private final URI uri;
-    private final RawHeaders headers;
+  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;
+  /** 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;
+  /**
+   * 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;
+  /**
+   * 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 int 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;
+  private int 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;
+  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;
-            }
+    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;
         }
-    }
+      }
+    };
 
-    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 int 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");
+    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;
         }
-        headers.add("Transfer-Encoding", "chunked");
-        this.transferEncoding = "chunked";
-    }
-
-    public void setContentLength(int contentLength) {
-        if (this.contentLength != -1) {
-            headers.removeAll("Content-Length");
+      } 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) {
         }
-        headers.add("Content-Length", Integer.toString(contentLength));
-        this.contentLength = contentLength;
+      } 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 void setUserAgent(String userAgent) {
-        if (this.userAgent != null) {
-            headers.removeAll("User-Agent");
-        }
-        headers.add("User-Agent", userAgent);
-        this.userAgent = userAgent;
-    }
+  public boolean isChunked() {
+    return "chunked".equalsIgnoreCase(transferEncoding);
+  }
 
-    public void setHost(String host) {
-        if (this.host != null) {
-            headers.removeAll("Host");
-        }
-        headers.add("Host", host);
-        this.host = host;
-    }
+  public boolean hasConnectionClose() {
+    return "close".equalsIgnoreCase(connection);
+  }
 
-    public void setConnection(String connection) {
-        if (this.connection != null) {
-            headers.removeAll("Connection");
-        }
-        headers.add("Connection", connection);
-        this.connection = connection;
-    }
+  public URI getUri() {
+    return uri;
+  }
 
-    public void setAcceptEncoding(String acceptEncoding) {
-        if (this.acceptEncoding != null) {
-            headers.removeAll("Accept-Encoding");
-        }
-        headers.add("Accept-Encoding", acceptEncoding);
-        this.acceptEncoding = acceptEncoding;
-    }
+  public RawHeaders getHeaders() {
+    return headers;
+  }
 
-    public void setContentType(String contentType) {
-        if (this.contentType != null) {
-            headers.removeAll("Content-Type");
-        }
-        headers.add("Content-Type", contentType);
-        this.contentType = contentType;
-    }
+  public boolean isNoCache() {
+    return noCache;
+  }
 
-    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 int getMaxAgeSeconds() {
+    return maxAgeSeconds;
+  }
 
-    public void setIfNoneMatch(String ifNoneMatch) {
-        if (this.ifNoneMatch != null) {
-            headers.removeAll("If-None-Match");
-        }
-        headers.add("If-None-Match", ifNoneMatch);
-        this.ifNoneMatch = ifNoneMatch;
-    }
+  public int getMaxStaleSeconds() {
+    return maxStaleSeconds;
+  }
 
-    /**
-     * 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 int getMinFreshSeconds() {
+    return minFreshSeconds;
+  }
 
-    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)) {
-                headers.addAll(key, entry.getValue());
-            }
-        }
+  public boolean isOnlyIfCached() {
+    return onlyIfCached;
+  }
+
+  public boolean hasAuthorization() {
+    return hasAuthorization;
+  }
+
+  public int 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(int contentLength) {
+    if (this.contentLength != -1) {
+      headers.removeAll("Content-Length");
+    }
+    headers.add("Content-Length", Integer.toString(contentLength));
+    this.contentLength = contentLength;
+  }
+
+  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)) {
+        headers.addAll(key, entry.getValue());
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
index d333d8b..4e520db 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -17,7 +17,6 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.ResponseSource;
-import static com.squareup.okhttp.internal.Util.equal;
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URI;
@@ -29,476 +28,462 @@
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 
-/**
- * Parsed HTTP response headers.
- */
+import static com.squareup.okhttp.internal.Util.equal;
+
+/** Parsed HTTP response headers. */
 final class ResponseHeaders {
 
-    /** HTTP header name for the local time when the request was sent. */
-    private static final String SENT_MILLIS = "X-Android-Sent-Millis";
+  /** HTTP header name for the local time when the request was sent. */
+  private static final String SENT_MILLIS = "X-Android-Sent-Millis";
 
-    /** HTTP header name for the local time when the response was received. */
-    private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
+  /** HTTP header name for the local time when the response was received. */
+  private static final String RECEIVED_MILLIS = "X-Android-Received-Millis";
 
-    private final URI uri;
-    private final RawHeaders headers;
+  private final URI uri;
+  private final RawHeaders headers;
 
-    /** The server's time when this response was served, if known. */
-    private Date servedDate;
+  /** 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 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;
+  /**
+   * 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 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;
+  /**
+   * 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;
+  /**
+   * 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;
+  /** 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 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;
+  /**
+   * 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;
+  /**
+   * 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();
+  /** 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;
+  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;
+  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);
-            }
+    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;
         }
-    }
+      }
+    };
 
-    public boolean isContentEncodingGzip() {
-        return "gzip".equalsIgnoreCase(contentEncoding);
-    }
-
-    public void stripContentEncoding() {
-        contentEncoding = null;
-        headers.removeAll("Content-Encoding");
-    }
-
-    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));
-    }
-
-    /**
-     * 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;
+    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;
         }
-        return 0;
+      } 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));
+  }
+
+  /**
+   * 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;
     }
 
-    /**
-     * 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;
+    // 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;
     }
 
-    /**
-     * 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;
+    if (noStore) {
+      return false;
     }
 
-    /**
-     * Returns true if a Vary header contains an asterisk. Such responses cannot
-     * be cached.
-     */
-    public boolean hasVaryAll() {
-        return varyFields.contains("*");
-    }
+    return true;
+  }
 
-    /**
-     * 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 true if a Vary header contains an asterisk. Such responses cannot
+   * be cached.
+   */
+  public boolean hasVaryAll() {
+    return varyFields.contains("*");
+  }
 
-    /**
-     * 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\"");
-            }
-            if (ageMillis > TimeUnit.HOURS.toMillis(24) && 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;
-        }
-
+  /**
+   * 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;
     }
 
-    /**
-     * 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);
+    if (request.isNoCache() || request.hasConditions()) {
+      return ResponseSource.NETWORK;
     }
 
-    /**
-     * 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);
+    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\"");
+      }
+      if (ageMillis > TimeUnit.HOURS.toMillis(24) && 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/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
index a5c5842..325327d 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java
@@ -16,58 +16,59 @@
 
 package com.squareup.okhttp.internal.http;
 
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
 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 AbstractHttpOutputStream {
-    private final int limit;
-    private final ByteArrayOutputStream content;
+  private final int limit;
+  private final ByteArrayOutputStream content;
 
-    public RetryableOutputStream(int limit) {
-        this.limit = limit;
-        this.content = new ByteArrayOutputStream(limit);
-    }
+  public RetryableOutputStream(int limit) {
+    this.limit = limit;
+    this.content = new ByteArrayOutputStream(limit);
+  }
 
-    public RetryableOutputStream() {
-        this.limit = -1;
-        this.content = new ByteArrayOutputStream();
-    }
+  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 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);
+  @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 synchronized int contentLength() throws IOException {
+    close();
+    return content.size();
+  }
 
-    public void writeToSocket(OutputStream socketOut) throws IOException  {
-        content.writeTo(socketOut);
-    }
+  public void writeToSocket(OutputStream socketOut) throws IOException {
+    content.writeTo(socketOut);
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index ac4bb6c..798cff3 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -19,7 +19,6 @@
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.internal.Dns;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -32,206 +31,206 @@
 import java.util.List;
 import java.util.NoSuchElementException;
 
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
 /**
  * Selects routes to connect to an origin server. Each connection requires a
  * choice of proxy server, IP address, and TLS mode. Connections may also be
  * recycled.
  */
-final class RouteSelector {
-    /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
-    private static final int TLS_MODE_MODERN = 1;
-    /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
-    private static final int TLS_MODE_COMPATIBLE = 0;
-    /** No TLS mode. */
-    private static final int TLS_MODE_NULL = -1;
+public final class RouteSelector {
+  /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
+  private static final int TLS_MODE_MODERN = 1;
+  /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
+  private static final int TLS_MODE_COMPATIBLE = 0;
+  /** No TLS mode. */
+  private static final int TLS_MODE_NULL = -1;
 
-    private final Address address;
-    private final URI uri;
-    private final ProxySelector proxySelector;
-    private final ConnectionPool pool;
-    private final Dns dns;
+  private final Address address;
+  private final URI uri;
+  private final ProxySelector proxySelector;
+  private final ConnectionPool pool;
+  private final Dns dns;
 
-    /* The most recently attempted route. */
-    private Proxy lastProxy;
-    private InetSocketAddress lastInetSocketAddress;
+  /* The most recently attempted route. */
+  private Proxy lastProxy;
+  private InetSocketAddress lastInetSocketAddress;
 
-    /* State for negotiating the next proxy to use. */
-    private boolean hasNextProxy;
-    private Proxy userSpecifiedProxy;
-    private Iterator<Proxy> proxySelectorProxies;
+  /* State for negotiating the next proxy to use. */
+  private boolean hasNextProxy;
+  private Proxy userSpecifiedProxy;
+  private Iterator<Proxy> proxySelectorProxies;
 
-    /* State for negotiating the next InetSocketAddress to use. */
-    private InetAddress[] socketAddresses;
-    private int nextSocketAddressIndex;
-    private String socketHost;
-    private int socketPort;
+  /* State for negotiating the next InetSocketAddress to use. */
+  private InetAddress[] socketAddresses;
+  private int nextSocketAddressIndex;
+  private String socketHost;
+  private int socketPort;
 
-    /* State for negotiating the next TLS configuration */
-    private int nextTlsMode = TLS_MODE_NULL;
+  /* State for negotiating the next TLS configuration */
+  private int nextTlsMode = TLS_MODE_NULL;
 
-    public RouteSelector(Address address, URI uri, ProxySelector proxySelector,
-            ConnectionPool pool, Dns dns) {
-        this.address = address;
-        this.uri = uri;
-        this.proxySelector = proxySelector;
-        this.pool = pool;
-        this.dns = dns;
+  public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
+      Dns dns) {
+    this.address = address;
+    this.uri = uri;
+    this.proxySelector = proxySelector;
+    this.pool = pool;
+    this.dns = dns;
 
-        resetNextProxy(uri, address.getProxy());
+    resetNextProxy(uri, address.getProxy());
+  }
+
+  /**
+   * Returns true if there's another route to attempt. Every address has at
+   * least one route.
+   */
+  public boolean hasNext() {
+    return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
+  }
+
+  /**
+   * Returns the next route address to attempt.
+   *
+   * @throws NoSuchElementException if there are no more routes to attempt.
+   */
+  public Connection next() throws IOException {
+    // Always prefer pooled connections over new connections.
+    Connection pooled = pool.get(address);
+    if (pooled != null) {
+      return pooled;
     }
 
-    /**
-     * Returns true if there's another route to attempt. Every address has at
-     * least one route.
-     */
-    public boolean hasNext() {
-        return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
-    }
-
-    /**
-     * Returns the next route address to attempt.
-     *
-     * @throws NoSuchElementException if there are no more routes to attempt.
-     */
-    public Connection next() throws IOException {
-        // Always prefer pooled connections over new connections.
-        Connection pooled = pool.get(address);
-        if (pooled != null) {
-            return pooled;
+    // Compute the next route to attempt.
+    if (!hasNextTlsMode()) {
+      if (!hasNextInetSocketAddress()) {
+        if (!hasNextProxy()) {
+          throw new NoSuchElementException();
         }
+        lastProxy = nextProxy();
+        resetNextInetSocketAddress(lastProxy);
+      }
+      lastInetSocketAddress = nextInetSocketAddress();
+      resetNextTlsMode();
+    }
+    boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
 
-        // Compute the next route to attempt.
-        if (!hasNextTlsMode()) {
-            if (!hasNextInetSocketAddress()) {
-                if (!hasNextProxy()) {
-                    throw new NoSuchElementException();
-                }
-                lastProxy = nextProxy();
-                resetNextInetSocketAddress(lastProxy);
-            }
-            lastInetSocketAddress = nextInetSocketAddress();
-            resetNextTlsMode();
+    return new Connection(address, lastProxy, lastInetSocketAddress, modernTls);
+  }
+
+  /**
+   * Clients should invoke this method when they encounter a connectivity
+   * failure on a connection returned by this route selector.
+   */
+  public void connectFailed(Connection connection, IOException failure) {
+    if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
+      // Tell the proxy selector when we fail to connect on a fresh connection.
+      proxySelector.connectFailed(uri, connection.getProxy().address(), failure);
+    }
+  }
+
+  /** Resets {@link #nextProxy} to the first option. */
+  private void resetNextProxy(URI uri, Proxy proxy) {
+    this.hasNextProxy = true; // This includes NO_PROXY!
+    if (proxy != null) {
+      this.userSpecifiedProxy = proxy;
+    } else {
+      List<Proxy> proxyList = proxySelector.select(uri);
+      if (proxyList != null) {
+        this.proxySelectorProxies = proxyList.iterator();
+      }
+    }
+  }
+
+  /** Returns true if there's another proxy to try. */
+  private boolean hasNextProxy() {
+    return hasNextProxy;
+  }
+
+  /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
+  private Proxy nextProxy() {
+    // If the user specifies a proxy, try that and only that.
+    if (userSpecifiedProxy != null) {
+      hasNextProxy = false;
+      return userSpecifiedProxy;
+    }
+
+    // Try each of the ProxySelector choices until one connection succeeds. If none succeed
+    // then we'll try a direct connection below.
+    if (proxySelectorProxies != null) {
+      while (proxySelectorProxies.hasNext()) {
+        Proxy candidate = proxySelectorProxies.next();
+        if (candidate.type() != Proxy.Type.DIRECT) {
+          return candidate;
         }
-        boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
-
-        return new Connection(address, lastProxy, lastInetSocketAddress, modernTls);
+      }
     }
 
-    /**
-     * Clients should invoke this method when they encounter a connectivity
-     * failure on a connection returned by this route selector.
-     */
-    public void connectFailed(Connection connection, IOException failure) {
-        if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
-            // Tell the proxy selector when we fail to connect on a fresh connection.
-            proxySelector.connectFailed(uri, connection.getProxy().address(), failure);
-        }
+    // Finally try a direct connection.
+    hasNextProxy = false;
+    return Proxy.NO_PROXY;
+  }
+
+  /** Resets {@link #nextInetSocketAddress} to the first option. */
+  private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
+    socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
+
+    if (proxy.type() == Proxy.Type.DIRECT) {
+      socketHost = uri.getHost();
+      socketPort = getEffectivePort(uri);
+    } else {
+      SocketAddress proxyAddress = proxy.address();
+      if (!(proxyAddress instanceof InetSocketAddress)) {
+        throw new IllegalArgumentException(
+            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
+      }
+      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
+      socketHost = proxySocketAddress.getHostName();
+      socketPort = proxySocketAddress.getPort();
     }
 
-    /** Resets {@link #nextProxy} to the first option. */
-    private void resetNextProxy(URI uri, Proxy proxy) {
-        this.hasNextProxy = true; // This includes NO_PROXY!
-        if (proxy != null) {
-            this.userSpecifiedProxy = proxy;
-        } else {
-            List<Proxy> proxyList = proxySelector.select(uri);
-            if (proxyList != null) {
-                this.proxySelectorProxies = proxyList.iterator();
-            }
-        }
+    // Try each address for best behavior in mixed IPv4/IPv6 environments.
+    socketAddresses = dns.getAllByName(socketHost);
+    nextSocketAddressIndex = 0;
+  }
+
+  /** Returns true if there's another socket address to try. */
+  private boolean hasNextInetSocketAddress() {
+    return socketAddresses != null;
+  }
+
+  /** Returns the next socket address to try. */
+  private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
+    InetSocketAddress result =
+        new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort);
+    if (nextSocketAddressIndex == socketAddresses.length) {
+      socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
+      nextSocketAddressIndex = 0;
     }
 
-    /** Returns true if there's another proxy to try. */
-    private boolean hasNextProxy() {
-        return hasNextProxy;
+    return result;
+  }
+
+  /** Resets {@link #nextTlsMode} to the first option. */
+  private void resetNextTlsMode() {
+    nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE;
+  }
+
+  /** Returns true if there's another TLS mode to try. */
+  private boolean hasNextTlsMode() {
+    return nextTlsMode != TLS_MODE_NULL;
+  }
+
+  /** Returns the next TLS mode to try. */
+  private int nextTlsMode() {
+    if (nextTlsMode == TLS_MODE_MODERN) {
+      nextTlsMode = TLS_MODE_COMPATIBLE;
+      return TLS_MODE_MODERN;
+    } else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
+      nextTlsMode = TLS_MODE_NULL;  // So that hasNextTlsMode() returns false.
+      return TLS_MODE_COMPATIBLE;
+    } else {
+      throw new AssertionError();
     }
-
-    /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
-    private Proxy nextProxy() {
-        // If the user specifies a proxy, try that and only that.
-        if (userSpecifiedProxy != null) {
-            hasNextProxy = false;
-            return userSpecifiedProxy;
-        }
-
-        // Try each of the ProxySelector choices until one connection succeeds. If none succeed
-        // then we'll try a direct connection below.
-        if (proxySelectorProxies != null) {
-            while (proxySelectorProxies.hasNext()) {
-                Proxy candidate = proxySelectorProxies.next();
-                if (candidate.type() != Proxy.Type.DIRECT) {
-                    return candidate;
-                }
-            }
-        }
-
-        // Finally try a direct connection.
-        hasNextProxy = false;
-        return Proxy.NO_PROXY;
-    }
-
-    /** Resets {@link #nextInetSocketAddress} to the first option. */
-    private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
-        socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
-
-        if (proxy.type() == Proxy.Type.DIRECT) {
-            socketHost = uri.getHost();
-            socketPort = getEffectivePort(uri);
-        } else {
-            SocketAddress proxyAddress = proxy.address();
-            if (!(proxyAddress instanceof InetSocketAddress)) {
-                throw new IllegalArgumentException("Proxy.address() is not an "
-                        + "InetSocketAddress: " + proxyAddress.getClass());
-            }
-            InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
-            socketHost = proxySocketAddress.getHostName();
-            socketPort = proxySocketAddress.getPort();
-        }
-
-        // Try each address for best behavior in mixed IPv4/IPv6 environments.
-        socketAddresses = dns.getAllByName(socketHost);
-        nextSocketAddressIndex = 0;
-    }
-
-    /** Returns true if there's another socket address to try. */
-    private boolean hasNextInetSocketAddress() {
-        return socketAddresses != null;
-    }
-
-    /** Returns the next socket address to try. */
-    private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
-        InetSocketAddress result = new InetSocketAddress(
-                socketAddresses[nextSocketAddressIndex++], socketPort);
-        if (nextSocketAddressIndex == socketAddresses.length) {
-            socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
-            nextSocketAddressIndex = 0;
-        }
-
-        return result;
-    }
-
-    /** Resets {@link #nextTlsMode} to the first option. */
-    private void resetNextTlsMode() {
-        nextTlsMode = (address.getSslSocketFactory() != null)
-                ? TLS_MODE_MODERN
-                : TLS_MODE_COMPATIBLE;
-    }
-
-    /** Returns true if there's another TLS mode to try. */
-    private boolean hasNextTlsMode() {
-        return nextTlsMode != TLS_MODE_NULL;
-    }
-
-    /** Returns the next TLS mode to try. */
-    private int nextTlsMode() {
-        if (nextTlsMode == TLS_MODE_MODERN) {
-            nextTlsMode = TLS_MODE_COMPATIBLE;
-            return TLS_MODE_MODERN;
-        } else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
-            nextTlsMode = TLS_MODE_NULL;  // So that hasNextTlsMode() returns false.
-            return TLS_MODE_COMPATIBLE;
-        } else {
-            throw new AssertionError();
-        }
-    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index 28a47ab..193beb6 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -22,64 +22,67 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.CacheRequest;
+import java.net.URL;
 import java.util.List;
 
 public final class SpdyTransport implements Transport {
-    private final HttpEngine httpEngine;
-    private final SpdyConnection spdyConnection;
-    private SpdyStream stream;
+  private final HttpEngine httpEngine;
+  private final SpdyConnection spdyConnection;
+  private SpdyStream stream;
 
-    // TODO: set sentMillis
-    // TODO: set cookie stuff
+  public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
+    this.httpEngine = httpEngine;
+    this.spdyConnection = spdyConnection;
+  }
 
-    public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
-        this.httpEngine = httpEngine;
-        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 void writeRequestHeaders() 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,
+        hasResponseBody);
+    stream.setReadTimeout(httpEngine.policy.getReadTimeout());
+  }
 
-    @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 void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+    throw new UnsupportedOperationException();
+  }
 
-    @Override public void writeRequestHeaders() throws IOException {
-        if (stream != null) {
-            return;
-        }
-        RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
-        String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
-        requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(),
-                HttpEngine.requestPath(httpEngine.policy.getURL()), version);
-        boolean hasRequestBody = httpEngine.hasRequestBody();
-        boolean hasResponseBody = true;
-        stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(),
-                hasRequestBody, hasResponseBody);
-        stream.setReadTimeout(httpEngine.policy.getReadTimeout());
-    }
+  @Override public void flushRequest() throws IOException {
+    stream.getOutputStream().close();
+  }
 
-    @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
-        throw new UnsupportedOperationException();
-    }
+  @Override public ResponseHeaders readResponseHeaders() throws IOException {
+    List<String> nameValueBlock = stream.getResponseHeaders();
+    RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
+    rawHeaders.computeResponseStatusLineFromSpdyHeaders();
+    httpEngine.receiveHeaders(rawHeaders);
+    return new ResponseHeaders(httpEngine.uri, rawHeaders);
+  }
 
-    @Override public void flushRequest() throws IOException {
-        stream.getOutputStream().close();
-    }
+  @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+    return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
+  }
 
-    @Override public ResponseHeaders readResponseHeaders() throws IOException {
-        // TODO: fix the SPDY implementation so this throws a (buffered) IOException
-        List<String> nameValueBlock = stream.getResponseHeaders();
-        RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
-        rawHeaders.computeResponseStatusLineFromSpdyHeaders();
-        return new ResponseHeaders(httpEngine.uri, rawHeaders);
+  @Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+      InputStream responseBodyIn) {
+    if (streamCancelled) {
+      stream.closeLater(SpdyStream.RST_CANCEL);
     }
-
-    @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
-        // TODO: handle HTTP responses that don't have a response body
-        return stream.getInputStream();
-    }
-
-    @Override public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
-        return true;
-    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index f1212d5..518827e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -22,50 +22,43 @@
 import java.net.CacheRequest;
 
 interface Transport {
-    /**
-     * Returns an output stream where the request body can be written. The
-     * returned stream will of one of two types:
-     * <ul>
-     *     <li><strong>Direct.</strong> Bytes are written to the socket and
-     *     forgotten. This is most efficient, particularly for large request
-     *     bodies. The returned stream may be buffered; the caller must call
-     *     {@link #flushRequest} before reading the response.</li>
-     *     <li><strong>Buffered.</strong> Bytes are written to an in memory
-     *     buffer, and must be explicitly flushed with a call to {@link
-     *     #writeRequestBody}. This allows HTTP authorization (401, 407)
-     *     responses to be retransmitted transparently.</li>
-     * </ul>
-     */
-    // 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;
+  /**
+   * Returns an output stream where the request body can be written. The
+   * returned stream will of one of two types:
+   * <ul>
+   * <li><strong>Direct.</strong> Bytes are written to the socket and
+   * forgotten. This is most efficient, particularly for large request
+   * bodies. The returned stream may be buffered; the caller must call
+   * {@link #flushRequest} before reading the response.</li>
+   * <li><strong>Buffered.</strong> Bytes are written to an in memory
+   * buffer, and must be explicitly flushed with a call to {@link
+   * #writeRequestBody}. This allows HTTP authorization (401, 407)
+   * responses to be retransmitted transparently.</li>
+   * </ul>
+   */
+  // 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;
 
-    /**
-     * This should update the HTTP engine's sentRequestMillis field.
-     */
-    void writeRequestHeaders() throws IOException;
+  /** This should update the HTTP engine's sentRequestMillis field. */
+  void writeRequestHeaders() throws IOException;
 
-    /**
-     * Sends the request body returned by {@link #createRequestBody} to the
-     * remote peer.
-     */
-    void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
+  /**
+   * Sends the request body returned by {@link #createRequestBody} to the
+   * remote peer.
+   */
+  void writeRequestBody(RetryableOutputStream requestBody) throws IOException;
 
-    /**
-     * Flush the request body to the underlying socket.
-     */
-    void flushRequest() 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;
+  /** Read response headers and update the cookie manager. */
+  ResponseHeaders readResponseHeaders() throws IOException;
 
-    // TODO: make this the content stream?
-    InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
+  // TODO: make this the content stream?
+  InputStream getTransferStream(CacheRequest cacheRequest) throws IOException;
 
-    /**
-     * Returns true if the underlying connection can be recycled.
-     */
-    boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn);
+  /** Returns true if the underlying connection can be recycled. */
+  boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut,
+      InputStream responseBodyIn);
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java b/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
new file mode 100644
index 0000000..729e0b9
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
@@ -0,0 +1,63 @@
+/*
+ * 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 is, CacheRequest cacheRequest, HttpEngine httpEngine)
+      throws IOException {
+    super(is, 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(false);
+      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/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
index fc554f4..875fff0 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
@@ -18,21 +18,19 @@
 
 import java.io.IOException;
 
-/**
- * Listener to be notified when a connected peer creates a new stream.
- */
+/** Listener to be notified when a connected peer creates a new stream. */
 public interface IncomingStreamHandler {
-    IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
-        @Override public void receive(SpdyStream stream) throws IOException {
-            stream.close(SpdyStream.RST_REFUSED_STREAM);
-        }
-    };
+  IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
+    @Override public void receive(SpdyStream stream) throws IOException {
+      stream.close(SpdyStream.RST_REFUSED_STREAM);
+    }
+  };
 
-    /**
-     * Handle a new stream from this connection's peer. Implementations should
-     * respond by either {@link SpdyStream#reply replying to the stream} or
-     * {@link SpdyStream#close closing it}. This response does not need to be
-     * synchronous.
-     */
-    void receive(SpdyStream stream) throws IOException;
+  /**
+   * Handle a new stream from this connection's peer. Implementations should
+   * respond by either {@link SpdyStream#reply replying to the stream} or
+   * {@link SpdyStream#close closing it}. This response does not need to be
+   * synchronous.
+   */
+  void receive(SpdyStream stream) throws IOException;
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java b/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
index 1fc3979..c585255 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
@@ -22,50 +22,50 @@
  * A locally-originated ping.
  */
 public final class Ping {
-    private final CountDownLatch latch = new CountDownLatch(1);
-    private long sent = -1;
-    private long received = -1;
+  private final CountDownLatch latch = new CountDownLatch(1);
+  private long sent = -1;
+  private long received = -1;
 
-    Ping() {
-    }
+  Ping() {
+  }
 
-    void send() {
-        if (sent != -1) throw new IllegalStateException();
-        sent = System.nanoTime();
-    }
+  void send() {
+    if (sent != -1) throw new IllegalStateException();
+    sent = System.nanoTime();
+  }
 
-    void receive() {
-        if (received != -1 || sent == -1) throw new IllegalStateException();
-        received = System.nanoTime();
-        latch.countDown();
-    }
+  void receive() {
+    if (received != -1 || sent == -1) throw new IllegalStateException();
+    received = System.nanoTime();
+    latch.countDown();
+  }
 
-    void cancel() {
-        if (received != -1 || sent == -1) throw new IllegalStateException();
-        received = sent - 1;
-        latch.countDown();
-    }
+  void cancel() {
+    if (received != -1 || sent == -1) throw new IllegalStateException();
+    received = sent - 1;
+    latch.countDown();
+  }
 
-    /**
-     * Returns the round trip time for this ping in nanoseconds, waiting for the
-     * response to arrive if necessary. Returns -1 if the response was
-     * cancelled.
-     */
-    public long roundTripTime() throws InterruptedException {
-        latch.await();
-        return received - sent;
-    }
+  /**
+   * Returns the round trip time for this ping in nanoseconds, waiting for the
+   * response to arrive if necessary. Returns -1 if the response was
+   * cancelled.
+   */
+  public long roundTripTime() throws InterruptedException {
+    latch.await();
+    return received - sent;
+  }
 
-    /**
-     * Returns the round trip time for this ping in nanoseconds, or -1 if the
-     * response was cancelled, or -2 if the timeout elapsed before the round
-     * trip completed.
-     */
-    public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException {
-        if (latch.await(timeout, unit)) {
-            return received - sent;
-        } else {
-            return -2;
-        }
+  /**
+   * Returns the round trip time for this ping in nanoseconds, or -1 if the
+   * response was cancelled, or -2 if the timeout elapsed before the round
+   * trip completed.
+   */
+  public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException {
+    if (latch.await(timeout, unit)) {
+      return received - sent;
+    } else {
+      return -2;
     }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
index f4136e6..774d791 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
@@ -16,156 +16,159 @@
 package com.squareup.okhttp.internal.spdy;
 
 final class Settings {
-    /** Peer request to clear durable settings. */
-    static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
+  /**
+   * From the spdy/3 spec, 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;
 
-    /** Sent by servers only. The peer requests this setting persisted for future connections. */
-    static final int PERSIST_VALUE = 0x1;
-    /** Sent by clients only. The client is reminding the server of a persisted value. */
-    static final int PERSISTED = 0x2;
+  /** Peer request to clear durable settings. */
+  static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
 
-    /** Sender's estimate of max incoming kbps. */
-    static final int UPLOAD_BANDWIDTH = 0x1;
-    /** Sender's estimate of max outgoing kbps. */
-    static final int DOWNLOAD_BANDWIDTH = 0x2;
-    /** Sender's estimate of milliseconds between sending a request and receiving a response. */
-    static final int ROUND_TRIP_TIME = 0x3;
-    /** Sender's maximum number of concurrent streams. */
-    static final int MAX_CONCURRENT_STREAMS = 0x4;
-    /** Current CWND in Packets. */
-    static final int CURRENT_CWND = 0x5;
-    /** Retransmission rate. Percentage */
-    static final int DOWNLOAD_RETRANS_RATE = 0x6;
-    /** Window size in bytes. */
-    static final int INITIAL_WINDOW_SIZE = 0x7;
-    /** Total number of settings. */
-    static final int COUNT = 0x8;
+  /** Sent by servers only. The peer requests this setting persisted for future connections. */
+  static final int PERSIST_VALUE = 0x1;
+  /** Sent by clients only. The client is reminding the server of a persisted value. */
+  static final int PERSISTED = 0x2;
 
-    /** Bitfield of which flags that values. */
-    private int set;
+  /** Sender's estimate of max incoming kbps. */
+  static final int UPLOAD_BANDWIDTH = 0x1;
+  /** Sender's estimate of max outgoing kbps. */
+  static final int DOWNLOAD_BANDWIDTH = 0x2;
+  /** Sender's estimate of milliseconds between sending a request and receiving a response. */
+  static final int ROUND_TRIP_TIME = 0x3;
+  /** Sender's maximum number of concurrent streams. */
+  static final int MAX_CONCURRENT_STREAMS = 0x4;
+  /** Current CWND in Packets. */
+  static final int CURRENT_CWND = 0x5;
+  /** Retransmission rate. Percentage */
+  static final int DOWNLOAD_RETRANS_RATE = 0x6;
+  /** Window size in bytes. */
+  static final int INITIAL_WINDOW_SIZE = 0x7;
+  /** Window size in bytes. */
+  static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 0x8;
+  /** Total number of settings. */
+  static final int COUNT = 0x9;
 
-    /** Bitfield of flags that have {@link #PERSIST_VALUE}. */
-    private int persistValue;
+  /** Bitfield of which flags that values. */
+  private int set;
 
-    /** Bitfield of flags that have {@link #PERSISTED}. */
-    private int persisted;
+  /** Bitfield of flags that have {@link #PERSIST_VALUE}. */
+  private int persistValue;
 
-    /** Flag values. */
-    private final int[] values = new int[COUNT];
+  /** Bitfield of flags that have {@link #PERSISTED}. */
+  private int persisted;
 
-    void set(int id, int idFlags, int value) {
-        if (id >= values.length) {
-            return; // Discard unknown settings.
-        }
+  /** Flag values. */
+  private final int[] values = new int[COUNT];
 
-        int bit = 1 << id;
-        set |= bit;
-        if ((idFlags & PERSIST_VALUE) != 0) {
-            persistValue |= bit;
-        } else {
-            persistValue &= ~bit;
-        }
-        if ((idFlags & PERSISTED) != 0) {
-            persisted |= bit;
-        } else {
-            persisted &= ~bit;
-        }
-
-        values[id] = value;
+  void set(int id, int idFlags, int value) {
+    if (id >= values.length) {
+      return; // Discard unknown settings.
     }
 
-    /**
-     * Returns true if a value has been assigned for the setting {@code id}.
-     */
-    boolean isSet(int id) {
-        int bit = 1 << id;
-        return (set & bit) != 0;
+    int bit = 1 << id;
+    set |= bit;
+    if ((idFlags & PERSIST_VALUE) != 0) {
+      persistValue |= bit;
+    } else {
+      persistValue &= ~bit;
+    }
+    if ((idFlags & PERSISTED) != 0) {
+      persisted |= bit;
+    } else {
+      persisted &= ~bit;
     }
 
-    /**
-     * Returns the value for the setting {@code id}, or 0 if unset.
-     */
-    int get(int id) {
-        return values[id];
-    }
+    values[id] = value;
+  }
 
-    /**
-     * Returns the flags for the setting {@code id}, or 0 if unset.
-     */
-    int flags(int id) {
-        int result = 0;
-        if (isPersisted(id)) result |= Settings.PERSISTED;
-        if (persistValue(id)) result |= Settings.PERSIST_VALUE;
-        return result;
-    }
+  /** Returns true if a value has been assigned for the setting {@code id}. */
+  boolean isSet(int id) {
+    int bit = 1 << id;
+    return (set & bit) != 0;
+  }
 
-    /**
-     * Returns the number of settings that have values assigned.
-     */
-    int size() {
-        return Integer.bitCount(set);
-    }
+  /** Returns the value for the setting {@code id}, or 0 if unset. */
+  int get(int id) {
+    return values[id];
+  }
 
-    int getUploadBandwidth(int defaultValue) {
-        int bit = 1 << UPLOAD_BANDWIDTH;
-        return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
-    }
+  /** Returns the flags for the setting {@code id}, or 0 if unset. */
+  int flags(int id) {
+    int result = 0;
+    if (isPersisted(id)) result |= Settings.PERSISTED;
+    if (persistValue(id)) result |= Settings.PERSIST_VALUE;
+    return result;
+  }
 
-    int getDownloadBandwidth(int defaultValue) {
-        int bit = 1 << DOWNLOAD_BANDWIDTH;
-        return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
-    }
+  /** Returns the number of settings that have values assigned. */
+  int size() {
+    return Integer.bitCount(set);
+  }
 
-    int getRoundTripTime(int defaultValue) {
-        int bit = 1 << ROUND_TRIP_TIME;
-        return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
-    }
+  int getUploadBandwidth(int defaultValue) {
+    int bit = 1 << UPLOAD_BANDWIDTH;
+    return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
+  }
 
-    int getMaxConcurrentStreams(int defaultValue) {
-        int bit = 1 << MAX_CONCURRENT_STREAMS;
-        return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
-    }
+  int getDownloadBandwidth(int defaultValue) {
+    int bit = 1 << DOWNLOAD_BANDWIDTH;
+    return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
+  }
 
-    int getCurrentCwnd(int defaultValue) {
-        int bit = 1 << CURRENT_CWND;
-        return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
-    }
+  int getRoundTripTime(int defaultValue) {
+    int bit = 1 << ROUND_TRIP_TIME;
+    return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
+  }
 
-    int getDownloadRetransRate(int defaultValue) {
-        int bit = 1 << DOWNLOAD_RETRANS_RATE;
-        return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
-    }
+  int getMaxConcurrentStreams(int defaultValue) {
+    int bit = 1 << MAX_CONCURRENT_STREAMS;
+    return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
+  }
 
-    int getInitialWindowSize(int defaultValue) {
-        int bit = 1 << INITIAL_WINDOW_SIZE;
-        return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
-    }
+  int getCurrentCwnd(int defaultValue) {
+    int bit = 1 << CURRENT_CWND;
+    return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
+  }
 
-    /**
-     * Returns true if this user agent should use this setting in future SPDY
-     * connections to the same host.
-     */
-    boolean persistValue(int id) {
-        int bit = 1 << id;
-        return (persistValue & bit) != 0;
-    }
+  int getDownloadRetransRate(int defaultValue) {
+    int bit = 1 << DOWNLOAD_RETRANS_RATE;
+    return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
+  }
 
-    /**
-     * Returns true if this setting was persisted.
-     */
-    boolean isPersisted(int id) {
-        int bit = 1 << id;
-        return (persisted & bit) != 0;
-    }
+  int getInitialWindowSize(int defaultValue) {
+    int bit = 1 << INITIAL_WINDOW_SIZE;
+    return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
+  }
 
-    /**
-     * Writes {@code other} into this. If any setting is populated by this and
-     * {@code other}, the value and flags from {@code other} will be kept.
-     */
-    void merge(Settings other) {
-        for (int i = 0; i < COUNT; i++) {
-            if (!other.isSet(i)) continue;
-            set(i, other.flags(i), other.get(i));
-        }
+  int getClientCertificateVectorSize(int defaultValue) {
+    int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
+    return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
+  }
+
+  /**
+   * Returns true if this user agent should use this setting in future SPDY
+   * connections to the same host.
+   */
+  boolean persistValue(int id) {
+    int bit = 1 << id;
+    return (persistValue & bit) != 0;
+  }
+
+  /** Returns true if this setting was persisted. */
+  boolean isPersisted(int id) {
+    int bit = 1 << id;
+    return (persisted & bit) != 0;
+  }
+
+  /**
+   * Writes {@code other} into this. If any setting is populated by this and
+   * {@code other}, the value and flags from {@code other} will be kept.
+   */
+  void merge(Settings other) {
+    for (int i = 0; i < COUNT; i++) {
+      if (!other.isSet(i)) continue;
+      set(i, other.flags(i), other.get(i));
     }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index a67e3e8..4100b9e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -21,7 +21,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.ProtocolException;
 import java.net.Socket;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -44,447 +43,541 @@
  */
 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 spdyWriter.
-     *
-     * Socket reads are unguarded but are only made by the reader thread.
-     *
-     * Certain operations (like SYN_STREAM) need to synchronize on both the
-     * spdyWriter (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'.
-     */
+  // Internal state of this connection is guarded by 'this'. No blocking
+  // operations may be performed while holding this lock!
+  //
+  // Socket writes are guarded by spdyWriter.
+  //
+  // Socket reads are unguarded but are only made by the reader thread.
+  //
+  // Certain operations (like SYN_STREAM) need to synchronize on both the
+  // spdyWriter (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'.
 
-    static final int FLAG_FIN = 0x1;
-    static final int FLAG_UNIDIRECTIONAL = 0x2;
+  static final int FLAG_FIN = 0x1;
+  static final int FLAG_UNIDIRECTIONAL = 0x2;
 
-    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 VERSION = 2;
+  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 VERSION = 3;
+
+  static final int GOAWAY_OK = 0;
+  static final int GOAWAY_PROTOCOL_ERROR = 1;
+  static final int GOAWAY_INTERNAL_ERROR = 2;
+
+  /** 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 SpdyReader spdyReader;
+  private final SpdyWriter spdyWriter;
+  private final ExecutorService readExecutor;
+  private final ExecutorService writeExecutor;
+  private final ExecutorService callbackExecutor;
+
+  private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
+  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 this connection. */
+  Settings settings;
+
+  private SpdyConnection(Builder builder) {
+    client = builder.client;
+    handler = builder.handler;
+    spdyReader = new SpdyReader(builder.in);
+    spdyWriter = new SpdyWriter(builder.out);
+    nextStreamId = builder.client ? 1 : 2;
+    nextPingId = builder.client ? 1 : 2;
+
+    String prefix = builder.client ? "Spdy Client " : "Spdy Server ";
+    readExecutor =
+        new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
+            Util.newThreadFactory(prefix + "Reader", false));
+    writeExecutor =
+        new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+            Util.newThreadFactory(prefix + "Writer", false));
+    callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+        new SynchronousQueue<Runnable>(), Util.newThreadFactory(prefix + "Callbacks", false));
+
+    readExecutor.execute(new Reader());
+  }
+
+  /**
+   * 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 void setIdle(boolean value) {
+    idleStartTimeNs = value ? System.nanoTime() : 0L;
+  }
+
+  /** Returns true if this connection is idle. */
+  public boolean isIdle() {
+    return idleStartTimeNs != 0L;
+  }
+
+  /** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
+  public 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 {
+    int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
+    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 (spdyWriter) {
+      synchronized (this) {
+        if (shutdown) {
+          throw new IOException("shutdown");
+        }
+        streamId = nextStreamId;
+        nextStreamId += 2;
+        stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, settings);
+        if (stream.isOpen()) {
+          streams.put(streamId, stream);
+          setIdle(false);
+        }
+      }
+
+      spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, requestHeaders);
+    }
+
+    return stream;
+  }
+
+  void writeSynReply(int streamId, int flags, List<String> alternating) throws IOException {
+    spdyWriter.synReply(flags, streamId, alternating);
+  }
+
+  /** Writes a complete data frame. */
+  void writeFrame(byte[] bytes, int offset, int length) throws IOException {
+    synchronized (spdyWriter) {
+      spdyWriter.out.write(bytes, offset, length);
+    }
+  }
+
+  void writeSynResetLater(final int streamId, final int statusCode) {
+    writeExecutor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          writeSynReset(streamId, statusCode);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  void writeSynReset(int streamId, int statusCode) throws IOException {
+    spdyWriter.rstStream(streamId, statusCode);
+  }
+
+  void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) {
+    writeExecutor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          writeWindowUpdate(streamId, deltaWindowSize);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException {
+    spdyWriter.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(pingId, ping);
+    return ping;
+  }
+
+  private void writePingLater(final int id, final Ping ping) {
+    writeExecutor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          writePing(id, ping);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  private void writePing(int id, Ping ping) throws IOException {
+    synchronized (spdyWriter) {
+      // Observe the sent time immediately before performing I/O.
+      if (ping != null) ping.send();
+      spdyWriter.ping(0, id);
+    }
+  }
+
+  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 {
+    spdyWriter.noop();
+  }
+
+  public void flush() throws IOException {
+    synchronized (spdyWriter) {
+      spdyWriter.out.flush();
+    }
+  }
+
+  private void shutdownLater(final int statusCode) {
+    writeExecutor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          shutdown(statusCode);
+        } catch (IOException ignored) {
+        }
+      }
+    });
+  }
+
+  /**
+   * 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.
+   *
+   * @param statusCode one of {@link #GOAWAY_OK}, {@link
+   * #GOAWAY_INTERNAL_ERROR} or {@link #GOAWAY_PROTOCOL_ERROR}.
+   */
+  public void shutdown(int statusCode) throws IOException {
+    synchronized (spdyWriter) {
+      int lastGoodStreamId;
+      synchronized (this) {
+        if (shutdown) {
+          return;
+        }
+        shutdown = true;
+        lastGoodStreamId = this.lastGoodStreamId;
+      }
+      spdyWriter.goAway(0, 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(GOAWAY_OK, SpdyStream.RST_CANCEL);
+  }
+
+  private void close(int shutdownStatusCode, int rstStatusCode) throws IOException {
+    assert (!Thread.holdsLock(this));
+    IOException thrown = null;
+    try {
+      shutdown(shutdownStatusCode);
+    } 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(rstStatusCode);
+        } catch (IOException e) {
+          if (thrown != null) thrown = e;
+        }
+      }
+    }
+
+    if (pingsToCancel != null) {
+      for (Ping ping : pingsToCancel) {
+        ping.cancel();
+      }
+    }
+
+    writeExecutor.shutdown();
+    callbackExecutor.shutdown();
+    readExecutor.shutdown();
+    try {
+      spdyReader.close();
+    } catch (IOException e) {
+      thrown = e;
+    }
+    try {
+      spdyWriter.close();
+    } catch (IOException e) {
+      if (thrown == null) thrown = e;
+    }
+    if (thrown != null) throw thrown;
+  }
+
+  public static class Builder {
+    private InputStream in;
+    private OutputStream out;
+    private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+    public boolean client;
 
     /**
-     * True if this peer initiated the connection.
+     * @param client true if this peer initiated the connection; false if
+     * this peer accepted 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 SpdyReader spdyReader;
-    private final SpdyWriter spdyWriter;
-    private final ExecutorService readExecutor;
-    private final ExecutorService writeExecutor;
-    private final ExecutorService callbackExecutor;
-
-    private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
-    private int lastGoodStreamId;
-    private int nextStreamId;
-    private boolean shutdown;
-
-    /** 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 this connection. */
-    Settings settings;
-
-    private SpdyConnection(Builder builder) {
-        client = builder.client;
-        handler = builder.handler;
-        spdyReader = new SpdyReader(builder.in);
-        spdyWriter = new SpdyWriter(builder.out);
-        nextStreamId = builder.client ? 1 : 2;
-        nextPingId = builder.client ? 1 : 2;
-
-        String prefix = builder.client ? "Spdy Client " : "Spdy Server ";
-        readExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
-                new SynchronousQueue<Runnable>(), Threads.newThreadFactory(prefix + "Reader", false));
-        writeExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS,
-                new LinkedBlockingQueue<Runnable>(), Threads.newThreadFactory(prefix + "Writer", false));
-        callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
-                new SynchronousQueue<Runnable>(), Threads.newThreadFactory(prefix + "Callbacks", false));
-
-        readExecutor.execute(new Reader());
+    public Builder(boolean client, Socket socket) throws IOException {
+      this(client, socket.getInputStream(), socket.getOutputStream());
     }
 
     /**
-     * Returns the number of {@link SpdyStream#isOpen() open streams} on this
-     * connection.
+     * @param client true if this peer initiated the connection; false if this
+     * peer accepted the connection.
      */
-    public synchronized int openStreamCount() {
-        return streams.size();
+    public Builder(boolean client, InputStream in, OutputStream out) {
+      this.client = client;
+      this.in = in;
+      this.out = out;
     }
 
-    private synchronized SpdyStream getStream(int id) {
-        return streams.get(id);
+    public Builder handler(IncomingStreamHandler handler) {
+      this.handler = handler;
+      return this;
     }
 
-    synchronized SpdyStream removeStream(int streamId) {
-        return streams.remove(streamId);
+    public SpdyConnection build() {
+      return new SpdyConnection(this);
     }
+  }
 
-    /**
-     * 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 {
-        int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL);
-        int associatedStreamId = 0;  // TODO: permit the caller to specify an associated stream.
-        int priority = 0; // TODO: permit the caller to specify a priority.
-        SpdyStream stream;
-        int streamId;
-
-        synchronized (spdyWriter) {
-            synchronized (this) {
-                if (shutdown) {
-                    throw new IOException("shutdown");
-                }
-                streamId = nextStreamId;
-                nextStreamId += 2;
-                stream = new SpdyStream(streamId, this, requestHeaders, flags);
-                if (stream.isOpen()) {
-                    streams.put(streamId, stream);
-                }
-            }
-
-            spdyWriter.synStream(flags, streamId, associatedStreamId, priority, requestHeaders);
+  private class Reader implements Runnable, SpdyReader.Handler {
+    @Override public void run() {
+      int shutdownStatusCode = GOAWAY_INTERNAL_ERROR;
+      int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR;
+      try {
+        while (spdyReader.nextFrame(this)) {
         }
-
-        return stream;
-    }
-
-    void writeSynReply(int streamId, int flags, List<String> alternating) throws IOException {
-        spdyWriter.synReply(flags, streamId, alternating);
-    }
-
-    /** Writes a complete data frame. */
-    void writeFrame(byte[] bytes, int offset, int length) throws IOException {
-        synchronized (spdyWriter) {
-            spdyWriter.out.write(bytes, offset, length);
+        shutdownStatusCode = GOAWAY_OK;
+        rstStatusCode = SpdyStream.RST_CANCEL;
+      } catch (IOException e) {
+        shutdownStatusCode = GOAWAY_PROTOCOL_ERROR;
+        rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR;
+      } finally {
+        try {
+          close(shutdownStatusCode, rstStatusCode);
+        } catch (IOException ignored) {
         }
+      }
     }
 
-    void writeSynResetLater(final int streamId, final int statusCode) {
-        writeExecutor.execute(new Runnable() {
-            @Override public void run() {
-                try {
-                    writeSynReset(streamId, statusCode);
-                } catch (IOException ignored) {
-                }
-            }
-        });
+    @Override public void data(int flags, int streamId, InputStream in, int length)
+        throws IOException {
+      SpdyStream dataStream = getStream(streamId);
+      if (dataStream == null) {
+        writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
+        Util.skipByReading(in, length);
+        return;
+      }
+      dataStream.receiveData(in, length);
+      if ((flags & SpdyConnection.FLAG_FIN) != 0) {
+        dataStream.receiveFin();
+      }
     }
 
-    void writeSynReset(int streamId, int statusCode) throws IOException {
-        spdyWriter.synReset(streamId, statusCode);
-    }
-
-    /**
-     * 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);
+    @Override
+    public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+        List<String> nameValueBlock) {
+      final SpdyStream synStream;
+      final SpdyStream previous;
+      synchronized (SpdyConnection.this) {
+        synStream =
+            new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock,
+                settings);
+        if (shutdown) {
+          return;
         }
-        writePing(pingId, ping);
-        return ping;
-    }
-
-    private void writePingLater(final int id, final Ping ping) {
-        writeExecutor.execute(new Runnable() {
-            @Override public void run() {
-                try {
-                    writePing(id, ping);
-                } catch (IOException ignored) {
-                }
-            }
-        });
-    }
-
-    private void writePing(int id, Ping ping) throws IOException {
-        synchronized (spdyWriter) {
-            // Observe the sent time immediately before performing I/O.
-            if (ping != null) ping.send();
-            spdyWriter.ping(0, id);
-        }
-    }
-
-    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 {
-        spdyWriter.noop();
-    }
-
-    public void flush() throws IOException {
-        synchronized (spdyWriter) {
-            spdyWriter.out.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() throws IOException {
-        synchronized (spdyWriter) {
-            int lastGoodStreamId;
-            synchronized (this) {
-                if (shutdown) {
-                    return;
-                }
-                shutdown = true;
-                lastGoodStreamId = this.lastGoodStreamId;
-            }
-            spdyWriter.goAway(0, lastGoodStreamId);
-        }
-    }
-
-    /**
-     * 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 {
-        shutdown();
-
-        SpdyStream[] streamsToClose = null;
-        Ping[] pingsToCancel = null;
-        synchronized (this) {
-            if (!streams.isEmpty()) {
-                streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
-                streams.clear();
-            }
-            if (pings != null) {
-                pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
-                pings = null;
-            }
-        }
-
-        if (streamsToClose != null) {
-            for (SpdyStream stream : streamsToClose) {
-                try {
-                    stream.close(SpdyStream.RST_CANCEL);
-                } catch (Throwable ignored) {
-                }
-            }
-        }
-
-        if (pingsToCancel != null) {
-            for (Ping ping : pingsToCancel) {
-                ping.cancel();
-            }
-        }
-
-        writeExecutor.shutdown();
-        callbackExecutor.shutdown();
-        readExecutor.shutdown();
-        Util.closeAll(spdyReader, spdyWriter);
-    }
-
-    public static class Builder {
-        private InputStream in;
-        private OutputStream out;
-        private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
-        public boolean client;
-
-        /**
-         * @param client true if this peer initiated the connection; false if
-         *     this peer accepted the connection.
-         */
-        public Builder(boolean client, Socket socket) throws IOException {
-            this(client, socket.getInputStream(), socket.getOutputStream());
-        }
-
-        /**
-         * @param client true if this peer initiated the connection; false if this
-         *     peer accepted the connection.
-         */
-        public Builder(boolean client, InputStream in, OutputStream out) {
-            this.client = client;
-            this.in = in;
-            this.out = out;
-        }
-
-        public Builder handler(IncomingStreamHandler handler) {
-            this.handler = handler;
-            return this;
-        }
-
-        public SpdyConnection build() {
-            return new SpdyConnection(this);
-        }
-    }
-
-    private class Reader implements Runnable, SpdyReader.Handler {
+        lastGoodStreamId = streamId;
+        previous = streams.put(streamId, synStream);
+      }
+      if (previous != null) {
+        previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
+        removeStream(streamId);
+        return;
+      }
+      callbackExecutor.execute(new Runnable() {
         @Override public void run() {
-            try {
-                while (spdyReader.nextFrame(this)) {
-                }
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            } finally {
-                Util.closeQuietly(SpdyConnection.this);
-            }
+          try {
+            handler.receive(synStream);
+          } catch (IOException e) {
+            throw new RuntimeException(e);
+          }
         }
-
-        @Override public void data(int flags, int streamId, InputStream in, int length)
-                throws IOException {
-            SpdyStream dataStream = getStream(streamId);
-            if (dataStream == null) {
-                writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
-                Util.skipByReading(in, length);
-                return;
-            }
-            try {
-                dataStream.receiveData(in, length);
-                if ((flags & SpdyConnection.FLAG_FIN) != 0) {
-                    dataStream.receiveFin();
-                }
-            } catch (ProtocolException e) {
-                Util.skipByReading(in, length);
-                dataStream.closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR);
-            }
-        }
-
-        @Override public void synStream(int flags, int streamId, int associatedStreamId,
-                int priority, List<String> nameValueBlock) {
-            final SpdyStream synStream = new SpdyStream(streamId, SpdyConnection.this,
-                    nameValueBlock, flags);
-            final SpdyStream previous;
-            synchronized (SpdyConnection.this) {
-                if (shutdown) {
-                    return;
-                }
-                lastGoodStreamId = streamId;
-                previous = streams.put(streamId, synStream);
-            }
-            if (previous != null) {
-                previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
-                removeStream(streamId);
-                return;
-            }
-            callbackExecutor.execute(new Runnable() {
-                @Override public void run() {
-                    try {
-                        handler.receive(synStream);
-                    } catch (IOException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-            });
-        }
-
-        @Override public void synReply(int flags, int streamId, List<String> nameValueBlock)
-                throws IOException {
-            SpdyStream replyStream = getStream(streamId);
-            if (replyStream == null) {
-                writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
-                return;
-            }
-            try {
-                replyStream.receiveReply(nameValueBlock);
-                if ((flags & SpdyConnection.FLAG_FIN) != 0) {
-                    replyStream.receiveFin();
-                }
-            } catch (ProtocolException e) {
-                replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
-            }
-        }
-
-        @Override public void headers(int flags, int streamId, List<String> nameValueBlock)
-                throws IOException {
-            SpdyStream replyStream = getStream(streamId);
-            if (replyStream != null) {
-                try {
-                    replyStream.receiveHeaders(nameValueBlock);
-                } catch (ProtocolException e) {
-                    replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
-                }
-            }
-        }
-
-        @Override public void rstStream(int flags, int streamId, int statusCode) {
-            SpdyStream rstStream = removeStream(streamId);
-            if (rstStream != null) {
-                rstStream.receiveRstStream(statusCode);
-            }
-        }
-
-        @Override public void settings(int flags, Settings newSettings) {
-            synchronized (SpdyConnection.this) {
-                if (settings == null
-                        || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
-                    settings = newSettings;
-                } else {
-                    settings.merge(newSettings);
-                }
-            }
-        }
-
-        @Override public void noop() {
-        }
-
-        @Override public void ping(int flags, int streamId) {
-            if (client != (streamId % 2 == 1)) {
-                // Respond to a client ping if this is a server and vice versa.
-                writePingLater(streamId, null);
-            } else {
-                Ping ping = removePing(streamId);
-                if (ping != null) {
-                    ping.receive();
-                }
-            }
-        }
-
-        @Override public void goAway(int flags, int lastGoodStreamId) {
-            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(SpdyStream.RST_REFUSED_STREAM);
-                        i.remove();
-                    }
-                }
-            }
-        }
+      });
     }
+
+    @Override public void synReply(int flags, int streamId, List<String> nameValueBlock)
+        throws IOException {
+      SpdyStream replyStream = getStream(streamId);
+      if (replyStream == null) {
+        writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
+        return;
+      }
+      replyStream.receiveReply(nameValueBlock);
+      if ((flags & SpdyConnection.FLAG_FIN) != 0) {
+        replyStream.receiveFin();
+      }
+    }
+
+    @Override public void headers(int flags, int streamId, List<String> nameValueBlock)
+        throws IOException {
+      SpdyStream replyStream = getStream(streamId);
+      if (replyStream != null) {
+        replyStream.receiveHeaders(nameValueBlock);
+      }
+    }
+
+    @Override public void rstStream(int flags, int streamId, int statusCode) {
+      SpdyStream rstStream = removeStream(streamId);
+      if (rstStream != null) {
+        rstStream.receiveRstStream(statusCode);
+      }
+    }
+
+    @Override public void settings(int flags, Settings newSettings) {
+      SpdyStream[] streamsToNotify = null;
+      synchronized (SpdyConnection.this) {
+        if (settings == null || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
+          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.
+          synchronized (stream) {
+            synchronized (this) {
+              stream.receiveSettings(settings);
+            }
+          }
+        }
+      }
+    }
+
+    @Override public void noop() {
+    }
+
+    @Override public void ping(int flags, int streamId) {
+      if (client != (streamId % 2 == 1)) {
+        // Respond to a client ping if this is a server and vice versa.
+        writePingLater(streamId, null);
+      } else {
+        Ping ping = removePing(streamId);
+        if (ping != null) {
+          ping.receive();
+        }
+      }
+    }
+
+    @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
+      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(SpdyStream.RST_REFUSED_STREAM);
+            i.remove();
+          }
+        }
+      }
+    }
+
+    @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
+      SpdyStream stream = getStream(streamId);
+      if (stream != null) {
+        stream.receiveWindowUpdate(deltaWindowSize);
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
index 98fc975..db3b50c 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -21,7 +21,7 @@
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Logger;
@@ -29,258 +29,286 @@
 import java.util.zip.Inflater;
 import java.util.zip.InflaterInputStream;
 
-/**
- * Read version 2 SPDY frames.
- */
+/** Read spdy/3 frames. */
 final class SpdyReader implements Closeable {
-    private static final String DICTIONARY_STRING = ""
-            + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-"
-            + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi"
-            + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser"
-            + "-agent10010120020120220320420520630030130230330430530630740040140240340440"
-            + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta"
-            + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic"
-            + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran"
-            + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati"
-            + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo"
-            + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe"
-            + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic"
-            + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1"
-            + ".1statusversionurl\0";
-    public static final byte[] DICTIONARY;
-    static {
-        try {
-            DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            throw new AssertionError(e);
+  static final byte[] 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);
+
+  private final DataInputStream in;
+  private final DataInputStream nameValueBlockIn;
+  private int compressedLimit;
+
+  SpdyReader(InputStream in) {
+    this.in = new DataInputStream(in);
+    this.nameValueBlockIn = newNameValueBlockStream();
+  }
+
+  /**
+   * Send the next frame to {@code handler}. Returns true unless there are no
+   * more frames on the stream.
+   */
+  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 SpdyConnection.TYPE_SYN_STREAM:
+          readSynStream(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_SYN_REPLY:
+          readSynReply(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_RST_STREAM:
+          readRstStream(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_SETTINGS:
+          readSettings(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_NOOP:
+          if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
+          handler.noop();
+          return true;
+
+        case SpdyConnection.TYPE_PING:
+          readPing(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_GOAWAY:
+          readGoAway(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_HEADERS:
+          readHeaders(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_WINDOW_UPDATE:
+          readWindowUpdate(handler, flags, length);
+          return true;
+
+        case SpdyConnection.TYPE_CREDENTIAL:
+          Util.skipByReading(in, length);
+          throw new UnsupportedOperationException("TODO"); // TODO: implement
+
+        default:
+          throw new IOException("Unexpected frame");
+      }
+    } else {
+      int streamId = w1 & 0x7fffffff;
+      handler.data(flags, 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 = readNameValueBlock(length - 10);
+    handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock);
+  }
+
+  private void readSynReply(Handler handler, int flags, int length) throws IOException {
+    int w1 = in.readInt();
+    int streamId = w1 & 0x7fffffff;
+    List<String> nameValueBlock = readNameValueBlock(length - 4);
+    handler.synReply(flags, streamId, nameValueBlock);
+  }
+
+  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 statusCode = in.readInt();
+    handler.rstStream(flags, streamId, statusCode);
+  }
+
+  private void readHeaders(Handler handler, int flags, int length) throws IOException {
+    int w1 = in.readInt();
+    int streamId = w1 & 0x7fffffff;
+    List<String> nameValueBlock = readNameValueBlock(length - 4);
+    handler.headers(flags, streamId, nameValueBlock);
+  }
+
+  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(flags, streamId, deltaWindowSize);
+  }
+
+  private DataInputStream newNameValueBlockStream() {
+    // Limit the inflater input stream to only those bytes in the Name/Value block.
+    final 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(DICTIONARY);
+          result = super.inflate(buffer, offset, count);
         }
+        return result;
+      }
+    };
+
+    return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
+  }
+
+  private List<String> readNameValueBlock(int length) throws IOException {
+    this.compressedLimit += length;
+    try {
+      int numberOfPairs = nameValueBlockIn.readInt();
+      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 ioException("name.length == 0");
+        if (values.length() == 0) throw ioException("values.length == 0");
+        entries.add(name);
+        entries.add(values);
+      }
+
+      if (compressedLimit != 0) {
+        Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit);
+      }
+
+      return entries;
+    } catch (DataFormatException e) {
+      throw new IOException(e);
     }
+  }
 
-    private final DataInputStream in;
-    private final DataInputStream nameValueBlockIn;
-    private int 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");
+  }
 
-    SpdyReader(InputStream in) {
-        this.in = new DataInputStream(in);
-        this.nameValueBlockIn = newNameValueBlockStream();
+  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();
+    handler.ping(flags, id);
+  }
+
+  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 statusCode = in.readInt();
+    handler.goAway(flags, lastGoodStreamId, statusCode);
+  }
+
+  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);
     }
-
-    /**
-     * Send the next frame to {@code handler}. Returns true unless there are no
-     * more frames on the stream.
-     */
-    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);
-
-            switch (type) {
-            case SpdyConnection.TYPE_SYN_STREAM:
-                readSynStream(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_SYN_REPLY:
-                readSynReply(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_RST_STREAM:
-                readRstStream(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_SETTINGS:
-                readSettings(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_NOOP:
-                if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
-                handler.noop();
-                return true;
-
-            case SpdyConnection.TYPE_PING:
-                readPing(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_GOAWAY:
-                readGoAway(handler, flags, length);
-                return true;
-
-            case SpdyConnection.TYPE_HEADERS:
-                readHeaders(handler, flags, length);
-                return true;
-
-            default:
-                throw new IOException("Unexpected frame");
-            }
-        } else {
-            int streamId = w1 & 0x7fffffff;
-            handler.data(flags, streamId, in, length);
-            return true;
-        }
+    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);
     }
+    handler.settings(flags, settings);
+  }
 
-    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 & 0xc000 >>> 14;
-        // int unused = s3 & 0x3fff;
-        List<String> nameValueBlock = readNameValueBlock(length - 10);
-        handler.synStream(flags, streamId, associatedStreamId, priority, nameValueBlock);
-    }
+  private static IOException ioException(String message, Object... args) throws IOException {
+    throw new IOException(String.format(message, args));
+  }
 
-    private void readSynReply(Handler handler, int flags, int length) throws IOException {
-        int w1 = in.readInt();
-        in.readShort(); // unused
-        int streamId = w1 & 0x7fffffff;
-        List<String> nameValueBlock = readNameValueBlock(length - 6);
-        handler.synReply(flags, streamId, nameValueBlock);
-    }
+  @Override public void close() throws IOException {
+    Util.closeAll(in, nameValueBlockIn);
+  }
 
-    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 statusCode = in.readInt();
-        handler.rstStream(flags, streamId, statusCode);
-    }
+  public interface Handler {
+    void data(int flags, int streamId, InputStream in, int length) throws IOException;
 
-    private void readHeaders(Handler handler, int flags, int length) throws IOException {
-        int w1 = in.readInt();
-        in.readShort(); // unused
-        int streamId = w1 & 0x7fffffff;
-        List<String> nameValueBlock = readNameValueBlock(length - 6);
-        handler.headers(flags, streamId, nameValueBlock);
-    }
+    void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+        List<String> nameValueBlock);
 
-    private DataInputStream newNameValueBlockStream() {
-        // Limit the inflater input stream to only those bytes in the Name/Value block.
-        final 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(DICTIONARY);
-                    result = super.inflate(buffer, offset, count);
-                }
-                return result;
-            }
-        };
-
-        return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
-    }
-
-    private List<String> readNameValueBlock(int length) throws IOException {
-        this.compressedLimit += length;
-        try {
-            int numberOfPairs = nameValueBlockIn.readShort();
-            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 ioException("name.length == 0");
-                if (values.length() == 0) throw ioException("values.length == 0");
-                entries.add(name);
-                entries.add(values);
-            }
-
-            if (compressedLimit != 0) {
-                Logger.getLogger(getClass().getName())
-                        .warning("compressedLimit > 0: " + compressedLimit);
-            }
-
-            return entries;
-        } catch (DataFormatException e) {
-            throw new IOException(e);
-        }
-    }
-
-    private String readString() throws DataFormatException, IOException {
-        int length = nameValueBlockIn.readShort();
-        byte[] bytes = new byte[length];
-        Util.readFully(nameValueBlockIn, bytes);
-        return new String(bytes, 0, length, "UTF-8");
-    }
-
-    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();
-        handler.ping(flags, id);
-    }
-
-    private void readGoAway(Handler handler, int flags, int length) throws IOException {
-        if (length != 4) throw ioException("TYPE_GOAWAY length: %d != 4", length);
-        int lastGoodStreamId = in.readInt() & 0x7fffffff;
-        handler.goAway(flags, lastGoodStreamId);
-    }
-
-    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();
-            // The ID is a 24 bit little-endian value, so 0xabcdefxx becomes 0x00efcdab.
-            int id = ((w1 & 0xff000000) >>> 24)
-                    | ((w1 & 0xff0000) >>> 8)
-                    | ((w1 & 0xff00) << 8);
-            int idFlags = (w1 & 0xff);
-            settings.set(id, idFlags, value);
-        }
-        handler.settings(flags, 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, nameValueBlockIn);
-    }
-
-    public interface Handler {
-        void data(int flags, int streamId, InputStream in, int length) throws IOException;
-        void synStream(int flags, int streamId, int associatedStreamId, int priority,
-                List<String> nameValueBlock);
-        void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
-        void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
-        void rstStream(int flags, int streamId, int statusCode);
-        void settings(int flags, Settings settings);
-        void noop();
-        void ping(int flags, int streamId);
-        void goAway(int flags, int lastGoodStreamId);
-    }
+    void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
+    void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
+    void rstStream(int flags, int streamId, int statusCode);
+    void settings(int flags, Settings settings);
+    void noop();
+    void ping(int flags, int streamId);
+    void goAway(int flags, int lastGoodStreamId, int statusCode);
+    void windowUpdate(int flags, int streamId, int deltaWindowSize);
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
index 5c3d971..744a04e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -17,603 +17,717 @@
 package com.squareup.okhttp.internal.spdy;
 
 import com.squareup.okhttp.internal.Util;
-import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
-import static com.squareup.okhttp.internal.Util.pokeInt;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
-import java.net.ProtocolException;
 import java.net.SocketTimeoutException;
-import static java.nio.ByteOrder.BIG_ENDIAN;
 import java.util.ArrayList;
 import java.util.List;
 
-/**
- * A logical bidirectional stream.
- */
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+import static com.squareup.okhttp.internal.Util.pokeInt;
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+/** 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.
-     */
+  // Internal state is guarded by this. No long-running or potentially
+  // blocking operations are performed while the lock is held.
 
-    private static final int DATA_FRAME_HEADER_LENGTH = 8;
+  private static final int DATA_FRAME_HEADER_LENGTH = 8;
 
-    private static final String[] STATUS_CODE_NAMES = {
-            null,
-            "PROTOCOL_ERROR",
-            "INVALID_STREAM",
-            "REFUSED_STREAM",
-            "UNSUPPORTED_VERSION",
-            "CANCEL",
-            "INTERNAL_ERROR",
-            "FLOW_CONTROL_ERROR",
-    };
+  private static final String[] STATUS_CODE_NAMES = {
+      null,
+      "PROTOCOL_ERROR",
+      "INVALID_STREAM",
+      "REFUSED_STREAM",
+      "UNSUPPORTED_VERSION",
+      "CANCEL",
+      "INTERNAL_ERROR",
+      "FLOW_CONTROL_ERROR",
+      "STREAM_IN_USE",
+      "STREAM_ALREADY_CLOSED",
+      "INVALID_CREDENTIALS",
+      "FRAME_TOO_LARGE"
+  };
 
-    public static final int RST_PROTOCOL_ERROR = 1;
-    public static final int RST_INVALID_STREAM = 2;
-    public static final int RST_REFUSED_STREAM = 3;
-    public static final int RST_UNSUPPORTED_VERSION = 4;
-    public static final int RST_CANCEL = 5;
-    public static final int RST_INTERNAL_ERROR = 6;
-    public static final int RST_FLOW_CONTROL_ERROR = 7;
+  public static final int RST_PROTOCOL_ERROR = 1;
+  public static final int RST_INVALID_STREAM = 2;
+  public static final int RST_REFUSED_STREAM = 3;
+  public static final int RST_UNSUPPORTED_VERSION = 4;
+  public static final int RST_CANCEL = 5;
+  public static final int RST_INTERNAL_ERROR = 6;
+  public static final int RST_FLOW_CONTROL_ERROR = 7;
+  public static final int RST_STREAM_IN_USE = 8;
+  public static final int RST_STREAM_ALREADY_CLOSED = 9;
+  public static final int RST_INVALID_CREDENTIALS = 10;
+  public static final int RST_FRAME_TOO_LARGE = 11;
 
-    private final int id;
-    private final SpdyConnection connection;
-    private long readTimeoutMillis = 0;
+  /**
+   * 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;
 
-    /** Headers sent by the stream initiator. Immutable and non null. */
-    private final List<String> requestHeaders;
+  private final int id;
+  private final SpdyConnection connection;
+  private final int priority;
+  private final int slot;
+  private long readTimeoutMillis = 0;
+  private int writeWindowSize;
 
-    /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
-    private List<String> responseHeaders;
+  /** Headers sent by the stream initiator. Immutable and non null. */
+  private final List<String> requestHeaders;
 
-    private final SpdyDataInputStream in = new SpdyDataInputStream();
-    private final SpdyDataOutputStream out = new SpdyDataOutputStream();
+  /** 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 int rstStatusCode = -1;
+
+  SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
+      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.priority = priority;
+    this.slot = slot;
+    this.requestHeaders = requestHeaders;
+
+    if (isLocallyInitiated()) {
+      // I am the sender
+      in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+      out.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+    } else {
+      // I am the receiver
+      in.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+      out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+    }
+
+    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 (rstStatusCode != -1) {
+      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 && rstStatusCode == -1) {
+        wait();
+      }
+      if (responseHeaders != null) {
+        return responseHeaders;
+      }
+      throw new IOException("stream was reset: " + rstStatusString());
+    } catch (InterruptedException e) {
+      InterruptedIOException rethrow = new InterruptedIOException();
+      rethrow.initCause(e);
+      throw rethrow;
+    }
+  }
+
+  /**
+   * Returns the reason why this stream was closed, or -1 if it closed
+   * normally or has not yet been closed. Valid reasons are {@link
+   * #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link
+   * #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link
+   * #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link
+   * #RST_FLOW_CONTROL_ERROR}.
+   */
+  public synchronized int getRstStatusCode() {
+    return rstStatusCode;
+  }
+
+  /**
+   * 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));
+    int flags = 0;
+    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;
+        flags |= SpdyConnection.FLAG_FIN;
+      }
+    }
+    connection.writeSynReply(id, flags, 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(int 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(int rstStatusCode) {
+    if (!closeInternal(rstStatusCode)) {
+      return; // Already closed.
+    }
+    connection.writeSynResetLater(id, rstStatusCode);
+  }
+
+  /** Returns true if this stream was closed. */
+  private boolean closeInternal(int rstStatusCode) {
+    assert (!Thread.holdsLock(this));
+    synchronized (this) {
+      if (this.rstStatusCode != -1) {
+        return false;
+      }
+      if (in.finished && out.finished) {
+        return false;
+      }
+      this.rstStatusCode = rstStatusCode;
+      notifyAll();
+    }
+    connection.removeStream(id);
+    return true;
+  }
+
+  void receiveReply(List<String> strings) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean streamInUseError = false;
+    boolean open = true;
+    synchronized (this) {
+      if (isLocallyInitiated() && responseHeaders == null) {
+        responseHeaders = strings;
+        open = isOpen();
+        notifyAll();
+      } else {
+        streamInUseError = true;
+      }
+    }
+    if (streamInUseError) {
+      closeLater(SpdyStream.RST_STREAM_IN_USE);
+    } else if (!open) {
+      connection.removeStream(id);
+    }
+  }
+
+  void receiveHeaders(List<String> headers) throws IOException {
+    assert (!Thread.holdsLock(SpdyStream.this));
+    boolean protocolError = false;
+    synchronized (this) {
+      if (responseHeaders != null) {
+        List<String> newHeaders = new ArrayList<String>();
+        newHeaders.addAll(responseHeaders);
+        newHeaders.addAll(headers);
+        this.responseHeaders = newHeaders;
+      } else {
+        protocolError = true;
+      }
+    }
+    if (protocolError) {
+      closeLater(SpdyStream.RST_PROTOCOL_ERROR);
+    }
+  }
+
+  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(int statusCode) {
+    if (rstStatusCode == -1) {
+      rstStatusCode = statusCode;
+      notifyAll();
+    }
+  }
+
+  private void setSettings(Settings settings) {
+    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();
+  }
+
+  private String rstStatusString() {
+    return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length
+        ? STATUS_CODE_NAMES[rstStatusCode] : Integer.toString(rstStatusCode);
+  }
+
+  int getPriority() {
+    return priority;
+  }
+
+  int getSlot() {
+    return slot;
+  }
+
+  /**
+   * 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;
 
     /**
-     * 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.
+     * True if either side has cleanly shut down this stream. We will
+     * receive no more bytes beyond those already in the buffer.
      */
-    private int rstStatusCode = -1;
+    private boolean finished;
 
-    SpdyStream(int id, SpdyConnection connection, List<String> requestHeaders, int flags) {
-        this.id = id;
-        this.connection = connection;
-        this.requestHeaders = requestHeaders;
+    /**
+     * 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;
 
-        if (isLocallyInitiated()) {
-            // I am the sender
-            in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
-            out.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+    @Override public int available() throws IOException {
+      synchronized (SpdyStream.this) {
+        checkNotClosed();
+        if (pos == -1) {
+          return 0;
+        } else if (limit > pos) {
+          return limit - pos;
         } else {
-            // I am the receiver
-            in.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
-            out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+          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 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.
-     * </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.
+     * Returns once the input stream is either readable or finished. Throws
+     * a {@link SocketTimeoutException} if the read timeout elapses before
+     * that happens.
      */
-    public synchronized boolean isOpen() {
-        if (rstStatusCode != -1) {
-            return false;
+    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 && rstStatusCode == -1) {
+          if (readTimeoutMillis == 0) {
+            SpdyStream.this.wait();
+          } else if (remaining > 0) {
+            SpdyStream.this.wait(remaining);
+            remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
+          } else {
+            throw new SocketTimeoutException();
+          }
         }
-        if ((in.finished || in.closed) && (out.finished || out.closed)) {
-            return false;
+      } 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(SpdyStream.RST_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;
         }
-        return true;
+      }
+      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 (rstStatusCode != -1) {
+        throw new IOException("stream was reset: " + rstStatusString());
+      }
+    }
+  }
+
+  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(RST_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 = DATA_FRAME_HEADER_LENGTH;
+
+    /** 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 > DATA_FRAME_HEADER_LENGTH) {
+        writeFrame(false);
+        connection.flush();
+      }
+    }
+
+    @Override public void close() throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+      synchronized (SpdyStream.this) {
+        if (closed) {
+          return;
+        }
+        closed = true;
+      }
+      writeFrame(true);
+      connection.flush();
+      cancelStreamIfNecessary();
+    }
+
+    private void writeFrame(boolean last) throws IOException {
+      assert (!Thread.holdsLock(SpdyStream.this));
+
+      int length = pos - DATA_FRAME_HEADER_LENGTH;
+      synchronized (SpdyStream.this) {
+        waitUntilWritable(length, last);
+        unacknowledgedBytes += length;
+      }
+      int flags = 0;
+      if (last) {
+        flags |= SpdyConnection.FLAG_FIN;
+      }
+      pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN);
+      pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN);
+      connection.writeFrame(buffer, 0, pos);
+      pos = DATA_FRAME_HEADER_LENGTH;
     }
 
     /**
-     * Returns true if this stream was created by this peer.
+     * 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.
      */
-    public boolean isLocallyInitiated() {
-        boolean streamIsClient = (id % 2 == 1);
-        return connection.client == streamIsClient;
-    }
+    private void waitUntilWritable(int count, boolean last) throws IOException {
+      try {
+        while (unacknowledgedBytes + count >= writeWindowSize) {
+          SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
 
-    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 && rstStatusCode == -1) {
-                wait();
-            }
-            if (responseHeaders != null) {
-                return responseHeaders;
-            }
+          // 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 (rstStatusCode != -1) {
             throw new IOException("stream was reset: " + rstStatusString());
-        } catch (InterruptedException e) {
-            InterruptedIOException rethrow = new InterruptedIOException();
-            rethrow.initCause(e);
-            throw rethrow;
+          }
         }
+      } catch (InterruptedException e) {
+        throw new InterruptedIOException();
+      }
     }
 
-    /**
-     * Returns the reason why this stream was closed, or -1 if it closed
-     * normally or has not yet been closed. Valid reasons are {@link
-     * #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link
-     * #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link
-     * #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link
-     * #RST_FLOW_CONTROL_ERROR}.
-     */
-    public synchronized int getRstStatusCode() {
-        return rstStatusCode;
+    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 (rstStatusCode != -1) {
+          throw new IOException("stream was reset: " + rstStatusString());
+        }
+      }
     }
-
-    /**
-     * 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));
-        int flags = 0;
-        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;
-                flags |= SpdyConnection.FLAG_FIN;
-            }
-        }
-        connection.writeSynReply(id, flags, 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.
-     */
-    public void close(int rstStatusCode) throws IOException {
-        if (!closeInternal(rstStatusCode)) {
-            return; // Already closed.
-        }
-        connection.writeSynReset(id, rstStatusCode);
-    }
-
-    void closeLater(int rstStatusCode) {
-        if (!closeInternal(rstStatusCode)) {
-            return; // Already closed.
-        }
-        connection.writeSynResetLater(id, rstStatusCode);
-    }
-
-    /**
-     * Returns true if this stream was closed.
-     */
-    private boolean closeInternal(int rstStatusCode) {
-        assert (!Thread.holdsLock(this));
-        synchronized (this) {
-            if (this.rstStatusCode != -1) {
-                return false;
-            }
-            if (in.finished && out.finished) {
-                return false;
-            }
-            this.rstStatusCode = rstStatusCode;
-            notifyAll();
-        }
-        connection.removeStream(id);
-        return true;
-    }
-
-    void receiveReply(List<String> strings) throws IOException {
-        assert (!Thread.holdsLock(SpdyStream.this));
-        synchronized (this) {
-            if (!isLocallyInitiated() || responseHeaders != null) {
-                throw new ProtocolException();
-            }
-            responseHeaders = strings;
-            notifyAll();
-        }
-    }
-
-    void receiveHeaders(List<String> headers) throws IOException {
-        assert (!Thread.holdsLock(SpdyStream.this));
-        synchronized (this) {
-            if (responseHeaders == null) {
-                throw new ProtocolException();
-            }
-            List<String> newHeaders = new ArrayList<String>();
-            newHeaders.addAll(responseHeaders);
-            newHeaders.addAll(headers);
-            this.responseHeaders = newHeaders;
-        }
-    }
-
-    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(int statusCode) {
-        if (rstStatusCode == -1) {
-            rstStatusCode = statusCode;
-            notifyAll();
-        }
-    }
-
-    private String rstStatusString() {
-        return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length
-                ? STATUS_CODE_NAMES[rstStatusCode]
-                : Integer.toString(rstStatusCode);
-    }
-
-    /**
-     * 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[64 * 1024]; // 64KiB specified by TODO
-
-        /** 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;
-
-        @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;
-                }
-
-                // TODO: notify peer of flow-control
-
-                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 && rstStatusCode == -1) {
-                    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;
-            synchronized (SpdyStream.this) {
-                finished = this.finished;
-                pos = this.pos;
-                firstNewByte = this.limit;
-                limit = this.limit;
-                if (byteCount > buffer.length - available()) {
-                    throw new ProtocolException();
-                }
-            }
-
-            // 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 (rstStatusCode != -1) {
-                throw new IOException("stream was reset: " + rstStatusString());
-            }
-        }
-    }
-
-    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(RST_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 = DATA_FRAME_HEADER_LENGTH;
-
-        /** 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;
-
-        @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 > DATA_FRAME_HEADER_LENGTH) {
-                writeFrame(false);
-                connection.flush();
-            }
-        }
-
-        @Override public void close() throws IOException {
-            assert (!Thread.holdsLock(SpdyStream.this));
-            synchronized (SpdyStream.this) {
-                if (closed) {
-                    return;
-                }
-                closed = true;
-            }
-            writeFrame(true);
-            connection.flush();
-            cancelStreamIfNecessary();
-        }
-
-        private void writeFrame(boolean last) throws IOException {
-            assert (!Thread.holdsLock(SpdyStream.this));
-            int flags = 0;
-            if (last) {
-                flags |= SpdyConnection.FLAG_FIN;
-            }
-            int length = pos - DATA_FRAME_HEADER_LENGTH;
-            pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN);
-            pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN);
-            connection.writeFrame(buffer, 0, pos);
-            pos = DATA_FRAME_HEADER_LENGTH;
-        }
-
-        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 (rstStatusCode != -1) {
-                    throw new IOException("stream was reset: " + rstStatusString());
-                }
-            }
-        }
-    }
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java
index 8cd6ae0..b3d1d1f 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java
@@ -26,148 +26,151 @@
 import java.util.List;
 import java.util.zip.Deflater;
 
-/**
- * Write version 2 SPDY frames.
- */
+/** Write spdy/3 frames. */
 final class SpdyWriter implements Closeable {
-    final DataOutputStream out;
-    private final ByteArrayOutputStream nameValueBlockBuffer;
-    private final DataOutputStream nameValueBlockOut;
+  final DataOutputStream out;
+  private final ByteArrayOutputStream nameValueBlockBuffer;
+  private final DataOutputStream nameValueBlockOut;
 
-    SpdyWriter(OutputStream out) {
-        this.out = new DataOutputStream(out);
+  SpdyWriter(OutputStream out) {
+    this.out = new DataOutputStream(out);
 
-        Deflater deflater = new Deflater();
-        deflater.setDictionary(SpdyReader.DICTIONARY);
-        nameValueBlockBuffer = new ByteArrayOutputStream();
-        nameValueBlockOut = new DataOutputStream(
-                Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
+    Deflater deflater = new Deflater();
+    deflater.setDictionary(SpdyReader.DICTIONARY);
+    nameValueBlockBuffer = new ByteArrayOutputStream();
+    nameValueBlockOut = new DataOutputStream(
+        Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
+  }
+
+  public synchronized void synStream(int flags, int streamId, int associatedStreamId, int priority,
+      int slot, List<String> nameValueBlock) throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int length = 10 + nameValueBlockBuffer.size();
+    int type = SpdyConnection.TYPE_SYN_STREAM;
+
+    int unused = 0;
+    out.writeInt(0x80000000 | (SpdyConnection.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();
+  }
+
+  public synchronized void synReply(int flags, int streamId, List<String> nameValueBlock)
+      throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int type = SpdyConnection.TYPE_SYN_REPLY;
+    int length = nameValueBlockBuffer.size() + 4;
+
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    nameValueBlockBuffer.writeTo(out);
+    out.flush();
+  }
+
+  public synchronized void headers(int flags, int streamId, List<String> nameValueBlock)
+      throws IOException {
+    writeNameValueBlockToBuffer(nameValueBlock);
+    int type = SpdyConnection.TYPE_HEADERS;
+    int length = nameValueBlockBuffer.size() + 4;
+
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    nameValueBlockBuffer.writeTo(out);
+    out.flush();
+  }
+
+  public synchronized void rstStream(int streamId, int statusCode) throws IOException {
+    int flags = 0;
+    int type = SpdyConnection.TYPE_RST_STREAM;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId & 0x7fffffff);
+    out.writeInt(statusCode);
+    out.flush();
+  }
+
+  public synchronized void data(int flags, int streamId, byte[] data) throws IOException {
+    int length = data.length;
+    out.writeInt(streamId & 0x7fffffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.write(data);
+    out.flush();
+  }
+
+  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();
+  }
 
-    public synchronized void synStream(int flags, int streamId, int associatedStreamId,
-            int priority, List<String> nameValueBlock) throws IOException {
-        writeNameValueBlockToBuffer(nameValueBlock);
-        int length = 10 + nameValueBlockBuffer.size();
-        int type = SpdyConnection.TYPE_SYN_STREAM;
-
-        int unused = 0;
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(streamId & 0x7fffffff);
-        out.writeInt(associatedStreamId & 0x7fffffff);
-        out.writeShort((priority & 0x3) << 30 | (unused & 0x3FFF) << 16);
-        nameValueBlockBuffer.writeTo(out);
-        out.flush();
+  public synchronized void settings(int flags, Settings settings) throws IOException {
+    int type = SpdyConnection.TYPE_SETTINGS;
+    int size = settings.size();
+    int length = 4 + size * 8;
+    out.writeInt(0x80000000 | (SpdyConnection.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();
+  }
 
-    public synchronized void synReply(
-            int flags, int streamId, List<String> nameValueBlock) throws IOException {
-        writeNameValueBlockToBuffer(nameValueBlock);
-        int type = SpdyConnection.TYPE_SYN_REPLY;
-        int length = nameValueBlockBuffer.size() + 6;
-        int unused = 0;
+  public synchronized void noop() throws IOException {
+    int type = SpdyConnection.TYPE_NOOP;
+    int length = 0;
+    int flags = 0;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.flush();
+  }
 
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(streamId & 0x7fffffff);
-        out.writeShort(unused);
-        nameValueBlockBuffer.writeTo(out);
-        out.flush();
-    }
+  public synchronized void ping(int flags, int id) throws IOException {
+    int type = SpdyConnection.TYPE_PING;
+    int length = 4;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(id);
+    out.flush();
+  }
 
-    public synchronized void headers(
-            int flags, int streamId, List<String> nameValueBlock) throws IOException {
-        writeNameValueBlockToBuffer(nameValueBlock);
-        int type = SpdyConnection.TYPE_HEADERS;
-        int length = nameValueBlockBuffer.size() + 6;
-        int unused = 0;
+  public synchronized void goAway(int flags, int lastGoodStreamId, int statusCode)
+      throws IOException {
+    int type = SpdyConnection.TYPE_GOAWAY;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(lastGoodStreamId);
+    out.writeInt(statusCode);
+    out.flush();
+  }
 
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(streamId & 0x7fffffff);
-        out.writeShort(unused);
-        nameValueBlockBuffer.writeTo(out);
-        out.flush();
-    }
+  public synchronized void windowUpdate(int streamId, int deltaWindowSize) throws IOException {
+    int type = SpdyConnection.TYPE_WINDOW_UPDATE;
+    int flags = 0;
+    int length = 8;
+    out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
+    out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
+    out.writeInt(streamId);
+    out.writeInt(deltaWindowSize);
+    out.flush();
+  }
 
-    public synchronized void synReset(int streamId, int statusCode) throws IOException {
-        int flags = 0;
-        int type = SpdyConnection.TYPE_RST_STREAM;
-        int length = 8;
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(streamId & 0x7fffffff);
-        out.writeInt(statusCode);
-        out.flush();
-    }
-
-    public synchronized void data(int flags, int streamId, byte[] data) throws IOException {
-        int length = data.length;
-        out.writeInt(streamId & 0x7fffffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.write(data);
-        out.flush();
-    }
-
-    private void writeNameValueBlockToBuffer(List<String> nameValueBlock) throws IOException {
-        nameValueBlockBuffer.reset();
-        int numberOfPairs = nameValueBlock.size() / 2;
-        nameValueBlockOut.writeShort(numberOfPairs);
-        for (String s : nameValueBlock) {
-            nameValueBlockOut.writeShort(s.length());
-            nameValueBlockOut.write(s.getBytes("UTF-8"));
-        }
-        nameValueBlockOut.flush();
-    }
-
-    public synchronized void settings(int flags, Settings settings) throws IOException {
-        int type = SpdyConnection.TYPE_SETTINGS;
-        int size = settings.size();
-        int length = 4 + size * 8;
-        out.writeInt(0x80000000 | (SpdyConnection.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);
-            // settingId 0x00efcdab and settingFlags 0x12 combine to 0xabcdef12.
-            out.writeInt(((i & 0xff0000) >>> 8)
-                    | ((i & 0xff00) << 8)
-                    | ((i & 0xff) << 24)
-                    | (settingsFlags & 0xff));
-            out.writeInt(settings.get(i));
-        }
-        out.flush();
-    }
-
-    public synchronized void noop() throws IOException {
-        int type = SpdyConnection.TYPE_NOOP;
-        int length = 0;
-        int flags = 0;
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.flush();
-    }
-
-    public synchronized void ping(int flags, int id) throws IOException {
-        int type = SpdyConnection.TYPE_PING;
-        int length = 4;
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(id);
-        out.flush();
-    }
-
-    public synchronized void goAway(int flags, int lastGoodStreamId) throws IOException {
-        int type = SpdyConnection.TYPE_GOAWAY;
-        int length = 4;
-        out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
-        out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
-        out.writeInt(lastGoodStreamId);
-        out.flush();
-    }
-
-    @Override public void close() throws IOException {
-        Util.closeAll(out, nameValueBlockOut);
-    }
+  @Override public void close() throws IOException {
+    Util.closeAll(out, nameValueBlockOut);
+  }
 }
diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/Threads.java b/src/main/java/com/squareup/okhttp/internal/spdy/Threads.java
deleted file mode 100644
index ef1d4f3..0000000
--- a/src/main/java/com/squareup/okhttp/internal/spdy/Threads.java
+++ /dev/null
@@ -1,34 +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.util.concurrent.ThreadFactory;
-
-final class Threads {
-    public static ThreadFactory newThreadFactory(final String name, final boolean daemon) {
-        return new ThreadFactory() {
-            @Override public Thread newThread(Runnable r) {
-                Thread result = new Thread(r, name);
-                result.setDaemon(daemon);
-                return result;
-            }
-        };
-    }
-
-    private Threads() {
-    }
-}
diff --git a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
new file mode 100644
index 0000000..dca9625
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.google.mockwebserver.MockWebServer;
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
+import java.io.IOException;
+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.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.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public final class ConnectionPoolTest {
+  private static final int KEEP_ALIVE_DURATION_MS = 500;
+  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 MockSpdyServer spdyServer = new MockSpdyServer(sslContext.getSocketFactory());
+  private InetSocketAddress spdySocketAddress;
+  private Address spdyAddress;
+
+  private final MockWebServer httpServer = new MockWebServer();
+  private Address httpAddress;
+  private InetSocketAddress httpSocketAddress;
+
+  private Connection httpA;
+  private Connection httpB;
+  private Connection httpC;
+  private Connection httpD;
+  private Connection httpE;
+  private Connection spdyA;
+  private Connection spdyB;
+
+  @Before public void setUp() throws Exception {
+    httpServer.play();
+    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null, null);
+    httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
+        httpServer.getPort());
+
+    spdyServer.play();
+    spdyAddress =
+        new Address(spdyServer.getHostName(), spdyServer.getPort(), sslContext.getSocketFactory(),
+            new RecordingHostnameVerifier(), null);
+    spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
+        spdyServer.getPort());
+
+    httpA = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpA.connect(100, 100, null);
+    httpB = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpB.connect(100, 100, null);
+    httpC = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpC.connect(100, 100, null);
+    httpD = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpD.connect(100, 100, null);
+    httpE = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    httpE.connect(100, 100, null);
+    spdyA = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+    spdyA.connect(100, 100, null);
+    spdyB = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+    spdyB.connect(100, 100, null);
+  }
+
+  @After public void tearDown() throws Exception {
+    httpServer.shutdown();
+    spdyServer.shutdown();
+
+    Util.closeQuietly(httpA);
+    Util.closeQuietly(httpB);
+    Util.closeQuietly(httpC);
+    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);
+    Connection connection = pool.get(httpAddress);
+    assertNull(connection);
+
+    connection = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+    connection.connect(100, 100, null);
+    assertEquals(0, pool.getConnectionCount());
+    pool.recycle(connection);
+    assertEquals(1, pool.getConnectionCount());
+    assertEquals(1, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+
+    Connection recycledConnection = pool.get(httpAddress);
+    assertEquals(connection, recycledConnection);
+    assertTrue(recycledConnection.isAlive());
+
+    recycledConnection = pool.get(httpAddress);
+    assertNull(recycledConnection);
+  }
+
+  @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+    pool.recycle(httpA);
+    pool.recycle(httpB);
+    pool.recycle(httpC);
+    assertPooled(pool, httpC, httpB);
+  }
+
+  @Test public void getSpdyConnection() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+    pool.maybeShare(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));
+    assertPooled(pool);
+  }
+
+  @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+    pool.recycle(httpA);
+    pool.recycle(httpB);
+    pool.recycle(httpC);
+    pool.recycle(httpD);
+    assertPooled(pool, httpD, httpC);
+  }
+
+  @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);
+    pool.get(spdyAddress); // Force the cleanup callable to run.
+    assertPooled(pool);
+  }
+
+  @Test public void nonAliveConnectionNotReturned() throws Exception {
+    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+    pool.recycle(httpA);
+    httpA.close();
+    assertNull(pool.get(httpAddress));
+    assertPooled(pool);
+  }
+
+  @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.recycle(httpA);
+    assertPooled(pool, httpA, spdyA);
+    assertSame(spdyA, pool.get(spdyAddress));
+    assertPooled(pool, spdyA, httpA);
+  }
+
+  @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);
+    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);
+    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
+    assertNull(pool.get(httpAddress));
+    assertPooled(pool, spdyA); // Connection should still be in the pool.
+    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
+    assertNull(pool.get(httpAddress));
+    assertPooled(pool);
+  }
+
+  @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));
+    assertPooled(pool, httpA); // Connection should still be in the pool.
+    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
+    assertNull(pool.get(spdyAddress));
+    assertPooled(pool);
+  }
+
+  @Test public void maxConnections() throws IOException, InterruptedException {
+    ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+
+    // Pool should be empty.
+    assertEquals(0, pool.getConnectionCount());
+
+    // http A should be added to the pool.
+    pool.recycle(httpA);
+    assertEquals(1, pool.getConnectionCount());
+    assertEquals(1, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+
+    // http B should be added to the pool.
+    pool.recycle(httpB);
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+
+    // http C should be added and http A should be removed.
+    pool.recycle(httpC);
+    Thread.sleep(50);
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+
+    // spdy A should be added and http B should be removed.
+    pool.maybeShare(spdyA);
+    Thread.sleep(50);
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(1, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // http C should be removed from the pool.
+    Connection recycledHttpConnection = pool.get(httpAddress);
+    assertNotNull(recycledHttpConnection);
+    assertTrue(recycledHttpConnection.isAlive());
+    assertEquals(1, pool.getConnectionCount());
+    assertEquals(0, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // spdy A will be returned and kept in the pool.
+    Connection sharedSpdyConnection = pool.get(spdyAddress);
+    assertNotNull(sharedSpdyConnection);
+    assertEquals(spdyA, sharedSpdyConnection);
+    assertEquals(1, pool.getConnectionCount());
+    assertEquals(0, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // Nothing should change.
+    pool.recycle(httpC);
+    Thread.sleep(50);
+    assertEquals(2, pool.getConnectionCount());
+    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);
+    assertTrue(recycledHttpConnection.isAlive());
+    assertEquals(1, pool.getConnectionCount());
+    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);
+    assertNotNull(sharedSpdyConnection);
+    assertEquals(1, pool.getConnectionCount());
+    assertEquals(0, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // http D should be added to the pool.
+    pool.recycle(httpD);
+    Thread.sleep(50);
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(1, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // http E should be added to the pool. spdy A should be removed from the pool.
+    pool.recycle(httpE);
+    Thread.sleep(50);
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+  }
+
+  @Test public void connectionCleanup() throws IOException, InterruptedException {
+    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+
+    // Add 3 connections to the pool.
+    pool.recycle(httpA);
+    pool.recycle(httpB);
+    pool.maybeShare(spdyA);
+    assertEquals(3, pool.getConnectionCount());
+    assertEquals(2, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    // Kill http A.
+    Util.closeQuietly(httpA);
+
+    // Force pool to run a clean up.
+    assertNotNull(pool.get(spdyAddress));
+    Thread.sleep(50);
+
+    assertEquals(2, pool.getConnectionCount());
+    assertEquals(1, pool.getHttpConnectionCount());
+    assertEquals(1, pool.getSpdyConnectionCount());
+
+    Thread.sleep(KEEP_ALIVE_DURATION_MS);
+    // Force pool to run a clean up.
+    assertNull(pool.get(httpAddress));
+    assertNull(pool.get(spdyAddress));
+
+    Thread.sleep(50);
+
+    assertEquals(0, pool.getConnectionCount());
+    assertEquals(0, pool.getHttpConnectionCount());
+    assertEquals(0, pool.getSpdyConnectionCount());
+  }
+
+  private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
+    assertEquals(Arrays.asList(connections), pool.getConnections());
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
index 160ec78..72cc70d 100644
--- a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
@@ -16,10 +16,6 @@
 
 package com.squareup.okhttp.internal;
 
-import com.squareup.okhttp.internal.DiskLruCache;
-import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
-import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
-import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -32,771 +28,775 @@
 import java.util.Arrays;
 import java.util.List;
 import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
+import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
+import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import org.junit.Before;
-import org.junit.Test;
 
 public final class DiskLruCacheTest {
-    private final int appVersion = 100;
-    private String javaTmpDir;
-    private File cacheDir;
-    private File journalFile;
-    private DiskLruCache cache;
+  private final int appVersion = 100;
+  private String javaTmpDir;
+  private File cacheDir;
+  private File journalFile;
+  private DiskLruCache cache;
 
-    @Before public void setUp() throws Exception {
-        javaTmpDir = System.getProperty("java.io.tmpdir");
-        cacheDir = new File(javaTmpDir, "DiskLruCacheTest");
-        cacheDir.mkdir();
-        journalFile = new File(cacheDir, JOURNAL_FILE);
-        for (File file : cacheDir.listFiles()) {
-            file.delete();
-        }
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+  @Before public void setUp() throws Exception {
+    javaTmpDir = System.getProperty("java.io.tmpdir");
+    cacheDir = new File(javaTmpDir, "DiskLruCacheTest");
+    cacheDir.mkdir();
+    journalFile = new File(cacheDir, JOURNAL_FILE);
+    for (File file : cacheDir.listFiles()) {
+      file.delete();
+    }
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+  }
+
+  @After public void tearDown() throws Exception {
+    cache.close();
+  }
+
+  @Test public void emptyCache() throws Exception {
+    cache.close();
+    assertJournalEquals();
+  }
+
+  @Test public void writeAndReadEntry() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(0, "ABC");
+    creator.set(1, "DE");
+    assertNull(creator.getString(0));
+    assertNull(creator.newInputStream(0));
+    assertNull(creator.getString(1));
+    assertNull(creator.newInputStream(1));
+    creator.commit();
+
+    DiskLruCache.Snapshot snapshot = cache.get("k1");
+    assertEquals("ABC", snapshot.getString(0));
+    assertEquals("DE", snapshot.getString(1));
+  }
+
+  @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(0, "A");
+    creator.set(1, "B");
+    creator.commit();
+    cache.close();
+
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    DiskLruCache.Snapshot snapshot = cache.get("k1");
+    assertEquals("A", snapshot.getString(0));
+    assertEquals("B", snapshot.getString(1));
+    snapshot.close();
+  }
+
+  @Test public void journalWithEditAndPublish() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
+    creator.set(0, "AB");
+    creator.set(1, "C");
+    creator.commit();
+    cache.close();
+    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
+  }
+
+  @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
+    creator.set(0, "AB");
+    creator.set(1, "C");
+    creator.abort();
+    cache.close();
+    assertJournalEquals("DIRTY k1", "REMOVE k1");
+  }
+
+  @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
+    cache.edit("k1");
+    cache.close();
+    assertJournalEquals("DIRTY k1", "REMOVE k1");
+  }
+
+  @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    assertNull(cache.get("k1"));
+    creator.set(0, "A");
+    creator.set(1, "BC");
+    creator.commit();
+    cache.close();
+    assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
+  }
+
+  @Test public void journalWithEditAndPublishAndRead() throws Exception {
+    DiskLruCache.Editor k1Creator = cache.edit("k1");
+    k1Creator.set(0, "AB");
+    k1Creator.set(1, "C");
+    k1Creator.commit();
+    DiskLruCache.Editor k2Creator = cache.edit("k2");
+    k2Creator.set(0, "DEF");
+    k2Creator.set(1, "G");
+    k2Creator.commit();
+    DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
+    k1Snapshot.close();
+    cache.close();
+    assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
+  }
+
+  @Test public void cannotOperateOnEditAfterPublish() throws Exception {
+    DiskLruCache.Editor editor = cache.edit("k1");
+    editor.set(0, "A");
+    editor.set(1, "B");
+    editor.commit();
+    assertInoperable(editor);
+  }
+
+  @Test public void cannotOperateOnEditAfterRevert() throws Exception {
+    DiskLruCache.Editor editor = cache.edit("k1");
+    editor.set(0, "A");
+    editor.set(1, "B");
+    editor.abort();
+    assertInoperable(editor);
+  }
+
+  @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
+    DiskLruCache.Editor editor = cache.edit("k1");
+    editor.set(0, "ABC");
+    editor.set(1, "B");
+    editor.commit();
+    File k1 = getCleanFile("k1", 0);
+    assertEquals("ABC", readFile(k1));
+    cache.remove("k1");
+    assertFalse(k1.exists());
+  }
+
+  /**
+   * Each read sees a snapshot of the file at the time read was called.
+   * This means that two reads of the same key can see different data.
+   */
+  @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
+    DiskLruCache.Editor v1Creator = cache.edit("k1");
+    v1Creator.set(0, "AAaa");
+    v1Creator.set(1, "BBbb");
+    v1Creator.commit();
+
+    DiskLruCache.Snapshot snapshot1 = cache.get("k1");
+    InputStream inV1 = snapshot1.getInputStream(0);
+    assertEquals('A', inV1.read());
+    assertEquals('A', inV1.read());
+
+    DiskLruCache.Editor v1Updater = cache.edit("k1");
+    v1Updater.set(0, "CCcc");
+    v1Updater.set(1, "DDdd");
+    v1Updater.commit();
+
+    DiskLruCache.Snapshot snapshot2 = cache.get("k1");
+    assertEquals("CCcc", snapshot2.getString(0));
+    assertEquals("DDdd", snapshot2.getString(1));
+    snapshot2.close();
+
+    assertEquals('a', inV1.read());
+    assertEquals('a', inV1.read());
+    assertEquals("BBbb", snapshot1.getString(1));
+    snapshot1.close();
+  }
+
+  @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
+    cache.close();
+    File cleanFile0 = getCleanFile("k1", 0);
+    File cleanFile1 = getCleanFile("k1", 1);
+    File dirtyFile0 = getDirtyFile("k1", 0);
+    File dirtyFile1 = getDirtyFile("k1", 1);
+    writeFile(cleanFile0, "A");
+    writeFile(cleanFile1, "B");
+    writeFile(dirtyFile0, "C");
+    writeFile(dirtyFile1, "D");
+    createJournal("CLEAN k1 1 1", "DIRTY   k1");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertFalse(cleanFile0.exists());
+    assertFalse(cleanFile1.exists());
+    assertFalse(dirtyFile0.exists());
+    assertFalse(dirtyFile1.exists());
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournalWithHeader(MAGIC, "0", "100", "2", "");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+  }
+
+  @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournalWithHeader(MAGIC, "1", "101", "2", "");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+  }
+
+  @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournalWithHeader(MAGIC, "1", "100", "1", "");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+  }
+
+  @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournalWithHeader(MAGIC, "1", "100", "2", "x");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+  }
+
+  @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournal("CLEAN k1 1 1", "BOGUS");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournal("CLEAN k1 0000x001 1");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
+    cache.close();
+    writeFile(getCleanFile("k1", 0), "A");
+    writeFile(getCleanFile("k1", 1), "B");
+    Writer writer = new FileWriter(journalFile);
+    writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
+    writer.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
+    cache.close();
+    generateSomeGarbageFiles();
+    createJournal("CLEAN k1 1 1 1");
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
+    assertGarbageFilesAllDeleted();
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void keyWithSpaceNotPermitted() throws Exception {
+    try {
+      cache.edit("my key");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void keyWithNewlineNotPermitted() throws Exception {
+    try {
+      cache.edit("my\nkey");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
+    try {
+      cache.edit("my\rkey");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void nullKeyThrows() throws Exception {
+    try {
+      cache.edit(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(1, "A");
+    try {
+      creator.commit();
+      fail();
+    } catch (IllegalStateException expected) {
     }
 
-    @After public void tearDown() throws Exception {
-        cache.close();
+    assertFalse(getCleanFile("k1", 0).exists());
+    assertFalse(getCleanFile("k1", 1).exists());
+    assertFalse(getDirtyFile("k1", 0).exists());
+    assertFalse(getDirtyFile("k1", 1).exists());
+    assertNull(cache.get("k1"));
+
+    DiskLruCache.Editor creator2 = cache.edit("k1");
+    creator2.set(0, "B");
+    creator2.set(1, "C");
+    creator2.commit();
+  }
+
+  @Test public void createNewEntryWithMissingFileAborts() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(0, "A");
+    creator.set(1, "A");
+    assertTrue(getDirtyFile("k1", 0).exists());
+    assertTrue(getDirtyFile("k1", 1).exists());
+    assertTrue(getDirtyFile("k1", 0).delete());
+    assertFalse(getDirtyFile("k1", 0).exists());
+    creator.commit();  // silently abort if file does not exist due to I/O issue
+
+    assertFalse(getCleanFile("k1", 0).exists());
+    assertFalse(getCleanFile("k1", 1).exists());
+    assertFalse(getDirtyFile("k1", 0).exists());
+    assertFalse(getDirtyFile("k1", 1).exists());
+    assertNull(cache.get("k1"));
+
+    DiskLruCache.Editor creator2 = cache.edit("k1");
+    creator2.set(0, "B");
+    creator2.set(1, "C");
+    creator2.commit();
+  }
+
+  @Test public void revertWithTooFewValues() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(1, "A");
+    creator.abort();
+    assertFalse(getCleanFile("k1", 0).exists());
+    assertFalse(getCleanFile("k1", 1).exists());
+    assertFalse(getDirtyFile("k1", 0).exists());
+    assertFalse(getDirtyFile("k1", 1).exists());
+    assertNull(cache.get("k1"));
+  }
+
+  @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
+    DiskLruCache.Editor creator = cache.edit("k1");
+    creator.set(0, "A");
+    creator.set(1, "B");
+    creator.commit();
+
+    DiskLruCache.Editor updater = cache.edit("k1");
+    updater.set(0, "C");
+    updater.commit();
+
+    DiskLruCache.Snapshot snapshot = cache.get("k1");
+    assertEquals("C", snapshot.getString(0));
+    assertEquals("B", snapshot.getString(1));
+    snapshot.close();
+  }
+
+  @Test public void evictOnInsert() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+
+    set("A", "a", "aaa"); // size 4
+    set("B", "bb", "bbbb"); // size 6
+    assertEquals(10, cache.size());
+
+    // cause the size to grow to 12 should evict 'A'
+    set("C", "c", "c");
+    cache.flush();
+    assertEquals(8, cache.size());
+    assertAbsent("A");
+    assertValue("B", "bb", "bbbb");
+    assertValue("C", "c", "c");
+
+    // causing the size to grow to 10 should evict nothing
+    set("D", "d", "d");
+    cache.flush();
+    assertEquals(10, cache.size());
+    assertAbsent("A");
+    assertValue("B", "bb", "bbbb");
+    assertValue("C", "c", "c");
+    assertValue("D", "d", "d");
+
+    // causing the size to grow to 18 should evict 'B' and 'C'
+    set("E", "eeee", "eeee");
+    cache.flush();
+    assertEquals(10, cache.size());
+    assertAbsent("A");
+    assertAbsent("B");
+    assertAbsent("C");
+    assertValue("D", "d", "d");
+    assertValue("E", "eeee", "eeee");
+  }
+
+  @Test public void evictOnUpdate() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+
+    set("A", "a", "aa"); // size 3
+    set("B", "b", "bb"); // size 3
+    set("C", "c", "cc"); // size 3
+    assertEquals(9, cache.size());
+
+    // causing the size to grow to 11 should evict 'A'
+    set("B", "b", "bbbb");
+    cache.flush();
+    assertEquals(8, cache.size());
+    assertAbsent("A");
+    assertValue("B", "b", "bbbb");
+    assertValue("C", "c", "cc");
+  }
+
+  @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+    set("A", "a", "a");
+    set("B", "b", "b");
+    set("C", "c", "c");
+    set("D", "d", "d");
+    set("E", "e", "e");
+    cache.get("B").close(); // 'B' is now least recently used
+
+    // causing the size to grow to 12 should evict 'A'
+    set("F", "f", "f");
+    // causing the size to grow to 12 should evict 'C'
+    set("G", "g", "g");
+    cache.flush();
+    assertEquals(10, cache.size());
+    assertAbsent("A");
+    assertValue("B", "b", "b");
+    assertAbsent("C");
+    assertValue("D", "d", "d");
+    assertValue("E", "e", "e");
+    assertValue("F", "f", "f");
+  }
+
+  @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
+    set("A", "a", "a");
+    set("B", "b", "b");
+    set("C", "c", "c");
+    set("D", "d", "d");
+    set("E", "e", "e");
+    set("F", "f", "f");
+    cache.get("B").close(); // 'B' is now least recently used
+    assertEquals(12, cache.size());
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+
+    set("G", "g", "g");
+    cache.flush();
+    assertEquals(10, cache.size());
+    assertAbsent("A");
+    assertValue("B", "b", "b");
+    assertAbsent("C");
+    assertValue("D", "d", "d");
+    assertValue("E", "e", "e");
+    assertValue("F", "f", "f");
+    assertValue("G", "g", "g");
+  }
+
+  @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+    set("A", "aaaaa", "aaaaaa"); // size=11
+    cache.flush();
+    assertAbsent("A");
+  }
+
+  @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+    set("A", "aaaaaaaaaaa", "a"); // size=12
+    cache.flush();
+    assertAbsent("A");
+  }
+
+  @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
+    try {
+      DiskLruCache.open(cacheDir, appVersion, 2, 0);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
+    try {
+      DiskLruCache.open(cacheDir, appVersion, 0, 10);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void removeAbsentElement() throws Exception {
+    cache.remove("A");
+  }
+
+  @Test public void readingTheSameStreamMultipleTimes() throws Exception {
+    set("A", "a", "b");
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0));
+    snapshot.close();
+  }
+
+  @Test public void rebuildJournalOnRepeatedReads() throws Exception {
+    set("A", "a", "a");
+    set("B", "b", "b");
+    long lastJournalLength = 0;
+    while (true) {
+      long journalLength = journalFile.length();
+      assertValue("A", "a", "a");
+      assertValue("B", "b", "b");
+      if (journalLength < lastJournalLength) {
+        System.out
+            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
+                journalLength);
+        break; // test passed!
+      }
+      lastJournalLength = journalLength;
+    }
+  }
+
+  @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
+    long lastJournalLength = 0;
+    while (true) {
+      long journalLength = journalFile.length();
+      set("A", "a", "a");
+      set("B", "b", "b");
+      if (journalLength < lastJournalLength) {
+        System.out
+            .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength,
+                journalLength);
+        break;
+      }
+      lastJournalLength = journalLength;
     }
 
-    @Test public void emptyCache() throws Exception {
-        cache.close();
-        assertJournalEquals();
+    // sanity check that a rebuilt journal behaves normally
+    assertValue("A", "a", "a");
+    assertValue("B", "b", "b");
+  }
+
+  @Test public void openCreatesDirectoryIfNecessary() throws Exception {
+    cache.close();
+    File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary");
+    cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
+    set("A", "a", "a");
+    assertTrue(new File(dir, "A.0").exists());
+    assertTrue(new File(dir, "A.1").exists());
+    assertTrue(new File(dir, "journal").exists());
+  }
+
+  @Test public void fileDeletedExternally() throws Exception {
+    set("A", "a", "a");
+    getCleanFile("A", 1).delete();
+    assertNull(cache.get("A"));
+  }
+
+  @Test public void editSameVersion() throws Exception {
+    set("A", "a", "a");
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    DiskLruCache.Editor editor = snapshot.edit();
+    editor.set(1, "a2");
+    editor.commit();
+    assertValue("A", "a", "a2");
+  }
+
+  @Test public void editSnapshotAfterChangeAborted() throws Exception {
+    set("A", "a", "a");
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    DiskLruCache.Editor toAbort = snapshot.edit();
+    toAbort.set(0, "b");
+    toAbort.abort();
+    DiskLruCache.Editor editor = snapshot.edit();
+    editor.set(1, "a2");
+    editor.commit();
+    assertValue("A", "a", "a2");
+  }
+
+  @Test public void editSnapshotAfterChangeCommitted() throws Exception {
+    set("A", "a", "a");
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    DiskLruCache.Editor toAbort = snapshot.edit();
+    toAbort.set(0, "b");
+    toAbort.commit();
+    assertNull(snapshot.edit());
+  }
+
+  @Test public void editSinceEvicted() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+    set("A", "aa", "aaa"); // size 5
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    set("B", "bb", "bbb"); // size 5
+    set("C", "cc", "ccc"); // size 5; will evict 'A'
+    cache.flush();
+    assertNull(snapshot.edit());
+  }
+
+  @Test public void editSinceEvictedAndRecreated() throws Exception {
+    cache.close();
+    cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
+    set("A", "aa", "aaa"); // size 5
+    DiskLruCache.Snapshot snapshot = cache.get("A");
+    set("B", "bb", "bbb"); // size 5
+    set("C", "cc", "ccc"); // size 5; will evict 'A'
+    set("A", "a", "aaaa"); // size 5; will evict 'B'
+    cache.flush();
+    assertNull(snapshot.edit());
+  }
+
+  private void assertJournalEquals(String... expectedBodyLines) throws Exception {
+    List<String> expectedLines = new ArrayList<String>();
+    expectedLines.add(MAGIC);
+    expectedLines.add(VERSION_1);
+    expectedLines.add("100");
+    expectedLines.add("2");
+    expectedLines.add("");
+    expectedLines.addAll(Arrays.asList(expectedBodyLines));
+    assertEquals(expectedLines, readJournalLines());
+  }
+
+  private void createJournal(String... bodyLines) throws Exception {
+    createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
+  }
+
+  private void createJournalWithHeader(String magic, String version, String appVersion,
+      String valueCount, String blank, String... bodyLines) throws Exception {
+    Writer writer = new FileWriter(journalFile);
+    writer.write(magic + "\n");
+    writer.write(version + "\n");
+    writer.write(appVersion + "\n");
+    writer.write(valueCount + "\n");
+    writer.write(blank + "\n");
+    for (String line : bodyLines) {
+      writer.write(line);
+      writer.write('\n');
     }
+    writer.close();
+  }
 
-    @Test public void writeAndReadEntry() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(0, "ABC");
-        creator.set(1, "DE");
-        assertNull(creator.getString(0));
-        assertNull(creator.newInputStream(0));
-        assertNull(creator.getString(1));
-        assertNull(creator.newInputStream(1));
-        creator.commit();
-
-        DiskLruCache.Snapshot snapshot = cache.get("k1");
-        assertEquals("ABC", snapshot.getString(0));
-        assertEquals("DE", snapshot.getString(1));
+  private List<String> readJournalLines() throws Exception {
+    List<String> result = new ArrayList<String>();
+    BufferedReader reader = new BufferedReader(new FileReader(journalFile));
+    String line;
+    while ((line = reader.readLine()) != null) {
+      result.add(line);
     }
+    reader.close();
+    return result;
+  }
 
-    @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(0, "A");
-        creator.set(1, "B");
-        creator.commit();
-        cache.close();
+  private File getCleanFile(String key, int index) {
+    return new File(cacheDir, key + "." + index);
+  }
 
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        DiskLruCache.Snapshot snapshot = cache.get("k1");
-        assertEquals("A", snapshot.getString(0));
-        assertEquals("B", snapshot.getString(1));
-        snapshot.close();
+  private File getDirtyFile(String key, int index) {
+    return new File(cacheDir, key + "." + index + ".tmp");
+  }
+
+  private String readFile(File file) throws Exception {
+    Reader reader = new FileReader(file);
+    StringWriter writer = new StringWriter();
+    char[] buffer = new char[1024];
+    int count;
+    while ((count = reader.read(buffer)) != -1) {
+      writer.write(buffer, 0, count);
     }
+    reader.close();
+    return writer.toString();
+  }
 
-    @Test public void journalWithEditAndPublish() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
-        creator.set(0, "AB");
-        creator.set(1, "C");
-        creator.commit();
-        cache.close();
-        assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
+  public void writeFile(File file, String content) throws Exception {
+    FileWriter writer = new FileWriter(file);
+    writer.write(content);
+    writer.close();
+  }
+
+  private void assertInoperable(DiskLruCache.Editor editor) throws Exception {
+    try {
+      editor.getString(0);
+      fail();
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed
-        creator.set(0, "AB");
-        creator.set(1, "C");
-        creator.abort();
-        cache.close();
-        assertJournalEquals("DIRTY k1", "REMOVE k1");
+    try {
+      editor.set(0, "A");
+      fail();
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
-        cache.edit("k1");
-        cache.close();
-        assertJournalEquals("DIRTY k1", "REMOVE k1");
+    try {
+      editor.newInputStream(0);
+      fail();
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        assertNull(cache.get("k1"));
-        creator.set(0, "A");
-        creator.set(1, "BC");
-        creator.commit();
-        cache.close();
-        assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
+    try {
+      editor.newOutputStream(0);
+      fail();
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void journalWithEditAndPublishAndRead() throws Exception {
-        DiskLruCache.Editor k1Creator = cache.edit("k1");
-        k1Creator.set(0, "AB");
-        k1Creator.set(1, "C");
-        k1Creator.commit();
-        DiskLruCache.Editor k2Creator = cache.edit("k2");
-        k2Creator.set(0, "DEF");
-        k2Creator.set(1, "G");
-        k2Creator.commit();
-        DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
-        k1Snapshot.close();
-        cache.close();
-        assertJournalEquals("DIRTY k1", "CLEAN k1 2 1",
-                "DIRTY k2", "CLEAN k2 3 1",
-                "READ k1");
+    try {
+      editor.commit();
+      fail();
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void cannotOperateOnEditAfterPublish() throws Exception {
-        DiskLruCache.Editor editor = cache.edit("k1");
-        editor.set(0, "A");
-        editor.set(1, "B");
-        editor.commit();
-        assertInoperable(editor);
+    try {
+      editor.abort();
+      fail();
+    } catch (IllegalStateException expected) {
     }
+  }
 
-    @Test public void cannotOperateOnEditAfterRevert() throws Exception {
-        DiskLruCache.Editor editor = cache.edit("k1");
-        editor.set(0, "A");
-        editor.set(1, "B");
-        editor.abort();
-        assertInoperable(editor);
+  private void generateSomeGarbageFiles() throws Exception {
+    File dir1 = new File(cacheDir, "dir1");
+    File dir2 = new File(dir1, "dir2");
+    writeFile(getCleanFile("g1", 0), "A");
+    writeFile(getCleanFile("g1", 1), "B");
+    writeFile(getCleanFile("g2", 0), "C");
+    writeFile(getCleanFile("g2", 1), "D");
+    writeFile(getCleanFile("g2", 1), "D");
+    writeFile(new File(cacheDir, "otherFile0"), "E");
+    dir1.mkdir();
+    dir2.mkdir();
+    writeFile(new File(dir2, "otherFile1"), "F");
+  }
+
+  private void assertGarbageFilesAllDeleted() throws Exception {
+    assertFalse(getCleanFile("g1", 0).exists());
+    assertFalse(getCleanFile("g1", 1).exists());
+    assertFalse(getCleanFile("g2", 0).exists());
+    assertFalse(getCleanFile("g2", 1).exists());
+    assertFalse(new File(cacheDir, "otherFile0").exists());
+    assertFalse(new File(cacheDir, "dir1").exists());
+  }
+
+  private void set(String key, String value0, String value1) throws Exception {
+    DiskLruCache.Editor editor = cache.edit(key);
+    editor.set(0, value0);
+    editor.set(1, value1);
+    editor.commit();
+  }
+
+  private void assertAbsent(String key) throws Exception {
+    DiskLruCache.Snapshot snapshot = cache.get(key);
+    if (snapshot != null) {
+      snapshot.close();
+      fail();
     }
+    assertFalse(getCleanFile(key, 0).exists());
+    assertFalse(getCleanFile(key, 1).exists());
+    assertFalse(getDirtyFile(key, 0).exists());
+    assertFalse(getDirtyFile(key, 1).exists());
+  }
 
-    @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
-        DiskLruCache.Editor editor = cache.edit("k1");
-        editor.set(0, "ABC");
-        editor.set(1, "B");
-        editor.commit();
-        File k1 = getCleanFile("k1", 0);
-        assertEquals("ABC", readFile(k1));
-        cache.remove("k1");
-        assertFalse(k1.exists());
-    }
-
-    /**
-     * Each read sees a snapshot of the file at the time read was called.
-     * This means that two reads of the same key can see different data.
-     */
-    @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
-        DiskLruCache.Editor v1Creator = cache.edit("k1");
-        v1Creator.set(0, "AAaa");
-        v1Creator.set(1, "BBbb");
-        v1Creator.commit();
-
-        DiskLruCache.Snapshot snapshot1 = cache.get("k1");
-        InputStream inV1 = snapshot1.getInputStream(0);
-        assertEquals('A', inV1.read());
-        assertEquals('A', inV1.read());
-
-        DiskLruCache.Editor v1Updater = cache.edit("k1");
-        v1Updater.set(0, "CCcc");
-        v1Updater.set(1, "DDdd");
-        v1Updater.commit();
-
-        DiskLruCache.Snapshot snapshot2 = cache.get("k1");
-        assertEquals("CCcc", snapshot2.getString(0));
-        assertEquals("DDdd", snapshot2.getString(1));
-        snapshot2.close();
-
-        assertEquals('a', inV1.read());
-        assertEquals('a', inV1.read());
-        assertEquals("BBbb", snapshot1.getString(1));
-        snapshot1.close();
-    }
-
-    @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
-        cache.close();
-        File cleanFile0 = getCleanFile("k1", 0);
-        File cleanFile1 = getCleanFile("k1", 1);
-        File dirtyFile0 = getDirtyFile("k1", 0);
-        File dirtyFile1 = getDirtyFile("k1", 1);
-        writeFile(cleanFile0, "A");
-        writeFile(cleanFile1, "B");
-        writeFile(dirtyFile0, "C");
-        writeFile(dirtyFile1, "D");
-        createJournal("CLEAN k1 1 1", "DIRTY   k1");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertFalse(cleanFile0.exists());
-        assertFalse(cleanFile1.exists());
-        assertFalse(dirtyFile0.exists());
-        assertFalse(dirtyFile1.exists());
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournalWithHeader(MAGIC, "0", "100", "2", "");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-    }
-
-    @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournalWithHeader(MAGIC, "1", "101", "2", "");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-    }
-
-    @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournalWithHeader(MAGIC, "1", "100", "1", "");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-    }
-
-    @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournalWithHeader(MAGIC, "1", "100", "2", "x");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-    }
-
-    @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournal("CLEAN k1 1 1", "BOGUS");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournal("CLEAN k1 0000x001 1");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
-        cache.close();
-        writeFile(getCleanFile("k1", 0), "A");
-        writeFile(getCleanFile("k1", 1), "B");
-        Writer writer = new FileWriter(journalFile);
-        writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
-        writer.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
-        cache.close();
-        generateSomeGarbageFiles();
-        createJournal("CLEAN k1 1 1 1");
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE);
-        assertGarbageFilesAllDeleted();
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void keyWithSpaceNotPermitted() throws Exception {
-        try {
-            cache.edit("my key");
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void keyWithNewlineNotPermitted() throws Exception {
-        try {
-            cache.edit("my\nkey");
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
-        try {
-            cache.edit("my\rkey");
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void nullKeyThrows() throws Exception {
-        try {
-            cache.edit(null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(1, "A");
-        try {
-            creator.commit();
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-
-        assertFalse(getCleanFile("k1", 0).exists());
-        assertFalse(getCleanFile("k1", 1).exists());
-        assertFalse(getDirtyFile("k1", 0).exists());
-        assertFalse(getDirtyFile("k1", 1).exists());
-        assertNull(cache.get("k1"));
-
-        DiskLruCache.Editor creator2 = cache.edit("k1");
-        creator2.set(0, "B");
-        creator2.set(1, "C");
-        creator2.commit();
-    }
-
-    @Test public void createNewEntryWithMissingFileAborts() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(0, "A");
-        creator.set(1, "A");
-        assertTrue(getDirtyFile("k1", 0).exists());
-        assertTrue(getDirtyFile("k1", 1).exists());
-        assertTrue(getDirtyFile("k1", 0).delete());
-        assertFalse(getDirtyFile("k1", 0).exists());
-        creator.commit();  // silently abort if file does not exist due to I/O issue
-
-        assertFalse(getCleanFile("k1", 0).exists());
-        assertFalse(getCleanFile("k1", 1).exists());
-        assertFalse(getDirtyFile("k1", 0).exists());
-        assertFalse(getDirtyFile("k1", 1).exists());
-        assertNull(cache.get("k1"));
-
-        DiskLruCache.Editor creator2 = cache.edit("k1");
-        creator2.set(0, "B");
-        creator2.set(1, "C");
-        creator2.commit();
-    }
-
-    @Test public void revertWithTooFewValues() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(1, "A");
-        creator.abort();
-        assertFalse(getCleanFile("k1", 0).exists());
-        assertFalse(getCleanFile("k1", 1).exists());
-        assertFalse(getDirtyFile("k1", 0).exists());
-        assertFalse(getDirtyFile("k1", 1).exists());
-        assertNull(cache.get("k1"));
-    }
-
-    @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
-        DiskLruCache.Editor creator = cache.edit("k1");
-        creator.set(0, "A");
-        creator.set(1, "B");
-        creator.commit();
-
-        DiskLruCache.Editor updater = cache.edit("k1");
-        updater.set(0, "C");
-        updater.commit();
-
-        DiskLruCache.Snapshot snapshot = cache.get("k1");
-        assertEquals("C", snapshot.getString(0));
-        assertEquals("B", snapshot.getString(1));
-        snapshot.close();
-    }
-
-    @Test public void evictOnInsert() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-        set("A", "a", "aaa"); // size 4
-        set("B", "bb", "bbbb"); // size 6
-        assertEquals(10, cache.size());
-
-        // cause the size to grow to 12 should evict 'A'
-        set("C", "c", "c");
-        cache.flush();
-        assertEquals(8, cache.size());
-        assertAbsent("A");
-        assertValue("B", "bb", "bbbb");
-        assertValue("C", "c", "c");
-
-        // causing the size to grow to 10 should evict nothing
-        set("D", "d", "d");
-        cache.flush();
-        assertEquals(10, cache.size());
-        assertAbsent("A");
-        assertValue("B", "bb", "bbbb");
-        assertValue("C", "c", "c");
-        assertValue("D", "d", "d");
-
-        // causing the size to grow to 18 should evict 'B' and 'C'
-        set("E", "eeee", "eeee");
-        cache.flush();
-        assertEquals(10, cache.size());
-        assertAbsent("A");
-        assertAbsent("B");
-        assertAbsent("C");
-        assertValue("D", "d", "d");
-        assertValue("E", "eeee", "eeee");
-    }
-
-    @Test public void evictOnUpdate() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-        set("A", "a", "aa"); // size 3
-        set("B", "b", "bb"); // size 3
-        set("C", "c", "cc"); // size 3
-        assertEquals(9, cache.size());
-
-        // causing the size to grow to 11 should evict 'A'
-        set("B", "b", "bbbb");
-        cache.flush();
-        assertEquals(8, cache.size());
-        assertAbsent("A");
-        assertValue("B", "b", "bbbb");
-        assertValue("C", "c", "cc");
-    }
-
-    @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-        set("A", "a", "a");
-        set("B", "b", "b");
-        set("C", "c", "c");
-        set("D", "d", "d");
-        set("E", "e", "e");
-        cache.get("B").close(); // 'B' is now least recently used
-
-        // causing the size to grow to 12 should evict 'A'
-        set("F", "f", "f");
-        // causing the size to grow to 12 should evict 'C'
-        set("G", "g", "g");
-        cache.flush();
-        assertEquals(10, cache.size());
-        assertAbsent("A");
-        assertValue("B", "b", "b");
-        assertAbsent("C");
-        assertValue("D", "d", "d");
-        assertValue("E", "e", "e");
-        assertValue("F", "f", "f");
-    }
-
-    @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
-        set("A", "a", "a");
-        set("B", "b", "b");
-        set("C", "c", "c");
-        set("D", "d", "d");
-        set("E", "e", "e");
-        set("F", "f", "f");
-        cache.get("B").close(); // 'B' is now least recently used
-        assertEquals(12, cache.size());
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-
-        set("G", "g", "g");
-        cache.flush();
-        assertEquals(10, cache.size());
-        assertAbsent("A");
-        assertValue("B", "b", "b");
-        assertAbsent("C");
-        assertValue("D", "d", "d");
-        assertValue("E", "e", "e");
-        assertValue("F", "f", "f");
-        assertValue("G", "g", "g");
-    }
-
-    @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-        set("A", "aaaaa", "aaaaaa"); // size=11
-        cache.flush();
-        assertAbsent("A");
-    }
-
-    @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-        set("A", "aaaaaaaaaaa", "a"); // size=12
-        cache.flush();
-        assertAbsent("A");
-    }
-
-    @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
-        try {
-            DiskLruCache.open(cacheDir, appVersion, 2, 0);
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
-        try {
-            DiskLruCache.open(cacheDir, appVersion, 0, 10);
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void removeAbsentElement() throws Exception {
-        cache.remove("A");
-    }
-
-    @Test public void readingTheSameStreamMultipleTimes() throws Exception {
-        set("A", "a", "b");
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0));
-        snapshot.close();
-    }
-
-    @Test public void rebuildJournalOnRepeatedReads() throws Exception {
-        set("A", "a", "a");
-        set("B", "b", "b");
-        long lastJournalLength = 0;
-        while (true) {
-            long journalLength = journalFile.length();
-            assertValue("A", "a", "a");
-            assertValue("B", "b", "b");
-            if (journalLength < lastJournalLength) {
-                System.out.printf("Journal compacted from %s bytes to %s bytes\n",
-                        lastJournalLength, journalLength);
-                break; // test passed!
-            }
-            lastJournalLength = journalLength;
-        }
-    }
-
-    @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
-        long lastJournalLength = 0;
-        while (true) {
-            long journalLength = journalFile.length();
-            set("A", "a", "a");
-            set("B", "b", "b");
-            if (journalLength < lastJournalLength) {
-                System.out.printf("Journal compacted from %s bytes to %s bytes\n",
-                        lastJournalLength, journalLength);
-                break;
-            }
-            lastJournalLength = journalLength;
-        }
-
-        // sanity check that a rebuilt journal behaves normally
-        assertValue("A", "a", "a");
-        assertValue("B", "b", "b");
-    }
-
-    @Test public void openCreatesDirectoryIfNecessary() throws Exception {
-        cache.close();
-        File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary");
-        cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE);
-        set("A", "a", "a");
-        assertTrue(new File(dir, "A.0").exists());
-        assertTrue(new File(dir, "A.1").exists());
-        assertTrue(new File(dir, "journal").exists());
-    }
-
-    @Test public void fileDeletedExternally() throws Exception {
-        set("A", "a", "a");
-        getCleanFile("A", 1).delete();
-        assertNull(cache.get("A"));
-    }
-
-    @Test public void editSameVersion() throws Exception {
-        set("A", "a", "a");
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        DiskLruCache.Editor editor = snapshot.edit();
-        editor.set(1, "a2");
-        editor.commit();
-        assertValue("A", "a", "a2");
-    }
-
-    @Test public void editSnapshotAfterChangeAborted() throws Exception {
-        set("A", "a", "a");
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        DiskLruCache.Editor toAbort = snapshot.edit();
-        toAbort.set(0, "b");
-        toAbort.abort();
-        DiskLruCache.Editor editor = snapshot.edit();
-        editor.set(1, "a2");
-        editor.commit();
-        assertValue("A", "a", "a2");
-    }
-
-    @Test public void editSnapshotAfterChangeCommitted() throws Exception {
-        set("A", "a", "a");
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        DiskLruCache.Editor toAbort = snapshot.edit();
-        toAbort.set(0, "b");
-        toAbort.commit();
-        assertNull(snapshot.edit());
-    }
-
-    @Test public void editSinceEvicted() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-        set("A", "aa", "aaa"); // size 5
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        set("B", "bb", "bbb"); // size 5
-        set("C", "cc", "ccc"); // size 5; will evict 'A'
-        cache.flush();
-        assertNull(snapshot.edit());
-    }
-
-    @Test public void editSinceEvictedAndRecreated() throws Exception {
-        cache.close();
-        cache = DiskLruCache.open(cacheDir, appVersion, 2, 10);
-        set("A", "aa", "aaa"); // size 5
-        DiskLruCache.Snapshot snapshot = cache.get("A");
-        set("B", "bb", "bbb"); // size 5
-        set("C", "cc", "ccc"); // size 5; will evict 'A'
-        set("A", "a", "aaaa"); // size 5; will evict 'B'
-        cache.flush();
-        assertNull(snapshot.edit());
-    }
-
-    private void assertJournalEquals(String... expectedBodyLines) throws Exception {
-        List<String> expectedLines = new ArrayList<String>();
-        expectedLines.add(MAGIC);
-        expectedLines.add(VERSION_1);
-        expectedLines.add("100");
-        expectedLines.add("2");
-        expectedLines.add("");
-        expectedLines.addAll(Arrays.asList(expectedBodyLines));
-        assertEquals(expectedLines, readJournalLines());
-    }
-
-    private void createJournal(String... bodyLines) throws Exception {
-        createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
-    }
-
-    private void createJournalWithHeader(String magic, String version, String appVersion,
-            String valueCount, String blank, String... bodyLines) throws Exception {
-        Writer writer = new FileWriter(journalFile);
-        writer.write(magic + "\n");
-        writer.write(version + "\n");
-        writer.write(appVersion + "\n");
-        writer.write(valueCount + "\n");
-        writer.write(blank + "\n");
-        for (String line : bodyLines) {
-            writer.write(line);
-            writer.write('\n');
-        }
-        writer.close();
-    }
-
-    private List<String> readJournalLines() throws Exception {
-        List<String> result = new ArrayList<String>();
-        BufferedReader reader = new BufferedReader(new FileReader(journalFile));
-        String line;
-        while ((line = reader.readLine()) != null) {
-            result.add(line);
-        }
-        reader.close();
-        return result;
-    }
-
-    private File getCleanFile(String key, int index) {
-        return new File(cacheDir, key + "." + index);
-    }
-
-    private File getDirtyFile(String key, int index) {
-        return new File(cacheDir, key + "." + index + ".tmp");
-    }
-
-    private String readFile(File file) throws Exception {
-        Reader reader = new FileReader(file);
-        StringWriter writer = new StringWriter();
-        char[] buffer = new char[1024];
-        int count;
-        while ((count = reader.read(buffer)) != -1) {
-            writer.write(buffer, 0, count);
-        }
-        reader.close();
-        return writer.toString();
-    }
-
-    public void writeFile(File file, String content) throws Exception {
-        FileWriter writer = new FileWriter(file);
-        writer.write(content);
-        writer.close();
-    }
-
-    private void assertInoperable(DiskLruCache.Editor editor) throws Exception {
-        try {
-            editor.getString(0);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            editor.set(0, "A");
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            editor.newInputStream(0);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            editor.newOutputStream(0);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            editor.commit();
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            editor.abort();
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    private void generateSomeGarbageFiles() throws Exception {
-        File dir1 = new File(cacheDir, "dir1");
-        File dir2 = new File(dir1, "dir2");
-        writeFile(getCleanFile("g1", 0), "A");
-        writeFile(getCleanFile("g1", 1), "B");
-        writeFile(getCleanFile("g2", 0), "C");
-        writeFile(getCleanFile("g2", 1), "D");
-        writeFile(getCleanFile("g2", 1), "D");
-        writeFile(new File(cacheDir, "otherFile0"), "E");
-        dir1.mkdir();
-        dir2.mkdir();
-        writeFile(new File(dir2, "otherFile1"), "F");
-    }
-
-    private void assertGarbageFilesAllDeleted() throws Exception {
-        assertFalse(getCleanFile("g1", 0).exists());
-        assertFalse(getCleanFile("g1", 1).exists());
-        assertFalse(getCleanFile("g2", 0).exists());
-        assertFalse(getCleanFile("g2", 1).exists());
-        assertFalse(new File(cacheDir, "otherFile0").exists());
-        assertFalse(new File(cacheDir, "dir1").exists());
-    }
-
-    private void set(String key, String value0, String value1) throws Exception {
-        DiskLruCache.Editor editor = cache.edit(key);
-        editor.set(0, value0);
-        editor.set(1, value1);
-        editor.commit();
-    }
-
-    private void assertAbsent(String key) throws Exception {
-        DiskLruCache.Snapshot snapshot = cache.get(key);
-        if (snapshot != null) {
-            snapshot.close();
-            fail();
-        }
-        assertFalse(getCleanFile(key, 0).exists());
-        assertFalse(getCleanFile(key, 1).exists());
-        assertFalse(getDirtyFile(key, 0).exists());
-        assertFalse(getDirtyFile(key, 1).exists());
-    }
-
-    private void assertValue(String key, String value0, String value1) throws Exception {
-        DiskLruCache.Snapshot snapshot = cache.get(key);
-        assertEquals(value0, snapshot.getString(0));
-        assertEquals(value1, snapshot.getString(1));
-        assertTrue(getCleanFile(key, 0).exists());
-        assertTrue(getCleanFile(key, 1).exists());
-        snapshot.close();
-    }
+  private void assertValue(String key, String value0, String value1) throws Exception {
+    DiskLruCache.Snapshot snapshot = cache.get(key);
+    assertEquals(value0, snapshot.getString(0));
+    assertEquals(value1, snapshot.getString(1));
+    assertTrue(getCleanFile(key, 0).exists());
+    assertTrue(getCleanFile(key, 1).exists());
+    snapshot.close();
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java b/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
new file mode 100644
index 0000000..9eff919
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 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.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class RecordingAuthenticator extends Authenticator {
+  /** base64("username:password") */
+  public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
+
+  public final List<String> calls = new ArrayList<String>();
+  public final PasswordAuthentication authentication;
+
+  public RecordingAuthenticator(PasswordAuthentication authentication) {
+    this.authentication = authentication;
+  }
+
+  public RecordingAuthenticator() {
+    this(new PasswordAuthentication("username", "password".toCharArray()));
+  }
+
+  @Override protected PasswordAuthentication getPasswordAuthentication() {
+    this.calls
+        .add("host="
+            + getRequestingHost()
+            + " port="
+            + getRequestingPort()
+            + " site="
+            + getRequestingSite()
+            + " url="
+            + getRequestingURL()
+            + " type="
+            + getRequestorType()
+            + " prompt="
+            + getRequestingPrompt()
+            + " protocol="
+            + getRequestingProtocol()
+            + " scheme="
+            + getRequestingScheme());
+    return authentication;
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
new file mode 100644
index 0000000..b3e2369
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 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.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public final class RecordingHostnameVerifier implements HostnameVerifier {
+  public final List<String> calls = new ArrayList<String>();
+
+  public boolean verify(String hostname, SSLSession session) {
+    calls.add("verify " + hostname);
+    return true;
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java b/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java
index c0a520c..0677263 100644
--- a/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -43,84 +43,82 @@
  * reuse SSL context instances where possible.
  */
 public final class SslContextBuilder {
-    static {
-        Security.addProvider(new BouncyCastleProvider());
+  static {
+    Security.addProvider(new BouncyCastleProvider());
+  }
+
+  private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
+  private final String hostName;
+  private long notBefore = System.currentTimeMillis();
+  private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
+
+  /**
+   * @param hostName the subject of the host. For TLS this should be the
+   * domain name that the client uses to identify the server.
+   */
+  public SslContextBuilder(String hostName) {
+    this.hostName = hostName;
+  }
+
+  public SSLContext build() throws GeneralSecurityException {
+    char[] password = "password".toCharArray();
+
+    // Generate public and private keys and use them to make a self-signed certificate.
+    KeyPair keyPair = generateKeyPair();
+    X509Certificate certificate = selfSignedCertificate(keyPair);
+
+    // Put 'em in a key store.
+    KeyStore keyStore = newEmptyKeyStore(password);
+    Certificate[] certificateChain = { certificate };
+    keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
+    keyStore.setCertificateEntry("cert", certificate);
+
+    // Wrap it up in an SSL context.
+    KeyManagerFactory keyManagerFactory =
+        KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+    keyManagerFactory.init(keyStore, password);
+    TrustManagerFactory trustManagerFactory =
+        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+    trustManagerFactory.init(keyStore);
+    SSLContext sslContext = SSLContext.getInstance("TLS");
+    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(),
+        new SecureRandom());
+    return sslContext;
+  }
+
+  private KeyPair generateKeyPair() throws GeneralSecurityException {
+    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+    keyPairGenerator.initialize(1024, new SecureRandom());
+    return keyPairGenerator.generateKeyPair();
+  }
+
+  /**
+   * Generates a certificate for {@code hostName} containing {@code keyPair}'s
+   * public key, signed by {@code keyPair}'s private key.
+   */
+  @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
+  private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException {
+    X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+    X500Principal issuer = new X500Principal("CN=" + hostName);
+    X500Principal subject = new X500Principal("CN=" + hostName);
+    generator.setSerialNumber(BigInteger.ONE);
+    generator.setIssuerDN(issuer);
+    generator.setNotBefore(new Date(notBefore));
+    generator.setNotAfter(new Date(notAfter));
+    generator.setSubjectDN(subject);
+    generator.setPublicKey(keyPair.getPublic());
+    generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+    return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
+  }
+
+  private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
+    try {
+      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+      InputStream in = null; // By convention, 'null' creates an empty key store.
+      keyStore.load(in, password);
+      return keyStore;
+    } catch (IOException e) {
+      throw new AssertionError(e);
     }
-
-    private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
-    private final String hostName;
-    private long notBefore = System.currentTimeMillis();
-    private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
-
-    /**
-     * @param hostName the subject of the host. For TLS this should be the
-     *     domain name that the client uses to identify the server.
-     */
-    public SslContextBuilder(String hostName) {
-        this.hostName = hostName;
-    }
-
-    public SSLContext build() throws GeneralSecurityException {
-        char[] password = "password".toCharArray();
-
-        // Generate public and private keys and use them to make a self-signed certificate.
-        KeyPair keyPair = generateKeyPair();
-        X509Certificate certificate = selfSignedCertificate(keyPair);
-
-        // Put 'em in a key store.
-        KeyStore keyStore = newEmptyKeyStore(password);
-        Certificate[] certificateChain = {
-                certificate
-        };
-        keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
-        keyStore.setCertificateEntry("cert", certificate);
-
-        // Wrap it up in an SSL context.
-        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
-                KeyManagerFactory.getDefaultAlgorithm());
-        keyManagerFactory.init(keyStore, password);
-        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
-                TrustManagerFactory.getDefaultAlgorithm());
-        trustManagerFactory.init(keyStore);
-        SSLContext sslContext = SSLContext.getInstance("TLS");
-        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(),
-                new SecureRandom());
-        return sslContext;
-    }
-
-    private KeyPair generateKeyPair() throws GeneralSecurityException {
-        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
-        keyPairGenerator.initialize(1024, new SecureRandom());
-        return keyPairGenerator.generateKeyPair();
-    }
-
-    /**
-     * Generates a certificate for {@code hostName} containing {@code keyPair}'s
-     * public key, signed by {@code keyPair}'s private key.
-     */
-    @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
-    private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException {
-        X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
-        X500Principal issuer = new X500Principal("CN=" + hostName);
-        X500Principal subject = new X500Principal("CN=" + hostName);
-        generator.setSerialNumber(BigInteger.ONE);
-        generator.setIssuerDN(issuer);
-        generator.setNotBefore(new Date(notBefore));
-        generator.setNotAfter(new Date(notAfter));
-        generator.setSubjectDN(subject);
-        generator.setPublicKey(keyPair.getPublic());
-        generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
-        return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
-    }
-
-    private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
-        try {
-            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
-            InputStream in = null; // By convention, 'null' creates an empty key store.
-            keyStore.load(in, password);
-            return keyStore;
-        } catch (IOException e) {
-            throw new AssertionError(e);
-        }
-    }
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java b/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java
index 5f85b52..252f6ac 100644
--- a/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java
@@ -16,60 +16,60 @@
 
 package com.squareup.okhttp.internal;
 
-import static com.squareup.okhttp.internal.Util.US_ASCII;
 import java.io.ByteArrayInputStream;
 import java.io.EOFException;
 import java.io.InputStream;
-import static org.junit.Assert.fail;
 import org.junit.Test;
 
-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();
-    }
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static org.junit.Assert.fail;
 
-    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());
+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/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java b/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
index f93c493..11d7239 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
@@ -25,24 +25,25 @@
 import javax.net.ssl.SSLSession;
 
 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);
+  public static void main(String[] args) throws Exception {
+    URL url = new URL("https://www.google.ca/");
+    HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient().open(url);
 
-        connection.setHostnameVerifier(new HostnameVerifier() {
-            @Override public boolean verify(String s, SSLSession sslSession) {
-                System.out.println("VERIFYING " + s);
-                return true;
-            }
-        });
+    connection.setHostnameVerifier(new HostnameVerifier() {
+      @Override public boolean verify(String s, SSLSession sslSession) {
+        System.out.println("VERIFYING " + s);
+        return true;
+      }
+    });
 
-        int responseCode = connection.getResponseCode();
-        System.out.println(responseCode);
+    int responseCode = connection.getResponseCode();
+    System.out.println(responseCode);
 
-        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
-        String line;
-        while ((line = reader.readLine()) != null) {
-            System.out.println(line);
-        }
+    BufferedReader reader =
+        new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+    String line;
+    while ((line = reader.readLine()) != null) {
+      System.out.println(line);
     }
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index 841647d..e54ed21 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -19,9 +19,7 @@
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
-import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
 import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.internal.http.HttpResponseCache;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -69,1803 +67,1723 @@
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSession;
 import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
 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;
-import org.junit.Before;
-import org.junit.Test;
 
-/**
- * Android's HttpResponseCacheTest.
- */
+/** Android's HttpResponseCacheTest. */
 public final class HttpResponseCacheTest {
-    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 HttpResponseCache cache;
-    private final CookieManager cookieManager = new CookieManager();
+  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 HttpResponseCache cache;
+  private final CookieManager cookieManager = new CookieManager();
 
-    private static final SSLContext sslContext;
-    static {
+  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());
+    cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
+    ResponseCache.setDefault(cache);
+    CookieHandler.setDefault(cookieManager);
+  }
+
+  @After public void tearDown() throws Exception {
+    server.shutdown();
+    ResponseCache.setDefault(null);
+    cache.getCache().delete();
+    CookieHandler.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);
+    }
+  }
+
+  /**
+   * 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 {
-            sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
-        } catch (GeneralSecurityException e) {
-            throw new RuntimeException(e);
-        } catch (UnknownHostException e) {
-            throw new RuntimeException(e);
+          httpConnection.getRequestProperties();
+          fail();
+        } catch (IllegalStateException expected) {
         }
-    }
-
-    @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);
-    }
-
-    @After public void tearDown() throws Exception {
-        server.shutdown();
-        ResponseCache.setDefault(null);
-        cache.getCache().delete();
-        CookieHandler.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);
-        }
-    }
-
-    /**
-     * 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();
+          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 responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
+    testResponseCaching(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
+    testResponseCaching(TransferKind.CHUNKED);
+  }
+
+  @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
+    testResponseCaching(TransferKind.END_OF_STREAM);
+  }
+
+  /**
+   * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
+   * http://code.google.com/p/android/issues/detail?id=8175
+   */
+  private void testResponseCaching(TransferKind transferKind) throws IOException {
+    MockResponse response =
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+            .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+            .setStatus("HTTP/1.1 200 Fantastic");
+    transferKind.setBody(response, "I love puppies but hate spiders", 1);
+    server.enqueue(response);
+    server.play();
+
+    // Make sure that calling skip() doesn't omit bytes from the cache.
+    HttpURLConnection urlConnection = openConnection(server.getUrl("/"));
+    InputStream in = urlConnection.getInputStream();
+    assertEquals("I love ", readAscii(urlConnection, "I love ".length()));
+    reliableSkip(in, "puppies but hate ".length());
+    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();
+    assertEquals("I love puppies but hate spiders",
+        readAscii(urlConnection, "I love puppies but hate spiders".length()));
+    assertEquals(200, urlConnection.getResponseCode());
+    assertEquals("Fantastic", urlConnection.getResponseMessage());
+
+    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 {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setBody("ABC"));
+    server.play();
+
+    HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
+    connection.setSSLSocketFactory(sslContext.getSocketFactory());
+    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(connection));
+
+    // 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();
+
+    connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
+    connection.setSSLSocketFactory(sslContext.getSocketFactory());
+    connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(connection));
+
+    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));
+  }
+
+  @Test public void responseCachingAndRedirects() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+        .addHeader("Location: /foo"));
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setBody("ABC"));
+    server.enqueue(new MockResponse().setBody("DEF"));
+    server.play();
+
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    assertEquals("ABC", readAscii(connection));
+
+    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 {
+    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("ABC"));
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+        .addHeader("Location: /foo"));
+    server.enqueue(new MockResponse().setBody("DEF"));
+    server.play();
+
+    assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
+    RecordedRequest request1 = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
+    assertEquals(0, request1.getSequenceNumber());
+
+    assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar"))));
+    RecordedRequest request2 = server.takeRequest();
+    assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
+    assertEquals(1, request2.getSequenceNumber());
+
+    // an unrelated request should reuse the pooled connection
+    assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz"))));
+    RecordedRequest request3 = server.takeRequest();
+    assertEquals("GET /baz HTTP/1.1", request3.getRequestLine());
+    assertEquals(2, request3.getSequenceNumber());
+  }
+
+  @Test public void secureResponseCachingAndRedirects() throws IOException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+        .addHeader("Location: /foo"));
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .setBody("ABC"));
+    server.enqueue(new MockResponse().setBody("DEF"));
+    server.play();
+
+    HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    connection1.setSSLSocketFactory(sslContext.getSocketFactory());
+    connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(connection1));
+
+    // Cached!
+    HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
+    connection1.setSSLSocketFactory(sslContext.getSocketFactory());
+    connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    assertEquals("ABC", readAscii(connection2));
+
+    assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
+    assertEquals(2, cache.getHitCount());
+  }
+
+  @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException {
+    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 {
+        return null;
+      }
+    });
+
+    URL url = server.getUrl("/");
+    URLConnection urlConnection = openConnection(url);
+    urlConnection.addRequestProperty("A", "android");
+    readAscii(urlConnection);
+    assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
+  }
+
+  @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
+    testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
+    testServerPrematureDisconnect(TransferKind.CHUNKED);
+  }
+
+  @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
+    // Intentionally empty. This case doesn't make sense because there's no
+    // such thing as a premature disconnect when the disconnect itself
+    // indicates the end of the data stream.
+  }
+
+  private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
+    MockResponse response = new MockResponse();
+    transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
+    server.enqueue(truncateViolently(response, 16));
+    server.enqueue(new MockResponse().setBody("Request #2"));
+    server.play();
+
+    BufferedReader reader = new BufferedReader(
+        new InputStreamReader(openConnection(server.getUrl("/")).getInputStream()));
+    assertEquals("ABCDE", reader.readLine());
+    try {
+      reader.readLine();
+      fail("This implementation silently ignored a truncated HTTP body.");
+    } catch (IOException expected) {
+    } finally {
+      reader.close();
     }
 
-    @Test public void responseCaching_410() throws Exception {
-        // the HTTP spec permits caching 410s, but the RI doesn't.
-        assertCached(true, 410);
+    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 {
+    testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException {
+    testClientPrematureDisconnect(TransferKind.CHUNKED);
+  }
+
+  @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException {
+    testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
+  }
+
+  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);
+    transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
+    server.enqueue(response);
+    server.enqueue(new MockResponse().setBody("Request #2"));
+    server.play();
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    assertEquals("ABCDE", readAscii(connection, 5));
+    in.close();
+    try {
+      in.read();
+      fail("Expected an IOException because the stream is closed.");
+    } catch (IOException expected) {
     }
 
-    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();
+    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());
+  }
 
-        URL url = server.getUrl("/");
-        HttpURLConnection conn = openConnection(url);
-        assertEquals(responseCode, conn.getResponseCode());
+  @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception {
+    //      last modified: 105 seconds ago
+    //             served:   5 seconds ago
+    //   default lifetime: (105 - 5) / 10 = 10 seconds
+    //            expires:  10 seconds from served date = 5 seconds from now
+    server.enqueue(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
+            .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
+            .setBody("A"));
+    server.play();
 
-        // exhaust the content stream
-        readAscii(conn);
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    URLConnection connection = openConnection(url);
+    assertEquals("A", readAscii(connection));
+    assertNull(connection.getHeaderField("Warning"));
+  }
 
-        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 public void defaultExpirationDateConditionallyCached() throws Exception {
+    //      last modified: 115 seconds ago
+    //             served:  15 seconds ago
+    //   default lifetime: (115 - 15) / 10 = 10 seconds
+    //            expires:  10 seconds from served date = 5 seconds ago
+    String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
+    //      last modified: 105 days ago
+    //             served:   5 days ago
+    //   default lifetime: (105 - 5) / 10 = 10 days
+    //            expires:  10 days from served date = 5 days from now
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
+        .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
+        .setBody("A"));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    URLConnection connection = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection));
+    assertEquals("113 HttpURLConnection \"Heuristic expiration\"",
+        connection.getHeaderField("Warning"));
+  }
+
+  @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
+            .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
+            .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/?foo=bar");
+    assertEquals("A", readAscii(openConnection(url)));
+    assertEquals("B", readAscii(openConnection(url)));
+  }
+
+  @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception {
+    String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+  }
+
+  @Test public void expirationDateInTheFuture() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+  }
+
+  @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception {
+    String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
+            .addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Cache-Control: max-age=60"));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
+    // Chrome interprets max-age relative to the local clock. Both our cache
+    // and Firefox both use the earlier of the local and server's clock.
+    assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
+        .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgeInTheFutureWithDateHeader() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+        .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception {
+    assertFullyCached(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+            .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception {
+    assertFullyCached(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+            .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+            .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+        .addHeader("Cache-Control: s-maxage=60")
+        .addHeader("Cache-Control: max-age=180"));
+  }
+
+  @Test public void maxAgePreferredOverHigherMaxAge() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+        .addHeader("Cache-Control: s-maxage=180")
+        .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void requestMethodOptionsIsNotCached() throws Exception {
+    testRequestMethod("OPTIONS", false);
+  }
+
+  @Test public void requestMethodGetIsCached() throws Exception {
+    testRequestMethod("GET", true);
+  }
+
+  @Test public void requestMethodHeadIsNotCached() throws Exception {
+    // We could support this but choose not to for implementation simplicity
+    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
+    server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .addHeader("X-Response-ID: 1"));
+    server.enqueue(new MockResponse().addHeader("X-Response-ID: 2"));
+    server.play();
+
+    URL url = server.getUrl("/");
+
+    HttpURLConnection request1 = openConnection(url);
+    request1.setRequestMethod(requestMethod);
+    addRequestBodyIfNecessary(requestMethod, request1);
+    assertEquals("1", request1.getHeaderField("X-Response-ID"));
+
+    URLConnection request2 = openConnection(url);
+    if (expectCached) {
+      assertEquals("1", request1.getHeaderField("X-Response-ID"));
+    } else {
+      assertEquals("2", request2.getHeaderField("X-Response-ID"));
     }
+  }
 
-    /**
-     * 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();
+  @Test public void postInvalidatesCache() throws Exception {
+    testMethodInvalidates("POST");
+  }
 
-        server.enqueue(new MockResponse()
-                .setStatus("HTTP/1.1 200 Fantastic")
-                .addHeader("fgh: ijk")
-                .setBody(body));
-        server.play();
+  @Test public void putInvalidatesCache() throws Exception {
+    testMethodInvalidates("PUT");
+  }
 
-        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;
-            }
-        });
+  @Test public void deleteMethodInvalidatesCache() throws Exception {
+    testMethodInvalidates("DELETE");
+  }
 
-        URL url = server.getUrl("/");
-        HttpURLConnection connection = openConnection(url);
-        assertEquals(body, readAscii(connection));
-        assertEquals(1, cacheCount.get());
+  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)));
+  }
+
+  @Test public void etag() throws Exception {
+    RecordedRequest conditionalRequest =
+        assertConditionallyCached(new MockResponse().addHeader("ETag: v1"));
+    assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1"));
+  }
+
+  @Test public void etagAndExpirationDateInThePast() throws Exception {
+    String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("ETag: v1")
+            .addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-None-Match: v1"));
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void etagAndExpirationDateInTheFuture() throws Exception {
+    assertFullyCached(new MockResponse().addHeader("ETag: v1")
+        .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+  }
+
+  @Test public void cacheControlNoCache() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache"));
+  }
+
+  @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception {
+    String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+            .addHeader("Cache-Control: no-cache"));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void pragmaNoCache() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Pragma: no-cache"));
+  }
+
+  @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception {
+    String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+    RecordedRequest conditionalRequest = assertConditionallyCached(
+        new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
+            .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+            .addHeader("Pragma: no-cache"));
+    List<String> headers = conditionalRequest.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  @Test public void cacheControlNoStore() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Cache-Control: no-store"));
+  }
+
+  @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception {
+    assertNotCached(new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .addHeader("Cache-Control: no-store"));
+  }
+
+  @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception {
+    // 1. request a range
+    // 2. request a full document, expecting a cache miss
+    server.enqueue(new MockResponse().setBody("AA")
+        .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+        .addHeader("Content-Range: bytes 1000-1001/2000"));
+    server.enqueue(new MockResponse().setBody("BB"));
+    server.play();
+
+    URL url = server.getUrl("/");
+
+    URLConnection range = openConnection(url);
+    range.addRequestProperty("Range", "bytes=1000-1001");
+    assertEquals("AA", readAscii(range));
+
+    assertEquals("BB", readAscii(openConnection(url)));
+  }
+
+  @Test public void serverReturnsDocumentOlderThanCache() throws Exception {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+        .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+    server.enqueue(new MockResponse().setBody("B")
+        .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
+    server.play();
+
+    URL url = server.getUrl("/");
+
+    assertEquals("A", readAscii(openConnection(url)));
+    assertEquals("A", readAscii(openConnection(url)));
+  }
+
+  @Test public void nonIdentityEncodingAndConditionalCache() throws Exception {
+    assertNonIdentityEncodingCached(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+            .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+  }
+
+  @Test public void nonIdentityEncodingAndFullCache() throws Exception {
+    assertNonIdentityEncodingCached(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+            .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+  }
+
+  private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
+    server.enqueue(
+        response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip"));
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+    server.play();
+    assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
+  }
+
+  @Test public void expiresDateBeforeModifiedDate() throws Exception {
+    assertConditionallyCached(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+            .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)));
+  }
+
+  @Test public void requestMaxAge() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+        .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))
+        .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+    server.enqueue(new MockResponse().setBody("B"));
+
+    server.play();
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "max-age=30");
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void requestMinFresh() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=60")
+        .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+    server.enqueue(new MockResponse().setBody("B"));
+
+    server.play();
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "min-fresh=120");
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void requestMaxStale() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=120")
+        .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
+    server.enqueue(new MockResponse().setBody("B"));
+
+    server.play();
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "max-stale=180");
+    assertEquals("A", readAscii(connection));
+    assertEquals("110 HttpURLConnection \"Response is stale\"",
+        connection.getHeaderField("Warning"));
+  }
+
+  @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=120, must-revalidate")
+        .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
+    server.enqueue(new MockResponse().setBody("B"));
+
+    server.play();
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "max-stale=180");
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
+    // (no responses enqueued)
+    server.play();
+
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "only-if-cached");
+    assertGatewayTimeout(connection);
+  }
+
+  @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=30")
+        .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "only-if-cached");
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+  }
+
+  @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
+    server.enqueue(new MockResponse().setBody("A")
+        .addHeader("Cache-Control: max-age=30")
+        .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "only-if-cached");
+    assertGatewayTimeout(connection);
+  }
+
+  @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    connection.addRequestProperty("Cache-Control", "only-if-cached");
+    assertGatewayTimeout(connection);
+  }
+
+  @Test public void requestCacheControlNoCache() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+            .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+            .addHeader("Cache-Control: max-age=60")
+            .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    URLConnection connection = openConnection(url);
+    connection.setRequestProperty("Cache-Control", "no-cache");
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void requestPragmaNoCache() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+            .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+            .addHeader("Cache-Control: max-age=60")
+            .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    URLConnection connection = openConnection(url);
+    connection.setRequestProperty("Pragma", "no-cache");
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception {
+    MockResponse response =
+        new MockResponse().addHeader("ETag: v3").addHeader("Cache-Control: max-age=0");
+    String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
+    RecordedRequest request =
+        assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate);
+    List<String> headers = request.getHeaders();
+    assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate));
+    assertFalse(headers.contains("If-None-Match: v3"));
+  }
+
+  @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
+    String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES);
+    MockResponse response = new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
+        .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+        .addHeader("Cache-Control: max-age=0");
+    RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1");
+    List<String> headers = request.getHeaders();
+    assertTrue(headers.contains("If-None-Match: v1"));
+    assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate));
+  }
+
+  private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
+      String conditionValue) throws Exception {
+    server.enqueue(seed.setBody("A"));
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+
+    HttpURLConnection connection = openConnection(url);
+    connection.addRequestProperty(conditionName, conditionValue);
+    assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
+    assertEquals("", readAscii(connection));
+
+    server.takeRequest(); // seed
+    return server.takeRequest();
+  }
+
+  @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());
+    assertEquals("A", readAscii(connection));
+    RecordedRequest request = server.takeRequest();
+    assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since)));
+  }
+
+  @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    HttpURLConnection connection = openConnection(server.getUrl("/"));
+    String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS);
+    connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince);
+    assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
+    assertEquals("", readAscii(connection));
+  }
+
+  @Test public void authorizationRequestHeaderPreventsCaching() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES))
+            .addHeader("Cache-Control: max-age=60")
+            .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    URLConnection connection = openConnection(url);
+    connection.addRequestProperty("Authorization", "password");
+    assertEquals("A", readAscii(connection));
+    assertEquals("B", readAscii(openConnection(url)));
+  }
+
+  @Test public void authorizationResponseCachedWithSMaxAge() throws Exception {
+    assertAuthorizationRequestFullyCached(
+        new MockResponse().addHeader("Cache-Control: s-maxage=60"));
+  }
+
+  @Test public void authorizationResponseCachedWithPublic() throws Exception {
+    assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public"));
+  }
+
+  @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception {
+    assertAuthorizationRequestFullyCached(
+        new MockResponse().addHeader("Cache-Control: must-revalidate"));
+  }
+
+  public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception {
+    server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    URLConnection connection = openConnection(url);
+    connection.addRequestProperty("Authorization", "password");
+    assertEquals("A", readAscii(connection));
+    assertEquals("A", readAscii(openConnection(url)));
+  }
+
+  @Test public void contentLocationDoesNotPopulateCache() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60")
+        .addHeader("Content-Location: /bar")
+        .setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/foo"))));
+    assertEquals("B", readAscii(openConnection(server.getUrl("/bar"))));
+  }
+
+  @Test public void useCachesFalseDoesNotWriteToCache() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.setUseCaches(false);
+    assertEquals("A", readAscii(connection));
+    assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
+  }
+
+  @Test public void useCachesFalseDoesNotReadFromCache() throws Exception {
+    server.enqueue(
+        new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    URLConnection connection = openConnection(server.getUrl("/"));
+    connection.setUseCaches(false);
+    assertEquals("B", readAscii(connection));
+  }
+
+  @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception {
+    URL url = new URL("http://localhost/");
+    URLConnection c1 = openConnection(url);
+    URLConnection c2 = openConnection(url);
+    assertTrue(c1.getDefaultUseCaches());
+    c1.setDefaultUseCaches(false);
+    try {
+      assertTrue(c1.getUseCaches());
+      assertTrue(c2.getUseCaches());
+      URLConnection c3 = openConnection(url);
+      assertFalse(c3.getUseCaches());
+    } finally {
+      c1.setDefaultUseCaches(true);
     }
+  }
 
+  @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() 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().setBody("B"));
+    server.play();
 
-    @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
-        testResponseCaching(TransferKind.FIXED_LENGTH);
+    assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
+    assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
+    assertEquals("B", readAscii(openConnection(server.getUrl("/b"))));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    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"));
+    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 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();
+
+    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));
+  }
+
+  @Test public void cachePlusCookies() throws Exception {
+    server.enqueue(new MockResponse().addHeader(
+        "Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";")
+        .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Cache-Control: max-age=0")
+        .setBody("A"));
+    server.enqueue(new MockResponse().addHeader(
+        "Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    assertCookies(url, "a=FIRST");
+    assertEquals("A", readAscii(openConnection(url)));
+    assertCookies(url, "a=SECOND");
+  }
+
+  @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD")
+        .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Cache-Control: max-age=0")
+        .setBody("A"));
+    server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT")
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    URLConnection connection1 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection1));
+    assertEquals("GET, HEAD", connection1.getHeaderField("Allow"));
+
+    URLConnection connection2 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection2));
+    assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow"));
+  }
+
+  @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Transfer-Encoding: identity")
+        .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+        .addHeader("Cache-Control: max-age=0")
+        .setBody("A"));
+    server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none")
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    URLConnection connection1 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection1));
+    assertEquals("identity", connection1.getHeaderField("Transfer-Encoding"));
+
+    URLConnection connection2 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection2));
+    assertEquals("identity", connection2.getHeaderField("Transfer-Encoding"));
+  }
+
+  @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Warning: 199 test danger")
+        .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.play();
+
+    URLConnection connection1 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection1));
+    assertEquals("199 test danger", connection1.getHeaderField("Warning"));
+
+    URLConnection connection2 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection2));
+    assertEquals(null, connection2.getHeaderField("Warning"));
+  }
+
+  @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Warning: 299 test danger")
+        .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.play();
+
+    URLConnection connection1 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection1));
+    assertEquals("299 test danger", connection1.getHeaderField("Warning"));
+
+    URLConnection connection2 = openConnection(server.getUrl("/"));
+    assertEquals("A", readAscii(connection2));
+    assertEquals("299 test danger", connection2.getHeaderField("Warning"));
+  }
+
+  public void assertCookies(URL url, String... expectedCookies) throws Exception {
+    List<String> actualCookies = new ArrayList<String>();
+    for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
+      actualCookies.add(cookie.toString());
     }
+    assertEquals(Arrays.asList(expectedCookies), actualCookies);
+  }
 
-    @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
-        testResponseCaching(TransferKind.CHUNKED);
+  @Test public void cachePlusRange() throws Exception {
+    assertNotCached(new MockResponse().setResponseCode(HttpURLConnection.HTTP_PARTIAL)
+        .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+        .addHeader("Content-Range: bytes 100-100/200")
+        .addHeader("Cache-Control: max-age=60"));
+  }
+
+  @Test public void conditionalHitUpdatesCache() throws Exception {
+    server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
+        .addHeader("Cache-Control: max-age=0")
+        .setBody("A"));
+    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"));
+    server.play();
+
+    // cache miss; seed the cache
+    HttpURLConnection connection1 = openConnection(server.getUrl("/a"));
+    assertEquals("A", readAscii(connection1));
+    assertEquals(null, connection1.getHeaderField("Allow"));
+
+    // conditional cache hit; update the cache
+    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
+    HttpURLConnection connection3 = openConnection(server.getUrl("/a"));
+    assertEquals("A", readAscii(connection3));
+    assertEquals("GET, HEAD", connection3.getHeaderField("Allow"));
+
+    assertEquals(2, server.getRequestCount());
+  }
+
+  /**
+   * @param delta the offset from the current date to use. Negative
+   * values yield dates in the past; positive values yield dates in the
+   * future.
+   */
+  private String formatDate(long delta, TimeUnit timeUnit) {
+    return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
+  }
+
+  private String formatDate(Date date) {
+    DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+    rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
+    return rfc1123.format(date);
+  }
+
+  private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate)
+      throws IOException {
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT")) {
+      invalidate.setDoOutput(true);
+      OutputStream requestBody = invalidate.getOutputStream();
+      requestBody.write('x');
+      requestBody.close();
     }
+  }
 
-    @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
-        testResponseCaching(TransferKind.END_OF_STREAM);
-    }
+  private void assertNotCached(MockResponse response) throws Exception {
+    server.enqueue(response.setBody("A"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
 
-    /**
-     * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
-     * http://code.google.com/p/android/issues/detail?id=8175
-     */
-    private void testResponseCaching(TransferKind transferKind) throws IOException {
-        MockResponse response = new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setStatus("HTTP/1.1 200 Fantastic");
-        transferKind.setBody(response, "I love puppies but hate spiders", 1);
-        server.enqueue(response);
-        server.play();
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    assertEquals("B", readAscii(openConnection(url)));
+  }
 
-        // Make sure that calling skip() doesn't omit bytes from the cache.
-        HttpURLConnection urlConnection = openConnection(server.getUrl("/"));
-        InputStream in = urlConnection.getInputStream();
-        assertEquals("I love ", readAscii(urlConnection, "I love ".length()));
-        reliableSkip(in, "puppies but hate ".length());
-        assertEquals("spiders", readAscii(urlConnection, "spiders".length()));
-        assertEquals(-1, in.read());
+  /** @return the request with the conditional get headers. */
+  private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception {
+    // scenario 1: condition succeeds
+    server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK"));
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+    // scenario 2: condition fails
+    server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK"));
+    server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C"));
+
+    server.play();
+
+    URL valid = server.getUrl("/valid");
+    HttpURLConnection connection1 = openConnection(valid);
+    assertEquals("A", readAscii(connection1));
+    assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode());
+    assertEquals("A-OK", connection1.getResponseMessage());
+    HttpURLConnection connection2 = openConnection(valid);
+    assertEquals("A", readAscii(connection2));
+    assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
+    assertEquals("A-OK", connection2.getResponseMessage());
+
+    URL invalid = server.getUrl("/invalid");
+    HttpURLConnection connection3 = openConnection(invalid);
+    assertEquals("B", readAscii(connection3));
+    assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode());
+    assertEquals("B-OK", connection3.getResponseMessage());
+    HttpURLConnection connection4 = openConnection(invalid);
+    assertEquals("C", readAscii(connection4));
+    assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode());
+    assertEquals("C-OK", connection4.getResponseMessage());
+
+    server.takeRequest(); // regular get
+    return server.takeRequest(); // conditional get
+  }
+
+  private void assertFullyCached(MockResponse response) throws Exception {
+    server.enqueue(response.setBody("A"));
+    server.enqueue(response.setBody("B"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertEquals("A", readAscii(openConnection(url)));
+    assertEquals("A", readAscii(openConnection(url)));
+  }
+
+  /**
+   * Shortens the body of {@code response} but not the corresponding headers.
+   * Only useful to test how clients respond to the premature conclusion of
+   * the HTTP body.
+   */
+  private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
+    response.setSocketPolicy(DISCONNECT_AT_END);
+    List<String> headers = new ArrayList<String>(response.getHeaders());
+    response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
+    response.getHeaders().clear();
+    response.getHeaders().addAll(headers);
+    return response;
+  }
+
+  /**
+   * Reads {@code count} characters from the stream. If the stream is
+   * exhausted before {@code count} characters can be read, the remaining
+   * characters are returned and the stream is closed.
+   */
+  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();
+    StringBuilder result = new StringBuilder();
+    for (int i = 0; i < count; i++) {
+      int value = in.read();
+      if (value == -1) {
         in.close();
-        assertEquals(1, cache.getWriteSuccessCount());
-        assertEquals(0, cache.getWriteAbortCount());
-
-        urlConnection = openConnection(server.getUrl("/")); // cached!
-        in = urlConnection.getInputStream();
-        assertEquals("I love puppies but hate spiders",
-                readAscii(urlConnection, "I love puppies but hate spiders".length()));
-        assertEquals(200, urlConnection.getResponseCode());
-        assertEquals("Fantastic", urlConnection.getResponseMessage());
-
-        assertEquals(-1, in.read());
-        in.close();
-        assertEquals(1, cache.getWriteSuccessCount());
-        assertEquals(0, cache.getWriteAbortCount());
-        assertEquals(2, cache.getRequestCount());
-        assertEquals(1, cache.getHitCount());
+        break;
+      }
+      result.append((char) value);
     }
+    return result.toString();
+  }
 
-    @Test public void secureResponseCaching() throws IOException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setBody("ABC"));
-        server.play();
+  private String readAscii(URLConnection connection) throws IOException {
+    return readAscii(connection, Integer.MAX_VALUE);
+  }
 
-        HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
-        connection.setSSLSocketFactory(sslContext.getSocketFactory());
-        connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-        assertEquals("ABC", readAscii(connection));
-
-        // 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();
-
-        connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
-        connection.setSSLSocketFactory(sslContext.getSocketFactory());
-        connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-        assertEquals("ABC", readAscii(connection));
-
-        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());
+  private void reliableSkip(InputStream in, int length) throws IOException {
+    while (length > 0) {
+      length -= in.skip(length);
     }
+  }
 
-    @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));
+  private void assertGatewayTimeout(HttpURLConnection connection) throws IOException {
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (FileNotFoundException expected) {
     }
+    assertEquals(504, connection.getResponseCode());
+    assertEquals(-1, connection.getErrorStream().read());
+  }
 
-    @Test public void responseCachingAndRedirects() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
-                .addHeader("Location: /foo"));
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setBody("ABC"));
-        server.enqueue(new MockResponse().setBody("DEF"));
-        server.play();
-
-        HttpURLConnection connection = openConnection(server.getUrl("/"));
-        assertEquals("ABC", readAscii(connection));
-
-        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 {
-        server.enqueue(new MockResponse()
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("ABC"));
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
-                .addHeader("Location: /foo"));
-        server.enqueue(new MockResponse().setBody("DEF"));
-        server.play();
-
-        assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
-        RecordedRequest request1 = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
-        assertEquals(0, request1.getSequenceNumber());
-
-        assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar"))));
-        RecordedRequest request2 = server.takeRequest();
-        assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
-        assertEquals(1, request2.getSequenceNumber());
-
-        // an unrelated request should reuse the pooled connection
-        assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz"))));
-        RecordedRequest request3 = server.takeRequest();
-        assertEquals("GET /baz HTTP/1.1", request3.getRequestLine());
-        assertEquals(2, request3.getSequenceNumber());
-    }
-
-    @Test public void secureResponseCachingAndRedirects() throws IOException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
-                .addHeader("Location: /foo"));
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .setBody("ABC"));
-        server.enqueue(new MockResponse().setBody("DEF"));
-        server.play();
-
-        HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
-        connection1.setSSLSocketFactory(sslContext.getSocketFactory());
-        connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-        assertEquals("ABC", readAscii(connection1));
-
-        // Cached!
-        HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
-        connection1.setSSLSocketFactory(sslContext.getSocketFactory());
-        connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
-        assertEquals("ABC", readAscii(connection2));
-
-        assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
-        assertEquals(2, cache.getHitCount());
-    }
-
-    @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException {
-        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 {
-                return null;
-            }
-        });
-
-        URL url = server.getUrl("/");
-        URLConnection urlConnection = openConnection(url);
-        urlConnection.addRequestProperty("A", "android");
-        readAscii(urlConnection);
-        assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
-    }
-
-
-    @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
-        testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
-    }
-
-    @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
-        testServerPrematureDisconnect(TransferKind.CHUNKED);
-    }
-
-    @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
-        /*
-         * Intentionally empty. This case doesn't make sense because there's no
-         * such thing as a premature disconnect when the disconnect itself
-         * indicates the end of the data stream.
-         */
-    }
-
-    private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
-        MockResponse response = new MockResponse();
-        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
-        server.enqueue(truncateViolently(response, 16));
-        server.enqueue(new MockResponse().setBody("Request #2"));
-        server.play();
-
-        BufferedReader reader = new BufferedReader(new InputStreamReader(
-                openConnection(server.getUrl("/")).getInputStream()));
-        assertEquals("ABCDE", reader.readLine());
-        try {
-            reader.readLine();
-            fail("This implementation silently ignored a truncated HTTP body.");
-        } catch (IOException expected) {
-        } 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 {
-        testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
-    }
-
-    @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException {
-        testClientPrematureDisconnect(TransferKind.CHUNKED);
-    }
-
-    @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException {
-        testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
-    }
-
-    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);
-        transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
-        server.enqueue(response);
-        server.enqueue(new MockResponse().setBody("Request #2"));
-        server.play();
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        InputStream in = connection.getInputStream();
-        assertEquals("ABCDE", readAscii(connection, 5));
-        in.close();
-        try {
-            in.read();
-            fail("Expected an IOException because the stream is closed.");
-        } 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 {
-        //      last modified: 105 seconds ago
-        //             served:   5 seconds ago
-        //   default lifetime: (105 - 5) / 10 = 10 seconds
-        //            expires:  10 seconds from served date = 5 seconds from now
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
-                .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
-                .setBody("A"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        URLConnection connection = openConnection(url);
-        assertEquals("A", readAscii(connection));
-        assertNull(connection.getHeaderField("Warning"));
-    }
-
-    @Test public void defaultExpirationDateConditionallyCached() throws Exception {
-        //      last modified: 115 seconds ago
-        //             served:  15 seconds ago
-        //   default lifetime: (115 - 15) / 10 = 10 seconds
-        //            expires:  10 seconds from served date = 5 seconds ago
-        String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
-        //      last modified: 105 days ago
-        //             served:   5 days ago
-        //   default lifetime: (105 - 5) / 10 = 10 days
-        //            expires:  10 days from served date = 5 days from now
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
-                .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
-                .setBody("A"));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-        URLConnection connection = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection));
-        assertEquals("113 HttpURLConnection \"Heuristic expiration\"",
-                connection.getHeaderField("Warning"));
-    }
-
-    @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
-                .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/?foo=bar");
-        assertEquals("A", readAscii(openConnection(url)));
-        assertEquals("B", readAscii(openConnection(url)));
-    }
-
-    @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception {
-        String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
-        assertNotCached(new MockResponse()
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
-    }
-
-    @Test public void expirationDateInTheFuture() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
-    }
-
-    @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception {
-        String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Cache-Control: max-age=60"));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
-        /*
-         * Chrome interprets max-age relative to the local clock. Both our cache
-         * and Firefox both use the earlier of the local and server's clock.
-         */
-        assertNotCached(new MockResponse()
-                .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgeInTheFutureWithDateHeader() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
-                .addHeader("Cache-Control: s-maxage=60")
-                .addHeader("Cache-Control: max-age=180"));
-    }
-
-    @Test public void maxAgePreferredOverHigherMaxAge() throws Exception {
-        assertNotCached(new MockResponse()
-                .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
-                .addHeader("Cache-Control: s-maxage=180")
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void requestMethodOptionsIsNotCached() throws Exception {
-        testRequestMethod("OPTIONS", false);
-    }
-
-    @Test public void requestMethodGetIsCached() throws Exception {
-        testRequestMethod("GET", true);
-    }
-
-    @Test public void requestMethodHeadIsNotCached() throws Exception {
-        // We could support this but choose not to for implementation simplicity
-        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
-         */
-        server.enqueue(new MockResponse()
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .addHeader("X-Response-ID: 1"));
-        server.enqueue(new MockResponse()
-                .addHeader("X-Response-ID: 2"));
-        server.play();
-
-        URL url = server.getUrl("/");
-
-        HttpURLConnection request1 = openConnection(url);
-        request1.setRequestMethod(requestMethod);
-        addRequestBodyIfNecessary(requestMethod, request1);
-        assertEquals("1", request1.getHeaderField("X-Response-ID"));
-
-        URLConnection request2 = openConnection(url);
-        if (expectCached) {
-            assertEquals("1", request1.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)));
-    }
-
-    @Test public void etag() throws Exception {
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("ETag: v1"));
-        assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1"));
-    }
-
-    @Test public void etagAndExpirationDateInThePast() throws Exception {
-        String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("ETag: v1")
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-None-Match: v1"));
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void etagAndExpirationDateInTheFuture() throws Exception {
-        assertFullyCached(new MockResponse()
-                .addHeader("ETag: v1")
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
-    }
-
-    @Test public void cacheControlNoCache() throws Exception {
-        assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache"));
-    }
-
-    @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception {
-        String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: no-cache"));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void pragmaNoCache() throws Exception {
-        assertNotCached(new MockResponse().addHeader("Pragma: no-cache"));
-    }
-
-    @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception {
-        String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
-        RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .addHeader("Pragma: no-cache"));
-        List<String> headers = conditionalRequest.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    @Test public void cacheControlNoStore() throws Exception {
-        assertNotCached(new MockResponse().addHeader("Cache-Control: no-store"));
-    }
-
-    @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception {
-        assertNotCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: no-store"));
-    }
-
-    @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception {
-        /*
-         * 1. request a range
-         * 2. request a full document, expecting a cache miss
-         */
-        server.enqueue(new MockResponse().setBody("AA")
-                .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
-                .addHeader("Content-Range: bytes 1000-1001/2000"));
-        server.enqueue(new MockResponse().setBody("BB"));
-        server.play();
-
-        URL url = server.getUrl("/");
-
-        URLConnection range = openConnection(url);
-        range.addRequestProperty("Range", "bytes=1000-1001");
-        assertEquals("AA", readAscii(range));
-
-        assertEquals("BB", readAscii(openConnection(url)));
-    }
-
-    @Test public void serverReturnsDocumentOlderThanCache() throws Exception {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
-        server.enqueue(new MockResponse().setBody("B")
-                .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
-        server.play();
-
-        URL url = server.getUrl("/");
-
-        assertEquals("A", readAscii(openConnection(url)));
-        assertEquals("A", readAscii(openConnection(url)));
-    }
-
-    @Test public void nonIdentityEncodingAndConditionalCache() throws Exception {
-        assertNonIdentityEncodingCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
-    }
-
-    @Test public void nonIdentityEncodingAndFullCache() throws Exception {
-        assertNonIdentityEncodingCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
-    }
-
-    private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
-        server.enqueue(response
-                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
-                .addHeader("Content-Encoding: gzip"));
-        server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-
-        server.play();
-        assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
-        assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
-    }
-
-    @Test public void expiresDateBeforeModifiedDate() throws Exception {
-        assertConditionallyCached(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)));
-    }
-
-    @Test public void requestMaxAge() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
-                .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))
-                .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
-        server.enqueue(new MockResponse().setBody("B"));
-
-        server.play();
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "max-age=30");
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void requestMinFresh() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Cache-Control: max-age=60")
-                .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
-        server.enqueue(new MockResponse().setBody("B"));
-
-        server.play();
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "min-fresh=120");
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void requestMaxStale() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Cache-Control: max-age=120")
-                .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
-        server.enqueue(new MockResponse().setBody("B"));
-
-        server.play();
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "max-stale=180");
-        assertEquals("A", readAscii(connection));
-        assertEquals("110 HttpURLConnection \"Response is stale\"",
-                connection.getHeaderField("Warning"));
-    }
-
-    @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Cache-Control: max-age=120, must-revalidate")
-                .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
-        server.enqueue(new MockResponse().setBody("B"));
-
-        server.play();
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "max-stale=180");
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
-        // (no responses enqueued)
-        server.play();
-
-        HttpURLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "only-if-cached");
-        assertGatewayTimeout(connection);
-    }
-
-    @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Cache-Control: max-age=30")
-                .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "only-if-cached");
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-    }
-
-    @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
-        server.enqueue(new MockResponse().setBody("A")
-                .addHeader("Cache-Control: max-age=30")
-                .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-        HttpURLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "only-if-cached");
-        assertGatewayTimeout(connection);
-    }
-
-    @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-        HttpURLConnection connection = openConnection(server.getUrl("/"));
-        connection.addRequestProperty("Cache-Control", "only-if-cached");
-        assertGatewayTimeout(connection);
-    }
-
-    @Test public void requestCacheControlNoCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        URLConnection connection = openConnection(url);
-        connection.setRequestProperty("Cache-Control", "no-cache");
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void requestPragmaNoCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
-                .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        URLConnection connection = openConnection(url);
-        connection.setRequestProperty("Pragma", "no-cache");
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception {
-        MockResponse response = new MockResponse()
-                .addHeader("ETag: v3")
-                .addHeader("Cache-Control: max-age=0");
-        String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
-        RecordedRequest request = assertClientSuppliedCondition(
-                response, "If-Modified-Since", ifModifiedSinceDate);
-        List<String> headers = request.getHeaders();
-        assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate));
-        assertFalse(headers.contains("If-None-Match: v3"));
-    }
-
-    @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
-        String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES);
-        MockResponse response = new MockResponse()
-                .addHeader("Last-Modified: " + lastModifiedDate)
-                .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
-                .addHeader("Cache-Control: max-age=0");
-        RecordedRequest request = assertClientSuppliedCondition(
-                response, "If-None-Match", "v1");
-        List<String> headers = request.getHeaders();
-        assertTrue(headers.contains("If-None-Match: v1"));
-        assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate));
-    }
-
-    private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
-            String conditionValue) throws Exception {
-        server.enqueue(seed.setBody("A"));
-        server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-
-        HttpURLConnection connection = openConnection(url);
-        connection.addRequestProperty(conditionName, conditionValue);
-        assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
-        assertEquals("", readAscii(connection));
-
-        server.takeRequest(); // seed
-        return server.takeRequest();
-    }
-
-    @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());
-        assertEquals("A", readAscii(connection));
-        RecordedRequest request = server.takeRequest();
-        assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since)));
-    }
-
-    @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-        server.play();
-
-        HttpURLConnection connection = openConnection(server.getUrl("/"));
-        String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS);
-        connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince);
-        assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
-        assertEquals("", readAscii(connection));
-    }
-
-    @Test public void authorizationRequestHeaderPreventsCaching() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES))
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        URLConnection connection = openConnection(url);
-        connection.addRequestProperty("Authorization", "password");
-        assertEquals("A", readAscii(connection));
-        assertEquals("B", readAscii(openConnection(url)));
-    }
-
-    @Test public void authorizationResponseCachedWithSMaxAge() throws Exception {
-        assertAuthorizationRequestFullyCached(new MockResponse()
-                .addHeader("Cache-Control: s-maxage=60"));
-    }
-
-    @Test public void authorizationResponseCachedWithPublic() throws Exception {
-        assertAuthorizationRequestFullyCached(new MockResponse()
-                .addHeader("Cache-Control: public"));
-    }
-
-    @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception {
-        assertAuthorizationRequestFullyCached(new MockResponse()
-                .addHeader("Cache-Control: must-revalidate"));
-    }
-
-    public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception {
-        server.enqueue(response
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        URLConnection connection = openConnection(url);
-        connection.addRequestProperty("Authorization", "password");
-        assertEquals("A", readAscii(connection));
-        assertEquals("A", readAscii(openConnection(url)));
-    }
-
-    @Test public void contentLocationDoesNotPopulateCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Cache-Control: max-age=60")
-                .addHeader("Content-Location: /bar")
-                .setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/foo"))));
-        assertEquals("B", readAscii(openConnection(server.getUrl("/bar"))));
-    }
-
-    @Test public void useCachesFalseDoesNotWriteToCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A").setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.setUseCaches(false);
-        assertEquals("A", readAscii(connection));
-        assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
-    }
-
-    @Test public void useCachesFalseDoesNotReadFromCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Cache-Control: max-age=60")
-                .setBody("A").setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
-        URLConnection connection = openConnection(server.getUrl("/"));
-        connection.setUseCaches(false);
-        assertEquals("B", readAscii(connection));
-    }
-
-    @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception {
-        URL url = new URL("http://localhost/");
-        URLConnection c1 = openConnection(url);
-        URLConnection c2 = openConnection(url);
-        assertTrue(c1.getDefaultUseCaches());
-        c1.setDefaultUseCaches(false);
-        try {
-            assertTrue(c1.getUseCaches());
-            assertTrue(c2.getUseCaches());
-            URLConnection c3 = openConnection(url);
-            assertFalse(c3.getUseCaches());
-        } finally {
-            c1.setDefaultUseCaches(true);
-        }
-    }
-
-    @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() 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().setBody("B"));
-        server.play();
-
-        assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
-        assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
-        assertEquals("B", readAscii(openConnection(server.getUrl("/b"))));
-
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertEquals(1, server.takeRequest().getSequenceNumber());
-        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"));
-        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 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();
-
-        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));
-    }
-
-    @Test public void cachePlusCookies() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";")
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: max-age=0")
-                .setBody("A"));
-        server.enqueue(new MockResponse()
-                .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
-                .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        assertCookies(url, "a=FIRST");
-        assertEquals("A", readAscii(openConnection(url)));
-        assertCookies(url, "a=SECOND");
-    }
-
-    @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Allow: GET, HEAD")
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: max-age=0")
-                .setBody("A"));
-        server.enqueue(new MockResponse()
-                .addHeader("Allow: GET, HEAD, PUT")
-                .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-        server.play();
-
-        URLConnection connection1 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection1));
-        assertEquals("GET, HEAD", connection1.getHeaderField("Allow"));
-
-        URLConnection connection2 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection2));
-        assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow"));
-    }
-
-    @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Transfer-Encoding: identity")
-                .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
-                .addHeader("Cache-Control: max-age=0")
-                .setBody("A"));
-        server.enqueue(new MockResponse()
-                .addHeader("Transfer-Encoding: none")
-                .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-        server.play();
-
-        URLConnection connection1 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection1));
-        assertEquals("identity", connection1.getHeaderField("Transfer-Encoding"));
-
-        URLConnection connection2 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection2));
-        assertEquals("identity", connection2.getHeaderField("Transfer-Encoding"));
-    }
-
-    @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Warning: 199 test danger")
-                .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.play();
-
-        URLConnection connection1 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection1));
-        assertEquals("199 test danger", connection1.getHeaderField("Warning"));
-
-        URLConnection connection2 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection2));
-        assertEquals(null, connection2.getHeaderField("Warning"));
-    }
-
-    @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Warning: 299 test danger")
-                .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.play();
-
-        URLConnection connection1 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection1));
-        assertEquals("299 test danger", connection1.getHeaderField("Warning"));
-
-        URLConnection connection2 = openConnection(server.getUrl("/"));
-        assertEquals("A", readAscii(connection2));
-        assertEquals("299 test danger", connection2.getHeaderField("Warning"));
-    }
-
-    public void assertCookies(URL url, String... expectedCookies) throws Exception {
-        List<String> actualCookies = new ArrayList<String>();
-        for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
-            actualCookies.add(cookie.toString());
-        }
-        assertEquals(Arrays.asList(expectedCookies), actualCookies);
-    }
-
-    @Test public void cachePlusRange() throws Exception {
-        assertNotCached(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
-                .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
-                .addHeader("Content-Range: bytes 100-100/200")
-                .addHeader("Cache-Control: max-age=60"));
-    }
-
-    @Test public void conditionalHitUpdatesCache() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
-                .addHeader("Cache-Control: max-age=0")
-                .setBody("A"));
-        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"));
-        server.play();
-
-        // cache miss; seed the cache
-        HttpURLConnection connection1 = openConnection(server.getUrl("/a"));
-        assertEquals("A", readAscii(connection1));
-        assertEquals(null, connection1.getHeaderField("Allow"));
-
-        // conditional cache hit; update the cache
-        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
-        HttpURLConnection connection3 = openConnection(server.getUrl("/a"));
-        assertEquals("A", readAscii(connection3));
-        assertEquals("GET, HEAD", connection3.getHeaderField("Allow"));
-
-        assertEquals(2, server.getRequestCount());
-    }
-
-    /**
-     * @param delta the offset from the current date to use. Negative
-     *     values yield dates in the past; positive values yield dates in the
-     *     future.
-     */
-    private String formatDate(long delta, TimeUnit timeUnit) {
-        return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
-    }
-
-    private String formatDate(Date date) {
-        DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
-        rfc1123.setTimeZone(TimeZone.getTimeZone("UTC"));
-        return rfc1123.format(date);
-    }
-
-    private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate)
-            throws IOException {
-        if (requestMethod.equals("POST") || requestMethod.equals("PUT")) {
-            invalidate.setDoOutput(true);
-            OutputStream requestBody = invalidate.getOutputStream();
-            requestBody.write('x');
-            requestBody.close();
-        }
-    }
-
-    private void assertNotCached(MockResponse response) throws Exception {
-        server.enqueue(response.setBody("A"));
-        server.enqueue(new MockResponse().setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        assertEquals("B", readAscii(openConnection(url)));
-    }
-
-    /**
-     * @return the request with the conditional get headers.
-     */
-    private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception {
-        // scenario 1: condition succeeds
-        server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK"));
-        server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
-
-        // scenario 2: condition fails
-        server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK"));
-        server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C"));
-
-        server.play();
-
-        URL valid = server.getUrl("/valid");
-        HttpURLConnection connection1 = openConnection(valid);
-        assertEquals("A", readAscii(connection1));
-        assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode());
-        assertEquals("A-OK", connection1.getResponseMessage());
-        HttpURLConnection connection2 = openConnection(valid);
-        assertEquals("A", readAscii(connection2));
-        assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
-        assertEquals("A-OK", connection2.getResponseMessage());
-
-        URL invalid = server.getUrl("/invalid");
-        HttpURLConnection connection3 = openConnection(invalid);
-        assertEquals("B", readAscii(connection3));
-        assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode());
-        assertEquals("B-OK", connection3.getResponseMessage());
-        HttpURLConnection connection4 = openConnection(invalid);
-        assertEquals("C", readAscii(connection4));
-        assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode());
-        assertEquals("C-OK", connection4.getResponseMessage());
-
-        server.takeRequest(); // regular get
-        return server.takeRequest(); // conditional get
-    }
-
-    private void assertFullyCached(MockResponse response) throws Exception {
-        server.enqueue(response.setBody("A"));
-        server.enqueue(response.setBody("B"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertEquals("A", readAscii(openConnection(url)));
-        assertEquals("A", readAscii(openConnection(url)));
-    }
-
-    /**
-     * Shortens the body of {@code response} but not the corresponding headers.
-     * Only useful to test how clients respond to the premature conclusion of
-     * the HTTP body.
-     */
-    private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
+  enum TransferKind {
+    CHUNKED() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+          throws IOException {
+        response.setChunkedBody(content, chunkSize);
+      }
+    },
+    FIXED_LENGTH() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+        response.setBody(content);
+      }
+    },
+    END_OF_STREAM() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+        response.setBody(content);
         response.setSocketPolicy(DISCONNECT_AT_END);
-        List<String> headers = new ArrayList<String>(response.getHeaders());
-        response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
-        response.getHeaders().clear();
-        response.getHeaders().addAll(headers);
-        return response;
-    }
-
-    /**
-     * Reads {@code count} characters from the stream. If the stream is
-     * exhausted before {@code count} characters can be read, the remaining
-     * characters are returned and the stream is closed.
-     */
-    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();
-        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);
+        for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
+          if (h.next().startsWith("Content-Length:")) {
+            h.remove();
+            break;
+          }
         }
-        return result.toString();
+      }
+    };
+
+    abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException;
+
+    void setBody(MockResponse response, String content, int chunkSize) throws IOException {
+      setBody(response, content.getBytes("UTF-8"), chunkSize);
+    }
+  }
+
+  private <T> List<T> toListOrNull(T[] arrayOrNull) {
+    return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null;
+  }
+
+  /** Returns a gzipped copy of {@code bytes}. */
+  public byte[] gzip(byte[] bytes) throws IOException {
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
+    gzippedOut.write(bytes);
+    gzippedOut.close();
+    return bytesOut.toByteArray();
+  }
+
+  private class InsecureResponseCache extends ResponseCache {
+    @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+      return cache.put(uri, connection);
     }
 
-    private String readAscii(URLConnection connection) throws IOException {
-        return readAscii(connection, Integer.MAX_VALUE);
-    }
-
-    private void reliableSkip(InputStream in, int length) throws IOException {
-        while (length > 0) {
-            length -= in.skip(length);
-        }
-    }
-
-    private void assertGatewayTimeout(HttpURLConnection connection) throws IOException {
-        try {
-            connection.getInputStream();
-            fail();
-        } catch (FileNotFoundException expected) {
-        }
-        assertEquals(504, connection.getResponseCode());
-        assertEquals(-1, connection.getErrorStream().read());
-    }
-
-    enum TransferKind {
-        CHUNKED() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
-                    throws IOException {
-                response.setChunkedBody(content, chunkSize);
-            }
-        },
-        FIXED_LENGTH() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
-                response.setBody(content);
-            }
-        },
-        END_OF_STREAM() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
-                response.setBody(content);
-                response.setSocketPolicy(DISCONNECT_AT_END);
-                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
-                    if (h.next().startsWith("Content-Length:")) {
-                        h.remove();
-                        break;
-                    }
-                }
-            }
+    @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();
+          }
         };
-
-        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
-                throws IOException;
-
-        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
-            setBody(response, content.getBytes("UTF-8"), chunkSize);
-        }
+      }
+      return response;
     }
-
-    private <T> List<T> toListOrNull(T[] arrayOrNull) {
-        return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null;
-    }
-
-    /**
-     * Returns a gzipped copy of {@code bytes}.
-     */
-    public byte[] gzip(byte[] bytes) throws IOException {
-        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
-        gzippedOut.write(bytes);
-        gzippedOut.close();
-        return bytesOut.toByteArray();
-    }
-
-    private class InsecureResponseCache extends ResponseCache {
-        @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
-            return cache.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);
-            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;
-        }
-    }
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
index 377227f..32649cf 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
@@ -15,49 +15,50 @@
  */
 package com.squareup.okhttp.internal.http;
 
-import com.squareup.okhttp.internal.http.RawHeaders;
 import java.util.Arrays;
 import java.util.List;
-import static org.junit.Assert.assertEquals;
 import org.junit.Test;
 
-public final class RawHeadersTest {
-    @Test public void parseNameValueBlock() {
-        List<String> nameValueBlock = Arrays.asList(
-                "cache-control",
-                "no-cache, no-store",
-                "set-cookie",
-                "Cookie1\u0000Cookie2",
-                "status", "200 OK"
-        );
-        RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
-        assertEquals("no-cache, no-store", rawHeaders.get("cache-control"));
-        assertEquals("Cookie2", rawHeaders.get("set-cookie"));
-        assertEquals("200 OK", rawHeaders.get("status"));
-        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));
-        assertEquals("status", rawHeaders.getFieldName(3));
-        assertEquals("200 OK", rawHeaders.getValue(3));
-    }
+import static org.junit.Assert.assertEquals;
 
-    @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");
-        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);
-    }
+public final class RawHeadersTest {
+  @Test public void parseNameValueBlock() {
+    List<String> nameValueBlock =
+        Arrays.asList("cache-control", "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2",
+            ":status", "200 OK");
+    // TODO: fromNameValueBlock should synthesize a request status line
+    RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
+    assertEquals("no-cache, no-store", rawHeaders.get("cache-control"));
+    assertEquals("Cookie2", rawHeaders.get("set-cookie"));
+    assertEquals("200 OK", rawHeaders.get(":status"));
+    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));
+    assertEquals(":status", rawHeaders.getFieldName(3));
+    assertEquals("200 OK", rawHeaders.getValue(3));
+  }
+
+  @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());
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index 14642e9..4dc2e32 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -24,7 +24,6 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
-import static java.net.Proxy.NO_PROXY;
 import java.net.ProxySelector;
 import java.net.SocketAddress;
 import java.net.URI;
@@ -36,338 +35,321 @@
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
+import org.junit.Test;
+
+import static java.net.Proxy.NO_PROXY;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import org.junit.Test;
 
 public final class RouteSelectorTest {
-    private static final int proxyAPort = 1001;
-    private static final String proxyAHost = "proxyA";
-    private static final Proxy proxyA
-            = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort));
-    private static final int proxyBPort = 1002;
-    private static final String proxyBHost = "proxyB";
-    private static final Proxy proxyB
-            = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
-    private static final URI uri;
-    private static final String uriHost = "hostA";
-    private static final int uriPort = 80;
+  private static final int proxyAPort = 1001;
+  private static final String proxyAHost = "proxyA";
+  private static final Proxy proxyA =
+      new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort));
+  private static final int proxyBPort = 1002;
+  private static final String proxyBHost = "proxyB";
+  private static final Proxy proxyB =
+      new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
+  private static final URI uri;
+  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 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();
-            pool = ConnectionPool.getDefault();
-            hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier();
-        } catch (Exception e) {
-            throw new AssertionError(e);
-        }
+  private static final SSLContext sslContext;
+  private static final SSLSocketFactory socketFactory;
+  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();
+      pool = ConnectionPool.getDefault();
+      hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier();
+    } catch (Exception e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  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, null);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(uriHost);
+
+    assertFalse(routeSelector.hasNext());
+    try {
+      routeSelector.next();
+      fail();
+    } catch (NoSuchElementException expected) {
+    }
+  }
+
+  @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, proxyA);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 2);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+        false);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort,
+        false);
+
+    assertFalse(routeSelector.hasNext());
+    dns.assertRequests(proxyAHost);
+    proxySelector.assertRequests(); // No proxy selector requests!
+  }
+
+  @Test public void explicitDirectProxy() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, NO_PROXY);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 2);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
+
+    assertFalse(routeSelector.hasNext());
+    dns.assertRequests(uri.getHost());
+    proxySelector.assertRequests(); // No proxy selector requests!
+  }
+
+  @Test public void proxySelectorReturnsNull() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, null);
+
+    proxySelector.proxies = null;
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    proxySelector.assertRequests(uri);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(uriHost);
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  @Test public void proxySelectorReturnsNoProxies() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, null);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 2);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
+
+    assertFalse(routeSelector.hasNext());
+    dns.assertRequests(uri.getHost());
+    proxySelector.assertRequests(uri);
+  }
+
+  @Test public void proxySelectorReturnsMultipleProxies() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, null);
+
+    proxySelector.proxies.add(proxyA);
+    proxySelector.proxies.add(proxyB);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    proxySelector.assertRequests(uri);
+
+    // First try the IP addresses of the first proxy, in sequence.
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 2);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+        false);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort,
+        false);
+    dns.assertRequests(proxyAHost);
+
+    // Next try the IP address of the second proxy.
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(254, 1);
+    assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort,
+        false);
+    dns.assertRequests(proxyBHost);
+
+    // Finally try the only IP address of the origin server.
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(253, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(uriHost);
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, null);
+
+    proxySelector.proxies.add(NO_PROXY);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    proxySelector.assertRequests(uri);
+
+    // Only the origin server will be attempted.
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(uriHost);
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
+    Address address = new Address(uriHost, uriPort, null, null, null);
+
+    proxySelector.proxies.add(proxyA);
+    proxySelector.proxies.add(proxyB);
+    proxySelector.proxies.add(proxyA);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+    proxySelector.assertRequests(uri);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+        false);
+    dns.assertRequests(proxyAHost);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = null;
+    try {
+      routeSelector.next();
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+    dns.assertRequests(proxyBHost);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+        false);
+    dns.assertRequests(proxyAHost);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(254, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(uriHost);
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  @Test public void multipleTlsModes() throws Exception {
+    Address address =
+        new Address(uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    assertTrue(routeSelector.hasNext());
+    dns.inetAddresses = makeFakeAddresses(255, 1);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
+    dns.assertRequests(uriHost);
+
+    assertTrue(routeSelector.hasNext());
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    dns.assertRequests(); // No more DNS requests since the previous!
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
+    Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, null);
+    proxySelector.proxies.add(proxyA);
+    proxySelector.proxies.add(proxyB);
+    RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+
+    // Proxy A
+    dns.inetAddresses = makeFakeAddresses(255, 2);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, true);
+    dns.assertRequests(proxyAHost);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+        false);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort, true);
+    assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort,
+        false);
+
+    // Proxy B
+    dns.inetAddresses = makeFakeAddresses(254, 2);
+    assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort, true);
+    dns.assertRequests(proxyBHost);
+    assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort,
+        false);
+    assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort, true);
+    assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort,
+        false);
+
+    // Origin
+    dns.inetAddresses = makeFakeAddresses(253, 2);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
+    dns.assertRequests(uriHost);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, true);
+    assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
+
+    assertFalse(routeSelector.hasNext());
+  }
+
+  private void assertConnection(Connection connection, Address address, Proxy proxy,
+      InetAddress socketAddress, int socketPort, boolean modernTls) {
+    assertEquals(address, connection.getAddress());
+    assertEquals(proxy, connection.getProxy());
+    assertEquals(socketAddress, connection.getSocketAddress().getAddress());
+    assertEquals(socketPort, connection.getSocketAddress().getPort());
+    assertEquals(modernTls, connection.isModernTls());
+  }
+
+  private static InetAddress[] makeFakeAddresses(int prefix, int count) {
+    try {
+      InetAddress[] result = new InetAddress[count];
+      for (int i = 0; i < count; i++) {
+        result[i] =
+            InetAddress.getByAddress(new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i });
+      }
+      return result;
+    } catch (UnknownHostException e) {
+      throw new AssertionError();
+    }
+  }
+
+  private static class FakeDns implements Dns {
+    List<String> requestedHosts = new ArrayList<String>();
+    InetAddress[] inetAddresses;
+
+    @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+      requestedHosts.add(host);
+      if (inetAddresses == null) throw new UnknownHostException();
+      return inetAddresses;
     }
 
-    private final FakeDns dns = new FakeDns();
-    private final FakeProxySelector proxySelector = new FakeProxySelector();
+    public void assertRequests(String... expectedHosts) {
+      assertEquals(Arrays.asList(expectedHosts), requestedHosts);
+      requestedHosts.clear();
+    }
+  }
 
-    @Test public void singleRoute() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
+  private static class FakeProxySelector extends ProxySelector {
+    List<URI> requestedUris = new ArrayList<URI>();
+    List<Proxy> proxies = new ArrayList<Proxy>();
+    List<String> failures = new ArrayList<String>();
 
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(uriHost);
-
-        assertFalse(routeSelector.hasNext());
-        try {
-            routeSelector.next();
-            fail();
-        } catch (NoSuchElementException expected) {
-        }
+    @Override public List<Proxy> select(URI uri) {
+      requestedUris.add(uri);
+      return proxies;
     }
 
-    @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, proxyA);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 2);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, false);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[1], proxyAPort, false);
-
-        assertFalse(routeSelector.hasNext());
-        dns.assertRequests(proxyAHost);
-        proxySelector.assertRequests(); // No proxy selector requests!
+    public void assertRequests(URI... expectedUris) {
+      assertEquals(Arrays.asList(expectedUris), requestedUris);
+      requestedUris.clear();
     }
 
-    @Test public void explicitDirectProxy() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, NO_PROXY);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 2);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
-
-        assertFalse(routeSelector.hasNext());
-        dns.assertRequests(uri.getHost());
-        proxySelector.assertRequests(); // No proxy selector requests!
+    @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+      InetSocketAddress socketAddress = (InetSocketAddress) sa;
+      failures.add(
+          String.format("%s %s:%d %s", uri, socketAddress.getHostName(), socketAddress.getPort(),
+              ioe.getMessage()));
     }
-
-    @Test public void proxySelectorReturnsNull() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-
-        proxySelector.proxies = null;
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-        proxySelector.assertRequests(uri);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(uriHost);
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    @Test public void proxySelectorReturnsNoProxies() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 2);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
-
-        assertFalse(routeSelector.hasNext());
-        dns.assertRequests(uri.getHost());
-        proxySelector.assertRequests(uri);
-    }
-
-    @Test public void proxySelectorReturnsMultipleProxies() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-
-        proxySelector.proxies.add(proxyA);
-        proxySelector.proxies.add(proxyB);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-        proxySelector.assertRequests(uri);
-
-        // First try the IP addresses of the first proxy, in sequence.
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 2);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, false);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[1], proxyAPort, false);
-        dns.assertRequests(proxyAHost);
-
-        // Next try the IP address of the second proxy.
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(254, 1);
-        assertConnection(routeSelector.next(),
-                address, proxyB, dns.inetAddresses[0], proxyBPort, false);
-        dns.assertRequests(proxyBHost);
-
-        // Finally try the only IP address of the origin server.
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(253, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(uriHost);
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-
-        proxySelector.proxies.add(NO_PROXY);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-        proxySelector.assertRequests(uri);
-
-        // Only the origin server will be attempted.
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(uriHost);
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
-        Address address = new Address(uriHost, uriPort, null, null, null);
-
-        proxySelector.proxies.add(proxyA);
-        proxySelector.proxies.add(proxyB);
-        proxySelector.proxies.add(proxyA);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-        proxySelector.assertRequests(uri);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, false);
-        dns.assertRequests(proxyAHost);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = null;
-        try {
-            routeSelector.next();
-            fail();
-        } catch (UnknownHostException expected) {
-        }
-        dns.assertRequests(proxyBHost);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, false);
-        dns.assertRequests(proxyAHost);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(254, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(uriHost);
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    @Test public void multipleTlsModes() throws Exception {
-        Address address = new Address(
-                uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-
-        assertTrue(routeSelector.hasNext());
-        dns.inetAddresses = makeFakeAddresses(255, 1);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
-        dns.assertRequests(uriHost);
-
-        assertTrue(routeSelector.hasNext());
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        dns.assertRequests(); // No more DNS requests since the previous!
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
-        Address address = new Address(
-                uriHost, uriPort, socketFactory, hostnameVerifier, null);
-        proxySelector.proxies.add(proxyA);
-        proxySelector.proxies.add(proxyB);
-        RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns);
-
-        // Proxy A
-        dns.inetAddresses = makeFakeAddresses(255, 2);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, true);
-        dns.assertRequests(proxyAHost);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[0], proxyAPort, false);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[1], proxyAPort, true);
-        assertConnection(routeSelector.next(),
-                address, proxyA, dns.inetAddresses[1], proxyAPort, false);
-
-        // Proxy B
-        dns.inetAddresses = makeFakeAddresses(254, 2);
-        assertConnection(routeSelector.next(),
-                address, proxyB, dns.inetAddresses[0], proxyBPort, true);
-        dns.assertRequests(proxyBHost);
-        assertConnection(routeSelector.next(),
-                address, proxyB, dns.inetAddresses[0], proxyBPort, false);
-        assertConnection(routeSelector.next(),
-                address, proxyB, dns.inetAddresses[1], proxyBPort, true);
-        assertConnection(routeSelector.next(),
-                address, proxyB, dns.inetAddresses[1], proxyBPort, false);
-
-        // Origin
-        dns.inetAddresses = makeFakeAddresses(253, 2);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, true);
-        dns.assertRequests(uriHost);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[0], uriPort, false);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[1], uriPort, true);
-        assertConnection(routeSelector.next(),
-                address, NO_PROXY, dns.inetAddresses[1], uriPort, false);
-
-        assertFalse(routeSelector.hasNext());
-    }
-
-    private void assertConnection(Connection connection, Address address,
-            Proxy proxy, InetAddress socketAddress, int socketPort, boolean modernTls) {
-        assertEquals(address, connection.getAddress());
-        assertEquals(proxy, connection.getProxy());
-        assertEquals(socketAddress, connection.getSocketAddress().getAddress());
-        assertEquals(socketPort, connection.getSocketAddress().getPort());
-        assertEquals(modernTls, connection.isModernTls());
-    }
-
-    private static InetAddress[] makeFakeAddresses(int prefix, int count) {
-        try {
-            InetAddress[] result = new InetAddress[count];
-            for (int i = 0; i < count; i++) {
-                result[i] = InetAddress.getByAddress(
-                        new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i });
-            }
-            return result;
-        } catch (UnknownHostException e) {
-            throw new AssertionError();
-        }
-    }
-
-    private static class FakeDns implements Dns {
-        List<String> requestedHosts = new ArrayList<String>();
-        InetAddress[] inetAddresses;
-
-        @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
-            requestedHosts.add(host);
-            if (inetAddresses == null) throw new UnknownHostException();
-            return inetAddresses;
-        }
-
-        public void assertRequests(String... expectedHosts) {
-            assertEquals(Arrays.asList(expectedHosts), requestedHosts);
-            requestedHosts.clear();
-        }
-    }
-
-    private static class FakeProxySelector extends ProxySelector {
-        List<URI> requestedUris = new ArrayList<URI>();
-        List<Proxy> proxies = new ArrayList<Proxy>();
-        List<String> failures = new ArrayList<String>();
-
-        @Override public List<Proxy> select(URI uri) {
-            requestedUris.add(uri);
-            return proxies;
-        }
-
-        public void assertRequests(URI... expectedUris) {
-            assertEquals(Arrays.asList(expectedUris), requestedUris);
-            requestedUris.clear();
-        }
-
-        @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
-            InetSocketAddress socketAddress = (InetSocketAddress) sa;
-            failures.add(String.format("%s %s:%d %s", uri, socketAddress.getHostName(),
-                    socketAddress.getPort(), ioe.getMessage()));
-        }
-    }
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 4d15189..2e7c598 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -20,12 +20,9 @@
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
 import com.google.mockwebserver.SocketPolicy;
-import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
-import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
-import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
-import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
 import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.internal.http.HttpResponseCache;
+import com.squareup.okhttp.internal.RecordingAuthenticator;
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -39,7 +36,6 @@
 import java.net.HttpRetryException;
 import java.net.HttpURLConnection;
 import java.net.InetAddress;
-import java.net.PasswordAuthentication;
 import java.net.ProtocolException;
 import java.net.Proxy;
 import java.net.ProxySelector;
@@ -65,2321 +61,2282 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
-import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
 import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
+import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END;
+import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
 
-/**
- * Android's URLConnectionTest.
- */
+/** Android's URLConnectionTest. */
 public final class URLConnectionTest {
-    /** base64("username:password") */
-    private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
+  private MockWebServer server = new MockWebServer();
+  private MockWebServer server2 = new MockWebServer();
 
-    private MockWebServer server = new MockWebServer();
-    private MockWebServer server2 = new MockWebServer();
+  private final OkHttpClient client = new OkHttpClient();
+  private HttpResponseCache cache;
+  private String hostName;
 
-    private final OkHttpClient client = new OkHttpClient();
-    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);
-        }
+  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();
-    }
+  @Before public void setUp() throws Exception {
+    hostName = server.getHostName();
+  }
 
-    @After public void tearDown() throws Exception {
-        System.clearProperty("proxyHost");
-        System.clearProperty("proxyPort");
-        System.clearProperty("http.proxyHost");
-        System.clearProperty("http.proxyPort");
-        System.clearProperty("https.proxyHost");
-        System.clearProperty("https.proxyPort");
-        server.shutdown();
-        server2.shutdown();
-        if (cache != null) {
-            cache.getCache().delete();
-        }
+  @After public void tearDown() throws Exception {
+    System.clearProperty("proxyHost");
+    System.clearProperty("proxyPort");
+    System.clearProperty("http.proxyHost");
+    System.clearProperty("http.proxyPort");
+    System.clearProperty("https.proxyHost");
+    System.clearProperty("https.proxyPort");
+    server.shutdown();
+    server2.shutdown();
+    if (cache != null) {
+      cache.getCache().delete();
     }
+  }
 
-    @Test public void requestHeaders() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse());
-        server.play();
+  @Test public void requestHeaders() throws IOException, InterruptedException {
+    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();
-        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
-        assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
-        try {
-            requestHeaders.put("G", Arrays.asList("h"));
-            fail("Modified an unmodifiable view.");
-        } catch (UnsupportedOperationException expected) {
-        }
-        try {
-            requestHeaders.get("D").add("i");
-            fail("Modified an unmodifiable view.");
-        } catch (UnsupportedOperationException expected) {
-        }
-        try {
-            urlConnection.setRequestProperty(null, "j");
-            fail();
-        } catch (NullPointerException expected) {
-        }
-        try {
-            urlConnection.addRequestProperty(null, "k");
-            fail();
-        } catch (NullPointerException expected) {
-        }
-        urlConnection.setRequestProperty("NullValue", null); // should fail silently!
-        assertNull(urlConnection.getRequestProperty("NullValue"));
-        urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
-        assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
-
-        urlConnection.getResponseCode();
-        RecordedRequest request = server.takeRequest();
-        assertContains(request.getHeaders(), "D: e");
-        assertContains(request.getHeaders(), "D: f");
-        assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
-        assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
-        assertContainsNoneMatching(request.getHeaders(), "G:.*");
-        assertContainsNoneMatching(request.getHeaders(), "null:.*");
-
-        try {
-            urlConnection.addRequestProperty("N", "o");
-            fail("Set header after connect");
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            urlConnection.setRequestProperty("P", "q");
-            fail("Set header after connect");
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            urlConnection.getRequestProperties();
-            fail();
-        } catch (IllegalStateException expected) {
-        }
+    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();
+    assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
+    assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
+    try {
+      requestHeaders.put("G", Arrays.asList("h"));
+      fail("Modified an unmodifiable view.");
+    } catch (UnsupportedOperationException expected) {
     }
-
-    @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"));
+    try {
+      requestHeaders.get("D").add("i");
+      fail("Modified an unmodifiable view.");
+    } catch (UnsupportedOperationException expected) {
     }
-
-    @Test public void responseHeaders() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse()
-                .setStatus("HTTP/1.0 200 Fantastic")
-                .addHeader("A: c")
-                .addHeader("B: d")
-                .addHeader("A: e")
-                .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();
-        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")));
-        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) {
-        }
-        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));
+    try {
+      urlConnection.setRequestProperty(null, "j");
+      fail();
+    } catch (NullPointerException expected) {
     }
-
-    @Test public void serverSendsInvalidResponseHeaders() throws Exception {
-        server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK"));
-        server.play();
-
-        HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-        try {
-            urlConnection.getResponseCode();
-            fail();
-        } catch (IOException expected) {
-        }
+    try {
+      urlConnection.addRequestProperty(null, "k");
+      fail();
+    } catch (NullPointerException expected) {
     }
+    urlConnection.setRequestProperty("NullValue", null); // should fail silently!
+    assertNull(urlConnection.getRequestProperty("NullValue"));
+    urlConnection.addRequestProperty("AnotherNullValue", null);  // should fail silently!
+    assertNull(urlConnection.getRequestProperty("AnotherNullValue"));
 
-    @Test public void serverSendsInvalidCodeTooLarge() throws Exception {
-        server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
-        server.play();
+    urlConnection.getResponseCode();
+    RecordedRequest request = server.takeRequest();
+    assertContains(request.getHeaders(), "D: e");
+    assertContains(request.getHeaders(), "D: f");
+    assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
+    assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
+    assertContainsNoneMatching(request.getHeaders(), "G:.*");
+    assertContainsNoneMatching(request.getHeaders(), "null:.*");
 
-        HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-        try {
-            urlConnection.getResponseCode();
-            fail();
-        } catch (IOException expected) {
-        }
+    try {
+      urlConnection.addRequestProperty("N", "o");
+      fail("Set header after connect");
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void serverSendsInvalidCodeNotANumber() throws Exception {
-        server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK"));
-        server.play();
-
-        HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-        try {
-            urlConnection.getResponseCode();
-            fail();
-        } catch (IOException expected) {
-        }
+    try {
+      urlConnection.setRequestProperty("P", "q");
+      fail("Set header after connect");
+    } catch (IllegalStateException expected) {
     }
-
-    @Test public void serverSendsUnnecessaryWhitespace() throws Exception {
-        server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK"));
-        server.play();
-
-        HttpURLConnection urlConnection = client.open(server.getUrl("/"));
-        try {
-            urlConnection.getResponseCode();
-            fail();
-        } catch (IOException expected) {
-        }
+    try {
+      urlConnection.getRequestProperties();
+      fail();
+    } catch (IllegalStateException expected) {
     }
+  }
 
-    @Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
-        server.play();
-        URL url = server.getUrl("/foo");
-        server.shutdown();
+  @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"));
+  }
 
-        HttpURLConnection connection = client.open(url);
-        try {
-            connection.connect();
-            fail();
-        } catch (IOException expected) {
-        }
-    }
+  @Test public void responseHeaders() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setStatus("HTTP/1.0 200 Fantastic")
+        .addHeader("A: c")
+        .addHeader("B: d")
+        .addHeader("A: e")
+        .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
+    server.play();
 
-    @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception {
-        testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH);
-    }
-
-    @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception {
-        testRequestBodySurvivesRetries(TransferKind.CHUNKED);
+    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();
+    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")));
+    try {
+      responseHeaders.put("N", Arrays.asList("o"));
+      fail("Modified an unmodifiable view.");
+    } catch (UnsupportedOperationException expected) {
     }
-
-    @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception {
-        testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM);
+    try {
+      responseHeaders.get("A").add("f");
+      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));
+  }
 
-    private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception {
-        server.enqueue(new MockResponse().setBody("abc"));
-        server.play();
+  @Test public void serverSendsInvalidResponseHeaders() throws Exception {
+    server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK"));
+    server.play();
 
-        // Use a misconfigured proxy to guarantee that the request is retried.
-        server2.play();
-        FakeProxySelector proxySelector = new FakeProxySelector();
-        proxySelector.proxies.add(server2.toProxyAddress());
-        client.setProxySelector(proxySelector);
-        server2.shutdown();
-
-        HttpURLConnection connection = client.open(server.getUrl("/def"));
-        connection.setDoOutput(true);
-        transferKind.setForRequest(connection, 4);
-        connection.getOutputStream().write("body".getBytes("UTF-8"));
-        assertContent("abc", connection);
-
-        assertEquals("body", server.takeRequest().getUtf8Body());
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    try {
+      urlConnection.getResponseCode();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void getErrorStreamOnSuccessfulRequest() throws Exception {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertNull(connection.getErrorStream());
-    }
+  @Test public void serverSendsInvalidCodeTooLarge() throws Exception {
+    server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
+    server.play();
 
-    @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
-        server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE));
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    try {
+      urlConnection.getResponseCode();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    // Check that if we don't read to the end of a response, the next request on the
-    // recycled connection doesn't get the unread tail of the first request's response.
-    // http://code.google.com/p/android/issues/detail?id=2939
-    @Test public void bug2939() throws Exception {
-        MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);
+  @Test public void serverSendsInvalidCodeNotANumber() throws Exception {
+    server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK"));
+    server.play();
 
-        server.enqueue(response);
-        server.enqueue(response);
-        server.play();
-
-        assertContent("ABCDE", client.open(server.getUrl("/")), 5);
-        assertContent("ABCDE", client.open(server.getUrl("/")), 5);
-    }
-
-    // Check that we recognize a few basic mime types by extension.
-    // http://code.google.com/p/android/issues/detail?id=10100
-    @Test public void bug10100() throws Exception {
-        assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
-        assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    try {
+      urlConnection.getResponseCode();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void connectionsArePooled() throws Exception {
-        MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
+  @Test public void serverSendsUnnecessaryWhitespace() throws Exception {
+    server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK"));
+    server.play();
 
-        server.enqueue(response);
-        server.enqueue(response);
-        server.enqueue(response);
-        server.play();
-
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux")));
-        assertEquals(1, server.takeRequest().getSequenceNumber());
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z")));
-        assertEquals(2, server.takeRequest().getSequenceNumber());
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    try {
+      urlConnection.getResponseCode();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void chunkedConnectionsArePooled() throws Exception {
-        MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
-
-        server.enqueue(response);
-        server.enqueue(response);
-        server.enqueue(response);
-        server.play();
+  @Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
+    server.play();
+    URL url = server.getUrl("/foo");
+    server.shutdown();
 
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux")));
-        assertEquals(1, server.takeRequest().getSequenceNumber());
-        assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z")));
-        assertEquals(2, server.takeRequest().getSequenceNumber());
+    HttpURLConnection connection = client.open(url);
+    try {
+      connection.connect();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void serverClosesSocket() throws Exception {
-        testServerClosesOutput(DISCONNECT_AT_END);
-    }
+  @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception {
+    testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH);
+  }
 
-    @Test public void serverShutdownInput() throws Exception {
-        testServerClosesOutput(SHUTDOWN_INPUT_AT_END);
-    }
+  @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception {
+    testRequestBodySurvivesRetries(TransferKind.CHUNKED);
+  }
 
-    @Test public void serverShutdownOutput() throws Exception {
-        testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
-    }
+  @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception {
+    testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM);
+  }
 
-    private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception {
-        server.enqueue(new MockResponse()
-                .setBody("This connection won't pool properly")
-                .setSocketPolicy(socketPolicy));
-        MockResponse responseAfter = new MockResponse()
-                .setBody("This comes after a busted connection");
-        server.enqueue(responseAfter);
-        server.enqueue(responseAfter); // Enqueue 2x because the broken connection may be reused.
-        server.play();
+  private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
 
-        HttpURLConnection connection1 = client.open(server.getUrl("/a"));
-        connection1.setReadTimeout(100);
-        assertContent("This connection won't pool properly", connection1);
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        HttpURLConnection connection2 = client.open(server.getUrl("/b"));
-        connection2.setReadTimeout(100);
-        assertContent("This comes after a busted connection", connection2);
+    // Use a misconfigured proxy to guarantee that the request is retried.
+    server2.play();
+    FakeProxySelector proxySelector = new FakeProxySelector();
+    proxySelector.proxies.add(server2.toProxyAddress());
+    client.setProxySelector(proxySelector);
+    server2.shutdown();
 
-        // Check that a fresh connection was created, either immediately or after attempting reuse.
-        RecordedRequest requestAfter = server.takeRequest();
-        if (server.getRequestCount() == 3) {
-            requestAfter = server.takeRequest(); // The failure consumed a response.
-        }
-        // sequence number 0 means the HTTP socket connection was not reused
-        assertEquals(0, requestAfter.getSequenceNumber());
-    }
+    HttpURLConnection connection = client.open(server.getUrl("/def"));
+    connection.setDoOutput(true);
+    transferKind.setForRequest(connection, 4);
+    connection.getOutputStream().write("body".getBytes("UTF-8"));
+    assertContent("abc", connection);
 
-    enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }
+    assertEquals("body", server.takeRequest().getUtf8Body());
+  }
 
-    @Test public void chunkedUpload_byteByByte() throws Exception {
-        doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
-    }
+  @Test public void getErrorStreamOnSuccessfulRequest() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertNull(connection.getErrorStream());
+  }
 
-    @Test public void chunkedUpload_smallBuffers() throws Exception {
-        doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
-    }
+  @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
+    server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE));
+  }
 
-    @Test public void chunkedUpload_largeBuffers() throws Exception {
-        doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
-    }
+  // Check that if we don't read to the end of a response, the next request on the
+  // recycled connection doesn't get the unread tail of the first request's response.
+  // http://code.google.com/p/android/issues/detail?id=2939
+  @Test public void bug2939() throws Exception {
+    MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);
 
-    @Test public void fixedLengthUpload_byteByByte() throws Exception {
-        doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
-    }
+    server.enqueue(response);
+    server.enqueue(response);
+    server.play();
 
-    @Test public void fixedLengthUpload_smallBuffers() throws Exception {
-        doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
-    }
+    assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+    assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+  }
 
-    @Test public void fixedLengthUpload_largeBuffers() throws Exception {
-        doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
-    }
+  // Check that we recognize a few basic mime types by extension.
+  // http://code.google.com/p/android/issues/detail?id=10100
+  @Test public void bug10100() throws Exception {
+    assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
+    assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
+  }
 
-    private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
-        int n = 512*1024;
-        server.setBodyLimit(0);
-        server.enqueue(new MockResponse());
-        server.play();
+  @Test public void connectionsArePooled() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
 
-        HttpURLConnection conn = client.open(server.getUrl("/"));
-        conn.setDoOutput(true);
-        conn.setRequestMethod("POST");
-        if (uploadKind == TransferKind.CHUNKED) {
-            conn.setChunkedStreamingMode(-1);
-        } else {
-            conn.setFixedLengthStreamingMode(n);
-        }
-        OutputStream out = conn.getOutputStream();
-        if (writeKind == WriteKind.BYTE_BY_BYTE) {
-            for (int i = 0; i < n; ++i) {
-                out.write('x');
-            }
-        } else {
-            byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024];
-            Arrays.fill(buf, (byte) 'x');
-            for (int i = 0; i < n; i += buf.length) {
-                out.write(buf, 0, Math.min(buf.length, n - i));
-            }
-        }
-        out.close();
-        assertEquals(200, conn.getResponseCode());
-        RecordedRequest request = server.takeRequest();
-        assertEquals(n, request.getBodySize());
-        if (uploadKind == TransferKind.CHUNKED) {
-            assertTrue(request.getChunkSizes().size() > 0);
-        } else {
-            assertTrue(request.getChunkSizes().isEmpty());
-        }
-    }
+    server.enqueue(response);
+    server.enqueue(response);
+    server.enqueue(response);
+    server.play();
 
-    @Test public void getResponseCodeNoResponseBody() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("abc: def"));
-        server.play();
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux")));
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z")));
+    assertEquals(2, server.takeRequest().getSequenceNumber());
+  }
 
-        URL url = server.getUrl("/");
-        HttpURLConnection conn = client.open(url);
-        conn.setDoInput(false);
-        assertEquals("def", conn.getHeaderField("abc"));
-        assertEquals(200, conn.getResponseCode());
-        try {
-            conn.getInputStream();
-            fail();
-        } catch (ProtocolException expected) {
-        }
-    }
-
-    @Test public void connectViaHttps() throws Exception {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
-        server.play();
+  @Test public void chunkedConnectionsArePooled() throws Exception {
+    MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
 
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    server.enqueue(response);
+    server.enqueue(response);
+    server.enqueue(response);
+    server.play();
 
-        assertContent("this response comes via HTTPS", connection);
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux")));
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+    assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z")));
+    assertEquals(2, server.takeRequest().getSequenceNumber());
+  }
 
-        RecordedRequest request = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
-    }
+  @Test public void serverClosesSocket() throws Exception {
+    testServerClosesOutput(DISCONNECT_AT_END);
+  }
 
-    @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
-        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
-        server.play();
+  @Test public void serverShutdownInput() throws Exception {
+    testServerClosesOutput(SHUTDOWN_INPUT_AT_END);
+  }
 
-        // The pool will only reuse sockets if the SSL socket factories are the same.
-        SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
-        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+  @Test public void serverShutdownOutput() throws Exception {
+    testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
+  }
 
-        client.setSSLSocketFactory(clientSocketFactory);
-        client.setHostnameVerifier(hostnameVerifier);
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertContent("this response comes via HTTPS", connection);
+  private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception {
+    server.enqueue(new MockResponse().setBody("This connection won't pool properly")
+        .setSocketPolicy(socketPolicy));
+    MockResponse responseAfter = new MockResponse().setBody("This comes after a busted connection");
+    server.enqueue(responseAfter);
+    server.enqueue(responseAfter); // Enqueue 2x because the broken connection may be reused.
+    server.play();
 
-        connection = client.open(server.getUrl("/"));
-        assertContent("another response via HTTPS", connection);
+    HttpURLConnection connection1 = client.open(server.getUrl("/a"));
+    connection1.setReadTimeout(100);
+    assertContent("This connection won't pool properly", connection1);
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    HttpURLConnection connection2 = client.open(server.getUrl("/b"));
+    connection2.setReadTimeout(100);
+    assertContent("This comes after a busted connection", connection2);
 
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertEquals(1, server.takeRequest().getSequenceNumber());
+    // Check that a fresh connection was created, either immediately or after attempting reuse.
+    RecordedRequest requestAfter = server.takeRequest();
+    if (server.getRequestCount() == 3) {
+      requestAfter = server.takeRequest(); // The failure consumed a response.
     }
+    // sequence number 0 means the HTTP socket connection was not reused
+    assertEquals(0, requestAfter.getSequenceNumber());
+  }
 
-    @Test public void connectViaHttpsReusingConnectionsDifferentFactories()
-            throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
-        server.enqueue(new MockResponse().setBody("another response via HTTPS"));
-        server.play();
+  enum WriteKind {BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS}
 
-        // install a custom SSL socket factory so the server can be authorized
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection1 = client.open(server.getUrl("/"));
-        assertContent("this response comes via HTTPS", connection1);
+  @Test public void chunkedUpload_byteByByte() throws Exception {
+    doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
+  }
 
-        client.setSSLSocketFactory(null);
-        HttpURLConnection connection2 = client.open(server.getUrl("/"));
-        try {
-            readAscii(connection2.getInputStream(), Integer.MAX_VALUE);
-            fail("without an SSL socket factory, the connection should fail");
-        } catch (SSLException expected) {
-        }
-    }
+  @Test public void chunkedUpload_smallBuffers() throws Exception {
+    doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
+  }
 
-    @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
-        server.enqueue(new MockResponse().setBody("this response comes via SSL"));
-        server.play();
+  @Test public void chunkedUpload_largeBuffers() throws Exception {
+    doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
+  }
 
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(server.getUrl("/foo"));
+  @Test public void fixedLengthUpload_byteByByte() throws Exception {
+    doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
+  }
 
-        assertContent("this response comes via SSL", connection);
+  @Test public void fixedLengthUpload_smallBuffers() throws Exception {
+    doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
+  }
 
-        RecordedRequest request = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
-    }
+  @Test public void fixedLengthUpload_largeBuffers() throws Exception {
+    doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
+  }
 
-    /**
-     * Verify that we don't retry connections on certificate verification errors.
-     *
-     * http://code.google.com/p/android/issues/detail?id=13178
-     */
-    @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse()); // unused
-        server.play();
+  private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
+    int n = 512 * 1024;
+    server.setBodyLimit(0);
+    server.enqueue(new MockResponse());
+    server.play();
 
-        HttpURLConnection connection = client.open(server.getUrl("/foo"));
-        try {
-            connection.getInputStream();
-            fail();
-        } catch (SSLHandshakeException expected) {
-            assertTrue(expected.getCause() instanceof CertificateException);
-        }
-        assertEquals(0, server.getRequestCount());
-    }
-
-    @Test public void connectViaProxyUsingProxyArg() throws Exception {
-        testConnectViaProxy(ProxyConfig.CREATE_ARG);
+    HttpURLConnection conn = client.open(server.getUrl("/"));
+    conn.setDoOutput(true);
+    conn.setRequestMethod("POST");
+    if (uploadKind == TransferKind.CHUNKED) {
+      conn.setChunkedStreamingMode(-1);
+    } else {
+      conn.setFixedLengthStreamingMode(n);
     }
-
-    @Test public void connectViaProxyUsingProxySystemProperty() throws Exception {
-        testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+    OutputStream out = conn.getOutputStream();
+    if (writeKind == WriteKind.BYTE_BY_BYTE) {
+      for (int i = 0; i < n; ++i) {
+        out.write('x');
+      }
+    } else {
+      byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64 * 1024];
+      Arrays.fill(buf, (byte) 'x');
+      for (int i = 0; i < n; i += buf.length) {
+        out.write(buf, 0, Math.min(buf.length, n - i));
+      }
     }
-
-    @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception {
-        testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+    out.close();
+    assertEquals(200, conn.getResponseCode());
+    RecordedRequest request = server.takeRequest();
+    assertEquals(n, request.getBodySize());
+    if (uploadKind == TransferKind.CHUNKED) {
+      assertTrue(request.getChunkSizes().size() > 0);
+    } else {
+      assertTrue(request.getChunkSizes().isEmpty());
     }
+  }
 
-    private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
-        MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
-        server.enqueue(mockResponse);
-        server.play();
-
-        URL url = new URL("http://android.com/foo");
-        HttpURLConnection connection = proxyConfig.connect(server, client, url);
-        assertContent("this response comes via a proxy", connection);
+  @Test public void getResponseCodeNoResponseBody() throws Exception {
+    server.enqueue(new MockResponse().addHeader("abc: def"));
+    server.play();
 
-        RecordedRequest request = server.takeRequest();
-        assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
-        assertContains(request.getHeaders(), "Host: android.com");
+    URL url = server.getUrl("/");
+    HttpURLConnection conn = client.open(url);
+    conn.setDoInput(false);
+    assertEquals("def", conn.getHeaderField("abc"));
+    assertEquals(200, conn.getResponseCode());
+    try {
+      conn.getInputStream();
+      fail();
+    } catch (ProtocolException expected) {
     }
+  }
 
-    @Test public void contentDisagreesWithContentLengthHeader() throws IOException {
-        server.enqueue(new MockResponse()
-                .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
-                .clearHeaders()
-                .addHeader("Content-Length: 3"));
-        server.play();
+  @Test public void connectViaHttps() throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+    server.play();
 
-        assertContent("abc", client.open(server.getUrl("/")));
-    }
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(server.getUrl("/foo"));
 
-    @Test public void contentDisagreesWithChunkedHeader() throws IOException {
-        MockResponse mockResponse = new MockResponse();
-        mockResponse.setChunkedBody("abc", 3);
-        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-        bytesOut.write(mockResponse.getBody());
-        bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8"));
-        mockResponse.setBody(bytesOut.toByteArray());
-        mockResponse.clearHeaders();
-        mockResponse.addHeader("Transfer-encoding: chunked");
+    assertContent("this response comes via HTTPS", connection);
 
-        server.enqueue(mockResponse);
-        server.play();
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+  }
 
-        assertContent("abc", client.open(server.getUrl("/")));
-    }
+  @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+    server.enqueue(new MockResponse().setBody("another response via HTTPS"));
+    server.play();
 
-    @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
-        testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
-    }
+    // The pool will only reuse sockets if the SSL socket factories are the same.
+    SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
-        // https should not use http proxy
-        testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
-    }
+    client.setSSLSocketFactory(clientSocketFactory);
+    client.setHostnameVerifier(hostnameVerifier);
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertContent("this response comes via HTTPS", connection);
 
-    private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
-        server.play();
+    connection = client.open(server.getUrl("/"));
+    assertContent("another response via HTTPS", connection);
 
-        URL url = server.getUrl("/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
 
-        assertContent("this response comes via HTTPS", connection);
+  @Test public void connectViaHttpsReusingConnectionsDifferentFactories()
+      throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+    server.enqueue(new MockResponse().setBody("another response via HTTPS"));
+    server.play();
 
-        RecordedRequest request = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
-    }
+    // install a custom SSL socket factory so the server can be authorized
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    assertContent("this response comes via HTTPS", connection1);
 
-    @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
-        testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
+    client.setSSLSocketFactory(null);
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    try {
+      readAscii(connection2.getInputStream(), Integer.MAX_VALUE);
+      fail("without an SSL socket factory, the connection should fail");
+    } catch (SSLException expected) {
     }
+  }
 
-    /**
-     * We weren't honoring all of the appropriate proxy system properties when
-     * connecting via HTTPS. http://b/3097518
-     */
-    @Test public void connectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception {
-        testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY);
-    }
-
-    @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
-        testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
-    }
-
-    /**
-     * We were verifying the wrong hostname when connecting to an HTTPS site
-     * through a proxy. http://b/3097277
-     */
-    private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
-        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+  @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+    server.enqueue(new MockResponse().setBody("this response comes via SSL"));
+    server.play();
 
-        server.useHttps(sslContext.getSocketFactory(), true);
-        server.enqueue(new MockResponse()
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .clearHeaders());
-        server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
-        server.play();
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(server.getUrl("/foo"));
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(hostnameVerifier);
-        HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    assertContent("this response comes via SSL", connection);
 
-        assertContent("this response comes via a secure proxy", connection);
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+  }
 
-        RecordedRequest connect = server.takeRequest();
-        assertEquals("Connect line failure on proxy",
-                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
-        assertContains(connect.getHeaders(), "Host: android.com");
+  /**
+   * Verify that we don't retry connections on certificate verification errors.
+   *
+   * http://code.google.com/p/android/issues/detail?id=13178
+   */
+  @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse()); // unused
+    server.play();
 
-        RecordedRequest get = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
-        assertContains(get.getHeaders(), "Host: android.com");
-        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (SSLHandshakeException expected) {
+      assertTrue(expected.getCause() instanceof CertificateException);
     }
+    assertEquals(0, server.getRequestCount());
+  }
 
-    /**
-     * Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912
-     */
-    @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache()
-            throws Exception {
-        initResponseCache();
+  @Test public void connectViaProxyUsingProxyArg() throws Exception {
+    testConnectViaProxy(ProxyConfig.CREATE_ARG);
+  }
 
-        server.useHttps(sslContext.getSocketFactory(), true);
-        MockResponse response = new MockResponse() // Key to reproducing b/6754912
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .setBody("bogus proxy connect response content");
+  @Test public void connectViaProxyUsingProxySystemProperty() throws Exception {
+    testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+  }
 
-        // Enqueue a pair of responses for every IP address held by localhost, because the
-        // route selector will try each in sequence.
-        // TODO: use the fake Dns implementation instead of a loop
-        for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
-            server.enqueue(response); // For the first TLS tolerant connection
-            server.enqueue(response); // For the backwards-compatible SSLv3 retry
-        }
-        server.play();
-        client.setProxy(server.toProxyAddress());
+  @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception {
+    testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+  }
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        HttpURLConnection connection = client.open(url);
+  private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
+    MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
+    server.enqueue(mockResponse);
+    server.play();
 
-        try {
-            connection.getResponseCode();
-            fail();
-        } catch (IOException expected) {
-            // Thrown when the connect causes SSLSocket.startHandshake() to throw
-            // when it sees the "bogus proxy connect response content"
-            // instead of a ServerHello handshake message.
-        }
+    URL url = new URL("http://android.com/foo");
+    HttpURLConnection connection = proxyConfig.connect(server, client, url);
+    assertContent("this response comes via a proxy", connection);
 
-        RecordedRequest connect = server.takeRequest();
-        assertEquals("Connect line failure on proxy",
-                "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
-        assertContains(connect.getHeaders(), "Host: android.com");
-    }
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
+    assertContains(request.getHeaders(), "Host: android.com");
+  }
 
-    private void initResponseCache() throws IOException {
-        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);
-    }
+  @Test public void contentDisagreesWithContentLengthHeader() throws IOException {
+    server.enqueue(new MockResponse().setBody("abc\r\nYOU SHOULD NOT SEE THIS")
+        .clearHeaders()
+        .addHeader("Content-Length: 3"));
+    server.play();
 
-    /**
-     * Test which headers are sent unencrypted to the HTTP proxy.
-     */
-    @Test public void proxyConnectIncludesProxyHeadersOnly()
-            throws IOException, InterruptedException {
-        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+    assertContent("abc", client.open(server.getUrl("/")));
+  }
 
-        server.useHttps(sslContext.getSocketFactory(), true);
-        server.enqueue(new MockResponse()
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .clearHeaders());
-        server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
-        server.play();
-        client.setProxy(server.toProxyAddress());
+  @Test public void contentDisagreesWithChunkedHeader() throws IOException {
+    MockResponse mockResponse = new MockResponse();
+    mockResponse.setChunkedBody("abc", 3);
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    bytesOut.write(mockResponse.getBody());
+    bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8"));
+    mockResponse.setBody(bytesOut.toByteArray());
+    mockResponse.clearHeaders();
+    mockResponse.addHeader("Transfer-encoding: chunked");
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(hostnameVerifier);
-        HttpURLConnection connection = client.open(url);
-        connection.addRequestProperty("Private", "Secret");
-        connection.addRequestProperty("Proxy-Authorization", "bar");
-        connection.addRequestProperty("User-Agent", "baz");
-        assertContent("encrypted response from the origin server", connection);
+    server.enqueue(mockResponse);
+    server.play();
 
-        RecordedRequest connect = server.takeRequest();
-        assertContainsNoneMatching(connect.getHeaders(), "Private.*");
-        assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
-        assertContains(connect.getHeaders(), "User-Agent: baz");
-        assertContains(connect.getHeaders(), "Host: android.com");
-        assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
+    assertContent("abc", client.open(server.getUrl("/")));
+  }
 
-        RecordedRequest get = server.takeRequest();
-        assertContains(get.getHeaders(), "Private: Secret");
-        assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
-    }
+  @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
+    testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
+  }
 
-    @Test public void proxyAuthenticateOnConnect() throws Exception {
-        Authenticator.setDefault(new RecordingAuthenticator());
-        server.useHttps(sslContext.getSocketFactory(), true);
-        server.enqueue(new MockResponse()
-                .setResponseCode(407)
-                .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
-        server.enqueue(new MockResponse()
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .clearHeaders());
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-        client.setProxy(server.toProxyAddress());
+  @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
+    // https should not use http proxy
+    testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+  }
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(url);
-        assertContent("A", connection);
+  private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+    server.play();
 
-        RecordedRequest connect1 = server.takeRequest();
-        assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
-        assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
+    URL url = server.getUrl("/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = proxyConfig.connect(server, client, url);
 
-        RecordedRequest connect2 = server.takeRequest();
-        assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
-        assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " + BASE_64_CREDENTIALS);
+    assertContent("this response comes via HTTPS", connection);
 
-        RecordedRequest get = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
-        assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
-    }
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+  }
 
-    // Don't disconnect after building a tunnel with CONNECT
-    // http://code.google.com/p/android/issues/detail?id=37221
-    @Test public void proxyWithConnectionClose() throws IOException {
-        server.useHttps(sslContext.getSocketFactory(), true);
-        server.enqueue(new MockResponse()
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .clearHeaders());
-        server.enqueue(new MockResponse().setBody("this response comes via a proxy"));
-        server.play();
-        client.setProxy(server.toProxyAddress());
+  @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
+    testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
+  }
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(url);
-        connection.setRequestProperty("Connection", "close");
+  /**
+   * We weren't honoring all of the appropriate proxy system properties when
+   * connecting via HTTPS. http://b/3097518
+   */
+  @Test public void connectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception {
+    testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+  }
 
-        assertContent("this response comes via a proxy", connection);
-    }
+  @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
+    testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
+  }
 
-    @Test public void proxyWithConnectionReuse() throws IOException {
-        SSLSocketFactory socketFactory = sslContext.getSocketFactory();
-        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+  /**
+   * We were verifying the wrong hostname when connecting to an HTTPS site
+   * through a proxy. http://b/3097277
+   */
+  private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-        server.useHttps(socketFactory, true);
-        server.enqueue(new MockResponse()
-                .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
-                .clearHeaders());
-        server.enqueue(new MockResponse().setBody("response 1"));
-        server.enqueue(new MockResponse().setBody("response 2"));
-        server.play();
-        client.setProxy(server.toProxyAddress());
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(
+        new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
+    server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
+    server.play();
 
-        URL url = new URL("https://android.com/foo");
-        client.setSSLSocketFactory(socketFactory);
-        client.setHostnameVerifier(hostnameVerifier);
-        assertContent("response 1", client.open(url));
-        assertContent("response 2", client.open(url));
-    }
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(hostnameVerifier);
+    HttpURLConnection connection = proxyConfig.connect(server, client, url);
 
-    @Test public void disconnectedConnection() throws IOException {
-        server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
-        server.play();
+    assertContent("this response comes via a secure proxy", connection);
 
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        InputStream in = connection.getInputStream();
-        assertEquals('A', (char) in.read());
-        connection.disconnect();
-        try {
-            in.read();
-            fail("Expected a connection closed exception");
-        } catch (IOException expected) {
-        }
-    }
-
-    @Test public void disconnectBeforeConnect() throws IOException {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.disconnect();
-
-        assertContent("A", connection);
-        assertEquals(200, connection.getResponseCode());
-    }
-
-    @SuppressWarnings("deprecation")
-    @Test public void defaultRequestProperty() throws Exception {
-        URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A");
-        assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty"));
-    }
-
-    /**
-     * Reads {@code count} characters from the stream. If the stream is
-     * exhausted before {@code count} characters can be read, the remaining
-     * characters are returned and the stream is closed.
-     */
-    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();
-    }
-
-    @Test public void markAndResetWithContentLengthHeader() throws IOException {
-        testMarkAndReset(TransferKind.FIXED_LENGTH);
-    }
-
-    @Test public void markAndResetWithChunkedEncoding() throws IOException {
-        testMarkAndReset(TransferKind.CHUNKED);
-    }
-
-    @Test public void markAndResetWithNoLengthHeaders() throws IOException {
-        testMarkAndReset(TransferKind.END_OF_STREAM);
-    }
-
-    private void testMarkAndReset(TransferKind transferKind) throws IOException {
-        MockResponse response = new MockResponse();
-        transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
-        server.enqueue(response);
-        server.enqueue(response);
-        server.play();
-
-        InputStream in = client.open(server.getUrl("/")).getInputStream();
-        assertFalse("This implementation claims to support mark().", in.markSupported());
-        in.mark(5);
-        assertEquals("ABCDE", readAscii(in, 5));
-        try {
-            in.reset();
-            fail();
-        } catch (IOException expected) {
-        }
-        assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
-        assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/")));
-    }
-
-    /**
-     * We've had a bug where we forget the HTTP response when we see response
-     * code 401. This causes a new HTTP request to be issued for every call into
-     * the URLConnection.
-     */
-    @Test public void unauthorizedResponseHandling() throws IOException {
-        MockResponse response = new MockResponse()
-                .addHeader("WWW-Authenticate: challenge")
-                .setResponseCode(401) // UNAUTHORIZED
-                .setBody("Unauthorized");
-        server.enqueue(response);
-        server.enqueue(response);
-        server.enqueue(response);
-        server.play();
-
-        URL url = server.getUrl("/");
-        HttpURLConnection conn = client.open(url);
-
-        assertEquals(401, conn.getResponseCode());
-        assertEquals(401, conn.getResponseCode());
-        assertEquals(401, conn.getResponseCode());
-        assertEquals(1, server.getRequestCount());
-    }
-
-    @Test public void nonHexChunkSize() throws IOException {
-        server.enqueue(new MockResponse()
-                .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
-                .clearHeaders()
-                .addHeader("Transfer-encoding: chunked"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        try {
-            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
-            fail();
-        } catch (IOException e) {
-        }
-    }
-
-    @Test public void missingChunkBody() throws IOException {
-        server.enqueue(new MockResponse()
-                .setBody("5")
-                .clearHeaders()
-                .addHeader("Transfer-encoding: chunked")
-                .setSocketPolicy(DISCONNECT_AT_END));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        try {
-            readAscii(connection.getInputStream(), Integer.MAX_VALUE);
-            fail();
-        } catch (IOException e) {
-        }
-    }
-
-    /**
-     * This test checks whether connections are gzipped by default. This
-     * behavior in not required by the API, so a failure of this test does not
-     * imply a bug in the implementation.
-     */
-    @Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse()
-                .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
-                .addHeader("Content-Encoding: gzip"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        assertNull(connection.getContentEncoding());
-
-        RecordedRequest request = server.takeRequest();
-        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
-    }
-
-    @Test public void clientConfiguredGzipContentEncoding() throws Exception {
-        server.enqueue(new MockResponse()
-                .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
-                .addHeader("Content-Encoding: gzip"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        connection.addRequestProperty("Accept-Encoding", "gzip");
-        InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
-        assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
-
-        RecordedRequest request = server.takeRequest();
-        assertContains(request.getHeaders(), "Accept-Encoding: gzip");
-    }
-
-    @Test public void gzipAndConnectionReuseWithFixedLength() throws Exception {
-        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, false);
-    }
-
-    @Test public void gzipAndConnectionReuseWithChunkedEncoding() throws Exception {
-        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, false);
-    }
-
-    @Test public void gzipAndConnectionReuseWithFixedLengthAndTls() throws Exception {
-        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, true);
-    }
-
-    @Test public void gzipAndConnectionReuseWithChunkedEncodingAndTls() throws Exception {
-        testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, true);
-    }
-
-    @Test public void clientConfiguredCustomContentEncoding() throws Exception {
-        server.enqueue(new MockResponse()
-                .setBody("ABCDE")
-                .addHeader("Content-Encoding: custom"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        connection.addRequestProperty("Accept-Encoding", "custom");
-        assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-
-        RecordedRequest request = server.takeRequest();
-        assertContains(request.getHeaders(), "Accept-Encoding: custom");
-    }
-
-    /**
-     * Test a bug where gzip input streams weren't exhausting the input stream,
-     * which corrupted the request that followed or prevented connection reuse.
-     * http://code.google.com/p/android/issues/detail?id=7059
-     * http://code.google.com/p/android/issues/detail?id=38817
-     */
-    private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
-            TransferKind transferKind, boolean tls) throws Exception {
-        if (tls) {
-            SSLSocketFactory socketFactory = sslContext.getSocketFactory();
-            RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
-            server.useHttps(socketFactory, false);
-            client.setSSLSocketFactory(socketFactory);
-            client.setHostnameVerifier(hostnameVerifier);
-        }
-
-        MockResponse responseOne = new MockResponse();
-        responseOne.addHeader("Content-Encoding: gzip");
-        transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
-        server.enqueue(responseOne);
-        MockResponse responseTwo = new MockResponse();
-        transferKind.setBody(responseTwo, "two (identity)", 5);
-        server.enqueue(responseTwo);
-        server.play();
-
-        HttpURLConnection connection1 = client.open(server.getUrl("/"));
-        connection1.addRequestProperty("Accept-Encoding", "gzip");
-        InputStream gunzippedIn = new GZIPInputStream(connection1.getInputStream());
-        assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-
-        HttpURLConnection connection2 = client.open(server.getUrl("/"));
-        assertEquals("two (identity)", readAscii(connection2.getInputStream(), Integer.MAX_VALUE));
-        assertEquals(1, server.takeRequest().getSequenceNumber());
-    }
-
-    @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
-        testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED);
-    }
-
-    @Test public void earlyDisconnectDoesntHarmPoolingWithFixedLengthEncoding() throws Exception {
-        testEarlyDisconnectDoesntHarmPooling(TransferKind.FIXED_LENGTH);
-    }
-
-    private void testEarlyDisconnectDoesntHarmPooling(TransferKind transferKind) throws Exception {
-        MockResponse response1 = new MockResponse();
-        transferKind.setBody(response1, "ABCDEFGHIJK", 1024);
-        server.enqueue(response1);
-
-        MockResponse response2 = new MockResponse();
-        transferKind.setBody(response2, "LMNOPQRSTUV", 1024);
-        server.enqueue(response2);
-
-        server.play();
-
-        URLConnection connection1 = client.open(server.getUrl("/"));
-        InputStream in1 = connection1.getInputStream();
-        assertEquals("ABCDE", readAscii(in1, 5));
-        in1.close();
-
-        HttpURLConnection connection2 = client.open(server.getUrl("/"));
-        InputStream in2 = connection2.getInputStream();
-        assertEquals("LMNOP", readAscii(in2, 5));
-        in2.close();
-
-        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 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());
-
-        RecordedRequest request = server.takeRequest();
-        assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
-        assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
-    }
-
-    @Test public void authenticateWithFixedLengthStreaming() throws Exception {
-        testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
-    }
-
-    @Test public void authenticateWithChunkedStreaming() throws Exception {
-        testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
-    }
-
-    private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
-        MockResponse pleaseAuthenticate = new MockResponse()
-                .setResponseCode(401)
-                .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
-                .setBody("Please authenticate.");
-        server.enqueue(pleaseAuthenticate);
-        server.play();
-
-        Authenticator.setDefault(new RecordingAuthenticator());
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        byte[] requestBody = { 'A', 'B', 'C', 'D' };
-        if (streamingMode == StreamingMode.FIXED_LENGTH) {
-            connection.setFixedLengthStreamingMode(requestBody.length);
-        } else if (streamingMode == StreamingMode.CHUNKED) {
-            connection.setChunkedStreamingMode(0);
-        }
-        OutputStream outputStream = connection.getOutputStream();
-        outputStream.write(requestBody);
-        outputStream.close();
-        try {
-            connection.getInputStream();
-            fail();
-        } catch (HttpRetryException expected) {
-        }
-
-        // no authorization header for the request...
-        RecordedRequest request = server.takeRequest();
-        assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
-        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
-    }
-
-    @Test public void nonStandardAuthenticationScheme() throws Exception {
-        List<String> calls = authCallsForHeader("WWW-Authenticate: Foo");
-        assertEquals(Collections.<String>emptyList(), calls);
-    }
-
-    @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception {
-        List<String> calls = authCallsForHeader("WWW-Authenticate: Foo realm=\"Bar\"");
-        assertEquals(1, calls.size());
-        String call = calls.get(0);
-        assertTrue(call, call.contains("scheme=Foo"));
-        assertTrue(call, call.contains("prompt=Bar"));
-    }
-
-    // Digest auth is currently unsupported. Test that digest requests should fail reasonably.
-    // http://code.google.com/p/android/issues/detail?id=11140
-    @Test public void digestAuthentication() throws Exception {
-        List<String> calls = authCallsForHeader("WWW-Authenticate: Digest "
-                + "realm=\"testrealm@host.com\", qop=\"auth,auth-int\", "
-                + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
-                + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"");
-        assertEquals(1, calls.size());
-        String call = calls.get(0);
-        assertTrue(call, call.contains("scheme=Digest"));
-        assertTrue(call, call.contains("prompt=testrealm@host.com"));
-    }
-
-    @Test public void allAttributesSetInServerAuthenticationCallbacks() throws Exception {
-        List<String> calls = authCallsForHeader("WWW-Authenticate: Basic realm=\"Bar\"");
-        assertEquals(1, calls.size());
-        URL url = server.getUrl("/");
-        String call = calls.get(0);
-        assertTrue(call, call.contains("host=" + url.getHost()));
-        assertTrue(call, call.contains("port=" + url.getPort()));
-        assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
-        assertTrue(call, call.contains("url=" + url));
-        assertTrue(call, call.contains("type=" + Authenticator.RequestorType.SERVER));
-        assertTrue(call, call.contains("prompt=Bar"));
-        assertTrue(call, call.contains("protocol=http"));
-        assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI.
-    }
-
-    @Test public void allAttributesSetInProxyAuthenticationCallbacks() throws Exception {
-        List<String> calls = authCallsForHeader("Proxy-Authenticate: Basic realm=\"Bar\"");
-        assertEquals(1, calls.size());
-        URL url = server.getUrl("/");
-        String call = calls.get(0);
-        assertTrue(call, call.contains("host=" + url.getHost()));
-        assertTrue(call, call.contains("port=" + url.getPort()));
-        assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
-        assertTrue(call, call.contains("url=http://android.com"));
-        assertTrue(call, call.contains("type=" + Authenticator.RequestorType.PROXY));
-        assertTrue(call, call.contains("prompt=Bar"));
-        assertTrue(call, call.contains("protocol=http"));
-        assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI.
-    }
-
-    private List<String> authCallsForHeader(String authHeader) throws IOException {
-        boolean proxy = authHeader.startsWith("Proxy-");
-        int responseCode = proxy ? 407 : 401;
-        RecordingAuthenticator authenticator = new RecordingAuthenticator(null);
-        Authenticator.setDefault(authenticator);
-        MockResponse pleaseAuthenticate = new MockResponse()
-                .setResponseCode(responseCode)
-                .addHeader(authHeader)
-                .setBody("Please authenticate.");
-        server.enqueue(pleaseAuthenticate);
-        server.play();
-
-        HttpURLConnection connection;
-        if (proxy) {
-            client.setProxy(server.toProxyAddress());
-            connection = client.open(new URL("http://android.com"));
-        } else {
-            connection = client.open(server.getUrl("/"));
-        }
-        assertEquals(responseCode, connection.getResponseCode());
-        return authenticator.calls;
-    }
-
-    @Test public void setValidRequestMethod() throws Exception {
-        server.play();
-        assertValidRequestMethod("GET");
-        assertValidRequestMethod("DELETE");
-        assertValidRequestMethod("HEAD");
-        assertValidRequestMethod("OPTIONS");
-        assertValidRequestMethod("POST");
-        assertValidRequestMethod("PUT");
-        assertValidRequestMethod("TRACE");
-    }
-
-    private void assertValidRequestMethod(String requestMethod) throws Exception {
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setRequestMethod(requestMethod);
-        assertEquals(requestMethod, connection.getRequestMethod());
-    }
-
-    @Test public void setInvalidRequestMethodLowercase() throws Exception {
-        server.play();
-        assertInvalidRequestMethod("get");
-    }
-
-    @Test public void setInvalidRequestMethodConnect() throws Exception {
-        server.play();
-        assertInvalidRequestMethod("CONNECT");
-    }
-
-    private void assertInvalidRequestMethod(String requestMethod) throws Exception {
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        try {
-            connection.setRequestMethod(requestMethod);
-            fail();
-        } catch (ProtocolException expected) {
-        }
-    }
-
-    @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception {
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        try {
-            connection.setFixedLengthStreamingMode(-2);
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test public void canSetNegativeChunkedStreamingMode() throws Exception {
-        server.play();
-        HttpURLConnection 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("/"));
-        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        try {
-            connection.setFixedLengthStreamingMode(1);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        try {
-            connection.setChunkedStreamingMode(1);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setChunkedStreamingMode(1);
-        try {
-            connection.setFixedLengthStreamingMode(1);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception {
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setFixedLengthStreamingMode(1);
-        try {
-            connection.setChunkedStreamingMode(1);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    @Test public void secureFixedLengthStreaming() throws Exception {
-        testSecureStreamingPost(StreamingMode.FIXED_LENGTH);
-    }
-
-    @Test public void secureChunkedStreaming() throws Exception {
-        testSecureStreamingPost(StreamingMode.CHUNKED);
-    }
-
-    /**
-     * Users have reported problems using HTTPS with streaming request bodies.
-     * http://code.google.com/p/android/issues/detail?id=12860
-     */
-    private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("Success!"));
-        server.play();
-
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        byte[] requestBody = { 'A', 'B', 'C', 'D' };
-        if (streamingMode == StreamingMode.FIXED_LENGTH) {
-            connection.setFixedLengthStreamingMode(requestBody.length);
-        } else if (streamingMode == StreamingMode.CHUNKED) {
-            connection.setChunkedStreamingMode(0);
-        }
-        OutputStream outputStream = connection.getOutputStream();
-        outputStream.write(requestBody);
-        outputStream.close();
-        assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-
-        RecordedRequest request = server.takeRequest();
-        assertEquals("POST / HTTP/1.1", request.getRequestLine());
-        if (streamingMode == StreamingMode.FIXED_LENGTH) {
-            assertEquals(Collections.<Integer>emptyList(), request.getChunkSizes());
-        } else if (streamingMode == StreamingMode.CHUNKED) {
-            assertEquals(Arrays.asList(4), request.getChunkSizes());
-        }
-        assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
-    }
-
-    enum StreamingMode {
-        FIXED_LENGTH, CHUNKED
-    }
-
-    @Test public void authenticateWithPost() 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
-        server.enqueue(new MockResponse().setBody("Successful auth!"));
-        server.play();
-
-        Authenticator.setDefault(new RecordingAuthenticator());
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        byte[] requestBody = { 'A', 'B', 'C', 'D' };
-        OutputStream outputStream = connection.getOutputStream();
-        outputStream.write(requestBody);
-        outputStream.close();
-        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 include an authorization header
-        for (int i = 0; i < 3; i++) {
-            request = server.takeRequest();
-            assertEquals("POST / HTTP/1.1", request.getRequestLine());
-            assertContains(request.getHeaders(), "Authorization: Basic " + BASE_64_CREDENTIALS);
-            assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
-        }
-    }
-
-    @Test public void authenticateWithGet() 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
-        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));
-
-        // 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 " + BASE_64_CREDENTIALS);
-        }
-    }
-
-    @Test public void redirectedWithChunkedEncoding() throws Exception {
-        testRedirected(TransferKind.CHUNKED, true);
-    }
-
-    @Test public void redirectedWithContentLengthHeader() throws Exception {
-        testRedirected(TransferKind.FIXED_LENGTH, true);
-    }
-
-    @Test public void redirectedWithNoLengthHeaders() throws Exception {
-        testRedirected(TransferKind.END_OF_STREAM, false);
-    }
-
-    private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
-        MockResponse response = new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-                .addHeader("Location: /foo");
-        transferKind.setBody(response, "This page has moved!", 10);
-        server.enqueue(response);
-        server.enqueue(new MockResponse().setBody("This is the new location!"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This is the new location!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-
-        RecordedRequest first = server.takeRequest();
-        assertEquals("GET / HTTP/1.1", first.getRequestLine());
-        RecordedRequest retry = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
-        if (reuse) {
-            assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
-        }
-    }
-
-    @Test public void redirectedOnHttps() throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        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();
-
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This is the new location!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-
-        RecordedRequest first = server.takeRequest();
-        assertEquals("GET / HTTP/1.1", first.getRequestLine());
-        RecordedRequest retry = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
-        assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
-    }
-
-    @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-                .addHeader("Location: http://anyhost/foo")
-                .setBody("This page has moved!"));
-        server.play();
-
-        client.setSSLSocketFactory(sslContext.getSocketFactory());
-        client.setHostnameVerifier(new RecordingHostnameVerifier());
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This page has moved!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-    }
-
-    @Test public void notRedirectedFromHttpToHttps() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-                .addHeader("Location: https://anyhost/foo")
-                .setBody("This page has moved!"));
-        server.play();
-
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This page has moved!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-    }
-
-    @Test public void redirectToAnotherOriginServer() throws Exception {
-        MockWebServer server2 = new MockWebServer();
-        server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
-        server2.play();
-
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-                .addHeader("Location: " + server2.getUrl("/").toString())
-                .setBody("This page has moved!"));
-        server.enqueue(new MockResponse().setBody("This is the first server again!"));
-        server.play();
-
-        URLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This is the 2nd server!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        assertEquals(server2.getUrl("/"), connection.getURL());
-
-        // make sure the first server was careful to recycle the connection
-        assertEquals("This is the first server again!",
-                readAscii(client.open(server.getUrl("/")).getInputStream(), Integer.MAX_VALUE));
-
-        RecordedRequest first = server.takeRequest();
-        assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort());
-        RecordedRequest second = server2.takeRequest();
-        assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort());
-        RecordedRequest third = server.takeRequest();
-        assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
-
-        server2.shutdown();
-    }
-
-    @Test public void response300MultipleChoiceWithPost() throws Exception {
-        // Chrome doesn't follow the redirect, but Firefox and the RI both do
-        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE);
-    }
-
-    @Test public void response301MovedPermanentlyWithPost() throws Exception {
-        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM);
-    }
-
-    @Test public void response302MovedTemporarilyWithPost() throws Exception {
-        testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP);
-    }
-
-    @Test public void response303SeeOtherWithPost() throws Exception {
-        testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER);
-    }
-
-    private void testResponseRedirectedWithPost(int redirectCode) 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.setDoOutput(true);
-        byte[] requestBody = { 'A', 'B', 'C', 'D' };
-        OutputStream outputStream = connection.getOutputStream();
-        outputStream.write(requestBody);
-        outputStream.close();
-        assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        assertTrue(connection.getDoOutput());
-
-        RecordedRequest page1 = server.takeRequest();
-        assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
-        assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
-
-        RecordedRequest page2 = server.takeRequest();
-        assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
-    }
-
-    @Test public void response305UseProxy() throws Exception {
-        server.play();
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_USE_PROXY)
-                .addHeader("Location: " + server.getUrl("/"))
-                .setBody("This page has moved!"));
-        server.enqueue(new MockResponse().setBody("Proxy Response"));
-
-        HttpURLConnection 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));
-
-        RecordedRequest page1 = server.takeRequest();
-        assertEquals("GET /foo HTTP/1.1", page1.getRequestLine());
-        assertEquals(1, server.getRequestCount());
-    }
-
-    @Test public void httpsWithCustomTrustManager() throws Exception {
-        RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
-        RecordingTrustManager trustManager = new RecordingTrustManager();
-        SSLContext sc = SSLContext.getInstance("TLS");
-        sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
-
-        client.setHostnameVerifier(hostnameVerifier);
-        client.setSSLSocketFactory(sc.getSocketFactory());
-        server.useHttps(sslContext.getSocketFactory(), false);
-        server.enqueue(new MockResponse().setBody("ABC"));
-        server.enqueue(new MockResponse().setBody("DEF"));
-        server.enqueue(new MockResponse().setBody("GHI"));
-        server.play();
-
-        URL url = server.getUrl("/");
-        assertContent("ABC", client.open(url));
-        assertContent("DEF", client.open(url));
-        assertContent("GHI", client.open(url));
-
-        assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
-        assertEquals(Arrays.asList("checkServerTrusted [CN=" + hostName + " 1]"),
-                trustManager.calls);
-    }
-
-    @Test public void readTimeouts() throws IOException {
-        /*
-         * This relies on the fact that MockWebServer doesn't close the
-         * connection after a response has been sent. This causes the client to
-         * try to read more bytes than are sent, which results in a timeout.
-         */
-        MockResponse timeout = new MockResponse()
-                .setBody("ABC")
-                .clearHeaders()
-                .addHeader("Content-Length: 4");
-        server.enqueue(timeout);
-        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();
-        assertEquals('A', in.read());
-        assertEquals('B', in.read());
-        assertEquals('C', in.read());
-        try {
-            in.read(); // if Content-Length was accurate, this would return -1 immediately
-            fail();
-        } catch (SocketTimeoutException expected) {
-        }
-    }
-
-    @Test public void setChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
-        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());
-
-        RecordedRequest request = server.takeRequest();
-        assertEquals("ABC", new String(request.getBody(), "UTF-8"));
-    }
+    RecordedRequest connect = server.takeRequest();
+    assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1",
+        connect.getRequestLine());
+    assertContains(connect.getHeaders(), "Host: android.com");
 
-    @Test public void connectionCloseInRequest() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
-        server.enqueue(new MockResponse());
-        server.play();
+    RecordedRequest get = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+    assertContains(get.getHeaders(), "Host: android.com");
+    assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+  }
 
-        HttpURLConnection a = client.open(server.getUrl("/"));
-        a.setRequestProperty("Connection", "close");
-        assertEquals(200, a.getResponseCode());
+  /** Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 */
+  @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
+    initResponseCache();
 
-        HttpURLConnection b = client.open(server.getUrl("/"));
-        assertEquals(200, b.getResponseCode());
+    server.useHttps(sslContext.getSocketFactory(), true);
+    MockResponse response = new MockResponse() // Key to reproducing b/6754912
+        .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+        .setBody("bogus proxy connect response content");
 
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertEquals("When connection: close is used, each request should get its own connection",
-                0, server.takeRequest().getSequenceNumber());
+    // Enqueue a pair of responses for every IP address held by localhost, because the
+    // route selector will try each in sequence.
+    // TODO: use the fake Dns implementation instead of a loop
+    for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
+      server.enqueue(response); // For the first TLS tolerant connection
+      server.enqueue(response); // For the backwards-compatible SSLv3 retry
     }
+    server.play();
+    client.setProxy(server.toProxyAddress());
 
-    @Test public void connectionCloseInResponse() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse().addHeader("Connection: close"));
-        server.enqueue(new MockResponse());
-        server.play();
-
-        HttpURLConnection a = client.open(server.getUrl("/"));
-        assertEquals(200, a.getResponseCode());
-
-        HttpURLConnection b = client.open(server.getUrl("/"));
-        assertEquals(200, b.getResponseCode());
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    HttpURLConnection connection = client.open(url);
 
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertEquals("When connection: close is used, each request should get its own connection",
-                0, server.takeRequest().getSequenceNumber());
+    try {
+      connection.getResponseCode();
+      fail();
+    } catch (IOException expected) {
+      // Thrown when the connect causes SSLSocket.startHandshake() to throw
+      // when it sees the "bogus proxy connect response content"
+      // instead of a ServerHello handshake message.
     }
 
-    @Test public void connectionCloseWithRedirect() throws IOException, InterruptedException {
-        MockResponse response = new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-                .addHeader("Location: /foo")
-                .addHeader("Connection: close");
-        server.enqueue(response);
-        server.enqueue(new MockResponse().setBody("This is the new location!"));
-        server.play();
+    RecordedRequest connect = server.takeRequest();
+    assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1",
+        connect.getRequestLine());
+    assertContains(connect.getHeaders(), "Host: android.com");
+  }
 
-        URLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This is the new location!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+  private void initResponseCache() throws IOException {
+    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);
+  }
 
-        assertEquals(0, server.takeRequest().getSequenceNumber());
-        assertEquals("When connection: close is used, each request should get its own connection",
-                0, server.takeRequest().getSequenceNumber());
-    }
+  /** Test which headers are sent unencrypted to the HTTP proxy. */
+  @Test public void proxyConnectIncludesProxyHeadersOnly()
+      throws IOException, InterruptedException {
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
-        server.enqueue(new MockResponse()
-                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
-                .setBody("This body is not allowed!"));
-        server.play();
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(
+        new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
+    server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
+    server.play();
+    client.setProxy(server.toProxyAddress());
 
-        URLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("This body is not allowed!",
-                readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-    }
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(hostnameVerifier);
+    HttpURLConnection connection = client.open(url);
+    connection.addRequestProperty("Private", "Secret");
+    connection.addRequestProperty("Proxy-Authorization", "bar");
+    connection.addRequestProperty("User-Agent", "baz");
+    assertContent("encrypted response from the origin server", connection);
 
-    @Test public void singleByteReadIsSigned() throws IOException {
-        server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
-        server.play();
+    RecordedRequest connect = server.takeRequest();
+    assertContainsNoneMatching(connect.getHeaders(), "Private.*");
+    assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
+    assertContains(connect.getHeaders(), "User-Agent: baz");
+    assertContains(connect.getHeaders(), "Host: android.com");
+    assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
 
-        URLConnection connection = client.open(server.getUrl("/"));
-        InputStream in = connection.getInputStream();
-        assertEquals(254, in.read());
-        assertEquals(255, in.read());
-        assertEquals(-1, in.read());
-    }
+    RecordedRequest get = server.takeRequest();
+    assertContains(get.getHeaders(), "Private: Secret");
+    assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+  }
 
-    @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
-        testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
-    }
+  @Test public void proxyAuthenticateOnConnect() throws Exception {
+    Authenticator.setDefault(new RecordingAuthenticator());
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(new MockResponse().setResponseCode(407)
+        .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
+    server.enqueue(
+        new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    client.setProxy(server.toProxyAddress());
 
-    @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException {
-        testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
-    }
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(url);
+    assertContent("A", connection);
 
-    @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
-        testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
-    }
+    RecordedRequest connect1 = server.takeRequest();
+    assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+    assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
 
-    /**
-     * We explicitly permit apps to close the upload stream even after it has
-     * been transmitted.  We also permit flush so that buffered streams can
-     * do a no-op flush when they are closed. http://b/3038470
-     */
-    private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
-        server.enqueue(new MockResponse().setBody("abc"));
-        server.play();
+    RecordedRequest connect2 = server.takeRequest();
+    assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+    assertContains(connect2.getHeaders(),
+        "Proxy-Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
 
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        byte[] upload = "def".getBytes("UTF-8");
-
-        if (transferKind == TransferKind.CHUNKED) {
-            connection.setChunkedStreamingMode(0);
-        } else if (transferKind == TransferKind.FIXED_LENGTH) {
-            connection.setFixedLengthStreamingMode(upload.length);
-        }
-
-        OutputStream out = connection.getOutputStream();
-        out.write(upload);
-        assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    RecordedRequest get = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+    assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
+  }
 
-        out.flush(); // dubious but permitted
-        try {
-            out.write("ghi".getBytes("UTF-8"));
-            fail();
-        } catch (IOException expected) {
-        }
-    }
+  // Don't disconnect after building a tunnel with CONNECT
+  // http://code.google.com/p/android/issues/detail?id=37221
+  @Test public void proxyWithConnectionClose() throws IOException {
+    server.useHttps(sslContext.getSocketFactory(), true);
+    server.enqueue(
+        new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
+    server.enqueue(new MockResponse().setBody("this response comes via a proxy"));
+    server.play();
+    client.setProxy(server.toProxyAddress());
 
-    @Test public void getHeadersThrows() throws IOException {
-        // Enqueue a response for every IP address held by localhost, because the route selector
-        // will try each in sequence.
-        // TODO: use the fake Dns implementation instead of a loop
-        for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
-            server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
-        }
-        server.play();
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(url);
+    connection.setRequestProperty("Connection", "close");
 
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        try {
-            connection.getInputStream();
-            fail();
-        } catch (IOException expected) {
-        }
+    assertContent("this response comes via a proxy", connection);
+  }
 
-        try {
-            connection.getInputStream();
-            fail();
-        } catch (IOException expected) {
-        }
-    }
+  @Test public void proxyWithConnectionReuse() throws IOException {
+    SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    @Test public void dnsFailureThrowsIOException() throws IOException {
-        HttpURLConnection connection = client.open(new URL("http://host.unlikelytld"));
-        try {
-            connection.connect();
-            fail();
-        } catch (IOException expected) {
-        }
-    }
+    server.useHttps(socketFactory, true);
+    server.enqueue(
+        new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
+    server.enqueue(new MockResponse().setBody("response 1"));
+    server.enqueue(new MockResponse().setBody("response 2"));
+    server.play();
+    client.setProxy(server.toProxyAddress());
 
-    @Test public void malformedUrlThrowsUnknownHostException() throws IOException {
-        HttpURLConnection connection = client.open(new URL("http:///foo.html"));
-        try {
-            connection.connect();
-            fail();
-        } catch (UnknownHostException expected) {
-        }
-    }
+    URL url = new URL("https://android.com/foo");
+    client.setSSLSocketFactory(socketFactory);
+    client.setHostnameVerifier(hostnameVerifier);
+    assertContent("response 1", client.open(url));
+    assertContent("response 2", client.open(url));
+  }
 
-    @Test public void getKeepAlive() throws Exception {
-        MockWebServer server = new MockWebServer();
-        server.enqueue(new MockResponse().setBody("ABC"));
-        server.play();
+  @Test public void disconnectedConnection() throws IOException {
+    server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+    server.play();
 
-        // The request should work once and then fail
-        URLConnection 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(""));
-            connection2.setReadTimeout(100);
-            connection2.getInputStream();
-            fail();
-        } catch (ConnectException expected) {
-        }
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    assertEquals('A', (char) in.read());
+    connection.disconnect();
+    try {
+      in.read();
+      fail("Expected a connection closed exception");
+    } catch (IOException expected) {
     }
+  }
 
-    /**
-     * 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.setResponseCache(new ResponseCache() {
-            @Override
-            public CacheResponse get(URI uri, String requestMethod,
-                    Map<String, List<String>> requestHeaders) throws IOException {
-                return null;
-            }
+  @Test public void disconnectBeforeConnect() throws IOException {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
 
-            @Override
-            public CacheRequest put(URI uri, URLConnection connection) throws IOException {
-                return new CacheRequest() {
-                    @Override
-                    public void abort() {
-                        aborted.set(true);
-                    }
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.disconnect();
 
-                    @Override
-                    public OutputStream getBody() throws IOException {
-                        return null;
-                    }
-                };
-            }
-        });
+    assertContent("A", connection);
+    assertEquals(200, connection.getResponseCode());
+  }
 
-        server.enqueue(new MockResponse().setBody("abcdef"));
-        server.play();
+  @SuppressWarnings("deprecation") @Test public void defaultRequestProperty() throws Exception {
+    URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A");
+    assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty"));
+  }
 
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        InputStream in = connection.getInputStream();
-        assertEquals("abc", readAscii(in, 3));
+  /**
+   * Reads {@code count} characters from the stream. If the stream is
+   * exhausted before {@code count} characters can be read, the remaining
+   * characters are returned and the stream is closed.
+   */
+  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();
-        assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+        break;
+      }
+      result.append((char) value);
+    }
+    return result.toString();
+  }
+
+  @Test public void markAndResetWithContentLengthHeader() throws IOException {
+    testMarkAndReset(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void markAndResetWithChunkedEncoding() throws IOException {
+    testMarkAndReset(TransferKind.CHUNKED);
+  }
+
+  @Test public void markAndResetWithNoLengthHeaders() throws IOException {
+    testMarkAndReset(TransferKind.END_OF_STREAM);
+  }
+
+  private void testMarkAndReset(TransferKind transferKind) throws IOException {
+    MockResponse response = new MockResponse();
+    transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
+    server.enqueue(response);
+    server.enqueue(response);
+    server.play();
+
+    InputStream in = client.open(server.getUrl("/")).getInputStream();
+    assertFalse("This implementation claims to support mark().", in.markSupported());
+    in.mark(5);
+    assertEquals("ABCDE", readAscii(in, 5));
+    try {
+      in.reset();
+      fail();
+    } catch (IOException expected) {
+    }
+    assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
+    assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/")));
+  }
+
+  /**
+   * We've had a bug where we forget the HTTP response when we see response
+   * code 401. This causes a new HTTP request to be issued for every call into
+   * the URLConnection.
+   */
+  @Test public void unauthorizedResponseHandling() throws IOException {
+    MockResponse response = new MockResponse().addHeader("WWW-Authenticate: challenge")
+        .setResponseCode(401) // UNAUTHORIZED
+        .setBody("Unauthorized");
+    server.enqueue(response);
+    server.enqueue(response);
+    server.enqueue(response);
+    server.play();
+
+    URL url = server.getUrl("/");
+    HttpURLConnection conn = client.open(url);
+
+    assertEquals(401, conn.getResponseCode());
+    assertEquals(401, conn.getResponseCode());
+    assertEquals(401, conn.getResponseCode());
+    assertEquals(1, server.getRequestCount());
+  }
+
+  @Test public void nonHexChunkSize() throws IOException {
+    server.enqueue(new MockResponse().setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
+        .clearHeaders()
+        .addHeader("Transfer-encoding: chunked"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    try {
+      readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+      fail();
+    } catch (IOException e) {
+    }
+  }
+
+  @Test public void missingChunkBody() throws IOException {
+    server.enqueue(new MockResponse().setBody("5")
+        .clearHeaders()
+        .addHeader("Transfer-encoding: chunked")
+        .setSocketPolicy(DISCONNECT_AT_END));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    try {
+      readAscii(connection.getInputStream(), Integer.MAX_VALUE);
+      fail();
+    } catch (IOException e) {
+    }
+  }
+
+  /**
+   * This test checks whether connections are gzipped by default. This
+   * behavior in not required by the API, so a failure of this test does not
+   * imply a bug in the implementation.
+   */
+  @Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody(gzip("ABCABCABC".getBytes("UTF-8")))
+        .addHeader("Content-Encoding: gzip"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    assertNull(connection.getContentEncoding());
+    assertEquals(-1, connection.getContentLength());
+
+    RecordedRequest request = server.takeRequest();
+    assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+  }
+
+  @Test public void clientConfiguredGzipContentEncoding() throws Exception {
+    byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"));
+    server.enqueue(new MockResponse()
+        .setBody(bodyBytes)
+        .addHeader("Content-Encoding: gzip"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    connection.addRequestProperty("Accept-Encoding", "gzip");
+    InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
+    assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
+    assertEquals(bodyBytes.length, connection.getContentLength());
+
+    RecordedRequest request = server.takeRequest();
+    assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+  }
+
+  @Test public void gzipAndConnectionReuseWithFixedLength() throws Exception {
+    testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, false);
+  }
+
+  @Test public void gzipAndConnectionReuseWithChunkedEncoding() throws Exception {
+    testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, false);
+  }
+
+  @Test public void gzipAndConnectionReuseWithFixedLengthAndTls() throws Exception {
+    testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, true);
+  }
+
+  @Test public void gzipAndConnectionReuseWithChunkedEncodingAndTls() throws Exception {
+    testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, true);
+  }
+
+  @Test public void clientConfiguredCustomContentEncoding() throws Exception {
+    server.enqueue(new MockResponse().setBody("ABCDE").addHeader("Content-Encoding: custom"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    connection.addRequestProperty("Accept-Encoding", "custom");
+    assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest request = server.takeRequest();
+    assertContains(request.getHeaders(), "Accept-Encoding: custom");
+  }
+
+  /**
+   * Test a bug where gzip input streams weren't exhausting the input stream,
+   * which corrupted the request that followed or prevented connection reuse.
+   * http://code.google.com/p/android/issues/detail?id=7059
+   * http://code.google.com/p/android/issues/detail?id=38817
+   */
+  private void testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind transferKind,
+      boolean tls) throws Exception {
+    if (tls) {
+      SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+      RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+      server.useHttps(socketFactory, false);
+      client.setSSLSocketFactory(socketFactory);
+      client.setHostnameVerifier(hostnameVerifier);
     }
 
-    /**
-     * http://code.google.com/p/android/issues/detail?id=14562
-     */
-    @Test public void readAfterLastByte() throws Exception {
-        server.enqueue(new MockResponse()
-                .setBody("ABC")
-                .clearHeaders()
-                .addHeader("Connection: close")
-                .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
-        server.play();
+    MockResponse responseOne = new MockResponse();
+    responseOne.addHeader("Content-Encoding: gzip");
+    transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
+    server.enqueue(responseOne);
+    MockResponse responseTwo = new MockResponse();
+    transferKind.setBody(responseTwo, "two (identity)", 5);
+    server.enqueue(responseTwo);
+    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()); // throws IOException in Gingerbread
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
+    connection1.addRequestProperty("Accept-Encoding", "gzip");
+    InputStream gunzippedIn = new GZIPInputStream(connection1.getInputStream());
+    assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    assertEquals("two (identity)", readAscii(connection2.getInputStream(), Integer.MAX_VALUE));
+    assertEquals(1, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
+    testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED);
+  }
+
+  @Test public void earlyDisconnectDoesntHarmPoolingWithFixedLengthEncoding() throws Exception {
+    testEarlyDisconnectDoesntHarmPooling(TransferKind.FIXED_LENGTH);
+  }
+
+  private void testEarlyDisconnectDoesntHarmPooling(TransferKind transferKind) throws Exception {
+    MockResponse response1 = new MockResponse();
+    transferKind.setBody(response1, "ABCDEFGHIJK", 1024);
+    server.enqueue(response1);
+
+    MockResponse response2 = new MockResponse();
+    transferKind.setBody(response2, "LMNOPQRSTUV", 1024);
+    server.enqueue(response2);
+
+    server.play();
+
+    URLConnection connection1 = client.open(server.getUrl("/"));
+    InputStream in1 = connection1.getInputStream();
+    assertEquals("ABCDE", readAscii(in1, 5));
+    in1.close();
+
+    HttpURLConnection connection2 = client.open(server.getUrl("/"));
+    InputStream in2 = connection2.getInputStream();
+    assertEquals("LMNOP", readAscii(in2, 5));
+    in2.close();
+
+    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 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());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
+    assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
+  }
+
+  @Test public void authenticateWithFixedLengthStreaming() throws Exception {
+    testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
+  }
+
+  @Test public void authenticateWithChunkedStreaming() throws Exception {
+    testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
+  }
+
+  private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
+    MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
+        .addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
+        .setBody("Please authenticate.");
+    server.enqueue(pleaseAuthenticate);
+    server.play();
+
+    Authenticator.setDefault(new RecordingAuthenticator());
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    byte[] requestBody = { 'A', 'B', 'C', 'D' };
+    if (streamingMode == StreamingMode.FIXED_LENGTH) {
+      connection.setFixedLengthStreamingMode(requestBody.length);
+    } else if (streamingMode == StreamingMode.CHUNKED) {
+      connection.setChunkedStreamingMode(0);
+    }
+    OutputStream outputStream = connection.getOutputStream();
+    outputStream.write(requestBody);
+    outputStream.close();
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (HttpRetryException expected) {
     }
 
-    @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("/"));
-        InputStream in = (InputStream) connection.getContent();
-        assertEquals("A", readAscii(in, Integer.MAX_VALUE));
+    // no authorization header for the request...
+    RecordedRequest request = server.takeRequest();
+    assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+    assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+  }
+
+  @Test public void nonStandardAuthenticationScheme() throws Exception {
+    List<String> calls = authCallsForHeader("WWW-Authenticate: Foo");
+    assertEquals(Collections.<String>emptyList(), calls);
+  }
+
+  @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception {
+    List<String> calls = authCallsForHeader("WWW-Authenticate: Foo realm=\"Bar\"");
+    assertEquals(1, calls.size());
+    String call = calls.get(0);
+    assertTrue(call, call.contains("scheme=Foo"));
+    assertTrue(call, call.contains("prompt=Bar"));
+  }
+
+  // Digest auth is currently unsupported. Test that digest requests should fail reasonably.
+  // http://code.google.com/p/android/issues/detail?id=11140
+  @Test public void digestAuthentication() throws Exception {
+    List<String> calls = authCallsForHeader("WWW-Authenticate: Digest "
+        + "realm=\"testrealm@host.com\", qop=\"auth,auth-int\", "
+        + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+        + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"");
+    assertEquals(1, calls.size());
+    String call = calls.get(0);
+    assertTrue(call, call.contains("scheme=Digest"));
+    assertTrue(call, call.contains("prompt=testrealm@host.com"));
+  }
+
+  @Test public void allAttributesSetInServerAuthenticationCallbacks() throws Exception {
+    List<String> calls = authCallsForHeader("WWW-Authenticate: Basic realm=\"Bar\"");
+    assertEquals(1, calls.size());
+    URL url = server.getUrl("/");
+    String call = calls.get(0);
+    assertTrue(call, call.contains("host=" + url.getHost()));
+    assertTrue(call, call.contains("port=" + url.getPort()));
+    assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
+    assertTrue(call, call.contains("url=" + url));
+    assertTrue(call, call.contains("type=" + Authenticator.RequestorType.SERVER));
+    assertTrue(call, call.contains("prompt=Bar"));
+    assertTrue(call, call.contains("protocol=http"));
+    assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI.
+  }
+
+  @Test public void allAttributesSetInProxyAuthenticationCallbacks() throws Exception {
+    List<String> calls = authCallsForHeader("Proxy-Authenticate: Basic realm=\"Bar\"");
+    assertEquals(1, calls.size());
+    URL url = server.getUrl("/");
+    String call = calls.get(0);
+    assertTrue(call, call.contains("host=" + url.getHost()));
+    assertTrue(call, call.contains("port=" + url.getPort()));
+    assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
+    assertTrue(call, call.contains("url=http://android.com"));
+    assertTrue(call, call.contains("type=" + Authenticator.RequestorType.PROXY));
+    assertTrue(call, call.contains("prompt=Bar"));
+    assertTrue(call, call.contains("protocol=http"));
+    assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI.
+  }
+
+  private List<String> authCallsForHeader(String authHeader) throws IOException {
+    boolean proxy = authHeader.startsWith("Proxy-");
+    int responseCode = proxy ? 407 : 401;
+    RecordingAuthenticator authenticator = new RecordingAuthenticator(null);
+    Authenticator.setDefault(authenticator);
+    MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(responseCode)
+        .addHeader(authHeader)
+        .setBody("Please authenticate.");
+    server.enqueue(pleaseAuthenticate);
+    server.play();
+
+    HttpURLConnection connection;
+    if (proxy) {
+      client.setProxy(server.toProxyAddress());
+      connection = client.open(new URL("http://android.com"));
+    } else {
+      connection = client.open(server.getUrl("/"));
+    }
+    assertEquals(responseCode, connection.getResponseCode());
+    return authenticator.calls;
+  }
+
+  @Test public void setValidRequestMethod() throws Exception {
+    server.play();
+    assertValidRequestMethod("GET");
+    assertValidRequestMethod("DELETE");
+    assertValidRequestMethod("HEAD");
+    assertValidRequestMethod("OPTIONS");
+    assertValidRequestMethod("POST");
+    assertValidRequestMethod("PUT");
+    assertValidRequestMethod("TRACE");
+  }
+
+  private void assertValidRequestMethod(String requestMethod) throws Exception {
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setRequestMethod(requestMethod);
+    assertEquals(requestMethod, connection.getRequestMethod());
+  }
+
+  @Test public void setInvalidRequestMethodLowercase() throws Exception {
+    server.play();
+    assertInvalidRequestMethod("get");
+  }
+
+  @Test public void setInvalidRequestMethodConnect() throws Exception {
+    server.play();
+    assertInvalidRequestMethod("CONNECT");
+  }
+
+  private void assertInvalidRequestMethod(String requestMethod) throws Exception {
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    try {
+      connection.setRequestMethod(requestMethod);
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+
+  @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception {
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    try {
+      connection.setFixedLengthStreamingMode(-2);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void canSetNegativeChunkedStreamingMode() throws Exception {
+    server.play();
+    HttpURLConnection 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("/"));
+    assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    try {
+      connection.setFixedLengthStreamingMode(1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    try {
+      connection.setChunkedStreamingMode(1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setChunkedStreamingMode(1);
+    try {
+      connection.setFixedLengthStreamingMode(1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception {
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setFixedLengthStreamingMode(1);
+    try {
+      connection.setChunkedStreamingMode(1);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test public void secureFixedLengthStreaming() throws Exception {
+    testSecureStreamingPost(StreamingMode.FIXED_LENGTH);
+  }
+
+  @Test public void secureChunkedStreaming() throws Exception {
+    testSecureStreamingPost(StreamingMode.CHUNKED);
+  }
+
+  /**
+   * Users have reported problems using HTTPS with streaming request bodies.
+   * http://code.google.com/p/android/issues/detail?id=12860
+   */
+  private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("Success!"));
+    server.play();
+
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    byte[] requestBody = { 'A', 'B', 'C', 'D' };
+    if (streamingMode == StreamingMode.FIXED_LENGTH) {
+      connection.setFixedLengthStreamingMode(requestBody.length);
+    } else if (streamingMode == StreamingMode.CHUNKED) {
+      connection.setChunkedStreamingMode(0);
+    }
+    OutputStream outputStream = connection.getOutputStream();
+    outputStream.write(requestBody);
+    outputStream.close();
+    assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("POST / HTTP/1.1", request.getRequestLine());
+    if (streamingMode == StreamingMode.FIXED_LENGTH) {
+      assertEquals(Collections.<Integer>emptyList(), request.getChunkSizes());
+    } else if (streamingMode == StreamingMode.CHUNKED) {
+      assertEquals(Arrays.asList(4), request.getChunkSizes());
+    }
+    assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+  }
+
+  enum StreamingMode {
+    FIXED_LENGTH, CHUNKED
+  }
+
+  @Test public void authenticateWithPost() 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
+    server.enqueue(new MockResponse().setBody("Successful auth!"));
+    server.play();
+
+    Authenticator.setDefault(new RecordingAuthenticator());
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    byte[] requestBody = { 'A', 'B', 'C', 'D' };
+    OutputStream outputStream = connection.getOutputStream();
+    outputStream.write(requestBody);
+    outputStream.close();
+    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 include an authorization header
+    for (int i = 0; i < 3; i++) {
+      request = server.takeRequest();
+      assertEquals("POST / HTTP/1.1", request.getRequestLine());
+      assertContains(request.getHeaders(),
+          "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+      assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+    }
+  }
+
+  @Test public void authenticateWithGet() 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
+    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));
+
+    // 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);
+    }
+  }
+
+  @Test public void redirectedWithChunkedEncoding() throws Exception {
+    testRedirected(TransferKind.CHUNKED, true);
+  }
+
+  @Test public void redirectedWithContentLengthHeader() throws Exception {
+    testRedirected(TransferKind.FIXED_LENGTH, true);
+  }
+
+  @Test public void redirectedWithNoLengthHeaders() throws Exception {
+    testRedirected(TransferKind.END_OF_STREAM, false);
+  }
+
+  private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
+    MockResponse response = new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: /foo");
+    transferKind.setBody(response, "This page has moved!", 10);
+    server.enqueue(response);
+    server.enqueue(new MockResponse().setBody("This is the new location!"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This is the new location!",
+        readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest first = server.takeRequest();
+    assertEquals("GET / HTTP/1.1", first.getRequestLine());
+    RecordedRequest retry = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
+    if (reuse) {
+      assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
+    }
+  }
+
+  @Test public void redirectedOnHttps() throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    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();
+
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This is the new location!",
+        readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest first = server.takeRequest();
+    assertEquals("GET / HTTP/1.1", first.getRequestLine());
+    RecordedRequest retry = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
+    assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
+  }
+
+  @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: http://anyhost/foo")
+        .setBody("This page has moved!"));
+    server.play();
+
+    client.setSSLSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+  }
+
+  @Test public void notRedirectedFromHttpToHttps() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: https://anyhost/foo")
+        .setBody("This page has moved!"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+  }
+
+  @Test public void redirectToAnotherOriginServer() throws Exception {
+    MockWebServer server2 = new MockWebServer();
+    server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
+    server2.play();
+
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: " + server2.getUrl("/").toString())
+        .setBody("This page has moved!"));
+    server.enqueue(new MockResponse().setBody("This is the first server again!"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This is the 2nd server!",
+        readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    assertEquals(server2.getUrl("/"), connection.getURL());
+
+    // make sure the first server was careful to recycle the connection
+    assertEquals("This is the first server again!",
+        readAscii(client.open(server.getUrl("/")).getInputStream(), Integer.MAX_VALUE));
+
+    RecordedRequest first = server.takeRequest();
+    assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort());
+    RecordedRequest second = server2.takeRequest();
+    assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort());
+    RecordedRequest third = server.takeRequest();
+    assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
+
+    server2.shutdown();
+  }
+
+  @Test public void response300MultipleChoiceWithPost() throws Exception {
+    // Chrome doesn't follow the redirect, but Firefox and the RI both do
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE);
+  }
+
+  @Test public void response301MovedPermanentlyWithPost() throws Exception {
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM);
+  }
+
+  @Test public void response302MovedTemporarilyWithPost() throws Exception {
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP);
+  }
+
+  @Test public void response303SeeOtherWithPost() throws Exception {
+    testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER);
+  }
+
+  private void testResponseRedirectedWithPost(int redirectCode) 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.setDoOutput(true);
+    byte[] requestBody = { 'A', 'B', 'C', 'D' };
+    OutputStream outputStream = connection.getOutputStream();
+    outputStream.write(requestBody);
+    outputStream.close();
+    assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    assertTrue(connection.getDoOutput());
+
+    RecordedRequest page1 = server.takeRequest();
+    assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
+    assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+
+    RecordedRequest page2 = server.takeRequest();
+    assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
+  }
+
+  @Test public void response305UseProxy() throws Exception {
+    server.play();
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_USE_PROXY)
+        .addHeader("Location: " + server.getUrl("/"))
+        .setBody("This page has moved!"));
+    server.enqueue(new MockResponse().setBody("Proxy Response"));
+
+    HttpURLConnection 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));
+
+    RecordedRequest page1 = server.takeRequest();
+    assertEquals("GET /foo HTTP/1.1", page1.getRequestLine());
+    assertEquals(1, server.getRequestCount());
+  }
+
+  @Test public void follow20Redirects() throws Exception {
+    for (int i = 0; i < 20; i++) {
+      server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.enqueue(new MockResponse().setBody("Success!"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/0"));
+    assertContent("Success!", connection);
+    assertEquals(server.getUrl("/20"), connection.getURL());
+  }
+
+  @Test public void doesNotFollow21Redirects() throws Exception {
+    for (int i = 0; i < 21; i++) {
+      server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+          .addHeader("Location: /" + (i + 1))
+          .setBody("Redirecting to /" + (i + 1)));
+    }
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/0"));
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (ProtocolException expected) {
+      assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, connection.getResponseCode());
+      assertEquals("Too many redirects: 21", expected.getMessage());
+      assertContent("Redirecting to /21", connection);
+      assertEquals(server.getUrl("/20"), connection.getURL());
+    }
+  }
+
+  @Test public void httpsWithCustomTrustManager() throws Exception {
+    RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+    RecordingTrustManager trustManager = new RecordingTrustManager();
+    SSLContext sc = SSLContext.getInstance("TLS");
+    sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
+
+    client.setHostnameVerifier(hostnameVerifier);
+    client.setSSLSocketFactory(sc.getSocketFactory());
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.enqueue(new MockResponse().setBody("ABC"));
+    server.enqueue(new MockResponse().setBody("DEF"));
+    server.enqueue(new MockResponse().setBody("GHI"));
+    server.play();
+
+    URL url = server.getUrl("/");
+    assertContent("ABC", client.open(url));
+    assertContent("DEF", client.open(url));
+    assertContent("GHI", client.open(url));
+
+    assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
+    assertEquals(Arrays.asList("checkServerTrusted [CN=" + hostName + " 1]"), trustManager.calls);
+  }
+
+  @Test public void readTimeouts() throws IOException {
+    // This relies on the fact that MockWebServer doesn't close the
+    // connection after a response has been sent. This causes the client to
+    // try to read more bytes than are sent, which results in a timeout.
+    MockResponse timeout =
+        new MockResponse().setBody("ABC").clearHeaders().addHeader("Content-Length: 4");
+    server.enqueue(timeout);
+    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();
+    assertEquals('A', in.read());
+    assertEquals('B', in.read());
+    assertEquals('C', in.read());
+    try {
+      in.read(); // if Content-Length was accurate, this would return -1 immediately
+      fail();
+    } catch (SocketTimeoutException expected) {
+    }
+  }
+
+  @Test public void setChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
+    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());
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("ABC", new String(request.getBody(), "UTF-8"));
+  }
+
+  @Test public void connectionCloseInRequest() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
+    server.enqueue(new MockResponse());
+    server.play();
+
+    HttpURLConnection a = client.open(server.getUrl("/"));
+    a.setRequestProperty("Connection", "close");
+    assertEquals(200, a.getResponseCode());
+
+    HttpURLConnection b = client.open(server.getUrl("/"));
+    assertEquals(200, b.getResponseCode());
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals("When connection: close is used, each request should get its own connection", 0,
+        server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void connectionCloseInResponse() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().addHeader("Connection: close"));
+    server.enqueue(new MockResponse());
+    server.play();
+
+    HttpURLConnection a = client.open(server.getUrl("/"));
+    assertEquals(200, a.getResponseCode());
+
+    HttpURLConnection b = client.open(server.getUrl("/"));
+    assertEquals(200, b.getResponseCode());
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals("When connection: close is used, each request should get its own connection", 0,
+        server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void connectionCloseWithRedirect() throws IOException, InterruptedException {
+    MockResponse response = new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .addHeader("Location: /foo")
+        .addHeader("Connection: close");
+    server.enqueue(response);
+    server.enqueue(new MockResponse().setBody("This is the new location!"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This is the new location!",
+        readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals("When connection: close is used, each request should get its own connection", 0,
+        server.takeRequest().getSequenceNumber());
+  }
+
+  /**
+   * Retry redirects if the socket is closed.
+   * https://code.google.com/p/android/issues/detail?id=41576
+   */
+  @Test public void sameConnectionRedirectAndReuse() throws Exception {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+        .setSocketPolicy(SHUTDOWN_INPUT_AT_END)
+        .addHeader("Location: /foo"));
+    server.enqueue(new MockResponse().setBody("This is the new page!"));
+    server.play();
+
+    assertContent("This is the new page!", client.open(server.getUrl("/")));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+  }
+
+  @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
+        .setBody("This body is not allowed!"));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    assertEquals("This body is not allowed!",
+        readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+  }
+
+  @Test public void singleByteReadIsSigned() throws IOException {
+    server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
+    server.play();
+
+    URLConnection connection = client.open(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    assertEquals(254, in.read());
+    assertEquals(255, in.read());
+    assertEquals(-1, in.read());
+  }
+
+  @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
+    testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
+  }
+
+  @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException {
+    testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
+    testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
+  }
+
+  /**
+   * We explicitly permit apps to close the upload stream even after it has
+   * been transmitted.  We also permit flush so that buffered streams can
+   * do a no-op flush when they are closed. http://b/3038470
+   */
+  private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
+    server.enqueue(new MockResponse().setBody("abc"));
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    byte[] upload = "def".getBytes("UTF-8");
+
+    if (transferKind == TransferKind.CHUNKED) {
+      connection.setChunkedStreamingMode(0);
+    } else if (transferKind == TransferKind.FIXED_LENGTH) {
+      connection.setFixedLengthStreamingMode(upload.length);
     }
 
-    @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("/"));
-        try {
-            connection.getContent(null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-        try {
-            connection.getContent(new Class[] { null });
-            fail();
-        } catch (NullPointerException expected) {
-        }
-        assertNull(connection.getContent(new Class[] { getClass() }));
-        connection.disconnect();
+    OutputStream out = connection.getOutputStream();
+    out.write(upload);
+    assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+
+    out.flush(); // dubious but permitted
+    try {
+      out.write("ghi".getBytes("UTF-8"));
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test public void getHeadersThrows() throws IOException {
+    // Enqueue a response for every IP address held by localhost, because the route selector
+    // will try each in sequence.
+    // TODO: use the fake Dns implementation instead of a loop
+    for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
+      server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
+    }
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (IOException expected) {
     }
 
-    @Test public void getOutputStreamOnGetFails() throws Exception {
-        server.enqueue(new MockResponse());
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        try {
-            connection.getOutputStream();
-            fail();
-        } catch (ProtocolException expected) {
-        }
+    try {
+      connection.getInputStream();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void getOutputAfterGetInputStreamFails() throws Exception {
-        server.enqueue(new MockResponse());
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        try {
-            connection.getInputStream();
-            connection.getOutputStream();
-            fail();
-        } catch (ProtocolException expected) {
-        }
+  @Test public void dnsFailureThrowsIOException() throws IOException {
+    HttpURLConnection connection = client.open(new URL("http://host.unlikelytld"));
+    try {
+      connection.connect();
+      fail();
+    } catch (IOException expected) {
     }
+  }
 
-    @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception {
-        server.enqueue(new MockResponse());
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.connect();
-        try {
-            connection.setDoOutput(true);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        try {
-            connection.setDoInput(true);
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-        connection.disconnect();
+  @Test public void malformedUrlThrowsUnknownHostException() throws IOException {
+    HttpURLConnection connection = client.open(new URL("http:///foo.html"));
+    try {
+      connection.connect();
+      fail();
+    } catch (UnknownHostException expected) {
     }
+  }
 
-    @Test public void clientSendsContentLength() throws Exception {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        connection.setDoOutput(true);
-        OutputStream out = connection.getOutputStream();
-        out.write(new byte[] { 'A', 'B', 'C' });
-        out.close();
-        assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
-        RecordedRequest request = server.takeRequest();
-        assertContains(request.getHeaders(), "Content-Length: 3");
+  @Test public void getKeepAlive() throws Exception {
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setBody("ABC"));
+    server.play();
+
+    // The request should work once and then fail
+    URLConnection 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(""));
+      connection2.setReadTimeout(100);
+      connection2.getInputStream();
+      fail();
+    } catch (ConnectException expected) {
     }
+  }
 
-    @Test public void getContentLengthConnects() throws Exception {
-        server.enqueue(new MockResponse().setBody("ABC"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals(3, connection.getContentLength());
-        connection.disconnect();
-    }
+  /** 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.setResponseCache(new ResponseCache() {
+      @Override
+      public CacheResponse get(URI uri, String requestMethod,
+          Map<String, List<String>> requestHeaders) throws IOException {
+        return null;
+      }
 
-    @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("/"));
-        assertEquals("text/plain", connection.getContentType());
-        connection.disconnect();
-    }
+      @Override
+      public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+        return new CacheRequest() {
+          @Override
+          public void abort() {
+            aborted.set(true);
+          }
 
-    @Test public void getContentEncodingConnects() throws Exception {
-        server.enqueue(new MockResponse()
-                .addHeader("Content-Encoding: identity")
-                .setBody("ABC"));
-        server.play();
-        HttpURLConnection connection = client.open(server.getUrl("/"));
-        assertEquals("identity", connection.getContentEncoding());
-        connection.disconnect();
-    }
-
-    // http://b/4361656
-    @Test public void urlContainsQueryButNoPath() throws Exception {
-        server.enqueue(new MockResponse().setBody("A"));
-        server.play();
-        URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
-        assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE));
-        RecordedRequest request = server.takeRequest();
-        assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
-    }
-
-    // http://code.google.com/p/android/issues/detail?id=20442
-    @Test public void inputStreamAvailableWithChunkedEncoding() throws Exception {
-        testInputStreamAvailable(TransferKind.CHUNKED);
-    }
-
-    @Test public void inputStreamAvailableWithContentLengthHeader() throws Exception {
-        testInputStreamAvailable(TransferKind.FIXED_LENGTH);
-    }
-
-    @Test public void inputStreamAvailableWithNoLengthHeaders() throws Exception {
-        testInputStreamAvailable(TransferKind.END_OF_STREAM);
-    }
-
-    private void testInputStreamAvailable(TransferKind transferKind) throws IOException {
-        String body = "ABCDEFGH";
-        MockResponse response = new MockResponse();
-        transferKind.setBody(response, body, 4);
-        server.enqueue(response);
-        server.play();
-        URLConnection connection = client.open(server.getUrl("/"));
-        InputStream in = connection.getInputStream();
-        for (int i = 0; i < body.length(); i++) {
-            assertTrue(in.available() >= 0);
-            assertEquals(body.charAt(i), in.read());
-        }
-        assertEquals(0, in.available());
-        assertEquals(-1, in.read());
-    }
-
-    @Test @Ignore public void testPooledConnectionsDetectHttp10() {
-        // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1)
-        fail("TODO");
-    }
-
-    @Test @Ignore public void postBodiesRetransmittedOnAuthProblems() {
-        fail("TODO");
-    }
-
-    @Test @Ignore public void cookiesAndTrailers() {
-        // Do cookie headers get processed too many times?
-        fail("TODO");
-    }
-
-    @Test @Ignore public void headerNamesContainingNullCharacter() {
-        // This is relevant for SPDY
-        fail("TODO");
-    }
-
-    @Test @Ignore public void headerValuesContainingNullCharacter() {
-        // This is relevant for SPDY
-        fail("TODO");
-    }
-
-    @Test @Ignore public void emptyHeaderName() {
-        // This is relevant for SPDY
-        fail("TODO");
-    }
-
-    @Test @Ignore public void emptyHeaderValue() {
-        // This is relevant for SPDY
-        fail("TODO");
-    }
-
-    @Test @Ignore public void deflateCompression() {
-        fail("TODO");
-    }
-
-    @Test @Ignore public void postBodiesRetransmittedOnIpAddressProblems() {
-        fail("TODO");
-    }
-
-    @Test @Ignore public void pooledConnectionProblemsNotReportedToProxySelector() {
-        fail("TODO");
-    }
-
-    /**
-     * Returns a gzipped copy of {@code bytes}.
-     */
-    public byte[] gzip(byte[] bytes) throws IOException {
-        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-        OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
-        gzippedOut.write(bytes);
-        gzippedOut.close();
-        return bytesOut.toByteArray();
-    }
-
-    /**
-     * 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)
-            throws IOException {
-        connection.connect();
-        assertEquals(expected, readAscii(connection.getInputStream(), limit));
-        ((HttpURLConnection) connection).disconnect();
-    }
-
-    private void assertContent(String expected, URLConnection connection) throws IOException {
-        assertContent(expected, connection, Integer.MAX_VALUE);
-    }
-
-    private void assertContains(List<String> headers, String header) {
-        assertTrue(headers.toString(), headers.contains(header));
-    }
-
-    private void assertContainsNoneMatching(List<String> headers, String pattern) {
-        for (String header : headers) {
-            if (header.matches(pattern)) {
-                fail("Header " + header + " matches " + pattern);
-            }
-        }
-    }
-
-    private Set<String> newSet(String... elements) {
-        return new HashSet<String>(Arrays.asList(elements));
-    }
-
-    enum TransferKind {
-        CHUNKED() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize)
-                    throws IOException {
-                response.setChunkedBody(content, chunkSize);
-            }
-            @Override void setForRequest(HttpURLConnection connection, int contentLength) {
-                connection.setChunkedStreamingMode(5);
-            }
-        },
-        FIXED_LENGTH() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
-                response.setBody(content);
-            }
-            @Override void setForRequest(HttpURLConnection connection, int contentLength) {
-                connection.setChunkedStreamingMode(contentLength);
-            }
-        },
-        END_OF_STREAM() {
-            @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
-                response.setBody(content);
-                response.setSocketPolicy(DISCONNECT_AT_END);
-                for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
-                    if (h.next().startsWith("Content-Length:")) {
-                        h.remove();
-                        break;
-                    }
-                }
-            }
-            @Override void setForRequest(HttpURLConnection connection, int contentLength) {
-            }
+          @Override
+          public OutputStream getBody() throws IOException {
+            return null;
+          }
         };
+      }
+    });
 
-        abstract void setBody(MockResponse response, byte[] content, int chunkSize)
-                throws IOException;
+    server.enqueue(new MockResponse().setBody("abcdef"));
+    server.play();
 
-        abstract void setForRequest(HttpURLConnection connection, int contentLength);
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    assertEquals("abc", readAscii(in, 3));
+    in.close();
+    assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+  }
 
-        void setBody(MockResponse response, String content, int chunkSize) throws IOException {
-            setBody(response, content.getBytes("UTF-8"), chunkSize);
+  /** http://code.google.com/p/android/issues/detail?id=14562 */
+  @Test public void readAfterLastByte() throws Exception {
+    server.enqueue(new MockResponse().setBody("ABC")
+        .clearHeaders()
+        .addHeader("Connection: close")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    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()); // throws IOException in Gingerbread
+  }
+
+  @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("/"));
+    InputStream in = (InputStream) connection.getContent();
+    assertEquals("A", readAscii(in, Integer.MAX_VALUE));
+  }
+
+  @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("/"));
+    try {
+      connection.getContent(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+    try {
+      connection.getContent(new Class[] { null });
+      fail();
+    } catch (NullPointerException expected) {
+    }
+    assertNull(connection.getContent(new Class[] { getClass() }));
+    connection.disconnect();
+  }
+
+  @Test public void getOutputStreamOnGetFails() throws Exception {
+    server.enqueue(new MockResponse());
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    try {
+      connection.getOutputStream();
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+
+  @Test public void getOutputAfterGetInputStreamFails() throws Exception {
+    server.enqueue(new MockResponse());
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.setDoOutput(true);
+    try {
+      connection.getInputStream();
+      connection.getOutputStream();
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+
+  @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception {
+    server.enqueue(new MockResponse());
+    server.play();
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    connection.connect();
+    try {
+      connection.setDoOutput(true);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      connection.setDoInput(true);
+      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.setDoOutput(true);
+    OutputStream out = connection.getOutputStream();
+    out.write(new byte[] { 'A', 'B', 'C' });
+    out.close();
+    assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+    RecordedRequest request = server.takeRequest();
+    assertContains(request.getHeaders(), "Content-Length: 3");
+  }
+
+  @Test public void getContentLengthConnects() throws Exception {
+    server.enqueue(new MockResponse().setBody("ABC"));
+    server.play();
+    HttpURLConnection 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("/"));
+    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("/"));
+    assertEquals("identity", connection.getContentEncoding());
+    connection.disconnect();
+  }
+
+  // http://b/4361656
+  @Test public void urlContainsQueryButNoPath() throws Exception {
+    server.enqueue(new MockResponse().setBody("A"));
+    server.play();
+    URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
+    assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE));
+    RecordedRequest request = server.takeRequest();
+    assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
+  }
+
+  // http://code.google.com/p/android/issues/detail?id=20442
+  @Test public void inputStreamAvailableWithChunkedEncoding() throws Exception {
+    testInputStreamAvailable(TransferKind.CHUNKED);
+  }
+
+  @Test public void inputStreamAvailableWithContentLengthHeader() throws Exception {
+    testInputStreamAvailable(TransferKind.FIXED_LENGTH);
+  }
+
+  @Test public void inputStreamAvailableWithNoLengthHeaders() throws Exception {
+    testInputStreamAvailable(TransferKind.END_OF_STREAM);
+  }
+
+  private void testInputStreamAvailable(TransferKind transferKind) throws IOException {
+    String body = "ABCDEFGH";
+    MockResponse response = new MockResponse();
+    transferKind.setBody(response, body, 4);
+    server.enqueue(response);
+    server.play();
+    URLConnection connection = client.open(server.getUrl("/"));
+    InputStream in = connection.getInputStream();
+    for (int i = 0; i < body.length(); i++) {
+      assertTrue(in.available() >= 0);
+      assertEquals(body.charAt(i), in.read());
+    }
+    assertEquals(0, in.available());
+    assertEquals(-1, in.read());
+  }
+
+  @Test @Ignore public void testPooledConnectionsDetectHttp10() {
+    // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1)
+    fail("TODO");
+  }
+
+  @Test @Ignore public void postBodiesRetransmittedOnAuthProblems() {
+    fail("TODO");
+  }
+
+  @Test @Ignore public void cookiesAndTrailers() {
+    // Do cookie headers get processed too many times?
+    fail("TODO");
+  }
+
+  @Test @Ignore public void headerNamesContainingNullCharacter() {
+    // This is relevant for SPDY
+    fail("TODO");
+  }
+
+  @Test @Ignore public void headerValuesContainingNullCharacter() {
+    // This is relevant for SPDY
+    fail("TODO");
+  }
+
+  @Test @Ignore public void emptyHeaderName() {
+    // This is relevant for SPDY
+    fail("TODO");
+  }
+
+  @Test @Ignore public void emptyHeaderValue() {
+    // This is relevant for SPDY
+    fail("TODO");
+  }
+
+  @Test @Ignore public void deflateCompression() {
+    fail("TODO");
+  }
+
+  @Test @Ignore public void postBodiesRetransmittedOnIpAddressProblems() {
+    fail("TODO");
+  }
+
+  @Test @Ignore public void pooledConnectionProblemsNotReportedToProxySelector() {
+    fail("TODO");
+  }
+
+  /** Returns a gzipped copy of {@code bytes}. */
+  public byte[] gzip(byte[] bytes) throws IOException {
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
+    gzippedOut.write(bytes);
+    gzippedOut.close();
+    return bytesOut.toByteArray();
+  }
+
+  /**
+   * 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)
+      throws IOException {
+    connection.connect();
+    assertEquals(expected, readAscii(connection.getInputStream(), limit));
+    ((HttpURLConnection) connection).disconnect();
+  }
+
+  private void assertContent(String expected, URLConnection connection) throws IOException {
+    assertContent(expected, connection, Integer.MAX_VALUE);
+  }
+
+  private void assertContains(List<String> headers, String header) {
+    assertTrue(headers.toString(), headers.contains(header));
+  }
+
+  private void assertContainsNoneMatching(List<String> headers, String pattern) {
+    for (String header : headers) {
+      if (header.matches(pattern)) {
+        fail("Header " + header + " matches " + pattern);
+      }
+    }
+  }
+
+  private Set<String> newSet(String... elements) {
+    return new HashSet<String>(Arrays.asList(elements));
+  }
+
+  enum TransferKind {
+    CHUNKED() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+          throws IOException {
+        response.setChunkedBody(content, chunkSize);
+      }
+      @Override void setForRequest(HttpURLConnection connection, int contentLength) {
+        connection.setChunkedStreamingMode(5);
+      }
+    },
+    FIXED_LENGTH() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+        response.setBody(content);
+      }
+      @Override void setForRequest(HttpURLConnection connection, int contentLength) {
+        connection.setChunkedStreamingMode(contentLength);
+      }
+    },
+    END_OF_STREAM() {
+      @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+        response.setBody(content);
+        response.setSocketPolicy(DISCONNECT_AT_END);
+        for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
+          if (h.next().startsWith("Content-Length:")) {
+            h.remove();
+            break;
+          }
         }
+      }
+      @Override void setForRequest(HttpURLConnection connection, int contentLength) {
+      }
+    };
+
+    abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException;
+
+    abstract void setForRequest(HttpURLConnection connection, int contentLength);
+
+    void setBody(MockResponse response, String content, int chunkSize) throws IOException {
+      setBody(response, content.getBytes("UTF-8"), chunkSize);
+    }
+  }
+
+  enum ProxyConfig {
+    NO_PROXY() {
+      @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+          throws IOException {
+        client.setProxy(Proxy.NO_PROXY);
+        return client.open(url);
+      }
+    },
+
+    CREATE_ARG() {
+      @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+          throws IOException {
+        client.setProxy(server.toProxyAddress());
+        return client.open(url);
+      }
+    },
+
+    PROXY_SYSTEM_PROPERTY() {
+      @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+          throws IOException {
+        System.setProperty("proxyHost", "localhost");
+        System.setProperty("proxyPort", Integer.toString(server.getPort()));
+        return client.open(url);
+      }
+    },
+
+    HTTP_PROXY_SYSTEM_PROPERTY() {
+      @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+          throws IOException {
+        System.setProperty("http.proxyHost", "localhost");
+        System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
+        return client.open(url);
+      }
+    },
+
+    HTTPS_PROXY_SYSTEM_PROPERTY() {
+      @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+          throws IOException {
+        System.setProperty("https.proxyHost", "localhost");
+        System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
+        return client.open(url);
+      }
+    };
+
+    public abstract HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+        throws IOException;
+  }
+
+  private static class RecordingTrustManager implements X509TrustManager {
+    private final List<String> calls = new ArrayList<String>();
+
+    public X509Certificate[] getAcceptedIssuers() {
+      return new X509Certificate[] { };
     }
 
-    enum ProxyConfig {
-        NO_PROXY() {
-            @Override public HttpURLConnection connect
-                    (MockWebServer server, OkHttpClient client, URL url) throws IOException {
-                client.setProxy(Proxy.NO_PROXY);
-                return client.open(url);
-            }
-        },
-
-        CREATE_ARG() {
-            @Override public HttpURLConnection connect(
-                    MockWebServer server, OkHttpClient client, URL url) throws IOException {
-                client.setProxy(server.toProxyAddress());
-                return client.open(url);
-            }
-        },
-
-        PROXY_SYSTEM_PROPERTY() {
-            @Override public HttpURLConnection connect(
-                    MockWebServer server, OkHttpClient client, URL url) throws IOException {
-                System.setProperty("proxyHost", "localhost");
-                System.setProperty("proxyPort", Integer.toString(server.getPort()));
-                return client.open(url);
-            }
-        },
-
-        HTTP_PROXY_SYSTEM_PROPERTY() {
-            @Override public HttpURLConnection connect(
-                    MockWebServer server, OkHttpClient client, URL url) throws IOException {
-                System.setProperty("http.proxyHost", "localhost");
-                System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
-                return client.open(url);
-            }
-        },
-
-        HTTPS_PROXY_SYSTEM_PROPERTY() {
-            @Override public HttpURLConnection connect(
-                    MockWebServer server, OkHttpClient client, URL url) throws IOException {
-                System.setProperty("https.proxyHost", "localhost");
-                System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
-                return client.open(url);
-            }
-        };
-
-        public abstract HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) throws IOException;
+    public void checkClientTrusted(X509Certificate[] chain, String authType)
+        throws CertificateException {
+      calls.add("checkClientTrusted " + certificatesToString(chain));
     }
 
-    private static class RecordingTrustManager implements X509TrustManager {
-        private final List<String> calls = new ArrayList<String>();
-
-        public X509Certificate[] getAcceptedIssuers() {
-            return new X509Certificate[] {};
-        }
-
-        public void checkClientTrusted(X509Certificate[] chain, String authType)
-                throws CertificateException {
-            calls.add("checkClientTrusted " + certificatesToString(chain));
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType)
-                throws CertificateException {
-            calls.add("checkServerTrusted " + certificatesToString(chain));
-        }
-
-        private String certificatesToString(X509Certificate[] certificates) {
-            List<String> result = new ArrayList<String>();
-            for (X509Certificate certificate : certificates) {
-                result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
-            }
-            return result.toString();
-        }
+    public void checkServerTrusted(X509Certificate[] chain, String authType)
+        throws CertificateException {
+      calls.add("checkServerTrusted " + certificatesToString(chain));
     }
 
-    private static class RecordingHostnameVerifier implements HostnameVerifier {
-        private final List<String> calls = new ArrayList<String>();
+    private String certificatesToString(X509Certificate[] certificates) {
+      List<String> result = new ArrayList<String>();
+      for (X509Certificate certificate : certificates) {
+        result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
+      }
+      return result.toString();
+    }
+  }
 
-        public boolean verify(String hostname, SSLSession session) {
-            calls.add("verify " + hostname);
-            return true;
-        }
+  private static class FakeProxySelector extends ProxySelector {
+    List<Proxy> proxies = new ArrayList<Proxy>();
+
+    @Override public List<Proxy> select(URI uri) {
+      // Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS).
+      return uri.getScheme().equals("http") || uri.getScheme().equals("https") ? proxies
+          : Collections.singletonList(Proxy.NO_PROXY);
     }
 
-    private static class RecordingAuthenticator extends Authenticator {
-        private final List<String> calls = new ArrayList<String>();
-        private final PasswordAuthentication authentication;
-
-        public RecordingAuthenticator(PasswordAuthentication authentication) {
-            this.authentication = authentication;
-        }
-
-        public RecordingAuthenticator() {
-            this(new PasswordAuthentication("username", "password".toCharArray()));
-        }
-
-        @Override protected PasswordAuthentication getPasswordAuthentication() {
-            this.calls.add("host=" + getRequestingHost()
-                    + " port=" + getRequestingPort()
-                    + " site=" + getRequestingSite()
-                    + " url=" + getRequestingURL()
-                    + " type=" + getRequestorType()
-                    + " prompt=" + getRequestingPrompt()
-                    + " protocol=" + getRequestingProtocol()
-                    + " scheme=" + getRequestingScheme());
-            return authentication;
-        }
+    @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
     }
-
-    private static class FakeProxySelector extends ProxySelector {
-        List<Proxy> proxies = new ArrayList<Proxy>();
-
-        @Override public List<Proxy> select(URI uri) {
-            // Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS).
-            return uri.getScheme().equals("http") || uri.getScheme().equals("https")
-                    ? proxies
-                    : Collections.singletonList(Proxy.NO_PROXY);
-        }
-
-        @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
-        }
-    }
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
index 0f8edca..6ca3756 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
@@ -29,122 +29,122 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
-import static org.junit.Assert.assertEquals;
 import org.junit.Ignore;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+
 /**
  * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI,
  * HttpURLConnection recovers from URLs with unescaped but unsupported URI
  * characters like '{' and '|' by escaping these characters.
  */
 public final class URLEncodingTest {
-    /**
-     * This test goes through the exhaustive set of interesting ASCII characters
-     * because most of those characters are interesting in some way according to
-     * RFC 2396 and RFC 2732. http://b/1158780
-     */
-    @Test @Ignore public void lenientUrlToUri() throws Exception {
-        // alphanum
-        testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09");
+  /**
+   * This test goes through the exhaustive set of interesting ASCII characters
+   * because most of those characters are interesting in some way according to
+   * RFC 2396 and RFC 2732. http://b/1158780
+   */
+  @Test @Ignore public void lenientUrlToUri() throws Exception {
+    // alphanum
+    testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09");
 
-        // control characters
-        testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01");
-        testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F");
+    // control characters
+    testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01");
+    testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F");
 
-        // ascii characters
-        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
-        testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
-        testUrlToUriMapping(" ", "%20", "%20", "%20", "%20");
-        testUrlToUriMapping("!", "!", "!", "!", "!");
-        testUrlToUriMapping("\"", "%22", "%22", "%22", "%22");
-        testUrlToUriMapping("#", null, null, null, "%23");
-        testUrlToUriMapping("$", "$", "$", "$", "$");
-        testUrlToUriMapping("&", "&", "&", "&", "&");
-        testUrlToUriMapping("'", "'", "'", "'", "'");
-        testUrlToUriMapping("(", "(", "(", "(", "(");
-        testUrlToUriMapping(")", ")", ")", ")", ")");
-        testUrlToUriMapping("*", "*", "*", "*", "*");
-        testUrlToUriMapping("+", "+", "+", "+", "+");
-        testUrlToUriMapping(",", ",", ",", ",", ",");
-        testUrlToUriMapping("-", "-", "-", "-", "-");
-        testUrlToUriMapping(".", ".", ".", ".", ".");
-        testUrlToUriMapping("/", null, "/", "/", "/");
-        testUrlToUriMapping(":", null, ":", ":", ":");
-        testUrlToUriMapping(";", ";", ";", ";", ";");
-        testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C");
-        testUrlToUriMapping("=", "=", "=", "=", "=");
-        testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E");
-        testUrlToUriMapping("?", null, null, "?", "?");
-        testUrlToUriMapping("@", "@", "@", "@", "@");
-        testUrlToUriMapping("[", null, "%5B", null, "%5B");
-        testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C");
-        testUrlToUriMapping("]", null, "%5D", null, "%5D");
-        testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E");
-        testUrlToUriMapping("_", "_", "_", "_", "_");
-        testUrlToUriMapping("`", "%60", "%60", "%60", "%60");
-        testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B");
-        testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C");
-        testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D");
-        testUrlToUriMapping("~", "~", "~", "~", "~");
-        testUrlToUriMapping("~", "~", "~", "~", "~");
-        testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F");
+    // ascii characters
+    testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
+    testUrlToUriMapping("%20", "%20", "%20", "%20", "%20");
+    testUrlToUriMapping(" ", "%20", "%20", "%20", "%20");
+    testUrlToUriMapping("!", "!", "!", "!", "!");
+    testUrlToUriMapping("\"", "%22", "%22", "%22", "%22");
+    testUrlToUriMapping("#", null, null, null, "%23");
+    testUrlToUriMapping("$", "$", "$", "$", "$");
+    testUrlToUriMapping("&", "&", "&", "&", "&");
+    testUrlToUriMapping("'", "'", "'", "'", "'");
+    testUrlToUriMapping("(", "(", "(", "(", "(");
+    testUrlToUriMapping(")", ")", ")", ")", ")");
+    testUrlToUriMapping("*", "*", "*", "*", "*");
+    testUrlToUriMapping("+", "+", "+", "+", "+");
+    testUrlToUriMapping(",", ",", ",", ",", ",");
+    testUrlToUriMapping("-", "-", "-", "-", "-");
+    testUrlToUriMapping(".", ".", ".", ".", ".");
+    testUrlToUriMapping("/", null, "/", "/", "/");
+    testUrlToUriMapping(":", null, ":", ":", ":");
+    testUrlToUriMapping(";", ";", ";", ";", ";");
+    testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C");
+    testUrlToUriMapping("=", "=", "=", "=", "=");
+    testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E");
+    testUrlToUriMapping("?", null, null, "?", "?");
+    testUrlToUriMapping("@", "@", "@", "@", "@");
+    testUrlToUriMapping("[", null, "%5B", null, "%5B");
+    testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C");
+    testUrlToUriMapping("]", null, "%5D", null, "%5D");
+    testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E");
+    testUrlToUriMapping("_", "_", "_", "_", "_");
+    testUrlToUriMapping("`", "%60", "%60", "%60", "%60");
+    testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B");
+    testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C");
+    testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D");
+    testUrlToUriMapping("~", "~", "~", "~", "~");
+    testUrlToUriMapping("~", "~", "~", "~", "~");
+    testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F");
 
-        // beyond ascii
-        testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80");
-        testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac");
-        testUrlToUriMapping("\ud842\udf9f",
-                "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f");
+    // beyond ascii
+    testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80");
+    testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac");
+    testUrlToUriMapping("\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f",
+        "\ud842\udf9f");
+  }
+
+  @Test @Ignore public void lenientUrlToUriNul() throws Exception {
+    testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this
+  }
+
+  private void testUrlToUriMapping(String string, String asAuthority, String asFile, String asQuery,
+      String asFragment) throws Exception {
+    if (asAuthority != null) {
+      assertEquals("http://host" + asAuthority + ".tld/",
+          backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString());
+    }
+    if (asFile != null) {
+      assertEquals("http://host.tld/file" + asFile + "/",
+          backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString());
+    }
+    if (asQuery != null) {
+      assertEquals("http://host.tld/file?q" + asQuery + "=x",
+          backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString());
+    }
+    assertEquals("http://host.tld/file#" + asFragment + "-x",
+        backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString());
+  }
+
+  private URI backdoorUrlToUri(URL url) throws Exception {
+    final AtomicReference<URI> uriReference = new AtomicReference<URI>();
+
+    OkHttpClient client = new OkHttpClient();
+    client.setResponseCache(new ResponseCache() {
+      @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+        return null;
+      }
+
+      @Override public CacheResponse get(URI uri, String requestMethod,
+          Map<String, List<String>> requestHeaders) throws IOException {
+        uriReference.set(uri);
+        throw new UnsupportedOperationException();
+      }
+    });
+
+    try {
+      HttpURLConnection connection = client.open(url);
+      connection.getResponseCode();
+    } catch (Exception expected) {
+      if (expected.getCause() instanceof URISyntaxException) {
+        expected.printStackTrace();
+      }
     }
 
-    @Test @Ignore public void lenientUrlToUriNul() throws Exception {
-        testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this
-    }
-
-    private void testUrlToUriMapping(String string, String asAuthority, String asFile,
-            String asQuery, String asFragment) throws Exception {
-        if (asAuthority != null) {
-            assertEquals("http://host" + asAuthority + ".tld/",
-                    backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString());
-        }
-        if (asFile != null) {
-            assertEquals("http://host.tld/file" + asFile + "/",
-                    backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString());
-        }
-        if (asQuery != null) {
-            assertEquals("http://host.tld/file?q" + asQuery + "=x",
-                    backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString());
-        }
-        assertEquals("http://host.tld/file#" + asFragment + "-x",
-                backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString());
-    }
-
-    private URI backdoorUrlToUri(URL url) throws Exception {
-        final AtomicReference<URI> uriReference = new AtomicReference<URI>();
-
-        OkHttpClient client = new OkHttpClient();
-        client.setResponseCache(new ResponseCache() {
-            @Override public CacheRequest put(URI uri, URLConnection connection)
-                    throws IOException {
-                return null;
-            }
-
-            @Override public CacheResponse get(URI uri, String requestMethod,
-                    Map<String, List<String>> requestHeaders) throws IOException {
-                uriReference.set(uri);
-                throw new UnsupportedOperationException();
-            }
-        });
-
-        try {
-            HttpURLConnection connection = client.open(url);
-            connection.getResponseCode();
-        } catch (Exception expected) {
-            if (expected.getCause() instanceof URISyntaxException) {
-                expected.printStackTrace();
-            }
-        }
-
-        return uriReference.get();
-    }
+    return uriReference.get();
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java b/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java
new file mode 100644
index 0000000..f7de138
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ * 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.mockspdyserver;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.QueueDispatcher;
+import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
+import com.squareup.okhttp.internal.spdy.SpdyConnection;
+import com.squareup.okhttp.internal.spdy.SpdyStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/** A scriptable spdy/3 + HTTP server. */
+public final class MockSpdyServer {
+  private static final byte[] NPN_PROTOCOLS = new byte[] { 6, 's', 'p', 'd', 'y', '/', '3', };
+  private static final Logger logger = Logger.getLogger(MockSpdyServer.class.getName());
+  private SSLSocketFactory sslSocketFactory;
+  private QueueDispatcher dispatcher = new QueueDispatcher();
+  private ServerSocket serverSocket;
+  private final Set<Socket> openClientSockets =
+      Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
+  private int port = -1;
+  private final BlockingQueue<RecordedRequest> requestQueue =
+      new LinkedBlockingQueue<RecordedRequest>();
+
+  public MockSpdyServer(SSLSocketFactory sslSocketFactory) {
+    this.sslSocketFactory = sslSocketFactory;
+  }
+
+  public String getHostName() {
+    try {
+      return InetAddress.getLocalHost().getHostName();
+    } catch (UnknownHostException e) {
+      throw new AssertionError();
+    }
+  }
+
+  public int getPort() {
+    if (port == -1) {
+      throw new IllegalStateException("Cannot retrieve port before calling play()");
+    }
+    return port;
+  }
+
+  public URL getUrl(String path) {
+    try {
+      return new URL("https://" + getHostName() + ":" + getPort() + path);
+    } catch (MalformedURLException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  /**
+   * Returns a cookie domain for this server. This returns the server's
+   * non-loopback host name if it is known. Otherwise this returns ".local"
+   * for this server's loopback name.
+   */
+  public String getCookieDomain() {
+    String hostName = getHostName();
+    return hostName.contains(".") ? hostName : ".local";
+  }
+
+  /**
+   * Awaits the next HTTP request, removes it, and returns it. Callers should
+   * use this to verify the request sent was as intended.
+   */
+  public RecordedRequest takeRequest() throws InterruptedException {
+    return requestQueue.take();
+  }
+
+  public void play() throws IOException {
+    serverSocket = new ServerSocket(0);
+    serverSocket.setReuseAddress(true);
+    port = serverSocket.getLocalPort();
+
+    Thread acceptThread = new Thread("MockSpdyServer-accept-" + port) {
+      @Override public void run() {
+        int sequenceNumber = 0;
+        try {
+          acceptConnections(sequenceNumber);
+        } catch (Throwable e) {
+          logger.log(Level.WARNING, "MockWebServer connection failed", e);
+        }
+
+        // This gnarly block of code will release all sockets and
+        // all thread, even if any close fails.
+        try {
+          serverSocket.close();
+        } catch (Throwable e) {
+          logger.log(Level.WARNING, "MockWebServer server socket close failed", e);
+        }
+        for (Iterator<Socket> s = openClientSockets.iterator(); s.hasNext(); ) {
+          try {
+            s.next().close();
+            s.remove();
+          } catch (Throwable e) {
+            logger.log(Level.WARNING, "MockWebServer socket close failed", e);
+          }
+        }
+      }
+    };
+    acceptThread.start();
+  }
+
+  public void enqueue(MockResponse response) {
+    dispatcher.enqueueResponse(response);
+  }
+
+  private void acceptConnections(int sequenceNumber) throws Exception {
+    while (true) {
+      Socket socket;
+      try {
+        socket = serverSocket.accept();
+      } catch (SocketException e) {
+        return;
+      }
+      openClientSockets.add(socket);
+      new SocketHandler(sequenceNumber++, socket).serve();
+    }
+  }
+
+  public void shutdown() throws IOException {
+    if (serverSocket != null) {
+      serverSocket.close(); // should cause acceptConnections() to break out
+    }
+  }
+
+  private class SocketHandler implements IncomingStreamHandler {
+    private final int sequenceNumber;
+    private Socket socket;
+
+    private SocketHandler(int sequenceNumber, Socket socket) throws IOException {
+      this.socket = socket;
+      this.sequenceNumber = sequenceNumber;
+    }
+
+    public void serve() throws IOException {
+      if (sslSocketFactory != null) {
+        socket = doSsl(socket);
+      }
+      new SpdyConnection.Builder(false, socket).handler(this).build();
+    }
+
+    private Socket doSsl(Socket socket) throws IOException {
+      SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
+          socket.getInetAddress().getHostAddress(), socket.getPort(), true);
+      sslSocket.setUseClientMode(false);
+      Platform.get().setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+      return sslSocket;
+    }
+
+    @Override public void receive(final SpdyStream stream) throws IOException {
+      RecordedRequest request = readRequest(stream);
+      requestQueue.add(request);
+      MockResponse response;
+      try {
+        response = dispatcher.dispatch(request);
+      } catch (InterruptedException e) {
+        throw new AssertionError(e);
+      }
+      writeResponse(stream, response);
+      logger.info("Received request: " + request + " and responded: " + response);
+    }
+
+    private RecordedRequest readRequest(SpdyStream stream) throws IOException {
+      List<String> 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)) {
+          method = value;
+        } else if (":path".equals(name)) {
+          path = value;
+        } else if (":version".equals(name)) {
+          version = value;
+        } else {
+          httpHeaders.add(name + ": " + value);
+        }
+      }
+
+      InputStream bodyIn = stream.getInputStream();
+      ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
+      byte[] buffer = new byte[8192];
+      int count;
+      while ((count = bodyIn.read(buffer)) != -1) {
+        bodyOut.write(buffer, 0, count);
+      }
+      bodyIn.close();
+      String requestLine = method + ' ' + path + ' ' + version;
+      List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
+      return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(),
+          bodyOut.toByteArray(), sequenceNumber, socket);
+    }
+
+    private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
+      List<String> spdyHeaders = new ArrayList<String>();
+      String[] statusParts = response.getStatus().split(" ", 2);
+      if (statusParts.length != 2) {
+        throw new AssertionError("Unexpected status: " + response.getStatus());
+      }
+      spdyHeaders.add(":status");
+      spdyHeaders.add(statusParts[1]);
+      spdyHeaders.add(":version");
+      spdyHeaders.add(statusParts[0]);
+      for (String header : response.getHeaders()) {
+        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());
+      }
+      byte[] body = response.getBody();
+      stream.reply(spdyHeaders, body.length > 0);
+      if (body.length > 0) {
+        stream.getOutputStream().write(body);
+        stream.getOutputStream().close();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
new file mode 100644
index 0000000..b8afeb2
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.RecordedRequest;
+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.internal.http.HttpResponseCache;
+import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
+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 MockSpdyServer server = new MockSpdyServer(sslContext.getSocketFactory());
+  private final String hostName = server.getHostName();
+  private final OkHttpClient client = new OkHttpClient();
+  private HttpResponseCache cache;
+
+  @Before public void setUp() throws Exception {
+    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 {
+    server.shutdown();
+  }
+
+  @Test public void get() throws Exception {
+    MockResponse response = new MockResponse().setBody("ABCDE");
+    server.enqueue(response);
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/foo"));
+    assertContent("ABCDE", connection, Integer.MAX_VALUE);
+
+    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(0, 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 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/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
index 652786a..54e058b 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -18,6 +18,7 @@
 
 import com.squareup.okhttp.internal.Util;
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -31,190 +32,231 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 
-/**
- * Replays prerecorded outgoing frames and records incoming frames.
- */
-public final class MockSpdyPeer {
-    private int frameCount = 0;
-    private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-    private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut);
-    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(
-            Threads.newThreadFactory("MockSpdyPeer", true));
+/** Replays prerecorded outgoing frames and records incoming frames. */
+public final class MockSpdyPeer implements Closeable {
+  private int frameCount = 0;
+  private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+  private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut);
+  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(Util.newThreadFactory("MockSpdyPeer", true));
+  private ServerSocket serverSocket;
+  private Socket socket;
 
-    public void acceptFrame() {
-        frameCount++;
+  public void acceptFrame() {
+    frameCount++;
+  }
+
+  public SpdyWriter sendFrame() {
+    outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE));
+    return spdyWriter;
+  }
+
+  /**
+   * Sends a frame, truncated to {@code truncateToLength} bytes. This is only
+   * useful for testing error handling as the truncated frame will be
+   * malformed.
+   */
+  public SpdyWriter sendTruncatedFrame(int truncateToLength) {
+    outFrames.add(new OutFrame(frameCount++, bytesOut.size(), truncateToLength));
+    return spdyWriter;
+  }
+
+  public int getPort() {
+    return port;
+  }
+
+  public InFrame takeFrame() throws InterruptedException {
+    return inFrames.take();
+  }
+
+  public void play() throws IOException {
+    if (serverSocket != null) throw new IllegalStateException();
+    serverSocket = new ServerSocket(0);
+    serverSocket.setReuseAddress(true);
+    this.port = serverSocket.getLocalPort();
+    executor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          readAndWriteFrames();
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    });
+  }
+
+  private void readAndWriteFrames() throws IOException {
+    if (socket != null) throw new IllegalStateException();
+    socket = serverSocket.accept();
+    OutputStream out = socket.getOutputStream();
+    InputStream in = socket.getInputStream();
+    SpdyReader reader = new SpdyReader(in);
+
+    Iterator<OutFrame> outFramesIterator = outFrames.iterator();
+    byte[] outBytes = bytesOut.toByteArray();
+    OutFrame nextOutFrame = null;
+
+    for (int i = 0; i < frameCount; i++) {
+      if (nextOutFrame == null && outFramesIterator.hasNext()) {
+        nextOutFrame = outFramesIterator.next();
+      }
+
+      if (nextOutFrame != null && nextOutFrame.sequence == i) {
+        int start = nextOutFrame.start;
+        int truncateToLength = nextOutFrame.truncateToLength;
+        int end;
+        if (outFramesIterator.hasNext()) {
+          nextOutFrame = outFramesIterator.next();
+          end = nextOutFrame.start;
+        } else {
+          end = outBytes.length;
+        }
+
+        // write a frame
+        int length = Math.min(end - start, truncateToLength);
+        out.write(outBytes, start, length);
+      } else {
+        // read a frame
+        InFrame inFrame = new InFrame(i, reader);
+        reader.nextFrame(inFrame);
+        inFrames.add(inFrame);
+      }
+    }
+    Util.closeQuietly(socket);
+  }
+
+  public Socket openSocket() throws IOException {
+    return new Socket("localhost", port);
+  }
+
+  @Override public void close() throws IOException {
+    Socket socket = this.socket;
+    if (socket != null) {
+      socket.close();
+      this.socket = null;
+    }
+    ServerSocket serverSocket = this.serverSocket;
+    if (serverSocket != null) {
+      serverSocket.close();
+      this.serverSocket = null;
+    }
+  }
+
+  private static class OutFrame {
+    private final int sequence;
+    private final int start;
+    private final int truncateToLength;
+
+    private OutFrame(int sequence, int start, int truncateToLength) {
+      this.sequence = sequence;
+      this.start = start;
+      this.truncateToLength = truncateToLength;
+    }
+  }
+
+  public static class InFrame implements SpdyReader.Handler {
+    public final int sequence;
+    public final SpdyReader reader;
+    public int type = -1;
+    public int flags;
+    public int streamId;
+    public int associatedStreamId;
+    public int priority;
+    public int slot;
+    public int statusCode;
+    public int deltaWindowSize;
+    public List<String> nameValueBlock;
+    public byte[] data;
+    public Settings settings;
+
+    public InFrame(int sequence, SpdyReader reader) {
+      this.sequence = sequence;
+      this.reader = reader;
     }
 
-    public SpdyWriter sendFrame() {
-        OutFrame frame = new OutFrame(frameCount++, bytesOut.size());
-        outFrames.add(frame);
-        return spdyWriter;
+    @Override public void settings(int flags, Settings settings) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_SETTINGS;
+      this.flags = flags;
+      this.settings = settings;
     }
 
-    public int getPort() {
-        return port;
+    @Override
+    public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+        List<String> nameValueBlock) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_SYN_STREAM;
+      this.flags = flags;
+      this.streamId = streamId;
+      this.associatedStreamId = associatedStreamId;
+      this.priority = priority;
+      this.slot = slot;
+      this.nameValueBlock = nameValueBlock;
     }
 
-    public InFrame takeFrame() throws InterruptedException {
-        return inFrames.take();
+    @Override public void synReply(int flags, int streamId, List<String> nameValueBlock) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_SYN_REPLY;
+      this.streamId = streamId;
+      this.flags = flags;
+      this.nameValueBlock = nameValueBlock;
     }
 
-    public void play() throws IOException {
-        final ServerSocket serverSocket = new ServerSocket(0);
-        serverSocket.setReuseAddress(true);
-        this.port = serverSocket.getLocalPort();
-        executor.execute(new Runnable() {
-            @Override public void run() {
-                try {
-                    readAndWriteFrames(serverSocket);
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        });
+    @Override public void headers(int flags, int streamId, List<String> nameValueBlock) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_HEADERS;
+      this.streamId = streamId;
+      this.flags = flags;
+      this.nameValueBlock = nameValueBlock;
     }
 
-    private void readAndWriteFrames(ServerSocket serverSocket) throws IOException {
-        Socket socket = serverSocket.accept();
-        OutputStream out = socket.getOutputStream();
-        InputStream in = socket.getInputStream();
-        SpdyReader reader = new SpdyReader(in);
-
-        Iterator<OutFrame> outFramesIterator = outFrames.iterator();
-        byte[] outBytes = bytesOut.toByteArray();
-        OutFrame nextOutFrame = null;
-
-        for (int i = 0; i < frameCount; i++) {
-            if (nextOutFrame == null && outFramesIterator.hasNext()) {
-                nextOutFrame = outFramesIterator.next();
-            }
-
-            if (nextOutFrame != null && nextOutFrame.sequence == i) {
-                int start = nextOutFrame.start;
-                int end;
-                if (outFramesIterator.hasNext()) {
-                    nextOutFrame = outFramesIterator.next();
-                    end = nextOutFrame.start;
-                } else {
-                    end = outBytes.length;
-                }
-
-                // write a frame
-                out.write(outBytes, start, end - start);
-
-            } else {
-                // read a frame
-                InFrame inFrame = new InFrame(i, reader);
-                reader.nextFrame(inFrame);
-                inFrames.add(inFrame);
-            }
-        }
+    @Override public void data(int flags, int streamId, InputStream in, int length)
+        throws IOException {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_DATA;
+      this.flags = flags;
+      this.streamId = streamId;
+      this.data = new byte[length];
+      Util.readFully(in, this.data);
     }
 
-    public Socket openSocket() throws IOException {
-        return new Socket("localhost", port);
+    @Override public void rstStream(int flags, int streamId, int statusCode) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_RST_STREAM;
+      this.flags = flags;
+      this.streamId = streamId;
+      this.statusCode = statusCode;
     }
 
-    private static class OutFrame {
-        private final int sequence;
-        private final int start;
-
-        private OutFrame(int sequence, int start) {
-            this.sequence = sequence;
-            this.start = start;
-        }
+    @Override public void ping(int flags, int streamId) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_PING;
+      this.flags = flags;
+      this.streamId = streamId;
     }
 
-    public static class InFrame implements SpdyReader.Handler {
-        public final int sequence;
-        public final SpdyReader reader;
-        public int type = -1;
-        public int flags;
-        public int streamId;
-        public int associatedStreamId;
-        public int priority;
-        public int statusCode;
-        public List<String> nameValueBlock;
-        public byte[] data;
-        public Settings settings;
-
-        public InFrame(int sequence, SpdyReader reader) {
-            this.sequence = sequence;
-            this.reader = reader;
-        }
-
-        @Override public void settings(int flags, Settings settings) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_SETTINGS;
-            this.flags = flags;
-            this.settings = settings;
-        }
-
-        @Override public void synStream(int flags, int streamId, int associatedStreamId,
-                int priority, List<String> nameValueBlock) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_SYN_STREAM;
-            this.flags = flags;
-            this.streamId = streamId;
-            this.associatedStreamId = associatedStreamId;
-            this.priority = priority;
-            this.nameValueBlock = nameValueBlock;
-        }
-
-        @Override public void synReply(int flags, int streamId, List<String> nameValueBlock) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_SYN_REPLY;
-            this.streamId = streamId;
-            this.flags = flags;
-            this.nameValueBlock = nameValueBlock;
-        }
-
-        @Override public void headers(int flags, int streamId, List<String> nameValueBlock) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_HEADERS;
-            this.streamId = streamId;
-            this.flags = flags;
-            this.nameValueBlock = nameValueBlock;
-        }
-
-        @Override public void data(int flags, int streamId, InputStream in, int length)
-                throws IOException {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_DATA;
-            this.flags = flags;
-            this.streamId = streamId;
-            this.data = new byte[length];
-            Util.readFully(in, this.data);
-        }
-
-        @Override public void rstStream(int flags, int streamId, int statusCode) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_RST_STREAM;
-            this.flags = flags;
-            this.streamId = streamId;
-            this.statusCode = statusCode;
-        }
-
-        @Override public void ping(int flags, int streamId) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_PING;
-            this.flags = flags;
-            this.streamId = streamId;
-        }
-
-        @Override public void noop() {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_NOOP;
-        }
-
-        @Override public void goAway(int flags, int lastGoodStreamId) {
-            if (this.type != -1) throw new IllegalStateException();
-            this.type = SpdyConnection.TYPE_GOAWAY;
-            this.flags = flags;
-            this.streamId = lastGoodStreamId;
-        }
+    @Override public void noop() {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_NOOP;
     }
+
+    @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_GOAWAY;
+      this.flags = flags;
+      this.streamId = lastGoodStreamId;
+      this.statusCode = statusCode;
+    }
+
+    @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) {
+      if (this.type != -1) throw new IllegalStateException();
+      this.type = SpdyConnection.TYPE_WINDOW_UPDATE;
+      this.flags = flags;
+      this.streamId = streamId;
+      this.deltaWindowSize = deltaWindowSize;
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
index 1c44493..a906fc7 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
@@ -15,7 +15,8 @@
  */
 package com.squareup.okhttp.internal.spdy;
 
-import com.squareup.okhttp.internal.spdy.Settings;
+import org.junit.Test;
+
 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;
@@ -25,127 +26,130 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import org.junit.Test;
 
 public final class SettingsTest {
-    @Test public void unsetField() {
-        Settings settings = new Settings();
-        assertEquals(-3, settings.getUploadBandwidth(-3));
-    }
+  @Test public void unsetField() {
+    Settings settings = new Settings();
+    assertEquals(-3, settings.getUploadBandwidth(-3));
+  }
 
-    @Test public void setFields() {
-        Settings settings = new Settings();
+  @Test public void setFields() {
+    Settings settings = new Settings();
 
-        assertEquals(-3, settings.getUploadBandwidth(-3));
-        settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42);
-        assertEquals(42, settings.getUploadBandwidth(-3));
+    assertEquals(-3, settings.getUploadBandwidth(-3));
+    settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42);
+    assertEquals(42, settings.getUploadBandwidth(-3));
 
-        assertEquals(-3, settings.getDownloadBandwidth(-3));
-        settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53);
-        assertEquals(53, settings.getDownloadBandwidth(-3));
+    assertEquals(-3, settings.getDownloadBandwidth(-3));
+    settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53);
+    assertEquals(53, settings.getDownloadBandwidth(-3));
 
-        assertEquals(-3, settings.getRoundTripTime(-3));
-        settings.set(Settings.ROUND_TRIP_TIME, 0, 64);
-        assertEquals(64, settings.getRoundTripTime(-3));
+    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);
-        assertEquals(75, settings.getMaxConcurrentStreams(-3));
+    assertEquals(-3, settings.getMaxConcurrentStreams(-3));
+    settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 75);
+    assertEquals(75, settings.getMaxConcurrentStreams(-3));
 
-        assertEquals(-3, settings.getCurrentCwnd(-3));
-        settings.set(Settings.CURRENT_CWND, 0, 86);
-        assertEquals(86, settings.getCurrentCwnd(-3));
+    assertEquals(-3, settings.getCurrentCwnd(-3));
+    settings.set(Settings.CURRENT_CWND, 0, 86);
+    assertEquals(86, settings.getCurrentCwnd(-3));
 
-        assertEquals(-3, settings.getDownloadRetransRate(-3));
-        settings.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 97);
-        assertEquals(97, settings.getDownloadRetransRate(-3));
+    assertEquals(-3, settings.getDownloadRetransRate(-3));
+    settings.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 97);
+    assertEquals(97, settings.getDownloadRetransRate(-3));
 
-        assertEquals(-3, settings.getInitialWindowSize(-3));
-        settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108);
-        assertEquals(108, settings.getInitialWindowSize(-3));
-    }
+    assertEquals(-3, settings.getInitialWindowSize(-3));
+    settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108);
+    assertEquals(108, settings.getInitialWindowSize(-3));
 
-    @Test public void isPersisted() {
-        Settings settings = new Settings();
+    assertEquals(-3, settings.getClientCertificateVectorSize(-3));
+    settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117);
+    assertEquals(117, settings.getClientCertificateVectorSize(-3));
+  }
 
-        // Initially false.
-        assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+  @Test public void isPersisted() {
+    Settings settings = new Settings();
 
-        // Set no flags.
-        settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
-        assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+    // Initially false.
+    assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-        // Set the wrong flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
-        assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+    // Set no flags.
+    settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+    assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-        // Set the right flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
-        assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+    // Set the wrong flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
+    assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-        // Set multiple flags.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
-        assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+    // Set the right flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
+    assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-        // Clear the flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
-        assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+    // Set multiple flags.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
+    assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-        // Clear all flags.
-        settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
-        assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
-    }
+    // Clear the flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
+    assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
 
-    @Test public void persistValue() {
-        Settings settings = new Settings();
+    // Clear all flags.
+    settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+    assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+  }
 
-        // Initially false.
-        assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+  @Test public void persistValue() {
+    Settings settings = new Settings();
 
-        // Set no flags.
-        settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
-        assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+    // Initially false.
+    assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-        // Set the wrong flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
-        assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+    // Set no flags.
+    settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+    assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-        // Set the right flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
-        assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME));
+    // Set the wrong flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
+    assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-        // Set multiple flags.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
-        assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME));
+    // Set the right flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
+    assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-        // Clear the flag.
-        settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
-        assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+    // Set multiple flags.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
+    assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-        // Clear all flags.
-        settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
-        assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
-    }
+    // Clear the flag.
+    settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
+    assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
 
-    @Test public void merge() {
-        Settings a = new Settings();
-        a.set(UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
-        a.set(DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
-        a.set(DOWNLOAD_RETRANS_RATE, 0, 300);
+    // Clear all flags.
+    settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+    assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+  }
 
-        Settings b = new Settings();
-        b.set(DOWNLOAD_BANDWIDTH, 0, 400);
-        b.set(DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500);
-        b.set(MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
+  @Test public void merge() {
+    Settings a = new Settings();
+    a.set(UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
+    a.set(DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
+    a.set(DOWNLOAD_RETRANS_RATE, 0, 300);
 
-        a.merge(b);
-        assertEquals(100, a.getUploadBandwidth(-1));
-        assertEquals(PERSIST_VALUE, a.flags(UPLOAD_BANDWIDTH));
-        assertEquals(400, a.getDownloadBandwidth(-1));
-        assertEquals(0, a.flags(DOWNLOAD_BANDWIDTH));
-        assertEquals(500, a.getDownloadRetransRate(-1));
-        assertEquals(PERSIST_VALUE, a.flags(DOWNLOAD_RETRANS_RATE));
-        assertEquals(600, a.getMaxConcurrentStreams(-1));
-        assertEquals(PERSIST_VALUE, a.flags(MAX_CONCURRENT_STREAMS));
-    }
+    Settings b = new Settings();
+    b.set(DOWNLOAD_BANDWIDTH, 0, 400);
+    b.set(DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500);
+    b.set(MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
+
+    a.merge(b);
+    assertEquals(100, a.getUploadBandwidth(-1));
+    assertEquals(PERSIST_VALUE, a.flags(UPLOAD_BANDWIDTH));
+    assertEquals(400, a.getDownloadBandwidth(-1));
+    assertEquals(0, a.flags(DOWNLOAD_BANDWIDTH));
+    assertEquals(500, a.getDownloadRetransRate(-1));
+    assertEquals(PERSIST_VALUE, a.flags(DOWNLOAD_RETRANS_RATE));
+    assertEquals(600, a.getMaxConcurrentStreams(-1));
+    assertEquals(PERSIST_VALUE, a.flags(MAX_CONCURRENT_STREAMS));
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
index d51fed7..7dd23f6 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
@@ -16,10 +16,24 @@
 
 package com.squareup.okhttp.internal.spdy;
 
+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.Settings.PERSIST_VALUE;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_FIN;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_UNIDIRECTIONAL;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_INTERNAL_ERROR;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_PROTOCOL_ERROR;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_DATA;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_GOAWAY;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_NOOP;
@@ -27,893 +41,992 @@
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_RST_STREAM;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_SYN_REPLY;
 import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_SYN_STREAM;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_WINDOW_UPDATE;
 import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_FLOW_CONTROL_ERROR;
 import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_INVALID_STREAM;
 import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_PROTOCOL_ERROR;
 import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_REFUSED_STREAM;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
+import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_STREAM_IN_USE;
+import static com.squareup.okhttp.internal.spdy.SpdyStream.WINDOW_UPDATE_THRESHOLD;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import org.junit.Test;
 
 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 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();
+
+  @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(0, 1, Arrays.asList("a", "android"));
+    peer.sendFrame().data(SpdyConnection.FLAG_FIN, 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_SYN_STREAM, synStream.type);
+    assertEquals(0, synStream.flags);
+    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(0, 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(FLAG_FIN, 1, Arrays.asList("a", "android"));
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+  }
+
+  @Test public void serverCreatesStreamAndClientReplies() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synStream(0, 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(-1, stream.getRstStatusCode());
+        assertEquals(5, stream.getPriority());
+        assertEquals(129, stream.getSlot());
+        stream.reply(Arrays.asList("b", "banana"), true);
+      }
     };
-    private final MockSpdyPeer peer = new MockSpdyPeer();
+    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
 
-    @Test public void clientCreatesStreamAndServerReplies() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
-        peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8"));
-        peer.acceptFrame();
-        peer.play();
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame reply = peer.takeFrame();
+    assertEquals(TYPE_SYN_REPLY, reply.type);
+    assertEquals(0, reply.flags);
+    assertEquals(2, reply.streamId);
+    assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock);
+    assertEquals(1, receiveCount.get());
+  }
 
-        // 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());
+  @Test public void replyWithNoData() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android"));
+    peer.acceptFrame(); // SYN_REPLY
+    peer.play();
 
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-        assertEquals(0, synStream.flags);
-        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));
+    // 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_SYN_REPLY, reply.type);
+    assertEquals(FLAG_FIN, reply.flags);
+    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);
+    assertEquals(0, ping.flags);
+  }
+
+  @Test public void serverPingsClient() throws Exception {
+    // write the mocking script
+    peer.sendFrame().ping(0, 2);
+    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(0, ping.flags);
+    assertEquals(2, ping.streamId);
+  }
+
+  @Test public void clientPingsServer() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 1);
+    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(0, pingFrame.flags);
+    assertEquals(1, pingFrame.streamId);
+  }
+
+  @Test public void unexpectedPingIsNotReturned() throws Exception {
+    // write the mocking script
+    peer.sendFrame().ping(0, 2);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 3); // This ping will not be returned.
+    peer.sendFrame().ping(0, 4);
+    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.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS, settings);
+    peer.sendFrame().ping(0, 2);
+    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(0, 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(0, settings2);
+    peer.sendFrame().ping(0, 2);
+    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(SpdyConnection.FLAG_FIN, 42, "bogus".getBytes("UTF-8"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(0, 2);
+    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(0, rstStream.flags);
+    assertEquals(42, rstStream.streamId);
+    assertEquals(RST_INVALID_STREAM, rstStream.statusCode);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(2, ping.streamId);
+  }
+
+  @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synReply(0, 42, Arrays.asList("a", "android"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(0, 2);
+    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(0, rstStream.flags);
+    assertEquals(42, rstStream.streamId);
+    assertEquals(RST_INVALID_STREAM, rstStream.statusCode);
+    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(0, 1, Arrays.asList("b", "banana"));
+    peer.acceptFrame(); // TYPE_DATA
+    peer.acceptFrame(); // TYPE_DATA with FLAG_FIN
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream.type);
+    assertEquals(FLAG_UNIDIRECTIONAL, synStream.flags);
+    MockSpdyPeer.InFrame data = peer.takeFrame();
+    assertEquals(TYPE_DATA, data.type);
+    assertEquals(0, data.flags);
+    assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
+    MockSpdyPeer.InFrame fin = peer.takeFrame();
+    assertEquals(TYPE_DATA, fin.type);
+    assertEquals(FLAG_FIN, fin.flags);
+    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, SpdyStream.RST_CANCEL);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream.type);
+    assertEquals(0, synStream.flags);
+    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);
+    assertEquals(FLAG_FIN, data.flags);
+  }
+
+  /**
+   * 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_SYN_STREAM, synStream.type);
+    assertEquals(SpdyConnection.FLAG_FIN, synStream.flags);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode);
+  }
+
+  /**
+   * 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_SYN_STREAM, synStream.type);
+    assertEquals(0, synStream.flags);
+    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);
+    assertEquals(FLAG_FIN, fin.flags);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode);
+  }
+
+  @Test public void serverClosesClientInputStream() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
+    peer.sendFrame().data(FLAG_FIN, 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_SYN_STREAM, synStream.type);
+    assertEquals(SpdyConnection.FLAG_FIN, synStream.flags);
+  }
+
+  @Test public void remoteDoubleSynReply() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
+    peer.sendFrame().ping(0, 1);
+    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());
     }
 
-    @Test public void headersOnlyStreamIsClosedImmediately() throws Exception {
-        peer.acceptFrame(); // SYN STREAM
-        peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
-        peer.play();
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_SYN_STREAM, synStream.type);
+    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(0, rstStream.flags);
+    assertEquals(RST_STREAM_IN_USE, rstStream.statusCode);
+  }
 
-        SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
-        connection.newStream(Arrays.asList("a", "android"), false, false);
-        assertEquals(0, connection.openStreamCount());
+  @Test public void remoteDoubleSynStream() throws Exception {
+    // write the mocking script
+    peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android"));
+    peer.acceptFrame(); // SYN_REPLY
+    peer.sendFrame().synStream(0, 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(-1, stream.getRstStatusCode());
+        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_SYN_REPLY, reply.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(2, rstStream.streamId);
+    assertEquals(0, rstStream.flags);
+    assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
+    assertEquals(1, receiveCount.intValue());
+  }
+
+  @Test public void remoteSendsDataAfterInFinished() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8"));
+    peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "c3po".getBytes("UTF-8")); // Ignored.
+    peer.sendFrame().ping(0, 2); // 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_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(2, ping.streamId);
+    assertEquals(0, ping.flags);
+  }
+
+  @Test public void remoteSendsTooMuchData() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
+    peer.sendFrame().data(0, 1, new byte[64 * 1024 + 1]);
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(0, 2); // 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_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(1, rstStream.streamId);
+    assertEquals(0, rstStream.flags);
+    assertEquals(RST_FLOW_CONTROL_ERROR, rstStream.statusCode);
+    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, RST_REFUSED_STREAM);
+    peer.sendFrame().ping(0, 2);
+    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_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    assertEquals(2, ping.streamId);
+    assertEquals(0, ping.flags);
+  }
+
+  @Test public void receiveGoAway() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM 1
+    peer.acceptFrame(); // SYN_STREAM 3
+    peer.sendFrame().goAway(0, 1, GOAWAY_PROTOCOL_ERROR);
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream1.type);
+    MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
+    assertEquals(TYPE_SYN_STREAM, 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(0, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored!
+    peer.sendFrame().ping(0, 1);
+    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(GOAWAY_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_SYN_STREAM, 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(GOAWAY_PROTOCOL_ERROR, goaway.statusCode);
+  }
+
+  @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(GOAWAY_INTERNAL_ERROR);
+    try {
+      connection.ping();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("shutdown", expected.getMessage());
     }
 
-    @Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN STREAM
-        peer.acceptFrame(); // PING
-        peer.sendFrame().synReply(FLAG_FIN, 1, Arrays.asList("a", "android"));
-        peer.sendFrame().ping(0, 1);
-        peer.play();
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame goaway = peer.takeFrame();
+    assertEquals(TYPE_GOAWAY, goaway.type);
+    assertEquals(GOAWAY_INTERNAL_ERROR, goaway.statusCode);
+  }
 
-        // 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());
+  @Test public void close() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // GOAWAY
+    peer.acceptFrame(); // RST_STREAM
+    peer.play();
 
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
+    // 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());
     }
 
-    @Test public void serverCreatesStreamAndClientReplies() throws Exception {
-        // write the mocking script
-        peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
-        peer.acceptFrame();
-        peer.play();
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_SYN_STREAM, synStream.type);
+    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);
+  }
 
-        // 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(-1, stream.getRstStatusCode());
-                stream.reply(Arrays.asList("b", "banana"), true);
+  @Test public void closeCancelsPings() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // PING
+    peer.acceptFrame(); // GOAWAY
+    peer.play();
 
-            }
-        };
-        new SpdyConnection.Builder(true, peer.openSocket())
-                .handler(handler)
-                .build();
+    // play it back
+    SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
+    Ping ping = connection.ping();
+    connection.close();
+    assertEquals(-1, ping.roundTripTime());
+  }
 
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame reply = peer.takeFrame();
-        assertEquals(TYPE_SYN_REPLY, reply.type);
-        assertEquals(0, reply.flags);
-        assertEquals(2, reply.streamId);
-        assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock);
-        assertEquals(1, receiveCount.get());
+  @Test public void readTimeoutExpires() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    peer.acceptFrame(); // PING
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream.type);
+  }
+
+  @Test public void headers() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.acceptFrame(); // PING
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po"));
+    peer.sendFrame().ping(0, 1);
+    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_SYN_STREAM, synStream.type);
+    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(0, 1, Arrays.asList("c", "c3po"));
+    peer.acceptFrame(); // RST_STREAM
+    peer.sendFrame().ping(0, 1);
+    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());
     }
 
-    @Test public void replyWithNoData() throws Exception {
-        // write the mocking script
-        peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
-        peer.acceptFrame();
-        peer.play();
+    // verify the peer received what was expected
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame ping = peer.takeFrame();
+    assertEquals(TYPE_PING, ping.type);
+    MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+    assertEquals(TYPE_RST_STREAM, rstStream.type);
+    assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
+  }
 
-        // 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();
+  @Test public void readSendsWindowUpdate() throws Exception {
+    // Write the mocking script.
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    for (int i = 0; i < 3; i++) {
+      peer.sendFrame().data(0, 1, new byte[WINDOW_UPDATE_THRESHOLD]);
+      peer.acceptFrame(); // WINDOW UPDATE
+    }
+    peer.sendFrame().data(FLAG_FIN, 1, new byte[0]);
+    peer.play();
 
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame reply = peer.takeFrame();
-        assertEquals(TYPE_SYN_REPLY, reply.type);
-        assertEquals(FLAG_FIN, reply.flags);
-        assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock);
-        assertEquals(1, receiveCount.get());
+    // 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_SYN_STREAM, 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) {
     }
 
-    @Test public void noop() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.play();
+    // Verify the peer received what was expected.
+    MockSpdyPeer.InFrame synStream = peer.takeFrame();
+    assertEquals(TYPE_SYN_STREAM, synStream.type);
+    MockSpdyPeer.InFrame data = peer.takeFrame();
+    assertEquals(TYPE_DATA, data.type);
+  }
 
-        // play it back
-        SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
-                .handler(REJECT_INCOMING_STREAMS)
-                .build();
-        connection.noop();
+  @Test public void testTruncatedDataFrame() throws Exception {
+    // write the mocking script
+    peer.acceptFrame(); // SYN_STREAM
+    peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
+    peer.sendTruncatedFrame(8 + 100).data(0, 1, new byte[1024]);
+    peer.play();
 
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_NOOP, ping.type);
-        assertEquals(0, ping.flags);
+    // 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());
     }
+  }
 
-    @Test public void serverPingsClient() throws Exception {
-        // write the mocking script
-        peer.sendFrame().ping(0, 2);
-        peer.acceptFrame();
-        peer.play();
+  private void writeAndClose(SpdyStream stream, String data) throws IOException {
+    OutputStream out = stream.getOutputStream();
+    out.write(data.getBytes("UTF-8"));
+    out.close();
+  }
 
-        // 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(0, ping.flags);
-        assertEquals(2, ping.streamId);
+  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);
+  }
 
-    @Test public void clientPingsServer() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.sendFrame().ping(0, 1);
-        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(0, pingFrame.flags);
-        assertEquals(1, pingFrame.streamId);
-    }
-
-    @Test public void unexpectedPingIsNotReturned() throws Exception {
-        // write the mocking script
-        peer.sendFrame().ping(0, 2);
-        peer.acceptFrame();
-        peer.sendFrame().ping(0, 3); // This ping will not be returned.
-        peer.sendFrame().ping(0, 4);
-        peer.acceptFrame();
-        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.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS, settings);
-        peer.sendFrame().ping(0, 2);
-        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(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(0, 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(0, settings2);
-        peer.sendFrame().ping(0, 2);
-        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(SpdyConnection.FLAG_FIN, 42, "bogus".getBytes("UTF-8"));
-        peer.acceptFrame(); // RST_STREAM
-        peer.sendFrame().ping(0, 2);
-        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(0, rstStream.flags);
-        assertEquals(42, rstStream.streamId);
-        assertEquals(RST_INVALID_STREAM, rstStream.statusCode);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(2, ping.streamId);
-    }
-
-    @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception {
-        // write the mocking script
-        peer.sendFrame().synReply(0, 42, Arrays.asList("a", "android"));
-        peer.acceptFrame(); // RST_STREAM
-        peer.sendFrame().ping(0, 2);
-        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(0, rstStream.flags);
-        assertEquals(42, rstStream.streamId);
-        assertEquals(RST_INVALID_STREAM, rstStream.statusCode);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(2, ping.streamId);
-    }
-
-    @Test public void clientClosesClientOutputStream() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN_STREAM
-        peer.acceptFrame(); // TYPE_DATA
-        peer.acceptFrame(); // TYPE_DATA with FLAG_FIN
-        peer.sendFrame().ping(0, 2);
-        peer.acceptFrame(); // PING response
-        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();
+  /** 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 {
-            out.write("round".getBytes(UTF_8));
-            fail();
-        } catch (Exception expected) {
-            assertEquals("stream closed", expected.getMessage());
+          Thread.sleep(delayMillis);
+          toInterrupt.interrupt();
+        } catch (InterruptedException e) {
+          throw new AssertionError();
         }
-        assertEquals(0, connection.openStreamCount());
-
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-        assertEquals(FLAG_UNIDIRECTIONAL, synStream.flags);
-        MockSpdyPeer.InFrame data = peer.takeFrame();
-        assertEquals(TYPE_DATA, data.type);
-        assertEquals(0, data.flags);
-        assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
-        MockSpdyPeer.InFrame fin = peer.takeFrame();
-        assertEquals(TYPE_DATA, fin.type);
-        assertEquals(FLAG_FIN, fin.flags);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
-        assertEquals(2, ping.streamId);
-    }
-
-    @Test public void serverClosesClientOutputStream() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN_STREAM
-        peer.sendFrame().synReset(1, SpdyStream.RST_CANCEL);
-        peer.acceptFrame(); // PING
-        peer.sendFrame().ping(0, 1);
-        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_SYN_STREAM, synStream.type);
-        assertEquals(0, synStream.flags);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
-        assertEquals(1, ping.streamId);
-    }
-
-    /**
-     * 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_SYN_STREAM, synStream.type);
-        assertEquals(SpdyConnection.FLAG_FIN, synStream.flags);
-        MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-        assertEquals(TYPE_RST_STREAM, rstStream.type);
-        assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode);
-    }
-
-    /**
-     * 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_SYN_STREAM, synStream.type);
-        assertEquals(0, synStream.flags);
-        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);
-        assertEquals(FLAG_FIN, fin.flags);
-        MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-        assertEquals(TYPE_RST_STREAM, rstStream.type);
-        assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode);
-    }
-
-    @Test public void serverClosesClientInputStream() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN_STREAM
-        peer.sendFrame().data(FLAG_FIN, 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_SYN_STREAM, synStream.type);
-        assertEquals(SpdyConnection.FLAG_FIN, synStream.flags);
-    }
-
-    @Test public void remoteDoubleSynReply() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
-        peer.acceptFrame(); // PING
-        peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
-        peer.sendFrame().ping(0, 1);
-        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 e) {
-            assertEquals("stream was reset: PROTOCOL_ERROR", e.getMessage());
-        }
-
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-        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(0, rstStream.flags);
-        assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
-    }
-
-    @Test public void remoteDoubleSynStream() throws Exception {
-        // write the mocking script
-        peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
-        peer.acceptFrame();
-        peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana"));
-        peer.acceptFrame();
-        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(-1, stream.getRstStatusCode());
-                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_SYN_REPLY, reply.type);
-        MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-        assertEquals(TYPE_RST_STREAM, rstStream.type);
-        assertEquals(2, rstStream.streamId);
-        assertEquals(0, rstStream.flags);
-        assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
-        assertEquals(1, receiveCount.intValue());
-    }
-
-    @Test public void remoteSendsDataAfterInFinished() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
-        peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8"));
-        peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "c3po".getBytes("UTF-8")); // Ignored.
-        peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded.
-        peer.acceptFrame();
-        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_SYN_STREAM, synStream.type);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
-        assertEquals(2, ping.streamId);
-        assertEquals(0, ping.flags);
-    }
-
-    @Test public void remoteSendsTooMuchData() throws Exception {
-        // write the mocking script
-        peer.acceptFrame();
-        peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
-        peer.sendFrame().data(0, 1, new byte[64 * 1024 + 1]);
-        peer.acceptFrame();
-        peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded.
-        peer.acceptFrame();
-        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_SYN_STREAM, synStream.type);
-        MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-        assertEquals(TYPE_RST_STREAM, rstStream.type);
-        assertEquals(1, rstStream.streamId);
-        assertEquals(0, rstStream.flags);
-        assertEquals(RST_FLOW_CONTROL_ERROR, rstStream.statusCode);
-        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();
-        peer.sendFrame().synReset(1, RST_REFUSED_STREAM);
-        peer.sendFrame().ping(0, 2);
-        peer.acceptFrame();
-        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_SYN_STREAM, synStream.type);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
-        assertEquals(2, ping.streamId);
-        assertEquals(0, ping.flags);
-    }
-
-    @Test public void receiveGoAway() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN STREAM 1
-        peer.acceptFrame(); // SYN STREAM 3
-        peer.sendFrame().goAway(0, 1);
-        peer.acceptFrame(); // PING
-        peer.sendFrame().ping(0, 1);
-        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_SYN_STREAM, synStream1.type);
-        MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, 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(0, 2, 0, 0, Arrays.asList("b", "banana")); // Should be ignored!
-        peer.sendFrame().ping(0, 1);
-        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();
-        ping.roundTripTime(); // Ensure that the SYN STREAM has been received.
-        assertEquals(1, connection.openStreamCount());
-
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, 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);
-    }
-
-    @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();
-        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);
-    }
-
-    @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_SYN_STREAM, synStream.type);
-        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(0, 1, Arrays.asList("a", "android"));
-        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());
-
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-    }
-
-    @Test public void headers() throws Exception {
-        // write the mocking script
-        peer.acceptFrame(); // SYN STREAM
-        peer.acceptFrame(); // PING
-        peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
-        peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po"));
-        peer.sendFrame().ping(0, 1);
-        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_SYN_STREAM, synStream.type);
-        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(0, 1, Arrays.asList("c", "c3po"));
-        peer.acceptFrame(); // RST STREAM
-        peer.sendFrame().ping(0, 1);
-        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 e) {
-            assertEquals("stream was reset: PROTOCOL_ERROR", e.getMessage());
-        }
-
-        // verify the peer received what was expected
-        MockSpdyPeer.InFrame synStream = peer.takeFrame();
-        assertEquals(TYPE_SYN_STREAM, synStream.type);
-        MockSpdyPeer.InFrame ping = peer.takeFrame();
-        assertEquals(TYPE_PING, ping.type);
-        MockSpdyPeer.InFrame rstStream = peer.takeFrame();
-        assertEquals(TYPE_RST_STREAM, rstStream.type);
-        assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode);
-    }
-
-    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);
-    }
+      }
+    }.start();
+  }
 }
diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
index 1b5574d..7371f2e 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
@@ -34,137 +34,128 @@
 import javax.net.ssl.SSLSocketFactory;
 import org.eclipse.jetty.npn.NextProtoNego;
 
-/**
- * A basic SPDY server that serves the contents of a local directory.
- */
+/** A basic SPDY server that serves the contents of a local directory. */
 public final class SpdyServer implements IncomingStreamHandler {
-    private final File baseDirectory;
-    private SSLSocketFactory sslSocketFactory;
+  private final File baseDirectory;
+  private SSLSocketFactory sslSocketFactory;
 
-    public SpdyServer(File baseDirectory) {
-        this.baseDirectory = baseDirectory;
+  public SpdyServer(File baseDirectory) {
+    this.baseDirectory = baseDirectory;
+  }
+
+  public void useHttps(SSLSocketFactory sslSocketFactory) {
+    this.sslSocketFactory = sslSocketFactory;
+  }
+
+  private void run() throws Exception {
+    ServerSocket serverSocket = new ServerSocket(8888);
+    serverSocket.setReuseAddress(true);
+
+    while (true) {
+      Socket socket = serverSocket.accept();
+      if (sslSocketFactory != null) {
+        socket = doSsl(socket);
+      }
+      new SpdyConnection.Builder(false, socket).handler(this).build();
+    }
+  }
+
+  private Socket doSsl(Socket socket) throws IOException {
+    SSLSocket sslSocket =
+        (SSLSocket) sslSocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(),
+            socket.getPort(), true);
+    sslSocket.setUseClientMode(false);
+    NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() {
+      @Override public void unsupported() {
+        System.out.println("UNSUPPORTED");
+      }
+      @Override public List<String> protocols() {
+        return Arrays.asList("spdy/3");
+      }
+      @Override public void protocolSelected(String protocol) {
+        System.out.println("PROTOCOL SELECTED: " + protocol);
+      }
+    });
+    return sslSocket;
+  }
+
+  @Override public void receive(final SpdyStream stream) throws IOException {
+    List<String> 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);
+        break;
+      }
     }
 
-    public void useHttps(SSLSocketFactory sslSocketFactory) {
-        this.sslSocketFactory = sslSocketFactory;
+    if (path == null) {
+      // TODO: send bad request error
+      throw new AssertionError();
     }
 
-    private void run() throws Exception {
-        ServerSocket serverSocket = new ServerSocket(8888);
-        serverSocket.setReuseAddress(true);
+    File file = new File(baseDirectory + path);
 
-        while (true) {
-            Socket socket = serverSocket.accept();
-            if (sslSocketFactory != null) {
-                socket = doSsl(socket);
-            }
-            new SpdyConnection.Builder(false, socket).handler(this).build();
-        }
+    if (file.isDirectory()) {
+      serveDirectory(stream, file.list());
+    } else if (file.exists()) {
+      serveFile(stream, file);
+    } else {
+      send404(stream, path);
+    }
+  }
+
+  private void send404(SpdyStream stream, String path) throws IOException {
+    List<String> responseHeaders =
+        Arrays.asList(":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"));
+    out.close();
+  }
+
+  private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
+    List<String> responseHeaders =
+        Arrays.asList(":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");
+    for (String file : files) {
+      writer.write("<a href='" + file + "'>" + file + "</a><br>");
+    }
+    writer.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)),
+        true);
+    OutputStream out = stream.getOutputStream();
+    int count;
+    while ((count = in.read(buffer)) != -1) {
+      out.write(buffer, 0, count);
+    }
+    out.close();
+  }
+
+  private String contentType(File file) {
+    return file.getName().endsWith(".html") ? "text/html" : "text/plain";
+  }
+
+  public static void main(String... args) throws Exception {
+    if (args.length != 1 || args[0].startsWith("-")) {
+      System.out.println("Usage: SpdyServer <base directory>");
+      return;
     }
 
-    private Socket doSsl(Socket socket) throws IOException {
-        SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
-                socket.getInetAddress().getHostAddress(), socket.getPort(), true);
-        sslSocket.setUseClientMode(false);
-        NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() {
-            @Override public void unsupported() {
-                System.out.println("UNSUPPORTED");
-            }
-            @Override public List<String> protocols() {
-                return Arrays.asList("spdy/2");
-            }
-            @Override public void protocolSelected(String protocol) {
-                System.out.println("PROTOCOL SELECTED: " + protocol);
-            }
-        });
-        return sslSocket;
-    }
-
-    @Override public void receive(final SpdyStream stream) throws IOException {
-        List<String> requestHeaders = stream.getRequestHeaders();
-        String path = null;
-        for (int i = 0; i < requestHeaders.size(); i += 2) {
-            String s = requestHeaders.get(i);
-            if ("url".equals(s)) {
-                path = requestHeaders.get(i + 1);
-                break;
-            }
-        }
-
-        if (path == null) {
-            // TODO: send bad request error
-            throw new AssertionError();
-        }
-
-        File file = new File(baseDirectory + path);
-
-        if (file.isDirectory()) {
-            serveDirectory(stream, file.list());
-        } else if (file.exists()) {
-            serveFile(stream, file);
-        } else {
-            send404(stream, path);
-        }
-    }
-
-    private void send404(SpdyStream stream, String path) throws IOException {
-        List<String> responseHeaders = Arrays.asList(
-                "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"));
-        out.close();
-    }
-
-    private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
-        List<String> responseHeaders = Arrays.asList(
-                "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");
-        for (String file : files) {
-            writer.write("<a href='" + file + "'>" + file + "</a><br>");
-        }
-        writer.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)
-        ), true);
-        OutputStream out = stream.getOutputStream();
-        int count;
-        while ((count = in.read(buffer)) != -1) {
-            out.write(buffer, 0, count);
-        }
-        out.close();
-    }
-
-    private String contentType(File file) {
-        return file.getName().endsWith(".html") ? "text/html" : "text/plain";
-    }
-
-    public static void main(String... args) throws Exception {
-        if (args.length != 1 || args[0].startsWith("-")) {
-            System.out.println("Usage: SpdyServer <base directory>");
-            return;
-        }
-
-        SpdyServer server = new SpdyServer(new File(args[0]));
-        SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName())
-                .build();
-        server.useHttps(sslContext.getSocketFactory());
-        server.run();
-    }
+    SpdyServer server = new SpdyServer(new File(args[0]));
+    SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
+    server.useHttps(sslContext.getSocketFactory());
+    server.run();
+  }
 }