Upgrade OkHttp to newer code

This update includes changes up to
68affbd24d63620e1785ea847f1936760947b9ae / master /
14th March 2014.

Importantly, it contains a DiskLruCache fix which
broke several apps.
https://github.com/square/okhttp/issues/646

Change-Id: I8a489e6d0937a58fad10423154bad939ea4da868
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a34e2d2..1fc6899 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,23 @@
 Change Log
 ==========
 
+## Version 1.5.1
+
+_2014-03-11_
+
+ * Fix 1.5.0 regression where connections should not have been recycled.
+ * Fix 1.5.0 regression where transparent Gzip was broken by attempting to
+   recover from another I/O failure.
+ * Fix problems where spdy/3.1 headers may not have been compressed properly.
+ * Fix problems with spdy/3.1 and http/2 where the wrong window size was being
+   used.
+ * Fix 1.5.0 regression where conditional cache responses could corrupt the
+   connection pool.
+
+
 ## Version 1.5.0
 
-_2014-03-05_
+_2014-03-07_
 
 
 ##### OkHttp no longer uses the default SSL context.
@@ -11,8 +25,8 @@
 Applications that want to use the global SSL context with OkHttp should configure their
 OkHttpClient instances with the following:
 
-```
-    okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
+```java
+okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
 ```
 
 A simpler solution is to avoid the shared default SSL socket factory. Instead, if you
@@ -39,6 +53,7 @@
  * Fix: Don't do DNS lookups on invalid hosts.
  * Fix: Exhaust the underlying stream when reading gzip streams.
  * Fix: Support the `PATCH` method.
+ * Fix: Support request bodies on `DELETE` method.
  * Fix: Drop the `okhttp-protocols` module.
  * Internal: Replaced internal byte array buffers with pooled buffers ("OkBuffer").
 
diff --git a/README.md b/README.md
index 2e298ea..cd3bd02 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@
 
 
  [1]: http://square.github.io/okhttp
- [2]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST&c=jar-with-dependencies
+ [2]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST
  [3]: http://wiki.eclipse.org/Jetty/Feature/NPN
  [4]: https://code.google.com/p/vogar/
  [5]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=mockwebserver&v=LATEST
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
index a3dcf5c..bd9ed97 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
@@ -152,9 +152,11 @@
     assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void cache() throws Exception {
+  @Test public void conditionalCacheHit() throws Exception {
     server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
     server.play();
 
     client.setOkResponseCache(cache);
@@ -174,6 +176,28 @@
     assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
   }
 
+  @Test public void conditionalCacheMiss() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    Request request1 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request1, receiver);
+    receiver.await(request1.url()).assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    Request request2 = new Request.Builder()
+        .url(server.getUrl("/"))
+        .build();
+    client.enqueue(request2, receiver);
+    receiver.await(request2.url()).assertCode(200).assertBody("B");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
   @Test public void redirect() throws Exception {
     server.enqueue(new MockResponse()
         .setResponseCode(301)
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
index f5b54ee..9bc8475 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
@@ -19,6 +19,7 @@
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -73,8 +74,10 @@
   public synchronized RecordedResponse await(URL url) throws Exception {
     long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
     while (true) {
-      for (RecordedResponse recordedResponse : responses) {
+      for (Iterator<RecordedResponse> i = responses.iterator(); i.hasNext(); ) {
+        RecordedResponse recordedResponse = i.next();
         if (recordedResponse.request.url().equals(url)) {
+          i.remove();
           return recordedResponse;
         }
       }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
index ae3c743..1153299 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
@@ -138,9 +138,11 @@
     assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
   }
 
-  @Test public void cache() throws Exception {
+  @Test public void conditionalCacheHit() throws Exception {
     server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
-    server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
     server.play();
 
     client.setOkResponseCache(cache);
@@ -154,6 +156,22 @@
     assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
   }
 
+  @Test public void conditionalCacheMiss() throws Exception {
+    server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+    server.enqueue(new MockResponse().setBody("B"));
+    server.play();
+
+    client.setOkResponseCache(cache);
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("A");
+    assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+    onSuccess(new Request.Builder().url(server.getUrl("/")).build())
+        .assertCode(200).assertBody("B");
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+  }
+
   @Test public void redirect() throws Exception {
     server.enqueue(new MockResponse()
         .setResponseCode(301)
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
index 1380ff0..b6edaae 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
@@ -16,6 +16,7 @@
 
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.OkResponseCache;
@@ -966,6 +967,22 @@
     assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/"))));
   }
 
+  @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
+    server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A"));
+    server.enqueue(new MockResponse()
+        .clearHeaders()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.play();
+
+    ConnectionPool pool = ConnectionPool.getDefault();
+    pool.evictAll();
+    client.setConnectionPool(pool);
+
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+    assertEquals(1, client.getConnectionPool().getConnectionCount());
+  }
+
   @Test public void expiresDateBeforeModifiedDate() throws Exception {
     assertConditionallyCached(
         new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 4e791ad..5e86c8e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -16,6 +16,7 @@
 
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.HttpResponseCache;
 import com.squareup.okhttp.OkAuthenticator.Credential;
 import com.squareup.okhttp.OkHttpClient;
@@ -1172,6 +1173,41 @@
     assertEquals(1, server.takeRequest().getSequenceNumber());
   }
 
+  @Test public void transparentGzipWorksAfterExceptionRecovery() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("a")
+        .setSocketPolicy(SHUTDOWN_INPUT_AT_END));
+    server.enqueue(new MockResponse()
+        .addHeader("Content-Encoding: gzip")
+        .setBody(gzip("b".getBytes(UTF_8))));
+    server.play();
+
+    // Seed the pool with a bad connection.
+    assertContent("a", client.open(server.getUrl("/")));
+
+    // This connection will need to be recovered. When it is, transparent gzip should still work!
+    assertContent("b", client.open(server.getUrl("/")));
+
+    assertEquals(0, server.takeRequest().getSequenceNumber());
+    assertEquals(0, server.takeRequest().getSequenceNumber()); // Connection is not pooled.
+  }
+
+  @Test public void endOfStreamResponseIsNotPooled() throws Exception {
+    server.enqueue(new MockResponse()
+        .setBody("{}")
+        .clearHeaders()
+        .setSocketPolicy(DISCONNECT_AT_END));
+    server.play();
+
+    ConnectionPool pool = ConnectionPool.getDefault();
+    pool.evictAll();
+    client.setConnectionPool(pool);
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    assertContent("{}", connection);
+    assertEquals(0, client.getConnectionPool().getConnectionCount());
+  }
+
   @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
     testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED);
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
index a44af61..fd6007d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -45,7 +45,7 @@
   private final List<OutFrame> outFrames = new ArrayList<OutFrame>();
   private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
   private int port;
-  private final ExecutorService executor = Executors.newCachedThreadPool(
+  private final ExecutorService executor = Executors.newSingleThreadExecutor(
       Util.threadFactory("MockSpdyPeer", false));
   private ServerSocket serverSocket;
   private Socket socket;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
index 44459b8..294684f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
@@ -17,6 +17,7 @@
 
 import org.junit.Test;
 
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
 import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_BANDWIDTH;
 import static com.squareup.okhttp.internal.spdy.Settings.DOWNLOAD_RETRANS_RATE;
 import static com.squareup.okhttp.internal.spdy.Settings.MAX_CONCURRENT_STREAMS;
@@ -68,9 +69,10 @@
     settings.set(DOWNLOAD_RETRANS_RATE, 0, 97);
     assertEquals(97, settings.getDownloadRetransRate(-3));
 
-    assertEquals(-1, settings.getInitialWindowSize());
+    assertEquals(DEFAULT_INITIAL_WINDOW_SIZE,
+        settings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
     settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108);
-    assertEquals(108, settings.getInitialWindowSize());
+    assertEquals(108, settings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
 
     assertEquals(-3, settings.getClientCertificateVectorSize(-3));
     settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
index fbfb2f5..2ef127e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
@@ -39,6 +39,7 @@
 import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
 import static com.squareup.okhttp.internal.spdy.ErrorCode.REFUSED_STREAM;
 import static com.squareup.okhttp.internal.spdy.ErrorCode.STREAM_IN_USE;
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
 import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
 import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_DATA;
 import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_GOAWAY;
@@ -47,7 +48,6 @@
 import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
 import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_SETTINGS;
 import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
-import static com.squareup.okhttp.internal.spdy.SpdyConnection.INITIAL_WINDOW_SIZE;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -301,7 +301,7 @@
     // This stream was created *after* the connection settings were adjusted.
     SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
 
-    assertEquals(3368, connection.peerSettings.getInitialWindowSize());
+    assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
     assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
     // New Stream is has the most recent initial window size.
     assertEquals(3368, stream.bytesLeftInWriteWindow);
@@ -405,6 +405,35 @@
     }
   }
 
+  @Test public void clearSettingsBeforeMerge() throws Exception {
+    // write the mocking script
+    Settings settings1 = new Settings();
+    settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100);
+    settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200);
+    settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300);
+    peer.sendFrame().settings(settings1);
+    peer.sendFrame().ping(false, 2, 0);
+    peer.acceptFrame();
+    peer.play();
+
+    // play it back
+    SpdyConnection connection = connection(peer, SPDY3);
+
+    peer.takeFrame(); // Guarantees that the Settings frame has been processed.
+
+    // fake a settings frame with clear flag set.
+    Settings settings2 = new Settings();
+    settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600);
+    connection.readerRunnable.settings(true, settings2);
+
+    synchronized (connection) {
+      assertEquals(-1, connection.peerSettings.getUploadBandwidth(-1));
+      assertEquals(-1, connection.peerSettings.getDownloadBandwidth(-1));
+      assertEquals(-1, connection.peerSettings.getDownloadRetransRate(-1));
+      assertEquals(600, connection.peerSettings.getMaxConcurrentStreams(-1));
+    }
+  }
+
   @Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception {
     // write the mocking script
     peer.sendFrame().data(true, 41, new OkBuffer().writeUtf8("bogus"));
@@ -1042,7 +1071,7 @@
       throws IOException, InterruptedException {
     peer.setVariantAndClient(variant, false);
 
-    int windowUpdateThreshold = INITIAL_WINDOW_SIZE / 2;
+    int windowUpdateThreshold = DEFAULT_INITIAL_WINDOW_SIZE / 2;
 
     // Write the mocking script.
     peer.acceptFrame(); // SYN_STREAM
@@ -1061,7 +1090,6 @@
 
     // Play it back.
     SpdyConnection connection = connection(peer, variant);
-    connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, INITIAL_WINDOW_SIZE);
     SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(0, stream.unacknowledgedBytesRead);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
@@ -1153,7 +1181,7 @@
   }
 
   @Test public void writeAwaitsWindowUpdate() throws Exception {
-    int framesThatFillWindow = roundUp(INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
 
     // Write the mocking script. This accepts more data frames than necessary!
     peer.acceptFrame(); // SYN_STREAM
@@ -1167,7 +1195,7 @@
     SpdyConnection connection = connection(peer, SPDY3);
     SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     BufferedSink out = Okio.buffer(stream.getSink());
-    out.write(new byte[INITIAL_WINDOW_SIZE]);
+    out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
     out.flush();
 
     // Check that we've filled the window for both the stream and also the connection.
@@ -1195,7 +1223,7 @@
   }
 
   @Test public void initialSettingsWithWindowSizeAdjustsConnection() throws Exception {
-    int framesThatFillWindow = roundUp(INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
 
     // Write the mocking script. This accepts more data frames than necessary!
     peer.acceptFrame(); // SYN_STREAM
@@ -1209,7 +1237,7 @@
     SpdyConnection connection = connection(peer, SPDY3);
     SpdyStream stream = connection.newStream(headerEntries("a", "apple"), true, true);
     BufferedSink out = Okio.buffer(stream.getSink());
-    out.write(new byte[INITIAL_WINDOW_SIZE]);
+    out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
     out.flush();
 
     // write 1 more than the window size
@@ -1222,7 +1250,7 @@
 
     // Receiving a Settings with a larger window size will unblock the streams.
     Settings initial = new Settings();
-    initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, INITIAL_WINDOW_SIZE + 1);
+    initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 1);
     connection.readerRunnable.settings(false, initial);
 
     assertEquals(1, connection.bytesLeftInWriteWindow);
@@ -1236,7 +1264,7 @@
 
     // Settings after the initial do not affect the connection window size.
     Settings next = new Settings();
-    next.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, INITIAL_WINDOW_SIZE + 2);
+    next.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 2);
     connection.readerRunnable.settings(false, next);
 
     assertEquals(0, connection.bytesLeftInWriteWindow); // connection wasn't affected.
@@ -1264,7 +1292,7 @@
   }
 
   @Test public void blockedStreamDoesntStarveNewStream() throws Exception {
-    int framesThatFillWindow = roundUp(INITIAL_WINDOW_SIZE, SPDY3.maxFrameSize());
+    int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, SPDY3.maxFrameSize());
 
     // Write the mocking script. This accepts more data frames than necessary!
     peer.acceptFrame(); // SYN_STREAM on stream 1
@@ -1279,7 +1307,7 @@
     SpdyConnection connection = connection(peer, SPDY3);
     SpdyStream stream1 = connection.newStream(headerEntries("a", "apple"), true, true);
     BufferedSink out1 = Okio.buffer(stream1.getSink());
-    out1.write(new byte[INITIAL_WINDOW_SIZE]);
+    out1.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
     out1.flush();
 
     // Check that we've filled the window for both the stream and also the connection.
@@ -1300,7 +1328,7 @@
 
     assertEquals(0, connection.bytesLeftInWriteWindow);
     assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
-    assertEquals(INITIAL_WINDOW_SIZE - 3, connection.getStream(3).bytesLeftInWriteWindow);
+    assertEquals(DEFAULT_INITIAL_WINDOW_SIZE - 3, connection.getStream(3).bytesLeftInWriteWindow);
   }
 
   @Test public void maxFrameSizeHonored() throws Exception {
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 25e165f..c7e3ec4 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -18,11 +18,6 @@
       <artifactId>okio</artifactId>
       <version>${project.version}</version>
     </dependency>
-    <dependency>
-      <groupId>com.squareup.okhttp</groupId>
-      <artifactId>okhttp-protocols</artifactId>
-      <version>${project.version}</version>
-    </dependency>
   </dependencies>
 
   <build>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index d9115a9..55884dc 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -25,16 +25,10 @@
 import com.squareup.okhttp.internal.spdy.SpdyConnection;
 import java.io.Closeable;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.net.Proxy;
 import java.net.Socket;
-import java.net.SocketTimeoutException;
 import javax.net.ssl.SSLSocket;
-import okio.BufferedSink;
-import okio.BufferedSource;
 import okio.ByteString;
-import okio.Okio;
 
 import static java.net.HttpURLConnection.HTTP_OK;
 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
@@ -70,10 +64,6 @@
   private final Route route;
 
   private Socket socket;
-  private InputStream in;
-  private OutputStream out;
-  private BufferedSource source;
-  private BufferedSink sink;
   private boolean connected = false;
   private HttpConnection httpConnection;
   private SpdyConnection spdyConnection;
@@ -94,14 +84,11 @@
     socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
     Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
     socket.setSoTimeout(readTimeout);
-    in = socket.getInputStream();
-    out = socket.getOutputStream();
 
     if (route.address.sslSocketFactory != null) {
       upgradeToTls(tunnelRequest);
     } else {
-      initSourceAndSink();
-      httpConnection = new HttpConnection(pool, this, source, sink);
+      httpConnection = new HttpConnection(pool, this, socket);
     }
     connected = true;
   }
@@ -128,19 +115,19 @@
       platform.supportTlsIntolerantServer(sslSocket);
     }
 
-    boolean useNpn = route.modernTls && (// Contains a spdy variant.
-        route.address.protocols.contains(Protocol.HTTP_2)
-     || route.address.protocols.contains(Protocol.SPDY_3)
-    );
-
-    if (useNpn) {
-      if (route.address.protocols.contains(Protocol.HTTP_2) // Contains both spdy variants.
-          && route.address.protocols.contains(Protocol.SPDY_3)) {
+    boolean useNpn = false;
+    if (route.modernTls) {
+      boolean http2 = route.address.protocols.contains(Protocol.HTTP_2);
+      boolean spdy3 = route.address.protocols.contains(Protocol.SPDY_3);
+      if (http2 && spdy3) {
         platform.setNpnProtocols(sslSocket, Protocol.HTTP2_SPDY3_AND_HTTP);
-      } else if (route.address.protocols.contains(Protocol.HTTP_2)) {
+        useNpn = true;
+      } else if (http2) {
         platform.setNpnProtocols(sslSocket, Protocol.HTTP2_AND_HTTP_11);
-      } else {
+        useNpn = true;
+      } else if (spdy3) {
         platform.setNpnProtocols(sslSocket, Protocol.SPDY3_AND_HTTP11);
+        useNpn = true;
       }
     }
 
@@ -152,10 +139,7 @@
       throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
     }
 
-    out = sslSocket.getOutputStream();
-    in = sslSocket.getInputStream();
     handshake = Handshake.get(sslSocket.getSession());
-    initSourceAndSink();
 
     ByteString maybeProtocol;
     Protocol selectedProtocol = Protocol.HTTP_11;
@@ -165,11 +149,11 @@
 
     if (selectedProtocol.spdyVariant) {
       sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
-      spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, source, sink)
+      spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
           .protocol(selectedProtocol).build();
       spdyConnection.sendConnectionHeader();
     } else {
-      httpConnection = new HttpConnection(pool, this, source, sink);
+      httpConnection = new HttpConnection(pool, this, socket);
     }
   }
 
@@ -206,28 +190,8 @@
    * #isAlive()}; callers should check {@link #isAlive()} first.
    */
   public boolean isReadable() {
-    if (source == null) {
-      return true; // Optimistic.
-    }
-    if (isSpdy()) {
-      return true; // Optimistic. We can't test SPDY because its streams are in use.
-    }
-    try {
-      int readTimeout = socket.getSoTimeout();
-      try {
-        socket.setSoTimeout(1);
-        if (source.exhausted()) {
-          return false; // Stream is exhausted; socket is closed.
-        }
-        return true;
-      } finally {
-        socket.setSoTimeout(readTimeout);
-      }
-    } catch (SocketTimeoutException ignored) {
-      return true; // Read timed out; socket is good.
-    } catch (IOException e) {
-      return false; // Couldn't read; socket is closed.
-    }
+    if (httpConnection != null) return httpConnection.isReadable();
+    return true; // SPDY connections, and connections before connect() are both optimistic.
   }
 
   public void resetIdleStartTime() {
@@ -320,9 +284,7 @@
    * retried if the proxy requires authorization.
    */
   private void makeTunnel(TunnelRequest tunnelRequest) throws IOException {
-    BufferedSource tunnelSource = Okio.buffer(Okio.source(in));
-    BufferedSink tunnelSink = Okio.buffer(Okio.sink(out));
-    HttpConnection tunnelConnection = new HttpConnection(pool, this, tunnelSource, tunnelSink);
+    HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
     Request request = tunnelRequest.getRequest();
     String requestLine = tunnelRequest.requestLine();
     while (true) {
@@ -335,7 +297,7 @@
         case HTTP_OK:
           // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If that
           // happens, then we will have buffered bytes that are needed by the SSLSocket!
-          if (tunnelSource.buffer().size() > 0) {
+          if (tunnelConnection.bufferSize() > 0) {
             throw new IOException("TLS tunnel buffered too many bytes!");
           }
           return;
@@ -352,9 +314,4 @@
       }
     }
   }
-
-  private void initSourceAndSink() throws IOException {
-    source = Okio.buffer(Okio.source(in));
-    sink = Okio.buffer(Okio.sink(out));
-  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
index ad0a229..c6edf4b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
@@ -413,29 +413,29 @@
     public Entry(InputStream in) throws IOException {
       try {
         BufferedSource source = Okio.buffer(Okio.source(in));
-        url = source.readUtf8Line(true);
-        requestMethod = source.readUtf8Line(true);
+        url = source.readUtf8LineStrict();
+        requestMethod = source.readUtf8LineStrict();
         Headers.Builder varyHeadersBuilder = new Headers.Builder();
         int varyRequestHeaderLineCount = readInt(source);
         for (int i = 0; i < varyRequestHeaderLineCount; i++) {
-          varyHeadersBuilder.addLine(source.readUtf8Line(true));
+          varyHeadersBuilder.addLine(source.readUtf8LineStrict());
         }
         varyHeaders = varyHeadersBuilder.build();
 
-        statusLine = source.readUtf8Line(true);
+        statusLine = source.readUtf8LineStrict();
         Headers.Builder responseHeadersBuilder = new Headers.Builder();
         int responseHeaderLineCount = readInt(source);
         for (int i = 0; i < responseHeaderLineCount; i++) {
-          responseHeadersBuilder.addLine(source.readUtf8Line(true));
+          responseHeadersBuilder.addLine(source.readUtf8LineStrict());
         }
         responseHeaders = responseHeadersBuilder.build();
 
         if (isHttps()) {
-          String blank = source.readUtf8Line(true);
+          String blank = source.readUtf8LineStrict();
           if (blank.length() > 0) {
             throw new IOException("expected \"\" but was \"" + blank + "\"");
           }
-          String cipherSuite = source.readUtf8Line(true);
+          String cipherSuite = source.readUtf8LineStrict();
           List<Certificate> peerCertificates = readCertificateList(source);
           List<Certificate> localCertificates = readCertificateList(source);
           handshake = Handshake.get(cipherSuite, peerCertificates, localCertificates);
@@ -494,7 +494,7 @@
         CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
         List<Certificate> result = new ArrayList<Certificate>(length);
         for (int i = 0; i < length; i++) {
-          String line = source.readUtf8Line(true);
+          String line = source.readUtf8LineStrict();
           byte[] bytes = ByteString.decodeBase64(line).toByteArray();
           result.add(certificateFactory.generateCertificate(new ByteArrayInputStream(bytes)));
         }
@@ -537,7 +537,7 @@
   }
 
   private static int readInt(BufferedSource source) throws IOException {
-    String line = source.readUtf8Line(true);
+    String line = source.readUtf8LineStrict();
     try {
       return Integer.parseInt(line);
     } catch (NumberFormatException e) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Job.java b/okhttp/src/main/java/com/squareup/okhttp/Job.java
index 54dff42..64ce188 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Job.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Job.java
@@ -149,10 +149,7 @@
       if (redirect == null) {
         engine.releaseConnection();
         return response.newBuilder()
-            // Cache body includes original content-length and content-type data.
-            .body(engine.responseSource().usesCache()
-                ? engine.getResponse().body()
-                : new RealResponseBody(response, engine.getResponseBody()))
+            .body(new RealResponseBody(response, engine.getResponseBody()))
             .redirectedBy(redirectedBy)
             .build();
       }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 9540608..bb4be68 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -40,7 +40,17 @@
 import javax.net.ssl.SSLSocketFactory;
 import okio.ByteString;
 
-/** Configures and creates HTTP connections. */
+/**
+ * Configures and creates HTTP connections. Most applications can use a single
+ * OkHttpClient for all of their HTTP requests - benefiting from a shared
+ * response cache, thread pool, connection re-use, etc.
+ *
+ * Instances of OkHttpClient are intended to be fully configured before they're
+ * shared - once shared they should be treated as immutable and can safely be used
+ * to concurrently open new connections. If required, threads can call
+ * {@link #clone()} to make a shallow copy of the OkHttpClient that can be
+ * safely modified with further configuration changes.
+ */
 public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable {
 
   private final RouteDatabase routeDatabase;
@@ -279,8 +289,9 @@
   }
 
   /**
-   * @deprecated OkHttp 2 enforces an enumeration of {@link Protocol protocols}
-   * that can be selected. Please switch to {@link #setProtocols(java.util.List)}.
+   * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
+   *     protocols} that can be selected. Please switch to {@link
+   *     #setProtocols(java.util.List)}.
    */
   @Deprecated
   public OkHttpClient setTransports(List<String> transports) {
@@ -337,8 +348,9 @@
   }
 
   /**
-   * @deprecated OkHttp 2 enforces an enumeration of {@link Protocol protocols}
-   * that can be selected. Please switch to {@link #getProtocols()}.
+   * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
+   *     protocols} that can be selected. Please switch to {@link
+   *     #getProtocols()}.
    */
   @Deprecated
   public List<String> getTransports() {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index 569da9b..7f4fa11 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -243,11 +243,11 @@
   private void readJournal() throws IOException {
     BufferedSource source = Okio.buffer(Okio.source(new FileInputStream(journalFile)));
     try {
-      String magic = source.readUtf8Line(true);
-      String version = source.readUtf8Line(true);
-      String appVersionString = source.readUtf8Line(true);
-      String valueCountString = source.readUtf8Line(true);
-      String blank = source.readUtf8Line(true);
+      String magic = source.readUtf8LineStrict();
+      String version = source.readUtf8LineStrict();
+      String appVersionString = source.readUtf8LineStrict();
+      String valueCountString = source.readUtf8LineStrict();
+      String blank = source.readUtf8LineStrict();
       if (!MAGIC.equals(magic)
           || !VERSION_1.equals(version)
           || !Integer.toString(appVersion).equals(appVersionString)
@@ -260,7 +260,7 @@
       int lineCount = 0;
       while (true) {
         try {
-          readJournalLine(source.readUtf8Line(true));
+          readJournalLine(source.readUtf8LineStrict());
           lineCount++;
         } catch (EOFException endOfJournal) {
           break;
@@ -376,8 +376,9 @@
   }
 
   private static void deleteIfExists(File file) throws IOException {
-    if (file.exists() && !file.delete()) {
-      throw new IOException();
+    // If delete() fails, make sure it's because the file didn't exist!
+    if (!file.delete() && file.exists()) {
+      throw new IOException("failed to delete " + file);
     }
   }
 
@@ -477,7 +478,7 @@
    * Returns the maximum number of bytes that this cache should use to store
    * its data.
    */
-  public long getMaxSize() {
+  public synchronized long getMaxSize() {
     return maxSize;
   }
 
@@ -580,9 +581,7 @@
 
     for (int i = 0; i < valueCount; i++) {
       File file = entry.getCleanFile(i);
-      if (!file.delete()) {
-        throw new IOException("failed to delete " + file);
-      }
+      deleteIfExists(file);
       size -= entry.lengths[i];
       entry.lengths[i] = 0;
     }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
index d1bf250..51e04e8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -265,7 +265,7 @@
   }
 
   /** Returns an immutable list containing {@code elements}. */
-  public static <T> List<T> immutableList(T[] elements) {
+  public static <T> List<T> immutableList(T... elements) {
     return Collections.unmodifiableList(Arrays.asList(elements.clone()));
   }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
index 7639bfa..b12b12d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -27,6 +27,7 @@
 import java.net.CacheRequest;
 import java.net.ProtocolException;
 import java.net.Socket;
+import java.net.SocketTimeoutException;
 import okio.BufferedSink;
 import okio.BufferedSource;
 import okio.Deadline;
@@ -72,18 +73,20 @@
 
   private final ConnectionPool pool;
   private final Connection connection;
+  private final Socket socket;
   private final BufferedSource source;
   private final BufferedSink sink;
 
   private int state = STATE_IDLE;
   private int onIdle = ON_IDLE_HOLD;
 
-  public HttpConnection(ConnectionPool pool, Connection connection, BufferedSource source,
-      BufferedSink sink) {
+  public HttpConnection(ConnectionPool pool, Connection connection, Socket socket)
+      throws IOException {
     this.pool = pool;
     this.connection = connection;
-    this.source = source;
-    this.sink = sink;
+    this.socket = socket;
+    this.source = Okio.buffer(Okio.source(socket.getInputStream()));
+    this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
   }
 
   /**
@@ -123,6 +126,31 @@
     sink.flush();
   }
 
+  /** Returns the number of buffered bytes immediately readable. */
+  public long bufferSize() {
+    return source.buffer().size();
+  }
+
+  /** Test for a stale socket. */
+  public boolean isReadable() {
+    try {
+      int readTimeout = socket.getSoTimeout();
+      try {
+        socket.setSoTimeout(1);
+        if (source.exhausted()) {
+          return false; // Stream is exhausted; socket is closed.
+        }
+        return true;
+      } finally {
+        socket.setSoTimeout(readTimeout);
+      }
+    } catch (SocketTimeoutException ignored) {
+      return true; // Read timed out; socket is good.
+    } catch (IOException e) {
+      return false; // Couldn't read; socket is closed.
+    }
+  }
+
   /** Returns bytes of a request header for sending on an HTTP transport. */
   public void writeRequest(Headers headers, String requestLine) throws IOException {
     if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
@@ -144,7 +172,7 @@
     }
 
     while (true) {
-      String statusLineString = source.readUtf8Line(true);
+      String statusLineString = source.readUtf8LineStrict();
       StatusLine statusLine = new StatusLine(statusLineString);
 
       Response.Builder responseBuilder = new Response.Builder()
@@ -165,7 +193,7 @@
   /** Reads headers or trailers into {@code builder}. */
   public void readHeaders(Headers.Builder builder) throws IOException {
     // parse the result headers until the first blank line
-    for (String line; (line = source.readUtf8Line(true)).length() != 0; ) {
+    for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
       builder.addLine(line);
     }
   }
@@ -177,7 +205,6 @@
    * that may never occur.
    */
   public boolean discard(Source in, int timeoutMillis) {
-    Socket socket = connection.getSocket();
     try {
       int socketTimeout = socket.getSoTimeout();
       socket.setSoTimeout(timeoutMillis);
@@ -356,7 +383,7 @@
      * Closes the cache entry and makes the socket available for reuse. This
      * should be invoked when the end of the body has been reached.
      */
-    protected final void endOfInput() throws IOException {
+    protected final void endOfInput(boolean recyclable) throws IOException {
       if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
 
       if (cacheRequest != null) {
@@ -364,7 +391,7 @@
       }
 
       state = STATE_IDLE;
-      if (onIdle == ON_IDLE_POOL) {
+      if (recyclable && onIdle == ON_IDLE_POOL) {
         onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
         pool.recycle(connection);
       } else if (onIdle == ON_IDLE_CLOSE) {
@@ -402,7 +429,7 @@
       super(cacheRequest);
       bytesRemaining = length;
       if (bytesRemaining == 0) {
-        endOfInput();
+        endOfInput(true);
       }
     }
 
@@ -421,7 +448,7 @@
       bytesRemaining -= read;
       cacheWrite(sink, read);
       if (bytesRemaining == 0) {
-        endOfInput();
+        endOfInput(true);
       }
       return read;
     }
@@ -478,9 +505,9 @@
     private void readChunkSize() throws IOException {
       // read the suffix of the previous chunk
       if (bytesRemainingInChunk != NO_CHUNK_YET) {
-        source.readUtf8Line(true);
+        source.readUtf8LineStrict();
       }
-      String chunkSizeString = source.readUtf8Line(true);
+      String chunkSizeString = source.readUtf8LineStrict();
       int index = chunkSizeString.indexOf(";");
       if (index != -1) {
         chunkSizeString = chunkSizeString.substring(0, index);
@@ -495,7 +522,7 @@
         Headers.Builder trailersBuilder = new Headers.Builder();
         readHeaders(trailersBuilder);
         httpEngine.receiveHeaders(trailersBuilder.build());
-        endOfInput();
+        endOfInput(true);
       }
     }
 
@@ -530,7 +557,7 @@
       long read = source.read(sink, byteCount);
       if (read == -1) {
         inputExhausted = true;
-        endOfInput();
+        endOfInput(false);
         return -1;
       }
       cacheWrite(sink, read);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 20bc3b7..1ce0f44 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -98,6 +98,7 @@
    */
   public final boolean bufferRequestBody;
 
+  private Request originalRequest;
   private Request request;
   private Sink requestBodyOut;
   private BufferedSink bufferedRequestBody;
@@ -135,6 +136,7 @@
   public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
       Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut) {
     this.client = client;
+    this.originalRequest = request;
     this.request = request;
     this.bufferRequestBody = bufferRequestBody;
     this.connection = connection;
@@ -328,7 +330,7 @@
     Connection connection = close();
 
     // For failure recovery, use the same route selector with a new connection.
-    return new HttpEngine(client, request, bufferRequestBody, connection, routeSelector,
+    return new HttpEngine(client, originalRequest, bufferRequestBody, connection, routeSelector,
         (RetryableSink) requestBodyOut);
   }
 
@@ -369,7 +371,7 @@
    * be released immediately.
    */
   public final void releaseConnection() throws IOException {
-    if (transport != null) {
+    if (transport != null && connection != null) {
       transport.releaseConnectionOnIdle();
     }
     connection = null;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index f3afcac..e775d34 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -50,6 +50,25 @@
 import static com.squareup.okhttp.internal.spdy.Header.VERSION;
 
 public final class SpdyTransport implements Transport {
+  /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
+  private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
+      ByteString.encodeUtf8("connection"),
+      ByteString.encodeUtf8("host"),
+      ByteString.encodeUtf8("keep-alive"),
+      ByteString.encodeUtf8("proxy-connection"),
+      ByteString.encodeUtf8("transfer-encoding"));
+
+  /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
+  private static final List<ByteString> HTTP_2_PROHIBITED_HEADERS = Util.immutableList(
+      ByteString.encodeUtf8("connection"),
+      ByteString.encodeUtf8("host"),
+      ByteString.encodeUtf8("keep-alive"),
+      ByteString.encodeUtf8("proxy-connection"),
+      ByteString.encodeUtf8("te"),
+      ByteString.encodeUtf8("transfer-encoding"),
+      ByteString.encodeUtf8("encoding"),
+      ByteString.encodeUtf8("upgrade"));
+
   private final HttpEngine httpEngine;
   private final SpdyConnection spdyConnection;
   private SpdyStream stream;
@@ -206,32 +225,13 @@
 
   /** When true, this header should not be emitted or consumed. */
   private static boolean isProhibitedHeader(Protocol protocol, ByteString name) {
-    boolean prohibited = false;
     if (protocol == Protocol.SPDY_3) {
-      // http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request
-      if (name.equalsAscii("connection")
-          || name.equalsAscii("host")
-          || name.equalsAscii("keep-alive")
-          || name.equalsAscii("proxy-connection")
-          || name.equalsAscii("transfer-encoding")) {
-        prohibited = true;
-      }
+      return SPDY_3_PROHIBITED_HEADERS.contains(name);
     } else if (protocol == Protocol.HTTP_2) {
-      // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3
-      if (name.equalsAscii("connection")
-          || name.equalsAscii("host")
-          || name.equalsAscii("keep-alive")
-          || name.equalsAscii("proxy-connection")
-          || name.equalsAscii("te")
-          || name.equalsAscii("transfer-encoding")
-          || name.equalsAscii("encoding")
-          || name.equalsAscii("upgrade")) {
-        prohibited = true;
-      }
+      return HTTP_2_PROHIBITED_HEADERS.contains(name);
     } else {
       throw new AssertionError(protocol);
     }
-    return prohibited;
   }
 
   /** An HTTP message body terminated by the end of the underlying stream. */
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
index c05d6b1..bf43088 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
@@ -22,6 +22,12 @@
  * Settings are {@link com.squareup.okhttp.internal.spdy.SpdyConnection connection} scoped.
  */
 final class Settings {
+  /**
+   * From the SPDY/3 and HTTP/2 specs, the default initial window size for all
+   * streams is 64 KiB. (Chrome 25 uses 10 MiB).
+   */
+  static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024;
+
   /** Peer request to clear durable settings. */
   static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
 
@@ -171,11 +177,9 @@
     return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
   }
 
-  // TODO: honor this setting in http/2.
-  /** Returns -1 if unset. */
-  int getInitialWindowSize() {
+  int getInitialWindowSize(int defaultValue) {
     int bit = 1 << INITIAL_WINDOW_SIZE;
-    return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : -1;
+    return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
   }
 
   /** spdy/3 only. */
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index fab8698..da7c4e1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -21,6 +21,7 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -38,6 +39,8 @@
 import okio.OkBuffer;
 import okio.Okio;
 
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+
 /**
  * A socket connection to a remote peer. A connection hosts streams which can
  * send and receive data.
@@ -89,8 +92,6 @@
   private final PushObserver pushObserver;
   private int nextPingId;
 
-  static final int INITIAL_WINDOW_SIZE = 65535;
-
   /**
    * The total number of bytes consumed by the application, but not yet
    * acknowledged by sending a {@code WINDOW_UPDATE} frame on this connection.
@@ -107,15 +108,12 @@
 
   /** Settings we communicate to the peer. */
   // TODO: Do we want to dynamically adjust settings, or KISS and only set once?
-  final Settings okHttpSettings = new Settings()
-      .set(Settings.INITIAL_WINDOW_SIZE, 0, INITIAL_WINDOW_SIZE);
-      // TODO: implement stream limit
+  final Settings okHttpSettings = new Settings();
       // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max);
 
   /** Settings we receive from the peer. */
   // TODO: MWS will need to guard on this setting before attempting to push.
-  final Settings peerSettings = new Settings()
-      .set(Settings.INITIAL_WINDOW_SIZE, 0, INITIAL_WINDOW_SIZE);
+  final Settings peerSettings = new Settings();
 
   private boolean receivedInitialPeerSettings = false;
   final FrameReader frameReader;
@@ -151,7 +149,7 @@
     } else {
       throw new AssertionError(protocol);
     }
-    bytesLeftInWriteWindow = peerSettings.getInitialWindowSize();
+    bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
     frameReader = variant.newReader(builder.source, client);
     frameWriter = variant.newWriter(builder.sink, client);
     maxFrameSize = variant.maxFrameSize();
@@ -502,19 +500,18 @@
     private boolean client;
 
     public Builder(boolean client, Socket socket) throws IOException {
-      this("", client, Okio.buffer(Okio.source(socket.getInputStream())),
-          Okio.buffer(Okio.sink(socket.getOutputStream())));
+      this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket);
     }
 
     /**
      * @param client true if this peer initiated the connection; false if this
      *     peer accepted the connection.
      */
-    public Builder(String hostName, boolean client, BufferedSource source, BufferedSink sink) {
+    public Builder(String hostName, boolean client, Socket socket) throws IOException {
       this.hostName = hostName;
       this.client = client;
-      this.source = source;
-      this.sink = sink;
+      this.source = Okio.buffer(Okio.source(socket.getInputStream()));
+      this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
     }
 
     public Builder handler(IncomingStreamHandler handler) {
@@ -657,16 +654,13 @@
       long delta = 0;
       SpdyStream[] streamsToNotify = null;
       synchronized (SpdyConnection.this) {
-        int priorWriteWindowSize = peerSettings.getInitialWindowSize();
-        if (clearPrevious) {
-          peerSettings.clear();
-        } else {
-          peerSettings.merge(newSettings);
-        }
+        int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+        if (clearPrevious) peerSettings.clear();
+        peerSettings.merge(newSettings);
         if (getProtocol() == Protocol.HTTP_2) {
           ackSettingsLater();
         }
-        int peerInitialWindowSize = peerSettings.getInitialWindowSize();
+        int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
         if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) {
           delta = peerInitialWindowSize - priorWriteWindowSize;
           if (!receivedInitialPeerSettings) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
index d48e3dc..0fcde2d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -28,6 +28,8 @@
 import okio.Sink;
 import okio.Source;
 
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+
 /** A logical bidirectional stream. */
 public final class SpdyStream {
   // Internal state is guarded by this. No long-running or potentially
@@ -76,8 +78,10 @@
     if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
     this.id = id;
     this.connection = connection;
-    this.bytesLeftInWriteWindow = connection.peerSettings.getInitialWindowSize();
-    this.source = new SpdyDataSource(connection.okHttpSettings.getInitialWindowSize());
+    this.bytesLeftInWriteWindow =
+        connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
+    this.source = new SpdyDataSource(
+        connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
     this.sink = new SpdyDataSink();
     this.source.finished = inFinished;
     this.sink.finished = outFinished;
@@ -370,7 +374,8 @@
 
         // Flow control: notify the peer that we're ready for more data!
         unacknowledgedBytesRead += read;
-        if (unacknowledgedBytesRead >= connection.okHttpSettings.getInitialWindowSize() / 2) {
+        if (unacknowledgedBytesRead
+            >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
           connection.writeWindowUpdateLater(id, unacknowledgedBytesRead);
           unacknowledgedBytesRead = 0;
         }
@@ -380,7 +385,7 @@
       synchronized (connection) { // Multiple application threads may hit this section.
         connection.unacknowledgedBytesRead += read;
         if (connection.unacknowledgedBytesRead
-            >= connection.okHttpSettings.getInitialWindowSize() / 2) {
+            >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
           connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead);
           connection.unacknowledgedBytesRead = 0;
         }
diff --git a/okio/src/main/java/okio/BufferedSink.java b/okio/src/main/java/okio/BufferedSink.java
index e5cc42d..3066011 100644
--- a/okio/src/main/java/okio/BufferedSink.java
+++ b/okio/src/main/java/okio/BufferedSink.java
@@ -43,16 +43,28 @@
   /** Encodes {@code string} in UTF-8 and writes it to this sink. */
   BufferedSink writeUtf8(String string) throws IOException;
 
-  /** Writes byte to the end of this sink. */
+  /** Writes a byte to this sink. */
   BufferedSink writeByte(int b) throws IOException;
 
-  /** Writes a Big-Endian short to the end of this sink. */
+  /** Writes a big-endian short to this sink using two bytes. */
   BufferedSink writeShort(int s) throws IOException;
 
-  /** Writes a Big-Endian int to the end of this sink. */
+  /** Writes a little-endian short to this sink using two bytes. */
+  BufferedSink writeShortLe(int s) throws IOException;
+
+  /** Writes a big-endian int to this sink using four bytes. */
   BufferedSink writeInt(int i) throws IOException;
 
-  /** Writes complete segments to the sink. Like {@link #flush}, but weaker. */
+  /** Writes a little-endian int to this sink using four bytes. */
+  BufferedSink writeIntLe(int i) throws IOException;
+
+  /** Writes a big-endian long to this sink using eight bytes. */
+  BufferedSink writeLong(long v) throws IOException;
+
+  /** Writes a little-endian long to this sink using eight bytes. */
+  BufferedSink writeLongLe(long v) throws IOException;
+
+  /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */
   BufferedSink emitCompleteSegments() throws IOException;
 
   /** Returns an output stream that writes to this sink. */
diff --git a/okio/src/main/java/okio/BufferedSource.java b/okio/src/main/java/okio/BufferedSource.java
index 678721b..2b48823 100644
--- a/okio/src/main/java/okio/BufferedSource.java
+++ b/okio/src/main/java/okio/BufferedSource.java
@@ -28,9 +28,8 @@
   OkBuffer buffer();
 
   /**
-   * Returns true if there are no more bytes in the buffer or the source. This
-   * will block until there are bytes to read or the source is definitely
-   * exhausted.
+   * Returns true if there are no more bytes in this source. This will block
+   * until there are bytes to read or the source is definitely exhausted.
    */
   boolean exhausted() throws IOException;
 
@@ -41,25 +40,31 @@
    */
   void require(long byteCount) throws IOException;
 
-  /** Removes a byte from the front of this buffer and returns it. */
+  /** Removes a byte from this source and returns it. */
   byte readByte() throws IOException;
 
-  /** Removes a Big-Endian short from the front of this buffer and returns it. */
+  /** Removes two bytes from this source and returns a big-endian short. */
   short readShort() throws IOException;
 
-  /** Removes a Little-Endian short from the front of this buffer and returns it. */
-  int readShortLe() throws IOException;
+  /** Removes two bytes from this source and returns a little-endian short. */
+  short readShortLe() throws IOException;
 
-  /** Removes a Big-Endian int from the front of this buffer and returns it. */
+  /** Removes four bytes from this source and returns a big-endian int. */
   int readInt() throws IOException;
 
-  /** Removes a Little-Endian int from the front of this buffer and returns it. */
+  /** Removes four bytes from this source and returns a little-endian int. */
   int readIntLe() throws IOException;
 
+  /** Removes eight bytes from this source and returns a big-endian long. */
+  long readLong() throws IOException;
+
+  /** Removes eight bytes from this source and returns a little-endian long. */
+  long readLongLe() throws IOException;
+
   /**
-   * Reads and discards {@code byteCount} bytes from {@code source} using {@code
-   * buffer} as a buffer. Throws an {@link java.io.EOFException} if the source
-   * is exhausted before the requested bytes can be skipped.
+   * Reads and discards {@code byteCount} bytes from this source. Throws an
+   * {@link java.io.EOFException} if the source is exhausted before the
+   * requested bytes can be skipped.
    */
   void skip(long byteCount) throws IOException;
 
@@ -77,28 +82,32 @@
    * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
    * not included in the result.
    *
-   * <p>This method supports two ways to handle the end of the stream:
-   * <ul>
-   *   <li><strong>Throw on EOF.</strong> Every call must consume either '\r\n'
-   *       or '\n'. If these characters are absent in the stream, an {@link
-   *       java.io.EOFException} is thrown. Use this for machine-generated data
-   *       where a missing line break implies truncated input.
-   *   <li><strong>Don't throw, just like BufferedReader.</strong> If the source
-   *       doesn't end with a line break then an implicit line break is assumed.
-   *       Null is returned once the source is exhausted. Use this for
-   *       human-generated data, where a trailing line breaks are optional.
-   * </ul>
+   * <p><strong>On the end of the stream this method returns null,</strong> just
+   * like {@link java.io.BufferedReader}. If the source doesn't end with a line
+   * break then an implicit line break is assumed. Null is returned once the
+   * source is exhausted. Use this for human-generated data, where a trailing
+   * line break is optional.
    */
-  String readUtf8Line(boolean throwOnEof) throws IOException;
+  String readUtf8Line() throws IOException;
+
+  /**
+   * Removes and returns characters up to but not including the next line break.
+   * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
+   * not included in the result.
+   *
+   * <p><strong>On the end of the stream this method throws.</strong> Every call
+   * must consume either '\r\n' or '\n'. If these characters are absent in the
+   * stream, an {@link java.io.EOFException} is thrown. Use this for
+   * machine-generated data where a missing line break implies truncated input.
+   */
+  String readUtf8LineStrict() throws IOException;
 
   /**
    * Returns the index of {@code b} in the buffer, refilling it if necessary
    * until it is found. This reads an unbounded number of bytes into the buffer.
-   *
-   * @throws java.io.EOFException if the stream is exhausted before the
-   *     requested byte is found.
+   * Returns -1 if the stream is exhausted before the requested byte is found.
    */
-  long seek(byte b) throws IOException;
+  long indexOf(byte b) throws IOException;
 
   /** Returns an input stream that reads from this source. */
   InputStream inputStream();
diff --git a/okio/src/main/java/okio/ByteString.java b/okio/src/main/java/okio/ByteString.java
index 608d652..6853adb 100644
--- a/okio/src/main/java/okio/ByteString.java
+++ b/okio/src/main/java/okio/ByteString.java
@@ -120,23 +120,6 @@
   }
 
   /**
-   * Returns true when {@code ascii} is not null and equals the bytes wrapped
-   * by this byte string.
-   */
-  public boolean equalsAscii(String ascii) {
-    if (ascii == null || data.length != ascii.length()) {
-      return false;
-    }
-    if (ascii == this.utf8) { // not using String.equals to avoid looping twice.
-      return true;
-    }
-    for (int i = 0; i < data.length; i++) {
-      if (data[i] != ascii.charAt(i)) return false;
-    }
-    return true;
-  }
-
-  /**
    * Reads {@code count} bytes from {@code in} and returns the result.
    *
    * @throws java.io.EOFException if {@code in} has fewer than {@code count}
@@ -176,18 +159,9 @@
     return this;
   }
 
-  public static ByteString concat(ByteString... byteStrings) {
-    int size = 0;
-    for (ByteString byteString : byteStrings) {
-      size += byteString.size();
-    }
-    byte[] result = new byte[size];
-    int pos = 0;
-    for (ByteString byteString : byteStrings) {
-      System.arraycopy(byteString.data, 0, result, pos, byteString.size());
-      pos += byteString.size();
-    }
-    return new ByteString(result);
+  /** Returns the byte at {@code pos}. */
+  public byte getByte(int pos) {
+    return data[pos];
   }
 
   /**
diff --git a/okio/src/main/java/okio/DeflaterSink.java b/okio/src/main/java/okio/DeflaterSink.java
index df9398f..7249f2d 100644
--- a/okio/src/main/java/okio/DeflaterSink.java
+++ b/okio/src/main/java/okio/DeflaterSink.java
@@ -37,6 +37,7 @@
 public final class DeflaterSink implements Sink {
   private final BufferedSink sink;
   private final Deflater deflater;
+  private boolean closed;
 
   public DeflaterSink(Sink sink, Deflater deflater) {
     this.sink = Okio.buffer(sink);
@@ -80,10 +81,13 @@
           ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH)
           : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit);
 
-      if (deflated == 0) return;
-      s.limit += deflated;
-      buffer.size += deflated;
-      sink.emitCompleteSegments();
+      if (deflated > 0) {
+        s.limit += deflated;
+        buffer.size += deflated;
+        sink.emitCompleteSegments();
+      } else if (deflater.needsInput()) {
+        return;
+      }
     }
   }
 
@@ -93,9 +97,32 @@
   }
 
   @Override public void close() throws IOException {
-    deflater.finish();
-    deflate(false);
-    sink.close();
+    if (closed) return;
+
+    // Emit deflated data to the underlying sink. If this fails, we still need
+    // to close the deflater and the sink; otherwise we risk leaking resources.
+    Throwable thrown = null;
+    try {
+      deflater.finish();
+      deflate(false);
+    } catch (Throwable e) {
+      thrown = e;
+    }
+
+    try {
+      deflater.end();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+
+    try {
+      sink.close();
+    } catch (Throwable e) {
+      if (thrown == null) thrown = e;
+    }
+    closed = true;
+
+    if (thrown != null) Util.sneakyRethrow(thrown);
   }
 
   @Override public Sink deadline(Deadline deadline) {
diff --git a/okio/src/main/java/okio/GzipSource.java b/okio/src/main/java/okio/GzipSource.java
index fe73c08..eae3a16 100644
--- a/okio/src/main/java/okio/GzipSource.java
+++ b/okio/src/main/java/okio/GzipSource.java
@@ -15,6 +15,7 @@
  */
 package okio;
 
+import java.io.EOFException;
 import java.io.IOException;
 import java.util.zip.CRC32;
 import java.util.zip.Inflater;
@@ -121,7 +122,7 @@
     if (((flags >> FEXTRA) & 1) == 1) {
       source.require(2);
       if (fhcrc) updateCrc(source.buffer(), 0, 2);
-      int xlen = source.buffer().readShortLe() & 0xffff;
+      int xlen = source.buffer().readShortLe();
       source.require(xlen);
       if (fhcrc) updateCrc(source.buffer(), 0, xlen);
       source.skip(xlen);
@@ -132,7 +133,8 @@
     // |...original file name, zero-terminated...| (more-->)
     // +=========================================+
     if (((flags >> FNAME) & 1) == 1) {
-      long index = source.seek((byte) 0);
+      long index = source.indexOf((byte) 0);
+      if (index == -1) throw new EOFException();
       if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
       source.skip(index + 1);
     }
@@ -142,7 +144,8 @@
     // |...file comment, zero-terminated...| (more-->)
     // +===================================+
     if (((flags >> FCOMMENT) & 1) == 1) {
-      long index = source.seek((byte) 0);
+      long index = source.indexOf((byte) 0);
+      if (index == -1) throw new EOFException();
       if (fhcrc) updateCrc(source.buffer(), 0, index + 1);
       source.skip(index + 1);
     }
diff --git a/okio/src/main/java/okio/OkBuffer.java b/okio/src/main/java/okio/OkBuffer.java
index a9dfa2f..8dc4290 100644
--- a/okio/src/main/java/okio/OkBuffer.java
+++ b/okio/src/main/java/okio/OkBuffer.java
@@ -16,6 +16,7 @@
 package okio;
 
 import java.io.EOFException;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.security.MessageDigest;
@@ -26,13 +27,14 @@
 
 import static okio.Util.UTF_8;
 import static okio.Util.checkOffsetAndCount;
+import static okio.Util.reverseBytesLong;
 
 /**
  * A collection of bytes in memory.
  *
  * <p><strong>Moving data from one OkBuffer to another is fast.</strong> Instead
  * of copying bytes from one place in memory to another, this class just changes
- * ownership of the underlying bytes.
+ * ownership of the underlying byte arrays.
  *
  * <p><strong>This buffer grows with your data.</strong> Just like ArrayList,
  * each OkBuffer starts small. It consumes only the memory it needs to.
@@ -92,12 +94,6 @@
     if (this.size < byteCount) throw new EOFException();
   }
 
-  @Override public long seek(byte b) throws EOFException {
-    long index = indexOf(b);
-    if (index == -1) throw new EOFException();
-    return index;
-  }
-
   @Override public InputStream inputStream() {
     return new InputStream() {
       @Override public int read() {
@@ -160,18 +156,18 @@
     return b;
   }
 
-  /** Returns the byte at {@code i}. */
-  public byte getByte(long i) {
-    checkOffsetAndCount(size, i, 1);
+  /** Returns the byte at {@code pos}. */
+  public byte getByte(long pos) {
+    checkOffsetAndCount(size, pos, 1);
     for (Segment s = head; true; s = s.next) {
       int segmentByteCount = s.limit - s.pos;
-      if (i < segmentByteCount) return s.data[s.pos + (int) i];
-      i -= segmentByteCount;
+      if (pos < segmentByteCount) return s.data[s.pos + (int) pos];
+      pos -= segmentByteCount;
     }
   }
 
   @Override public short readShort() {
-    if (size < 2) throw new IllegalArgumentException("size < 2: " + size);
+    if (size < 2) throw new IllegalStateException("size < 2: " + size);
 
     Segment segment = head;
     int pos = segment.pos;
@@ -200,7 +196,7 @@
   }
 
   @Override public int readInt() {
-    if (size < 4) throw new IllegalArgumentException("size < 4: " + size);
+    if (size < 4) throw new IllegalStateException("size < 4: " + size);
 
     Segment segment = head;
     int pos = segment.pos;
@@ -210,14 +206,14 @@
     if (limit - pos < 4) {
       return (readByte() & 0xff) << 24
           |  (readByte() & 0xff) << 16
-          |  (readByte() & 0xff) << 8
+          |  (readByte() & 0xff) <<  8
           |  (readByte() & 0xff);
     }
 
     byte[] data = segment.data;
     int i = (data[pos++] & 0xff) << 24
         |   (data[pos++] & 0xff) << 16
-        |   (data[pos++] & 0xff) << 8
+        |   (data[pos++] & 0xff) <<  8
         |   (data[pos++] & 0xff);
     size -= 4;
 
@@ -231,19 +227,57 @@
     return i;
   }
 
-  public int readShortLe() {
+  @Override public long readLong() {
+    if (size < 8) throw new IllegalStateException("size < 8: " + size);
+
+    Segment segment = head;
+    int pos = segment.pos;
+    int limit = segment.limit;
+
+    // If the long is split across multiple segments, delegate to readInt().
+    if (limit - pos < 8) {
+      return (readInt() & 0xffffffffL) << 32
+          |  (readInt() & 0xffffffffL);
+    }
+
+    byte[] data = segment.data;
+    long v = (data[pos++] & 0xffL) << 56
+        |    (data[pos++] & 0xffL) << 48
+        |    (data[pos++] & 0xffL) << 40
+        |    (data[pos++] & 0xffL) << 32
+        |    (data[pos++] & 0xffL) << 24
+        |    (data[pos++] & 0xffL) << 16
+        |    (data[pos++] & 0xffL) <<  8
+        |    (data[pos++] & 0xffL);
+    size -= 8;
+
+    if (pos == limit) {
+      head = segment.pop();
+      SegmentPool.INSTANCE.recycle(segment);
+    } else {
+      segment.pos = pos;
+    }
+
+    return v;
+  }
+
+  @Override public short readShortLe() {
     return Util.reverseBytesShort(readShort());
   }
 
-  public int readIntLe() {
+  @Override public int readIntLe() {
     return Util.reverseBytesInt(readInt());
   }
 
-  public ByteString readByteString(long byteCount) {
+  @Override public long readLongLe() {
+    return Util.reverseBytesLong(readLong());
+  }
+
+  @Override public ByteString readByteString(long byteCount) {
     return new ByteString(readBytes(byteCount));
   }
 
-  public String readUtf8(long byteCount) {
+  @Override public String readUtf8(long byteCount) {
     checkOffsetAndCount(this.size, 0, byteCount);
     if (byteCount > Integer.MAX_VALUE) {
       throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
@@ -268,14 +302,23 @@
     return result;
   }
 
-  @Override public String readUtf8Line(boolean throwOnEof) throws EOFException {
+  @Override public String readUtf8Line() throws IOException {
     long newline = indexOf((byte) '\n');
 
     if (newline == -1) {
-      if (throwOnEof) throw new EOFException();
       return size != 0 ? readUtf8(size) : null;
     }
 
+    return readUtf8Line(newline);
+  }
+
+  @Override public String readUtf8LineStrict() throws IOException {
+    long newline = indexOf((byte) '\n');
+    if (newline == -1) throw new EOFException();
+    return readUtf8Line(newline);
+  }
+
+  String readUtf8Line(long newline) {
     if (newline > 0 && getByte(newline - 1) == '\r') {
       // Read everything until '\r\n', then skip the '\r\n'.
       String result = readUtf8((newline - 1));
@@ -319,9 +362,8 @@
 
   /** Like {@link InputStream#read}. */
   int read(byte[] sink, int offset, int byteCount) {
-    if (byteCount == 0) return -1;
-
     Segment s = this.head;
+    if (s == null) return -1;
     int toCopy = Math.min(byteCount, s.limit - s.pos);
     System.arraycopy(s.data, s.pos, sink, offset, toCopy);
 
@@ -345,7 +387,7 @@
   }
 
   /** Discards {@code byteCount} bytes from the head of this buffer. */
-  public void skip(long byteCount) {
+  @Override public void skip(long byteCount) {
     checkOffsetAndCount(this.size, 0, byteCount);
 
     this.size -= byteCount;
@@ -362,13 +404,12 @@
     }
   }
 
-  /** Appends {@code byteString} to this. */
   @Override public OkBuffer write(ByteString byteString) {
     return write(byteString.data, 0, byteString.data.length);
   }
 
-  /** Encodes {@code string} as UTF-8 and appends the bytes to this. */
   @Override public OkBuffer writeUtf8(String string) {
+    // TODO: inline UTF-8 encoding to save allocating a byte[]?
     byte[] data = string.getBytes(Util.UTF_8);
     return write(data, 0, data.length);
   }
@@ -393,7 +434,6 @@
     return this;
   }
 
-  /** Appends a Big-Endian byte to the end of this buffer. */
   @Override public OkBuffer writeByte(int b) {
     Segment tail = writableSegment(1);
     tail.data[tail.limit++] = (byte) b;
@@ -401,32 +441,59 @@
     return this;
   }
 
-  /** Appends a Big-Endian short to the end of this buffer. */
   @Override public OkBuffer writeShort(int s) {
     Segment tail = writableSegment(2);
     byte[] data = tail.data;
     int limit = tail.limit;
-    data[limit++] = (byte) ((s >> 8) & 0xff);
-    data[limit++] = (byte)  (s       & 0xff);
+    data[limit++] = (byte) ((s >>> 8) & 0xff);
+    data[limit++] = (byte)  (s        & 0xff);
     tail.limit = limit;
     size += 2;
     return this;
   }
 
-  /** Appends a Big-Endian int to the end of this buffer. */
+  @Override public BufferedSink writeShortLe(int s) {
+    return writeShort(Util.reverseBytesShort((short) s));
+  }
+
   @Override public OkBuffer writeInt(int i) {
     Segment tail = writableSegment(4);
     byte[] data = tail.data;
     int limit = tail.limit;
-    data[limit++] = (byte) ((i >> 24) & 0xff);
-    data[limit++] = (byte) ((i >> 16) & 0xff);
-    data[limit++] = (byte) ((i >>  8) & 0xff);
-    data[limit++] = (byte)  (i        & 0xff);
+    data[limit++] = (byte) ((i >>> 24) & 0xff);
+    data[limit++] = (byte) ((i >>> 16) & 0xff);
+    data[limit++] = (byte) ((i >>>  8) & 0xff);
+    data[limit++] = (byte)  (i         & 0xff);
     tail.limit = limit;
     size += 4;
     return this;
   }
 
+  @Override public BufferedSink writeIntLe(int i) {
+    return writeInt(Util.reverseBytesInt(i));
+  }
+
+  @Override public OkBuffer writeLong(long v) {
+    Segment tail = writableSegment(8);
+    byte[] data = tail.data;
+    int limit = tail.limit;
+    data[limit++] = (byte) ((v >>> 56L) & 0xff);
+    data[limit++] = (byte) ((v >>> 48L) & 0xff);
+    data[limit++] = (byte) ((v >>> 40L) & 0xff);
+    data[limit++] = (byte) ((v >>> 32L) & 0xff);
+    data[limit++] = (byte) ((v >>> 24L) & 0xff);
+    data[limit++] = (byte) ((v >>> 16L) & 0xff);
+    data[limit++] = (byte) ((v >>>  8L) & 0xff);
+    data[limit++] = (byte)  (v          & 0xff);
+    tail.limit = limit;
+    size += 8;
+    return this;
+  }
+
+  @Override public BufferedSink writeLongLe(long v) {
+    return writeLong(reverseBytesLong(v));
+  }
+
   /**
    * Returns a tail segment that we can write at least {@code minimumCapacity}
    * bytes to, creating it if necessary.
@@ -549,11 +616,7 @@
     return this;
   }
 
-  /**
-   * Returns the index of {@code b} in this, or -1 if this buffer does not
-   * contain {@code b}.
-   */
-  public long indexOf(byte b) {
+  @Override public long indexOf(byte b) {
     return indexOf(b, 0);
   }
 
diff --git a/okio/src/main/java/okio/RealBufferedSink.java b/okio/src/main/java/okio/RealBufferedSink.java
index 691c070..74454c6 100644
--- a/okio/src/main/java/okio/RealBufferedSink.java
+++ b/okio/src/main/java/okio/RealBufferedSink.java
@@ -39,55 +39,79 @@
 
   @Override public void write(OkBuffer source, long byteCount)
       throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.write(source, byteCount);
     emitCompleteSegments();
   }
 
   @Override public BufferedSink write(ByteString byteString) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.write(byteString);
     return emitCompleteSegments();
   }
 
   @Override public BufferedSink writeUtf8(String string) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.writeUtf8(string);
     return emitCompleteSegments();
   }
 
   @Override public BufferedSink write(byte[] source) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.write(source);
     return emitCompleteSegments();
   }
 
   @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.write(source, offset, byteCount);
     return emitCompleteSegments();
   }
 
   @Override public BufferedSink writeByte(int b) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.writeByte(b);
     return emitCompleteSegments();
   }
 
   @Override public BufferedSink writeShort(int s) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.writeShort(s);
     return emitCompleteSegments();
   }
 
+  @Override public BufferedSink writeShortLe(int s) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeShortLe(s);
+    return emitCompleteSegments();
+  }
+
   @Override public BufferedSink writeInt(int i) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     buffer.writeInt(i);
     return emitCompleteSegments();
   }
 
+  @Override public BufferedSink writeIntLe(int i) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeIntLe(i);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeLong(long v) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeLong(v);
+    return emitCompleteSegments();
+  }
+
+  @Override public BufferedSink writeLongLe(long v) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
+    buffer.writeLongLe(v);
+    return emitCompleteSegments();
+  }
+
   @Override public BufferedSink emitCompleteSegments() throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     long byteCount = buffer.completeSegmentByteCount();
     if (byteCount > 0) sink.write(buffer, byteCount);
     return this;
@@ -96,20 +120,20 @@
   @Override public OutputStream outputStream() {
     return new OutputStream() {
       @Override public void write(int b) throws IOException {
-        checkNotClosed();
+        if (closed) throw new IOException("closed");
         buffer.writeByte((byte) b);
         emitCompleteSegments();
       }
 
       @Override public void write(byte[] data, int offset, int byteCount) throws IOException {
-        checkNotClosed();
+        if (closed) throw new IOException("closed");
         buffer.write(data, offset, byteCount);
         emitCompleteSegments();
       }
 
       @Override public void flush() throws IOException {
         // For backwards compatibility, a flush() on a closed stream is a no-op.
-        if (!RealBufferedSink.this.closed) {
+        if (!closed) {
           RealBufferedSink.this.flush();
         }
       }
@@ -121,18 +145,11 @@
       @Override public String toString() {
         return RealBufferedSink.this + ".outputStream()";
       }
-
-      private void checkNotClosed() throws IOException {
-        // By convention in java.io, IOException and not IllegalStateException is used.
-        if (RealBufferedSink.this.closed) {
-          throw new IOException("closed");
-        }
-      }
     };
   }
 
   @Override public void flush() throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     if (buffer.size > 0) {
       sink.write(buffer, buffer.size);
     }
@@ -171,10 +188,4 @@
   @Override public String toString() {
     return "buffer(" + sink + ")";
   }
-
-  private void checkNotClosed() {
-    if (closed) {
-      throw new IllegalStateException("closed");
-    }
-  }
 }
diff --git a/okio/src/main/java/okio/RealBufferedSource.java b/okio/src/main/java/okio/RealBufferedSource.java
index 9e9a34a..0189d0f 100644
--- a/okio/src/main/java/okio/RealBufferedSource.java
+++ b/okio/src/main/java/okio/RealBufferedSource.java
@@ -42,7 +42,7 @@
 
   @Override public long read(OkBuffer sink, long byteCount) throws IOException {
     if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
 
     if (buffer.size == 0) {
       long read = source.read(buffer, Segment.SIZE);
@@ -54,12 +54,12 @@
   }
 
   @Override public boolean exhausted() throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1;
   }
 
   @Override public void require(long byteCount) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     while (buffer.size < byteCount) {
       if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException();
     }
@@ -80,30 +80,20 @@
     return buffer.readUtf8(byteCount);
   }
 
-  @Override public String readUtf8Line(boolean throwOnEof) throws IOException {
-    checkNotClosed();
-    long start = 0;
-    long newline;
-    while ((newline = buffer.indexOf((byte) '\n', start)) == -1) {
-      start = buffer.size;
-      if (source.read(buffer, Segment.SIZE) == -1) {
-        if (throwOnEof) throw new EOFException();
-        return buffer.size != 0 ? readUtf8(buffer.size) : null;
-      }
+  @Override public String readUtf8Line() throws IOException {
+    long newline = indexOf((byte) '\n');
+
+    if (newline == -1) {
+      return buffer.size != 0 ? readUtf8(buffer.size) : null;
     }
 
-    if (newline > 0 && buffer.getByte(newline - 1) == '\r') {
-      // Read everything until '\r\n', then skip the '\r\n'.
-      String result = readUtf8((newline - 1));
-      skip(2);
-      return result;
+    return buffer.readUtf8Line(newline);
+  }
 
-    } else {
-      // Read everything until '\n', then skip the '\n'.
-      String result = readUtf8((newline));
-      skip(1);
-      return result;
-    }
+  @Override public String readUtf8LineStrict() throws IOException {
+    long newline = indexOf((byte) '\n');
+    if (newline == -1L) throw new EOFException();
+    return buffer.readUtf8Line(newline);
   }
 
   @Override public short readShort() throws IOException {
@@ -111,7 +101,7 @@
     return buffer.readShort();
   }
 
-  @Override public int readShortLe() throws IOException {
+  @Override public short readShortLe() throws IOException {
     require(2);
     return buffer.readShortLe();
   }
@@ -126,8 +116,18 @@
     return buffer.readIntLe();
   }
 
+  @Override public long readLong() throws IOException {
+    require(8);
+    return buffer.readLong();
+  }
+
+  @Override public long readLongLe() throws IOException {
+    require(8);
+    return buffer.readLongLe();
+  }
+
   @Override public void skip(long byteCount) throws IOException {
-    checkNotClosed();
+    if (closed) throw new IllegalStateException("closed");
     while (byteCount > 0) {
       if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
         throw new EOFException();
@@ -138,13 +138,13 @@
     }
   }
 
-  @Override public long seek(byte b) throws IOException {
-    checkNotClosed();
+  @Override public long indexOf(byte b) throws IOException {
+    if (closed) throw new IllegalStateException("closed");
     long start = 0;
     long index;
     while ((index = buffer.indexOf(b, start)) == -1) {
       start = buffer.size;
-      if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException();
+      if (source.read(buffer, Segment.SIZE) == -1) return -1L;
     }
     return index;
   }
@@ -152,7 +152,7 @@
   @Override public InputStream inputStream() {
     return new InputStream() {
       @Override public int read() throws IOException {
-        checkNotClosed();
+        if (closed) throw new IOException("closed");
         if (buffer.size == 0) {
           long count = source.read(buffer, Segment.SIZE);
           if (count == -1) return -1;
@@ -161,7 +161,7 @@
       }
 
       @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
-        checkNotClosed();
+        if (closed) throw new IOException("closed");
         checkOffsetAndCount(data.length, offset, byteCount);
 
         if (buffer.size == 0) {
@@ -173,7 +173,7 @@
       }
 
       @Override public int available() throws IOException {
-        checkNotClosed();
+        if (closed) throw new IOException("closed");
         return (int) Math.min(buffer.size, Integer.MAX_VALUE);
       }
 
@@ -184,14 +184,6 @@
       @Override public String toString() {
         return RealBufferedSource.this + ".inputStream()";
       }
-
-      private void checkNotClosed() throws IOException {
-        if (RealBufferedSource.this.closed) {
-          // By convention in java.io, IOException and not IllegalStateException is used.
-          throw new IOException("closed");
-        }
-      }
-
     };
   }
 
@@ -210,10 +202,4 @@
   @Override public String toString() {
     return "buffer(" + source + ")";
   }
-
-  private void checkNotClosed() {
-    if (closed) {
-      throw new IllegalStateException("closed");
-    }
-  }
 }
diff --git a/okio/src/main/java/okio/Util.java b/okio/src/main/java/okio/Util.java
index e5c1ddc..4759488 100644
--- a/okio/src/main/java/okio/Util.java
+++ b/okio/src/main/java/okio/Util.java
@@ -30,17 +30,29 @@
     }
   }
 
-  public static int reverseBytesShort(short s) {
+  public static short reverseBytesShort(short s) {
     int i = s & 0xffff;
-    return (i & 0xff00) >>> 8
-        |  (i & 0x00ff) << 8;
+    int reversed = (i & 0xff00) >>> 8
+        |          (i & 0x00ff)  << 8;
+    return (short) reversed;
   }
 
   public static int reverseBytesInt(int i) {
     return (i & 0xff000000) >>> 24
-        |  (i & 0x00ff0000) >>> 8
-        |  (i & 0x0000ff00) << 8
-        |  (i & 0x000000ff) << 24;
+        |  (i & 0x00ff0000) >>>  8
+        |  (i & 0x0000ff00)  <<  8
+        |  (i & 0x000000ff)  << 24;
+  }
+
+  public static long reverseBytesLong(long v) {
+    return (v & 0xff00000000000000L) >>> 56
+        |  (v & 0x00ff000000000000L) >>> 40
+        |  (v & 0x0000ff0000000000L) >>> 24
+        |  (v & 0x000000ff00000000L) >>>  8
+        |  (v & 0x00000000ff000000L)  <<  8
+        |  (v & 0x0000000000ff0000L)  << 24
+        |  (v & 0x000000000000ff00L)  << 40
+        |  (v & 0x00000000000000ffL)  << 56;
   }
 
   /**
diff --git a/okio/src/test/java/okio/ByteStringTest.java b/okio/src/test/java/okio/ByteStringTest.java
index e714056..16b8e2d 100644
--- a/okio/src/test/java/okio/ByteStringTest.java
+++ b/okio/src/test/java/okio/ByteStringTest.java
@@ -29,6 +29,21 @@
 
 public class ByteStringTest {
 
+  @Test public void getByte() throws Exception {
+    ByteString byteString = ByteString.decodeHex("ab12");
+    assertEquals(-85, byteString.getByte(0));
+    assertEquals(18, byteString.getByte(1));
+  }
+
+  @Test public void getByteOutOfBounds() throws Exception {
+    ByteString byteString = ByteString.decodeHex("ab12");
+    try {
+      byteString.getByte(2);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
   @Test public void equals() throws Exception {
     ByteString byteString = ByteString.decodeHex("000102");
     assertTrue(byteString.equals(byteString));
@@ -48,15 +63,6 @@
     assertEquals(byteString.utf8(), bronzeHorseman);
   }
 
-  @Test public void equalsAscii() throws Exception {
-    ByteString byteString = ByteString.encodeUtf8("Content-Length");
-    assertTrue(byteString.equalsAscii("Content-Length"));
-    assertFalse(byteString.equalsAscii("content-length"));
-    assertFalse(ByteString.of((byte) 0x63).equalsAscii(null));
-    assertFalse(byteString.equalsAscii(bronzeHorseman));
-    assertFalse(ByteString.encodeUtf8("Content-Length").equalsAscii("content-length"));
-  }
-
   @Test public void testHashCode() throws Exception {
     ByteString byteString = ByteString.decodeHex("0102");
     assertEquals(byteString.hashCode(), byteString.hashCode());
@@ -96,16 +102,6 @@
     assertByteArraysEquals(new byte[] { 0x61, 0x62, 0x63 }, out.toByteArray());
   }
 
-  @Test public void concat() {
-    assertEquals(ByteString.of(), ByteString.concat());
-    assertEquals(ByteString.of(), ByteString.concat(ByteString.EMPTY));
-    assertEquals(ByteString.of(), ByteString.concat(ByteString.EMPTY, ByteString.EMPTY));
-    ByteString foo = ByteString.encodeUtf8("foo");
-    ByteString bar = ByteString.encodeUtf8("bar");
-    assertEquals(foo, ByteString.concat(foo));
-    assertEquals(ByteString.encodeUtf8("foobar"), ByteString.concat(foo, bar));
-  }
-
   @Test public void encodeBase64() {
     assertEquals("", ByteString.encodeUtf8("").base64());
     assertEquals("AA==", ByteString.encodeUtf8("\u0000").base64());
diff --git a/okio/src/test/java/okio/DeflaterSinkTest.java b/okio/src/test/java/okio/DeflaterSinkTest.java
index 025215b..0f6b8c2 100644
--- a/okio/src/test/java/okio/DeflaterSinkTest.java
+++ b/okio/src/test/java/okio/DeflaterSinkTest.java
@@ -25,6 +25,7 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 public final class DeflaterSinkTest {
   @Test public void deflateWithClose() throws Exception {
@@ -75,6 +76,39 @@
     assertEquals(original, inflated.readByteString(inflated.size()));
   }
 
+  @Test public void multipleSegmentsWithoutCompression() throws IOException {
+    OkBuffer buffer = new OkBuffer();
+    Deflater deflater = new Deflater();
+    deflater.setLevel(Deflater.NO_COMPRESSION);
+    DeflaterSink deflaterSink = new DeflaterSink(buffer, deflater);
+    int byteCount = Segment.SIZE * 4;
+    deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', byteCount)), byteCount);
+    deflaterSink.close();
+    assertEquals(repeat('a', byteCount), inflate(buffer).readUtf8(byteCount));
+  }
+
+  /**
+   * This test deflates a single segment of without compression because that's
+   * the easiest way to force close() to emit a large amount of data to the
+   * underlying sink.
+   */
+  @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+    MockSink mockSink = new MockSink();
+    mockSink.scheduleThrow(0, new IOException("first"));
+    mockSink.scheduleThrow(1, new IOException("second"));
+    Deflater deflater = new Deflater();
+    deflater.setLevel(Deflater.NO_COMPRESSION);
+    DeflaterSink deflaterSink = new DeflaterSink(mockSink, deflater);
+    deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', Segment.SIZE)), Segment.SIZE);
+    try {
+      deflaterSink.close();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("first", expected.getMessage());
+    }
+    mockSink.assertLogContains("close()");
+  }
+
   /**
    * Uses streaming decompression to inflate {@code deflated}. The input must
    * either be finished or have a trailing sync flush.
diff --git a/okio/src/test/java/okio/MockSink.java b/okio/src/test/java/okio/MockSink.java
new file mode 100644
index 0000000..bae3259
--- /dev/null
+++ b/okio/src/test/java/okio/MockSink.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/** A scriptable sink. Like Mockito, but worse and requiring less configuration. */
+class MockSink implements Sink {
+  private final List<String> log = new ArrayList<String>();
+  private final Map<Integer, IOException> callThrows = new LinkedHashMap<Integer, IOException>();
+
+  public void assertLog(String... messages) {
+    assertEquals(Arrays.asList(messages), log);
+  }
+
+  public void assertLogContains(String message) {
+    assertTrue(log.contains(message));
+  }
+
+  public void scheduleThrow(int call, IOException e) {
+    callThrows.put(call, e);
+  }
+
+  private void throwIfScheduled() throws IOException {
+    IOException exception = callThrows.get(log.size() - 1);
+    if (exception != null) throw exception;
+  }
+
+  @Override public void write(OkBuffer source, long byteCount) throws IOException {
+    log.add("write(" + source + ", " + byteCount + ")");
+    source.skip(byteCount);
+    throwIfScheduled();
+  }
+
+  @Override public void flush() throws IOException {
+    log.add("flush()");
+    throwIfScheduled();
+  }
+
+  @Override public Sink deadline(Deadline deadline) {
+    log.add("deadline()");
+    return this;
+  }
+
+  @Override public void close() throws IOException {
+    log.add("close()");
+    throwIfScheduled();
+  }
+}
diff --git a/okio/src/test/java/okio/OkBufferTest.java b/okio/src/test/java/okio/OkBufferTest.java
index e4ba277..f69613a 100644
--- a/okio/src/test/java/okio/OkBufferTest.java
+++ b/okio/src/test/java/okio/OkBufferTest.java
@@ -365,6 +365,13 @@
     assertEquals("OkBuffer[size=4 data=abcd4321]", data.toString());
   }
 
+  @Test public void writeShortLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeShortLe(0xabcd);
+    data.writeShortLe(0x4321);
+    assertEquals("OkBuffer[size=4 data=cdab2143]", data.toString());
+  }
+
   @Test public void writeInt() throws Exception {
     OkBuffer data = new OkBuffer();
     data.writeInt(0xabcdef01);
@@ -392,6 +399,27 @@
     assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
   }
 
+  @Test public void writeIntLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeIntLe(0xabcdef01);
+    data.writeIntLe(0x87654321);
+    assertEquals("OkBuffer[size=8 data=01efcdab21436587]", data.toString());
+  }
+
+  @Test public void writeLong() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeLong(0xabcdef0187654321L);
+    data.writeLong(0xcafebabeb0b15c00L);
+    assertEquals("OkBuffer[size=16 data=abcdef0187654321cafebabeb0b15c00]", data.toString());
+  }
+
+  @Test public void writeLongLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeLongLe(0xabcdef0187654321L);
+    data.writeLongLe(0xcafebabeb0b15c00L);
+    assertEquals("OkBuffer[size=16 data=2143658701efcdab005cb1b0bebafeca]", data.toString());
+  }
+
   @Test public void readByte() throws Exception {
     OkBuffer data = new OkBuffer();
     data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
@@ -410,6 +438,16 @@
     assertEquals(0, data.size());
   }
 
+  @Test public void readShortLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10
+    });
+    assertEquals((short) 0xcdab, data.readShortLe());
+    assertEquals((short) 0x10ef, data.readShortLe());
+    assertEquals(0, data.size());
+  }
+
   @Test public void readShortSplitAcrossMultipleSegments() throws Exception {
     OkBuffer data = new OkBuffer();
     data.writeUtf8(repeat('a', Segment.SIZE - 1));
@@ -430,6 +468,17 @@
     assertEquals(0, data.size());
   }
 
+  @Test public void readIntLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21
+    });
+    assertEquals(0x10efcdab, data.readIntLe());
+    assertEquals(0x21436587, data.readIntLe());
+    assertEquals(0, data.size());
+  }
+
   @Test public void readIntSplitAcrossMultipleSegments() throws Exception {
     OkBuffer data = new OkBuffer();
     data.writeUtf8(repeat('a', Segment.SIZE - 3));
@@ -441,6 +490,44 @@
     assertEquals(0, data.size());
   }
 
+  @Test public void readLong() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+        (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
+        (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
+    });
+    assertEquals(0xabcdef1087654321L, data.readLong());
+    assertEquals(0x3647586912233445L, data.readLong());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readLongLe() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+        (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
+        (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
+    });
+    assertEquals(0x2143658710efcdabL, data.readLongLe());
+    assertEquals(0x4534231269584736L, data.readLongLe());
+    assertEquals(0, data.size());
+  }
+
+  @Test public void readLongSplitAcrossMultipleSegments() throws Exception {
+    OkBuffer data = new OkBuffer();
+    data.writeUtf8(repeat('a', Segment.SIZE - 7));
+    data.write(new byte[] {
+        (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01,
+        (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
+    });
+    data.readUtf8(Segment.SIZE - 7);
+    assertEquals(0xabcdef0187654321L, data.readLong());
+    assertEquals(0, data.size());
+  }
+
   @Test public void byteAt() throws Exception {
     OkBuffer buffer = new OkBuffer();
     buffer.writeUtf8("a");
diff --git a/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/src/test/java/okio/ReadUtf8LineTest.java
index db36a79..79b4c8a 100644
--- a/okio/src/test/java/okio/ReadUtf8LineTest.java
+++ b/okio/src/test/java/okio/ReadUtf8LineTest.java
@@ -28,10 +28,10 @@
 
   @Test public void readLines() throws IOException {
     BufferedSource source = newSource("abc\ndef\n");
-    assertEquals("abc", source.readUtf8Line(true));
-    assertEquals("def", source.readUtf8Line(true));
+    assertEquals("abc", source.readUtf8LineStrict());
+    assertEquals("def", source.readUtf8LineStrict());
     try {
-      source.readUtf8Line(true);
+      source.readUtf8LineStrict();
       fail();
     } catch (EOFException expected) {
     }
@@ -39,30 +39,30 @@
 
   @Test public void emptyLines() throws IOException {
     BufferedSource source = newSource("\n\n\n");
-    assertEquals("", source.readUtf8Line(true));
-    assertEquals("", source.readUtf8Line(true));
-    assertEquals("", source.readUtf8Line(true));
+    assertEquals("", source.readUtf8LineStrict());
+    assertEquals("", source.readUtf8LineStrict());
+    assertEquals("", source.readUtf8LineStrict());
     assertTrue(source.exhausted());
   }
 
   @Test public void crDroppedPrecedingLf() throws IOException {
     BufferedSource source = newSource("abc\r\ndef\r\nghi\rjkl\r\n");
-    assertEquals("abc", source.readUtf8Line(true));
-    assertEquals("def", source.readUtf8Line(true));
-    assertEquals("ghi\rjkl", source.readUtf8Line(true));
+    assertEquals("abc", source.readUtf8LineStrict());
+    assertEquals("def", source.readUtf8LineStrict());
+    assertEquals("ghi\rjkl", source.readUtf8LineStrict());
   }
 
   @Test public void bufferedReaderCompatible() throws IOException {
     BufferedSource source = newSource("abc\ndef");
-    assertEquals("abc", source.readUtf8Line(false));
-    assertEquals("def", source.readUtf8Line(false));
-    assertEquals(null, source.readUtf8Line(false));
+    assertEquals("abc", source.readUtf8Line());
+    assertEquals("def", source.readUtf8Line());
+    assertEquals(null, source.readUtf8Line());
   }
 
   @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException {
     BufferedSource source = newSource("abc\ndef\n");
-    assertEquals("abc", source.readUtf8Line(false));
-    assertEquals("def", source.readUtf8Line(false));
-    assertEquals(null, source.readUtf8Line(false));
+    assertEquals("abc", source.readUtf8Line());
+    assertEquals("def", source.readUtf8Line());
+    assertEquals(null, source.readUtf8Line());
   }
 }
diff --git a/okio/src/test/java/okio/RealBufferedSinkTest.java b/okio/src/test/java/okio/RealBufferedSinkTest.java
index 67d3a80..80a1317 100644
--- a/okio/src/test/java/okio/RealBufferedSinkTest.java
+++ b/okio/src/test/java/okio/RealBufferedSinkTest.java
@@ -17,11 +17,7 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
 import org.junit.Test;
 
 import static okio.Util.UTF_8;
@@ -216,43 +212,4 @@
     Arrays.fill(array, c);
     return new String(array);
   }
-
-  /** A scriptable sink. Like Mockito, but worse and requiring less configuration. */
-  private static class MockSink implements Sink {
-    private final List<String> log = new ArrayList<String>();
-    private final Map<Integer, IOException> callThrows = new LinkedHashMap<Integer, IOException>();
-
-    public void assertLog(String... messages) {
-      assertEquals(Arrays.asList(messages), log);
-    }
-
-    public void scheduleThrow(int call, IOException e) {
-      callThrows.put(call, e);
-    }
-
-    private void throwIfScheduled() throws IOException {
-      IOException exception = callThrows.get(log.size() - 1);
-      if (exception != null) throw exception;
-    }
-
-    @Override public void write(OkBuffer source, long byteCount) throws IOException {
-      log.add("write(" + source + ", " + byteCount + ")");
-      throwIfScheduled();
-    }
-
-    @Override public void flush() throws IOException {
-      log.add("flush()");
-      throwIfScheduled();
-    }
-
-    @Override public Sink deadline(Deadline deadline) {
-      log.add("deadline()");
-      return this;
-    }
-
-    @Override public void close() throws IOException {
-      log.add("close()");
-      throwIfScheduled();
-    }
-  }
 }
diff --git a/okio/src/test/java/okio/RealBufferedSourceTest.java b/okio/src/test/java/okio/RealBufferedSourceTest.java
index c191e1c..a77eaf2 100644
--- a/okio/src/test/java/okio/RealBufferedSourceTest.java
+++ b/okio/src/test/java/okio/RealBufferedSourceTest.java
@@ -18,7 +18,6 @@
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.Arrays;
 import org.junit.Test;
 
@@ -163,7 +162,7 @@
 
     // Test a sample set of methods.
     try {
-      bufferedSource.seek((byte) 1);
+      bufferedSource.indexOf((byte) 1);
       fail();
     } catch (IllegalStateException expected) {
     }
diff --git a/pom.xml b/pom.xml
index 2216c73..c534a1d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -138,21 +138,28 @@
           <artifactId>maven-javadoc-plugin</artifactId>
           <version>2.9</version>
         </plugin>
-
-        <plugin>
-          <groupId>org.apache.maven.plugins</groupId>
-          <artifactId>maven-release-plugin</artifactId>
-          <version>2.3.2</version>
-          <configuration>
-            <autoVersionSubmodules>true</autoVersionSubmodules>
-          </configuration>
-        </plugin>
       </plugins>
     </pluginManagement>
 
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-release-plugin</artifactId>
+        <version>2.4.2</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.apache.maven.scm</groupId>
+            <artifactId>maven-scm-provider-gitexe</artifactId>
+            <version>1.9</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <autoVersionSubmodules>true</autoVersionSubmodules>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
         <version>2.10</version>
         <configuration>
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
new file mode 100644
index 0000000..0015f7c
--- /dev/null
+++ b/samples/crawler/pom.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.squareup.okhttp.sample</groupId>
+    <artifactId>sample-parent</artifactId>
+    <version>2.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>crawler</artifactId>
+  <name>Sample: Crawler</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+      <version>1.7.3</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
new file mode 100644
index 0000000..d80c13f
--- /dev/null
+++ b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.sample;
+
+import com.squareup.okhttp.HttpResponseCache;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+/**
+ * Fetches HTML from a requested URL, follows the links, and repeats.
+ */
+public final class Crawler {
+  public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private final OkHttpClient client;
+  private final Set<URL> fetchedUrls = Collections.synchronizedSet(new LinkedHashSet<URL>());
+  private final LinkedBlockingQueue<URL> queue = new LinkedBlockingQueue<URL>();
+
+  public Crawler(OkHttpClient client) {
+    this.client = client;
+  }
+
+  private void parallelDrainQueue(int threadCount) {
+    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+    for (int i = 0; i < threadCount; i++) {
+      executor.execute(new Runnable() {
+        @Override public void run() {
+          try {
+            drainQueue();
+          } catch (Exception e) {
+            e.printStackTrace();
+          }
+        }
+      });
+    }
+    executor.shutdown();
+  }
+
+  private void drainQueue() throws Exception {
+    for (URL url; (url = queue.take()) != null; ) {
+      if (!fetchedUrls.add(url)) {
+        continue;
+      }
+
+      try {
+        fetch(url);
+      } catch (IOException e) {
+        System.out.printf("XXX: %s %s%n", url, e);
+      }
+    }
+  }
+
+  public void fetch(URL url) throws IOException {
+    HttpURLConnection connection = client.open(url);
+    String responseSource = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
+    String contentType = connection.getHeaderField("Content-Type");
+    int responseCode = connection.getResponseCode();
+
+    System.out.printf("%03d: %s %s%n", responseCode, url, responseSource);
+
+    if (responseCode >= 400) {
+      connection.getErrorStream().close();
+      return;
+    }
+
+    InputStream in = connection.getInputStream();
+    if (responseCode != 200 || contentType == null) {
+      in.close();
+      return;
+    }
+
+    MediaType mediaType = MediaType.parse(contentType);
+    Document document = Jsoup.parse(in, mediaType.charset(UTF_8).name(), url.toString());
+    for (Element element : document.select("a[href]")) {
+      String href = element.attr("href");
+      URL link = parseUrl(url, href);
+      if (link != null) queue.add(link);
+    }
+
+    in.close();
+  }
+
+  private URL parseUrl(URL url, String href) {
+    try {
+      URL result = new URL(url, href);
+      return result.getProtocol().equals("http") || result.getProtocol().equals("https")
+          ? result
+          : null;
+    } catch (MalformedURLException e) {
+      return null;
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    if (args.length != 2) {
+      System.out.println("Usage: Crawler <cache dir> <root>");
+      return;
+    }
+
+    int threadCount = 20;
+    long cacheByteCount = 1024L * 1024L * 100L;
+
+    OkHttpClient client = new OkHttpClient();
+    HttpResponseCache httpResponseCache = new HttpResponseCache(new File(args[0]), cacheByteCount);
+    client.setOkResponseCache(httpResponseCache);
+
+    Crawler crawler = new Crawler(client);
+    crawler.queue.add(new URL(args[1]));
+    crawler.parallelDrainQueue(threadCount);
+  }
+}
diff --git a/samples/pom.xml b/samples/pom.xml
index b212e99..62d1240 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -16,6 +16,7 @@
 
   <modules>
     <module>guide</module>
+    <module>crawler</module>
     <module>simple-client</module>
     <module>static-server</module>
   </modules>
diff --git a/website/index.html b/website/index.html
index f2ed4e8..9ed87bb 100644
--- a/website/index.html
+++ b/website/index.html
@@ -210,8 +210,7 @@
         // Look up the latest version of the library.
         $.fn.artifactVersion({
           'groupId': 'com.squareup.okhttp',
-          'artifactId': 'okhttp',
-          'classifier': 'jar-with-dependencies'
+          'artifactId': 'okhttp'
         }, function(version, url) {
           $('.version').text(version);
           $('.version-tag').text('v' + version);