Pull latest code from upstream okhttp and okio

This change contains the OkHttp and Okio changes without
modification. The only additions are the
MODULE_LICENSE_APACHE2 files.

This corresponds closely to OkHttp 2.5.0 and
Okio 1.6.0. Behavior changes are documented in
CHANGELOG.md.

This change does not compile as is. The next
commit makes the Android modifications required.

okhttp: 4305dc3fabeab392eb56f2db51538e06c3a54e51
okio: 313436764bf35794e158c6171e319fee868298df

Change-Id: I97ce07ff0472cdbce09f588863a1e5ccdcea0c20
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index b8e8222..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,83 +0,0 @@
-#
-# Copyright (C) 2012 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-LOCAL_PATH := $(call my-dir)
-
-okhttp_common_src_files := $(call all-java-files-under,okhttp/src/main/java)
-okhttp_common_src_files += $(call all-java-files-under,okhttp-urlconnection/src/main/java)
-okhttp_common_src_files += $(call all-java-files-under,okhttp-android-support/src/main/java)
-okhttp_common_src_files += $(call all-java-files-under,okio/okio/src/main/java)
-okhttp_system_src_files := $(filter-out %/Platform.java, $(okhttp_common_src_files))
-okhttp_system_src_files += $(call all-java-files-under, android/main/java)
-
-okhttp_test_src_files := $(call all-java-files-under,okhttp-tests/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-urlconnection/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-android-support/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,okio/okio/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/main/java)
-okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/test/java)
-okhttp_test_src_files += $(call all-java-files-under,android/test/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-ws/src/main/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-ws-tests/src/test/java)
-
-# Exclude tests Android currently has problems with:
-# 1) Parameterized (requires JUnit 4.11).
-# 2) New dependencies like gson.
-okhttp_test_src_excludes := \
-    okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java \
-    okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
-
-okhttp_test_src_files := \
-    $(filter-out $(okhttp_test_src_excludes), $(okhttp_test_src_files))
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := core-libart conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-include $(BUILD_JAVA_LIBRARY)
-
-# non-jarjar'd version of okhttp to compile the tests against
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-nojarjar
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JAVA_LIBRARIES := core-libart conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-tests-nojarjar
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_test_src_files)
-LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-ifeq ($(HOST_OS),linux)
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-hostdex
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := conscrypt-hostdex
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
-endif  # ($(HOST_OS),linux)
diff --git a/BUG-BOUNTY.md b/BUG-BOUNTY.md
new file mode 100644
index 0000000..b2c35b2
--- /dev/null
+++ b/BUG-BOUNTY.md
@@ -0,0 +1,10 @@
+Serious about security
+======================
+
+Square recognizes the important contributions the security research community
+can make. We therefore encourage reporting security issues with the code
+contained in this repository.
+
+If you believe you have discovered a security vulnerability, please follow the
+guidelines at https://hackerone.com/square-open-source
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 820c4fc..00ad4c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,146 @@
 Change Log
 ==========
 
+## Version 2.5.0
+
+_2015-08-25_
+
+ *  **Timeouts now default to 10 seconds.** Previously we defaulted to never
+    timing out, and that was a lousy policy. If establishing a connection,
+    reading the next byte from a connection, or writing the next byte to a
+    connection takes more than 10 seconds to complete, you’ll need to adjust
+    the timeouts manually.
+
+ *  **OkHttp now rejects request headers that contain invalid characters.** This
+    includes potential security problems (newline characters) as well as simple
+    non-ASCII characters (including international characters and emoji).
+
+ *  **Call canceling is more reliable.**  We had a bug where a socket being
+     connected wasn't being closed when the application used `Call.cancel()`.
+
+ *  **Changing a HttpUrl’s scheme now tracks the default port.** We had a bug
+    where changing a URL from `http` to `https` would leave it on port 80.
+
+ *  **Okio has been updated to 1.6.0.**
+     ```
+     <dependency>
+       <groupId>com.squareup.okio</groupId>
+       <artifactId>okio</artifactId>
+       <version>1.6.0</version>
+     </dependency>
+     ```
+
+ *  New: `Cache.initialize()`. Call this on a background thread to eagerly
+    initialize the response cache.
+ *  New: Fold `MockWebServerRule` into `MockWebServer`. This makes it easier to
+    write JUnit tests with `MockWebServer`. The `MockWebServer` library now
+    depends on JUnit, though it continues to work with all testing frameworks.
+ *  Fix: `FormEncodingBuilder` is now consistent with browsers in which
+    characters it escapes. Previously we weren’t percent-encoding commas,
+    parens, and other characters.
+ *  Fix: Relax `FormEncodingBuilder` to support building empty forms.
+ *  Fix: Timeouts throw `SocketTimeoutException`, not `InterruptedIOException`.
+ *  Fix: Change `MockWebServer` to use the same logic as OkHttp when determining
+    whether an HTTP request permits a body.
+ *  Fix: `HttpUrl` now uses the canonical form for IPv6 addresses.
+ *  Fix: Use `HttpUrl` internally.
+ *  Fix: Recover from Android 4.2.2 EBADF crashes.
+ *  Fix: Don't crash with an `IllegalStateException` if an HTTP/2 or SPDY
+    write fails, leaving the connection in an inconsistent state.
+ *  Fix: Make sure the default user agent is ASCII.
+
+
+## Version 2.4.0
+
+_2015-05-22_
+
+ *  **Forbid response bodies on HTTP 204 and 205 responses.** Webservers that
+    return such malformed responses will now trigger a `ProtocolException` in
+    the client.
+
+ *  **WebSocketListener has incompatible changes.** The `onOpen()` method is now
+    called on the reader thread, so implementations must return before further
+    websocket messages will be delivered. The `onFailure()` method now includes
+    an HTTP response if one was returned.
+
+## Version 2.4.0-RC1
+
+_2015-05-16_
+
+ *  **New HttpUrl API.** It's like `java.net.URL` but good. Note that
+    `Request.Builder.url()` now throws `IllegalArgumentException` on malformed
+    URLs. (Previous releases would throw a `MalformedURLException` when calling
+    a malformed URL.)
+
+ *  **We've improved connect failure recovery.** We now differentiate between
+    setup, connecting, and connected and implement appropriate recovery rules
+    for each. This changes `Address` to no longer use `ConnectionSpec`. (This is
+    an incompatible API change).
+
+ *  **`FormEncodingBuilder` now uses `%20` instead of `+` for encoded spaces.**
+    Both are permitted-by-spec, but `%20` requires fewer special cases.
+
+ *  **Okio has been updated to 1.4.0.**
+     ```
+     <dependency>
+       <groupId>com.squareup.okio</groupId>
+       <artifactId>okio</artifactId>
+       <version>1.4.0</version>
+     </dependency>
+     ```
+
+ *  **`Request.Builder` no longer accepts null if a request body is required.**
+    Passing null will now fail for request methods that require a body. Instead
+    use an empty body such as this one:
+
+    ```
+        RequestBody.create(null, new byte[0]);
+    ```
+
+ * **`CertificatePinner` now supports wildcard hostnames.** As always with
+   certificate pinning, you must be very careful to avoid [bricking][brick]
+   your app. You'll need to pin both the top-level domain and the `*.` domain
+   for full coverage.
+
+    ```
+     client.setCertificatePinner(new CertificatePinner.Builder()
+         .add("publicobject.com",   "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+         .add("*.publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+         .add("publicobject.com",   "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
+         .add("*.publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
+         .add("publicobject.com",   "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
+         .add("*.publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
+         .add("publicobject.com",   "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
+         .add("*.publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
+         .build());
+    ```
+
+ *  **Interceptors lists are now deep-copied by `OkHttpClient.clone()`.**
+    Previously clones shared interceptors, which made it difficult to customize
+    the interceptors on a request-by-request basis.
+
+ *  New: `Headers.toMultimap()`.
+ *  New: `RequestBody.create(MediaType, ByteString)`.
+ *  New: `ConnectionSpec.isCompatible(SSLSocket)`.
+ *  New: `Dispatcher.getQueuedCallCount()` and
+    `Dispatcher.getRunningCallCount()`. These can be useful in diagnostics.
+ *  Fix: OkHttp no longer shares timeouts between pooled connections. This was
+    causing some applications to crash when connections were reused.
+ *  Fix: `OkApacheClient` now allows an empty `PUT` and `POST`.
+ *  Fix: Websockets no longer rebuffer socket streams.
+ *  Fix: Websockets are now better at handling close frames.
+ *  Fix: Content type matching is now case insensitive.
+ *  Fix: `Vary` headers are not lost with `android.net.http.HttpResponseCache`.
+ *  Fix: HTTP/2 wasn't enforcing stream timeouts when writing the underlying
+    connection. Now it is.
+ *  Fix: Never return null on `call.proceed()`. This was a bug in call
+    cancelation.
+ *  Fix: When a network interceptor mutates a request, that change is now
+    reflected in `Response.networkResponse()`.
+ *  Fix: Badly-behaving caches now throw a checked exception instead of a
+    `NullPointerException`.
+ *  Fix: Better handling of uncaught exceptions in MockWebServer with HTTP/2.
+
 ## Version 2.3.0
 
 _2015-03-16_
@@ -66,7 +206,7 @@
     This is a source-incompatible change. If you have code that calls
     `RequestBody.contentLength()`, your compile will break with this
     update. The change is binary-compatible, however: code compiled
-    for OkHttp 2.0 and 2.1 will continue work with this update.
+    for OkHttp 2.0 and 2.1 will continue to work with this update.
 
  *  **`COMPATIBLE_TLS` no longer supports SSLv3.** In response to the
     [POODLE](http://googleonlinesecurity.blogspot.ca/2014/10/this-poodle-bites-exploiting-ssl-30.html)
@@ -326,7 +466,7 @@
  *  **TunnelRequest is gone.** It specified how to connect to an HTTP proxy.
     OkHttp 2 uses the new `Request` class for this.
 
- *  **Dispatcher** is a new class to manages the queue of asynchronous calls. It
+ *  **Dispatcher** is a new class that manages the queue of asynchronous calls. It
     implements limits on total in-flight calls and in-flight calls per host.
 
 #### Implementation changes
@@ -543,3 +683,4 @@
 
 Initial release.
 
+ [brick]: (https://noncombatant.org/2015/05/01/about-http-public-key-pinning/)
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index d645695..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/README.android b/README.android
deleted file mode 100644
index 0a1b91b..0000000
--- a/README.android
+++ /dev/null
@@ -1,22 +0,0 @@
-URL: https://github.com/square/okhttp
-License: Apache 2
-Description: "OkHttp: An HTTP+SPDY client for Android and Java applications."
-
-Local patches
--------------
-
-Addition of classes in android/ :
-  - com.squareup.okhttp.internal.Platform - to replace the Platform class that
-    comes with okhttp. No use of reflection.
-  - com.squareup.okhttp.Http(s)Handler - integration with Android's corelibs.
-  - com.squareup.okhttp.ConfigAwareConnectionPool - support for a
-    ConnectionPool that listens for network configuration changes.
-  - com.squareup.okhttp.internal.Version - a hard-crafted version of
-    okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
-    for Android.
-
-All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
-  - Commenting of code that references APIs not present on Android.
-
-okio/ contains a snapshot of the Okio project. See okio/README.android for
-details.
diff --git a/README.md b/README.md
index 9f99634..4fde155 100644
--- a/README.md
+++ b/README.md
@@ -11,12 +11,12 @@
 <dependency>
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>okhttp</artifactId>
-  <version>2.3.0</version>
+  <version>2.5.0</version>
 </dependency>
 ```
 or Gradle:
 ```groovy
-compile 'com.squareup.okhttp:okhttp:2.3.0'
+compile 'com.squareup.okhttp:okhttp:2.5.0'
 ```
 
 Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
@@ -36,13 +36,13 @@
 <dependency>
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>mockwebserver</artifactId>
-  <version>2.3.0</version>
+  <version>2.5.0</version>
   <scope>test</scope>
 </dependency>
 ```
 or Gradle:
 ```groovy
-testCompile 'com.squareup.okhttp:mockwebserver:2.3.0'
+testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
 ```
 
 
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
deleted file mode 100644
index 36c3101..0000000
--- a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import libcore.net.event.NetworkEventDispatcher;
-import libcore.net.event.NetworkEventListener;
-
-/**
- * A provider of the shared Android {@link ConnectionPool}. This class is aware of network
- * configuration change events: When the network configuration changes the pool object is discarded
- * and a later calls to {@link #get()} will return a new pool.
- */
-public class ConfigAwareConnectionPool {
-
-  private static final long CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
-
-  private static final int CONNECTION_POOL_MAX_IDLE_CONNECTIONS;
-  private static final long CONNECTION_POOL_KEEP_ALIVE_DURATION_MS;
-  static {
-    String keepAliveProperty = System.getProperty("http.keepAlive");
-    String keepAliveDurationProperty = System.getProperty("http.keepAliveDuration");
-    String maxIdleConnectionsProperty = System.getProperty("http.maxConnections");
-    CONNECTION_POOL_KEEP_ALIVE_DURATION_MS = (keepAliveDurationProperty != null
-        ? Long.parseLong(keepAliveDurationProperty)
-        : CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS);
-    if (keepAliveProperty != null && !Boolean.parseBoolean(keepAliveProperty)) {
-      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 0;
-    } else if (maxIdleConnectionsProperty != null) {
-      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = Integer.parseInt(maxIdleConnectionsProperty);
-    } else {
-      CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 5;
-    }
-  }
-
-  private static final ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool();
-
-  private final NetworkEventDispatcher networkEventDispatcher;
-
-  /**
-   * {@code true} if the ConnectionPool reset has been registered with the
-   * {@link NetworkEventDispatcher}.
-   */
-  private boolean networkEventListenerRegistered;
-
-  private ConnectionPool connectionPool;
-
-  /** Visible for testing. Use {@link #getInstance()} */
-  protected ConfigAwareConnectionPool(NetworkEventDispatcher networkEventDispatcher) {
-    this.networkEventDispatcher = networkEventDispatcher;
-  }
-
-  private ConfigAwareConnectionPool() {
-    networkEventDispatcher = NetworkEventDispatcher.getInstance();
-  }
-
-  public static ConfigAwareConnectionPool getInstance() {
-    return instance;
-  }
-
-  /**
-   * Returns the current {@link ConnectionPool} to use.
-   */
-  public synchronized ConnectionPool get() {
-    if (connectionPool == null) {
-      // Only register the listener once the first time a ConnectionPool is created.
-      if (!networkEventListenerRegistered) {
-        networkEventDispatcher.addListener(new NetworkEventListener() {
-          @Override
-          public void onNetworkConfigurationChanged() {
-            synchronized (ConfigAwareConnectionPool.this) {
-              // If the network config has changed then existing pooled connections should not be
-              // re-used. By setting connectionPool to null it ensures that the next time
-              // getConnectionPool() is called a new pool will be created.
-              connectionPool = null;
-            }
-          }
-        });
-        networkEventListenerRegistered = true;
-      }
-      connectionPool = new ConnectionPool(
-          CONNECTION_POOL_MAX_IDLE_CONNECTIONS, CONNECTION_POOL_KEEP_ALIVE_DURATION_MS);
-    }
-    return connectionPool;
-  }
-}
diff --git a/android/main/java/com/squareup/okhttp/HttpHandler.java b/android/main/java/com/squareup/okhttp/HttpHandler.java
deleted file mode 100644
index 22a0b15..0000000
--- a/android/main/java/com/squareup/okhttp/HttpHandler.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import libcore.net.NetworkSecurityPolicy;
-import java.io.IOException;
-import java.net.Proxy;
-import java.net.ResponseCache;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-public class HttpHandler extends URLStreamHandler {
-
-    private final static List<ConnectionSpec> CLEARTEXT_ONLY =
-        Collections.singletonList(ConnectionSpec.CLEARTEXT);
-
-    private final ConfigAwareConnectionPool configAwareConnectionPool =
-            ConfigAwareConnectionPool.getInstance();
-
-    @Override protected URLConnection openConnection(URL url) throws IOException {
-        return newOkUrlFactory(null /* proxy */).open(url);
-    }
-
-    @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
-        if (url == null || proxy == null) {
-            throw new IllegalArgumentException("url == null || proxy == null");
-        }
-        return newOkUrlFactory(proxy).open(url);
-    }
-
-    @Override protected int getDefaultPort() {
-        return 80;
-    }
-
-    protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
-        OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
-        // For HttpURLConnections created through java.net.URL Android uses a connection pool that
-        // is aware when the default network changes so that pooled connections are not re-used when
-        // the default network changes.
-        okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
-        return okUrlFactory;
-    }
-
-    /**
-     * Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
-     * Android.
-     */
-    // Visible for android.net.Network.
-    public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
-        OkHttpClient client = new OkHttpClient();
-
-        // Explicitly set the timeouts to infinity.
-        client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
-        client.setReadTimeout(0, TimeUnit.MILLISECONDS);
-        client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
-
-        // Do not permit http -> https and https -> http redirects.
-        client.setFollowSslRedirects(false);
-
-        if (NetworkSecurityPolicy.isCleartextTrafficPermitted()) {
-          // Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).
-          client.setConnectionSpecs(CLEARTEXT_ONLY);
-        } else {
-          // Cleartext HTTP denied by policy. Make okhttp deny cleartext HTTP attempts using the
-          // only mechanism it currently provides -- pretend there are no suitable routes.
-          client.setConnectionSpecs(Collections.<ConnectionSpec>emptyList());
-        }
-
-        // When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using
-        // ProxySelector.getDefault().
-        if (proxy != null) {
-            client.setProxy(proxy);
-        }
-
-        // OkHttp requires that we explicitly set the response cache.
-        OkUrlFactory okUrlFactory = new OkUrlFactory(client);
-        ResponseCache responseCache = ResponseCache.getDefault();
-        if (responseCache != null) {
-            AndroidInternal.setResponseCache(okUrlFactory, responseCache);
-        }
-        return okUrlFactory;
-    }
-
-}
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
deleted file mode 100644
index 149d860..0000000
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import java.net.Proxy;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.net.ssl.HttpsURLConnection;
-
-public final class HttpsHandler extends HttpHandler {
-
-    /**
-     * The initial connection spec to use when connecting to an https:// server, and the prototype
-     * for the others below. Note that Android does not set the cipher suites to use so the socket's
-     * defaults enabled cipher suites will be used instead. When the SSLSocketFactory is provided by
-     * the app or GMS core we will not override the enabled ciphers set on the sockets it produces
-     * with a list hardcoded at release time. This is deliberate.
-     * For the TLS versions we <em>will</em> select a known subset from the set of enabled TLS
-     * versions on the socket.
-     */
-    private static final ConnectionSpec TLS_1_2_AND_BELOW = new ConnectionSpec.Builder(true)
-        .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
-        .supportsTlsExtensions(true)
-        .build();
-
-    private static final ConnectionSpec TLS_1_1_AND_BELOW =
-        new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
-            .tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
-            .supportsTlsExtensions(true)
-            .build();
-
-    private static final ConnectionSpec TLS_1_0_AND_BELOW =
-        new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
-            .tlsVersions(TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
-            .build();
-
-    private static final ConnectionSpec SSL_3_0 =
-        new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
-            .tlsVersions(TlsVersion.SSL_3_0)
-            .build();
-
-    /** Try up to 4 times to negotiate a connection with each server. */
-    private static final List<ConnectionSpec> SECURE_CONNECTION_SPECS =
-        Arrays.asList(TLS_1_2_AND_BELOW, TLS_1_1_AND_BELOW, TLS_1_0_AND_BELOW, SSL_3_0);
-
-    private static final List<Protocol> HTTP_1_1_ONLY = Arrays.asList(Protocol.HTTP_1_1);
-
-    private final ConfigAwareConnectionPool configAwareConnectionPool =
-            ConfigAwareConnectionPool.getInstance();
-
-    @Override protected int getDefaultPort() {
-        return 443;
-    }
-
-    @Override
-    protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
-        OkUrlFactory okUrlFactory = createHttpsOkUrlFactory(proxy);
-        // For HttpsURLConnections created through java.net.URL Android uses a connection pool that
-        // is aware when the default network changes so that pooled connections are not re-used when
-        // the default network changes.
-        okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
-        return okUrlFactory;
-    }
-
-    /**
-     * Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
-     * Android.
-     */
-    // Visible for android.net.Network.
-    public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
-        // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
-        OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
-
-        OkHttpClient okHttpClient = okUrlFactory.client();
-
-        // Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.
-        okHttpClient.setProtocols(HTTP_1_1_ONLY);
-
-        // Use Android's preferred fallback approach and cipher suite selection.
-        okHttpClient.setConnectionSpecs(SECURE_CONNECTION_SPECS);
-
-        // OkHttp does not automatically honor the system-wide HostnameVerifier set with
-        // HttpsURLConnection.setDefaultHostnameVerifier().
-        okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
-        // OkHttp does not automatically honor the system-wide SSLSocketFactory set with
-        // HttpsURLConnection.setDefaultSSLSocketFactory().
-        // See https://github.com/square/okhttp/issues/184 for details.
-        okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
-
-        return okUrlFactory;
-    }
-}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
deleted file mode 100644
index 8c44ad9..0000000
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2012 Square, Inc.
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-import dalvik.system.SocketTagger;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.List;
-import javax.net.ssl.SSLSocket;
-
-import com.squareup.okhttp.Protocol;
-
-import okio.Buffer;
-
-/**
- * Access to proprietary Android APIs. Doesn't use reflection.
- */
-public final class Platform {
-    private static final Platform PLATFORM = new Platform();
-
-    public static Platform get() {
-        return PLATFORM;
-    }
-
-    /** setUseSessionTickets(boolean) */
-    private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
-            new OptionalMethod<Socket>(null, "setUseSessionTickets", Boolean.TYPE);
-    /** setHostname(String) */
-    private static final OptionalMethod<Socket> SET_HOSTNAME =
-            new OptionalMethod<Socket>(null, "setHostname", String.class);
-    /** byte[] getAlpnSelectedProtocol() */
-    private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
-            new OptionalMethod<Socket>(byte[].class, "getAlpnSelectedProtocol");
-    /** setAlpnSelectedProtocol(byte[]) */
-    private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
-            new OptionalMethod<Socket>(null, "setAlpnProtocols", byte[].class );
-
-    public void logW(String warning) {
-        System.logW(warning);
-    }
-
-    public void tagSocket(Socket socket) throws SocketException {
-        SocketTagger.get().tag(socket);
-    }
-
-    public void untagSocket(Socket socket) throws SocketException {
-        SocketTagger.get().untag(socket);
-    }
-
-    public URI toUriLenient(URL url) throws URISyntaxException {
-        return url.toURILenient();
-    }
-
-    public void configureTlsExtensions(
-            SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
-        // Enable SNI and session tickets.
-        if (hostname != null) {
-            SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
-            SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
-        }
-
-        // Enable ALPN.
-        boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(sslSocket);
-        if (!alpnSupported) {
-            return;
-        }
-
-        Object[] parameters = { concatLengthPrefixed(protocols) };
-        if (alpnSupported) {
-            SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
-        }
-    }
-
-    /**
-     * Called after the TLS handshake to release resources allocated by {@link
-     * #configureTlsExtensions}.
-     */
-    public void afterHandshake(SSLSocket sslSocket) {
-    }
-
-    public String getSelectedProtocol(SSLSocket socket) {
-        boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
-        if (!alpnSupported) {
-            return null;
-        }
-
-        byte[] alpnResult =
-                (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
-        if (alpnResult != null) {
-            return new String(alpnResult, Util.UTF_8);
-        }
-        return null;
-    }
-
-    public void connectSocket(Socket socket, InetSocketAddress address,
-              int connectTimeout) throws IOException {
-        socket.connect(address, connectTimeout);
-    }
-
-    /** Prefix used on custom headers. */
-    public String getPrefix() {
-        return "X-Android";
-    }
-
-    /**
-     * Returns the concatenation of 8-bit, length prefixed protocol names.
-     * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
-     */
-    static byte[] concatLengthPrefixed(List<Protocol> protocols) {
-        Buffer result = new Buffer();
-        for (int i = 0, size = protocols.size(); i < size; i++) {
-            Protocol protocol = protocols.get(i);
-            if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
-            result.writeByte(protocol.toString().length());
-            result.writeUtf8(protocol.toString());
-        }
-        return result.readByteArray();
-    }
-}
diff --git a/android/main/java/com/squareup/okhttp/internal/Version.java b/android/main/java/com/squareup/okhttp/internal/Version.java
deleted file mode 100644
index 6a63f9b..0000000
--- a/android/main/java/com/squareup/okhttp/internal/Version.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2014 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-public final class Version {
-  public static String userAgent() {
-    String agent = System.getProperty("http.agent");
-    return agent != null ? agent : ("Java" + System.getProperty("java.version"));
-  }
-
-  private Version() {
-  }
-}
diff --git a/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java b/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
deleted file mode 100644
index 825f980..0000000
--- a/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import org.junit.Test;
-
-import libcore.net.event.NetworkEventDispatcher;
-
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-
-/**
- * Tests for {@link ConfigAwareConnectionPool}.
- */
-public class ConfigAwareConnectionPoolTest {
-
-  @Test
-  public void getInstance() {
-    assertSame(ConfigAwareConnectionPool.getInstance(), ConfigAwareConnectionPool.getInstance());
-  }
-
-  @Test
-  public void get() throws Exception {
-    NetworkEventDispatcher networkEventDispatcher = new NetworkEventDispatcher() {};
-    ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool(networkEventDispatcher) {};
-    assertSame(instance.get(), instance.get());
-
-    ConnectionPool beforeEventInstance = instance.get();
-    networkEventDispatcher.onNetworkConfigurationChanged();
-
-    assertNotSame(beforeEventInstance, instance.get());
-  }
-}
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
deleted file mode 100644
index 2e5dcdd..0000000
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.squareup.okhttp.internal;
-
-import com.android.org.conscrypt.OpenSSLSocketImpl;
-import com.squareup.okhttp.Protocol;
-
-import org.junit.Test;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.net.ssl.HandshakeCompletedListener;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
-
-import okio.ByteString;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Tests for {@link Platform}.
- */
-public class PlatformTest {
-
-  @Test
-  public void enableTlsExtensionOptionalMethods() throws Exception {
-    Platform platform = new Platform();
-
-    // Expect no error
-    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
-    List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1, Protocol.SPDY_3);
-    platform.configureTlsExtensions(arbitrarySocketImpl, "host", protocols);
-    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
-    platform.configureTlsExtensions(npnOnlySSLSocketImpl, "host", protocols);
-
-    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
-    platform.configureTlsExtensions(openSslSocket, "host", protocols);
-    assertTrue(openSslSocket.useSessionTickets);
-    assertEquals("host", openSslSocket.hostname);
-    assertArrayEquals(Platform.concatLengthPrefixed(protocols), openSslSocket.alpnProtocols);
-  }
-
-  @Test
-  public void getSelectedProtocol() throws Exception {
-    Platform platform = new Platform();
-    String selectedProtocol = "alpn";
-
-    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
-    assertNull(platform.getSelectedProtocol(arbitrarySocketImpl));
-
-    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
-    assertNull(platform.getSelectedProtocol(npnOnlySSLSocketImpl));
-
-    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
-    openSslSocket.alpnProtocols = selectedProtocol.getBytes(StandardCharsets.UTF_8);
-    assertEquals(selectedProtocol, platform.getSelectedProtocol(openSslSocket));
-  }
-
-  private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
-    private boolean useSessionTickets;
-    private String hostname;
-    private byte[] alpnProtocols;
-
-    public FullOpenSSLSocketImpl() throws IOException {
-      super(null);
-    }
-
-    @Override
-    public void setUseSessionTickets(boolean useSessionTickets) {
-      this.useSessionTickets = useSessionTickets;
-    }
-
-    @Override
-    public void setHostname(String hostname) {
-      this.hostname = hostname;
-    }
-
-    @Override
-    public void setAlpnProtocols(byte[] alpnProtocols) {
-      this.alpnProtocols = alpnProtocols;
-    }
-
-    @Override
-    public byte[] getAlpnSelectedProtocol() {
-      return alpnProtocols;
-    }
-  }
-
-  // Legacy case - NPN support has been dropped.
-  private static class NpnOnlySSLSocketImpl extends TestSSLSocketImpl {
-
-    private byte[] npnProtocols;
-
-    public void setNpnProtocols(byte[] npnProtocols) {
-      this.npnProtocols = npnProtocols;
-    }
-
-    public byte[] getNpnSelectedProtocol() {
-      return npnProtocols;
-    }
-  }
-
-  private static class TestSSLSocketImpl extends SSLSocket {
-
-    @Override
-    public String[] getSupportedCipherSuites() {
-      return new String[0];
-    }
-
-    @Override
-    public String[] getEnabledCipherSuites() {
-      return new String[0];
-    }
-
-    @Override
-    public void setEnabledCipherSuites(String[] suites) {
-    }
-
-    @Override
-    public String[] getSupportedProtocols() {
-      return new String[0];
-    }
-
-    @Override
-    public String[] getEnabledProtocols() {
-      return new String[0];
-    }
-
-    @Override
-    public void setEnabledProtocols(String[] protocols) {
-    }
-
-    @Override
-    public SSLSession getSession() {
-      return null;
-    }
-
-    @Override
-    public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
-    }
-
-    @Override
-    public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
-    }
-
-    @Override
-    public void startHandshake() throws IOException {
-    }
-
-    @Override
-    public void setUseClientMode(boolean mode) {
-    }
-
-    @Override
-    public boolean getUseClientMode() {
-      return false;
-    }
-
-    @Override
-    public void setNeedClientAuth(boolean need) {
-    }
-
-    @Override
-    public void setWantClientAuth(boolean want) {
-    }
-
-    @Override
-    public boolean getNeedClientAuth() {
-      return false;
-    }
-
-    @Override
-    public boolean getWantClientAuth() {
-      return false;
-    }
-
-    @Override
-    public void setEnableSessionCreation(boolean flag) {
-    }
-
-    @Override
-    public boolean getEnableSessionCreation() {
-      return false;
-    }
-  }
-}
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 3a5fccd..d0d2566 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>benchmarks</artifactId>
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
index cb8e719..3f2609d 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
@@ -15,10 +15,10 @@
  */
 package com.squareup.okhttp.benchmarks;
 
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URL;
 import java.util.concurrent.TimeUnit;
 import java.util.zip.GZIPInputStream;
 import javax.net.ssl.SSLContext;
@@ -49,14 +49,14 @@
     client = new DefaultHttpClient(connectionManager);
   }
 
-  @Override public Runnable request(URL url) {
+  @Override public Runnable request(HttpUrl url) {
     return new ApacheHttpClientRequest(url);
   }
 
   class ApacheHttpClientRequest implements Runnable {
-    private final URL url;
+    private final HttpUrl url;
 
-    public ApacheHttpClientRequest(URL url) {
+    public ApacheHttpClientRequest(HttpUrl url) {
       this.url = url;
     }
 
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
index 7f0073c..04b7f7f 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
@@ -18,6 +18,7 @@
 import com.google.caliper.Param;
 import com.google.caliper.model.ArbitraryMeasurement;
 import com.google.caliper.runner.CaliperMain;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.mockwebserver.Dispatcher;
@@ -25,7 +26,6 @@
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
 import java.io.IOException;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -101,7 +101,7 @@
     // Prepare the client & server
     httpClient.prepare(this);
     MockWebServer server = startServer();
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     int requestCount = 0;
     long reportStart = System.nanoTime();
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java
index 136c5d8..2820dc1 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpClient.java
@@ -15,11 +15,11 @@
  */
 package com.squareup.okhttp.benchmarks;
 
-import java.net.URL;
+import com.squareup.okhttp.HttpUrl;
 
 /** An HTTP client to benchmark. */
 interface HttpClient {
   void prepare(Benchmark benchmark);
-  void enqueue(URL url) throws Exception;
+  void enqueue(HttpUrl url) throws Exception;
   boolean acceptingJobs();
 }
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
index 5d8cec5..1b5571b 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
@@ -15,8 +15,8 @@
  */
 package com.squareup.okhttp.benchmarks;
 
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.PooledByteBufAllocator;
@@ -41,7 +41,6 @@
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.ssl.SslHandler;
-import java.net.URL;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.concurrent.TimeUnit;
@@ -54,7 +53,7 @@
 
   // Guarded by this. Real apps need more capable connection management.
   private final Deque<HttpChannel> freeChannels = new ArrayDeque<>();
-  private final Deque<URL> backlog = new ArrayDeque<>();
+  private final Deque<HttpUrl> backlog = new ArrayDeque<>();
 
   private int totalChannels = 0;
   private int concurrencyLevel;
@@ -89,7 +88,7 @@
         .handler(channelInitializer);
   }
 
-  @Override public void enqueue(URL url) throws Exception {
+  @Override public void enqueue(HttpUrl url) throws Exception {
     HttpChannel httpChannel = null;
     synchronized (this) {
       if (!freeChannels.isEmpty()) {
@@ -102,7 +101,7 @@
       }
     }
     if (httpChannel == null) {
-      Channel channel = bootstrap.connect(url.getHost(), Util.getEffectivePort(url))
+      Channel channel = bootstrap.connect(url.host(), url.port())
           .sync().channel();
       httpChannel = (HttpChannel) channel.pipeline().last();
     }
@@ -119,7 +118,7 @@
   }
 
   private void release(HttpChannel httpChannel) {
-    URL url;
+    HttpUrl url;
     synchronized (this) {
       url = backlog.pop();
       if (url == null) {
@@ -143,12 +142,12 @@
       this.channel = channel;
     }
 
-    private void sendRequest(URL url) {
+    private void sendRequest(HttpUrl url) {
       start = System.nanoTime();
       total = 0;
       HttpRequest request = new DefaultFullHttpRequest(
-          HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath());
-      request.headers().set(HttpHeaders.Names.HOST, url.getHost());
+          HttpVersion.HTTP_1_1, HttpMethod.GET, url.encodedPath());
+      request.headers().set(HttpHeaders.Names.HOST, url.host());
       request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);
       channel.writeAndFlush(request);
     }
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
index 3885ed7..496e8d3 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
@@ -15,12 +15,12 @@
  */
 package com.squareup.okhttp.benchmarks;
 
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.OkUrlFactory;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
 import java.net.HttpURLConnection;
-import java.net.URL;
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
@@ -50,21 +50,21 @@
     }
   }
 
-  @Override public Runnable request(URL url) {
+  @Override public Runnable request(HttpUrl url) {
     return new OkHttpRequest(url);
   }
 
   class OkHttpRequest implements Runnable {
-    private final URL url;
+    private final HttpUrl url;
 
-    public OkHttpRequest(URL url) {
+    public OkHttpRequest(HttpUrl url) {
       this.url = url;
     }
 
     public void run() {
       long start = System.nanoTime();
       try {
-        HttpURLConnection urlConnection = new OkUrlFactory(client).open(url);
+        HttpURLConnection urlConnection = new OkUrlFactory(client).open(url.url());
         long total = readAllAndClose(urlConnection.getInputStream());
         long finish = System.nanoTime();
 
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
index ab78490..cf0ad4a 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
@@ -17,13 +17,13 @@
 
 import com.squareup.okhttp.Callback;
 import com.squareup.okhttp.Dispatcher;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
-import java.net.URL;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -83,7 +83,7 @@
     };
   }
 
-  @Override public void enqueue(URL url) throws Exception {
+  @Override public void enqueue(HttpUrl url) throws Exception {
     requestsInFlight.incrementAndGet();
     client.newCall(new Request.Builder().tag(System.nanoTime()).url(url).build()).enqueue(callback);
   }
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java
index b15eedc..3b96315 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/SynchronousHttpClient.java
@@ -15,9 +15,9 @@
  */
 package com.squareup.okhttp.benchmarks;
 
+import com.squareup.okhttp.HttpUrl;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URL;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -33,7 +33,7 @@
         1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
   }
 
-  @Override public void enqueue(URL url) {
+  @Override public void enqueue(HttpUrl url) {
     executor.execute(request(url));
   }
 
@@ -51,5 +51,5 @@
     return total;
   }
 
-  abstract Runnable request(URL url);
+  abstract Runnable request(HttpUrl url);
 }
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
index 630ec91..e177430 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
@@ -15,11 +15,11 @@
  */
 package com.squareup.okhttp.benchmarks;
 
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
-import java.net.URL;
 import java.util.concurrent.TimeUnit;
 import java.util.zip.GZIPInputStream;
 import javax.net.ssl.HostnameVerifier;
@@ -46,21 +46,21 @@
     }
   }
 
-  @Override public Runnable request(URL url) {
+  @Override public Runnable request(HttpUrl url) {
     return new UrlConnectionRequest(url);
   }
 
   static class UrlConnectionRequest implements Runnable {
-    private final URL url;
+    private final HttpUrl url;
 
-    public UrlConnectionRequest(URL url) {
+    public UrlConnectionRequest(HttpUrl url) {
       this.url = url;
     }
 
     public void run() {
       long start = System.nanoTime();
       try {
-        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+        HttpURLConnection urlConnection = (HttpURLConnection) url.url().openConnection();
         InputStream in = urlConnection.getInputStream();
         if ("gzip".equals(urlConnection.getHeaderField("Content-Encoding"))) {
           in = new GZIPInputStream(in);
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
deleted file mode 100644
index c84813d..0000000
--- a/jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-rule com.squareup.** com.android.@1
-rule okio.** com.android.okhttp.okio.@1
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
index eb698c3..3fd6ca6 100644
--- a/mockwebserver/README.md
+++ b/mockwebserver/README.md
@@ -42,7 +42,7 @@
   server.start();
 
   // Ask the server for its URL. You'll need this to make HTTP requests.
-  URL baseUrl = server.getUrl("/v1/chat/");
+  URL baseUrl = server.url("/v1/chat/");
 
   // Exercise your application code, which should make those HTTP requests.
   // Responses are returned in the same order that they are enqueued.
@@ -125,7 +125,7 @@
     public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
 
         if (request.getPath().equals("/v1/login/auth/")){
-            return new MockResponse().setResponseCode(200));
+            return new MockResponse().setResponseCode(200);
         } else if (request.getPath().equals("v1/check/version/")){
             return new MockResponse().setResponseCode(200).setBody("version=9");
         } else if (request.getPath().equals("/v1/profile/info")) {
@@ -140,8 +140,7 @@
 
 ### Download
 
-The best way to get MockWebServer is via Maven:
-
+Get MockWebServer via Maven:
 ```xml
 <dependency>
   <groupId>com.squareup.okhttp</groupId>
@@ -151,6 +150,11 @@
 </dependency>
 ```
 
+or via Gradle 
+```groovy
+testCompile 'com.squareup.okhttp:mockwebserver:(insert latest version)'
+```
+
 ### License
 
     Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index 3603a84..9ef5211 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>mockwebserver</artifactId>
@@ -36,7 +36,6 @@
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
-      <optional>true</optional>
     </dependency>
   </dependencies>
 
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
similarity index 80%
rename from mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
rename to mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
index 8e93b47..b95b64d 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.Platform;
@@ -36,15 +36,16 @@
 import okio.Source;
 
 /** A basic SPDY/HTTP_2 server that serves the contents of a local directory. */
-public final class SpdyServer implements IncomingStreamHandler {
-  static final Logger logger = Logger.getLogger(SpdyServer.class.getName());
+public final class FramedServer implements IncomingStreamHandler {
+  static final Logger logger = Logger.getLogger(FramedServer.class.getName());
 
-  private final List<Protocol> spdyProtocols = Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3);
+  private final List<Protocol> framedProtocols =
+      Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3);
 
   private final File baseDirectory;
   private final SSLSocketFactory sslSocketFactory;
 
-  public SpdyServer(File baseDirectory, SSLSocketFactory sslSocketFactory) {
+  public FramedServer(File baseDirectory, SSLSocketFactory sslSocketFactory) {
     this.baseDirectory = baseDirectory;
     this.sslSocketFactory = sslSocketFactory;
   }
@@ -61,19 +62,19 @@
         SSLSocket sslSocket = doSsl(socket);
         String protocolString = Platform.get().getSelectedProtocol(sslSocket);
         Protocol protocol = protocolString != null ? Protocol.get(protocolString) : null;
-        if (protocol == null || !spdyProtocols.contains(protocol)) {
+        if (protocol == null || !framedProtocols.contains(protocol)) {
           throw new ProtocolException("Protocol " + protocol + " unsupported");
         }
-        SpdyConnection spdyConnection = new SpdyConnection.Builder(false, sslSocket)
+        FramedConnection framedConnection = new FramedConnection.Builder(false, sslSocket)
             .protocol(protocol)
             .handler(this)
             .build();
-        spdyConnection.sendConnectionPreface();
+        framedConnection.sendConnectionPreface();
       } catch (IOException e) {
-        logger.log(Level.INFO, "SpdyServer connection failure: " + e);
+        logger.log(Level.INFO, "FramedServer connection failure: " + e);
         Util.closeQuietly(socket);
       } catch (Exception e) {
-        logger.log(Level.WARNING, "SpdyServer unexpected failure", e);
+        logger.log(Level.WARNING, "FramedServer unexpected failure", e);
         Util.closeQuietly(socket);
       }
     }
@@ -83,12 +84,12 @@
     SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
         socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
     sslSocket.setUseClientMode(false);
-    Platform.get().configureTlsExtensions(sslSocket, null, spdyProtocols);
+    Platform.get().configureTlsExtensions(sslSocket, null, framedProtocols);
     sslSocket.startHandshake();
     return sslSocket;
   }
 
-  @Override public void receive(final SpdyStream stream) throws IOException {
+  @Override public void receive(final FramedStream stream) throws IOException {
     try {
       List<Header> requestHeaders = stream.getRequestHeaders();
       String path = null;
@@ -118,7 +119,7 @@
     }
   }
 
-  private void send404(SpdyStream stream, String path) throws IOException {
+  private void send404(FramedStream stream, String path) throws IOException {
     List<Header> responseHeaders = Arrays.asList(
         new Header(":status", "404"),
         new Header(":version", "HTTP/1.1"),
@@ -130,7 +131,7 @@
     out.close();
   }
 
-  private void serveDirectory(SpdyStream stream, File[] files) throws IOException {
+  private void serveDirectory(FramedStream stream, File[] files) throws IOException {
     List<Header> responseHeaders = Arrays.asList(
         new Header(":status", "200"),
         new Header(":version", "HTTP/1.1"),
@@ -145,7 +146,7 @@
     out.close();
   }
 
-  private void serveFile(SpdyStream stream, File file) throws IOException {
+  private void serveFile(FramedStream stream, File file) throws IOException {
     List<Header> responseHeaders = Arrays.asList(
         new Header(":status", "200"),
         new Header(":version", "HTTP/1.1"),
@@ -175,11 +176,11 @@
 
   public static void main(String... args) throws Exception {
     if (args.length != 1 || args[0].startsWith("-")) {
-      System.out.println("Usage: SpdyServer <base directory>");
+      System.out.println("Usage: FramedServer <base directory>");
       return;
     }
 
-    SpdyServer server = new SpdyServer(new File(args[0]),
+    FramedServer server = new FramedServer(new File(args[0]),
         SslContextBuilder.localhost().getSocketFactory());
     server.run();
   }
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
index 09dda56..bc43bd4 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -16,6 +16,7 @@
 package com.squareup.okhttp.mockwebserver;
 
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.ws.WebSocketListener;
 import java.util.ArrayList;
 import java.util.List;
@@ -106,6 +107,16 @@
   }
 
   /**
+   * Adds a new header with the name and value. This may be used to add multiple
+   * headers with the same name. Unlike {@link #addHeader(String, Object)} this
+   * does not validate the name and value.
+   */
+  public MockResponse addHeaderLenient(String name, Object value) {
+    Internal.instance.addLenient(headers, name, String.valueOf(value));
+    return this;
+  }
+
+  /**
    * Removes all headers named {@code name}, then adds a new header with the
    * name and value.
    */
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
index 259cf3e..458c6f9 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -18,24 +18,25 @@
 package com.squareup.okhttp.mockwebserver;
 
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.NamedRunnable;
 import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.spdy.ErrorCode;
-import com.squareup.okhttp.internal.spdy.Header;
-import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
-import com.squareup.okhttp.internal.spdy.SpdyConnection;
-import com.squareup.okhttp.internal.spdy.SpdyStream;
+import com.squareup.okhttp.internal.framed.ErrorCode;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.framed.FramedStream;
+import com.squareup.okhttp.internal.framed.Header;
+import com.squareup.okhttp.internal.framed.IncomingStreamHandler;
+import com.squareup.okhttp.internal.http.HttpMethod;
 import com.squareup.okhttp.internal.ws.RealWebSocket;
 import com.squareup.okhttp.internal.ws.WebSocketProtocol;
 import com.squareup.okhttp.ws.WebSocketListener;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
-import java.net.MalformedURLException;
 import java.net.ProtocolException;
 import java.net.Proxy;
 import java.net.ServerSocket;
@@ -76,8 +77,12 @@
 import okio.Okio;
 import okio.Sink;
 import okio.Timeout;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -85,7 +90,7 @@
  * A scriptable web server. Callers supply canned responses and the server
  * replays them upon request in sequence.
  */
-public final class MockWebServer {
+public final class MockWebServer implements TestRule {
   private static final X509TrustManager UNTRUSTED_TRUST_MANAGER = new X509TrustManager() {
     @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
         throws CertificateException {
@@ -107,8 +112,8 @@
 
   private final Set<Socket> openClientSockets =
       Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
-  private final Set<SpdyConnection> openSpdyConnections =
-      Collections.newSetFromMap(new ConcurrentHashMap<SpdyConnection, Boolean>());
+  private final Set<FramedConnection> openFramedConnections =
+      Collections.newSetFromMap(new ConcurrentHashMap<FramedConnection, Boolean>());
   private final AtomicInteger requestCount = new AtomicInteger();
   private long bodyLimit = Long.MAX_VALUE;
   private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
@@ -124,43 +129,75 @@
   private List<Protocol> protocols
       = Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1);
 
-  public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
-    if (serverSocketFactory == null) throw new IllegalArgumentException("null serverSocketFactory");
-    this.serverSocketFactory = serverSocketFactory;
+  private boolean started;
+
+  private synchronized void maybeStart() {
+    if (started) return;
+    try {
+      start();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override public Statement apply(final Statement base, Description description) {
+    return new Statement() {
+      @Override public void evaluate() throws Throwable {
+        maybeStart();
+        try {
+          base.evaluate();
+        } finally {
+          try {
+            shutdown();
+          } catch (IOException e) {
+            logger.log(Level.WARNING, "MockWebServer shutdown failed", e);
+          }
+        }
+      }
+    };
   }
 
   public int getPort() {
-    if (port == -1) throw new IllegalStateException("Call start() before getPort()");
+    maybeStart();
     return port;
   }
 
   public String getHostName() {
-    if (inetSocketAddress == null) {
-      throw new IllegalStateException("Call start() before getHostName()");
-    }
+    maybeStart();
     return inetSocketAddress.getHostName();
   }
 
   public Proxy toProxyAddress() {
-    if (inetSocketAddress == null) {
-      throw new IllegalStateException("Call start() before toProxyAddress()");
-    }
+    maybeStart();
     InetSocketAddress address = new InetSocketAddress(inetSocketAddress.getAddress(), getPort());
     return new Proxy(Proxy.Type.HTTP, address);
   }
 
+  public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
+    this.serverSocketFactory = serverSocketFactory;
+  }
+
   /**
    * Returns a URL for connecting to this server.
    * @param path the request path, such as "/".
    */
+  @Deprecated
   public URL getUrl(String path) {
-    try {
-      return sslSocketFactory != null
-          ? new URL("https://" + getHostName() + ":" + getPort() + path)
-          : new URL("http://" + getHostName() + ":" + getPort() + path);
-    } catch (MalformedURLException e) {
-      throw new AssertionError(e);
-    }
+    return url(path).url();
+  }
+
+  /**
+   * Returns a URL for connecting to this server.
+   *
+   * @param path the request path, such as "/".
+   */
+  public HttpUrl url(String path) {
+    return new HttpUrl.Builder()
+        .scheme(sslSocketFactory != null ? "https" : "http")
+        .host(getHostName())
+        .port(getPort())
+        .build()
+        .resolve(path);
   }
 
   /**
@@ -266,16 +303,6 @@
     ((QueueDispatcher) dispatcher).enqueueResponse(response.clone());
   }
 
-  /** @deprecated Use {@link #start()}. */
-  public void play() throws IOException {
-    start();
-  }
-
-  /** @deprecated Use {@link #start(int)}. */
-  public void play(int port) throws IOException {
-    start(port);
-  }
-
   /** Equivalent to {@code start(0)}. */
   public void start() throws IOException {
     start(0);
@@ -310,8 +337,10 @@
    *
    * @param inetSocketAddress the socket address to bind the server on
    */
-  private void start(InetSocketAddress inetSocketAddress) throws IOException {
-    if (executor != null) throw new IllegalStateException("start() already called");
+  private synchronized void start(InetSocketAddress inetSocketAddress) throws IOException {
+    if (started) throw new IllegalStateException("start() already called");
+    started = true;
+
     executor = Executors.newCachedThreadPool(Util.threadFactory("MockWebServer", false));
     this.inetSocketAddress = inetSocketAddress;
     serverSocket = serverSocketFactory.createServerSocket();
@@ -335,7 +364,7 @@
           Util.closeQuietly(s.next());
           s.remove();
         }
-        for (Iterator<SpdyConnection> s = openSpdyConnections.iterator(); s.hasNext(); ) {
+        for (Iterator<FramedConnection> s = openFramedConnections.iterator(); s.hasNext(); ) {
           Util.closeQuietly(s.next());
           s.remove();
         }
@@ -364,7 +393,8 @@
     });
   }
 
-  public void shutdown() throws IOException {
+  public synchronized void shutdown() throws IOException {
+    if (!started) return;
     if (serverSocket == null) throw new IllegalStateException("shutdown() before start()");
 
     // Cause acceptConnections() to break out.
@@ -431,12 +461,12 @@
         }
 
         if (protocol != Protocol.HTTP_1_1) {
-          SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, protocol);
-          SpdyConnection spdyConnection =
-              new SpdyConnection.Builder(false, socket).protocol(protocol)
-                  .handler(spdySocketHandler)
+          FramedSocketHandler framedSocketHandler = new FramedSocketHandler(socket, protocol);
+          FramedConnection framedConnection =
+              new FramedConnection.Builder(false, socket).protocol(protocol)
+                  .handler(framedSocketHandler)
                   .build();
-          openSpdyConnections.add(spdyConnection);
+          openFramedConnections.add(framedConnection);
           openClientSockets.remove(socket);
           return;
         }
@@ -499,11 +529,13 @@
           throw new ProtocolException("unexpected data");
         }
 
+        boolean reuseSocket = true;
         boolean requestWantsWebSockets = "Upgrade".equalsIgnoreCase(request.getHeader("Connection"))
             && "websocket".equalsIgnoreCase(request.getHeader("Upgrade"));
         boolean responseWantsWebSockets = response.getWebSocketListener() != null;
         if (requestWantsWebSockets && responseWantsWebSockets) {
           handleWebSocketUpgrade(socket, source, sink, request, response);
+          reuseSocket = false;
         } else {
           writeHttpResponse(socket, sink, response);
         }
@@ -523,7 +555,7 @@
         }
 
         sequenceNumber++;
-        return true;
+        return reuseSocket;
       }
     });
   }
@@ -592,10 +624,10 @@
     boolean hasBody = false;
     TruncatingBuffer requestBody = new TruncatingBuffer(bodyLimit);
     List<Integer> chunkSizes = new ArrayList<>();
-    MockResponse throttlePolicy = dispatcher.peek();
+    MockResponse policy = dispatcher.peek();
     if (contentLength != -1) {
       hasBody = contentLength > 0;
-      throttledTransfer(throttlePolicy, socket, source, Okio.buffer(requestBody), contentLength);
+      throttledTransfer(policy, socket, source, Okio.buffer(requestBody), contentLength, true);
     } else if (chunked) {
       hasBody = true;
       while (true) {
@@ -605,24 +637,14 @@
           break;
         }
         chunkSizes.add(chunkSize);
-        throttledTransfer(throttlePolicy, socket, source, Okio.buffer(requestBody), chunkSize);
+        throttledTransfer(policy, socket, source, Okio.buffer(requestBody), chunkSize, true);
         readEmptyLine(source);
       }
     }
 
-    if (request.startsWith("OPTIONS ")
-        || request.startsWith("GET ")
-        || request.startsWith("HEAD ")
-        || request.startsWith("TRACE ")
-        || request.startsWith("CONNECT ")) {
-      if (hasBody) {
-        throw new IllegalArgumentException("Request must not have a body: " + request);
-      }
-    } else if (!request.startsWith("POST ")
-        && !request.startsWith("PUT ")
-        && !request.startsWith("PATCH ")
-        && !request.startsWith("DELETE ")) { // Permitted as spec is ambiguous.
-      throw new UnsupportedOperationException("Unexpected method: " + request);
+    String method = request.substring(0, request.indexOf(' '));
+    if (hasBody && !HttpMethod.permitsRequestBody(method)) {
+      throw new IllegalArgumentException("Request must not have a body: " + request);
     }
 
     return new RecordedRequest(request, headers.build(), chunkSizes, requestBody.receivedByteCount,
@@ -654,8 +676,10 @@
         };
 
     // Adapt the request and response into our Request and Response domain model.
+    String scheme = request.getTlsVersion() != null ? "https" : "http";
+    String authority = request.getHeader("Host"); // Has host and port.
     final Request fancyRequest = new Request.Builder()
-        .get().url(request.getPath())
+        .url(scheme + "://" + authority + "/")
         .headers(request.getHeaders())
         .build();
     final Response fancyResponse = new Response.Builder()
@@ -666,19 +690,8 @@
         .protocol(Protocol.HTTP_1_1)
         .build();
 
-    // The callback might act synchronously. Give it its own thread.
-    new Thread(new Runnable() {
-      @Override public void run() {
-        try {
-          listener.onOpen(webSocket, fancyRequest, fancyResponse);
-        } catch (IOException e) {
-          // TODO try to write close frame?
-          connectionClose.countDown();
-        }
-      }
-    }, "MockWebServer WebSocket Writer " + request.getPath()).start();
+    listener.onOpen(webSocket, fancyResponse);
 
-    // Use this thread to continuously read messages.
     while (webSocket.readMessage()) {
     }
 
@@ -711,7 +724,7 @@
     Buffer body = response.getBody();
     if (body == null) return;
     sleepIfDelayed(response);
-    throttledTransfer(response, socket, body, sink, Long.MAX_VALUE);
+    throttledTransfer(response, socket, body, sink, body.size(), false);
   }
 
   private void sleepIfDelayed(MockResponse response) {
@@ -728,19 +741,29 @@
   /**
    * Transfer bytes from {@code source} to {@code sink} until either {@code byteCount}
    * bytes have been transferred or {@code source} is exhausted. The transfer is
-   * throttled according to {@code throttlePolicy}.
+   * throttled according to {@code policy}.
    */
-  private void throttledTransfer(MockResponse throttlePolicy, Socket socket, BufferedSource source,
-      BufferedSink sink, long byteCount) throws IOException {
+  private void throttledTransfer(MockResponse policy, Socket socket, BufferedSource source,
+      BufferedSink sink, long byteCount, boolean isRequest) throws IOException {
     if (byteCount == 0) return;
 
     Buffer buffer = new Buffer();
-    long bytesPerPeriod = throttlePolicy.getThrottleBytesPerPeriod();
-    long periodDelayMs = throttlePolicy.getThrottlePeriod(TimeUnit.MILLISECONDS);
+    long bytesPerPeriod = policy.getThrottleBytesPerPeriod();
+    long periodDelayMs = policy.getThrottlePeriod(TimeUnit.MILLISECONDS);
+
+    long halfByteCount = byteCount / 2;
+    boolean disconnectHalfway =
+        !isRequest && policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
 
     while (!socket.isClosed()) {
       for (int b = 0; b < bytesPerPeriod; ) {
-        long toRead = Math.min(Math.min(2048, byteCount), bytesPerPeriod - b);
+        // Ensure we do not read past the allotted bytes in this period.
+        long toRead = Math.min(byteCount, bytesPerPeriod - b);
+        // Ensure we do not read past halfway if the policy will kill the connection.
+        if (disconnectHalfway) {
+          toRead = Math.min(toRead, byteCount - halfByteCount);
+        }
+
         long read = source.read(buffer, toRead);
         if (read == -1) return;
 
@@ -749,6 +772,11 @@
         b += read;
         byteCount -= read;
 
+        if (disconnectHalfway && byteCount == halfByteCount) {
+          socket.close();
+          return;
+        }
+
         if (byteCount == 0) return;
       }
 
@@ -816,18 +844,18 @@
     }
   }
 
-  /** Processes HTTP requests layered over SPDY/3. */
-  private class SpdySocketHandler implements IncomingStreamHandler {
+  /** Processes HTTP requests layered over framed protocols. */
+  private class FramedSocketHandler implements IncomingStreamHandler {
     private final Socket socket;
     private final Protocol protocol;
     private final AtomicInteger sequenceNumber = new AtomicInteger();
 
-    private SpdySocketHandler(Socket socket, Protocol protocol) {
+    private FramedSocketHandler(Socket socket, Protocol protocol) {
       this.socket = socket;
       this.protocol = protocol;
     }
 
-    @Override public void receive(SpdyStream stream) throws IOException {
+    @Override public void receive(FramedStream stream) throws IOException {
       RecordedRequest request = readRequest(stream);
       requestQueue.add(request);
       MockResponse response;
@@ -843,15 +871,15 @@
       }
     }
 
-    private RecordedRequest readRequest(SpdyStream stream) throws IOException {
-      List<Header> spdyHeaders = stream.getRequestHeaders();
+    private RecordedRequest readRequest(FramedStream stream) throws IOException {
+      List<Header> streamHeaders = stream.getRequestHeaders();
       Headers.Builder httpHeaders = new Headers.Builder();
       String method = "<:method omitted>";
       String path = "<:path omitted>";
       String version = protocol == Protocol.SPDY_3 ? "<:version omitted>" : "HTTP/1.1";
-      for (int i = 0, size = spdyHeaders.size(); i < size; i++) {
-        ByteString name = spdyHeaders.get(i).name;
-        String value = spdyHeaders.get(i).value.utf8();
+      for (int i = 0, size = streamHeaders.size(); i < size; i++) {
+        ByteString name = streamHeaders.get(i).name;
+        String value = streamHeaders.get(i).value.utf8();
         if (name.equals(Header.TARGET_METHOD)) {
           method = value;
         } else if (name.equals(Header.TARGET_PATH)) {
@@ -873,7 +901,7 @@
           sequenceNumber.getAndIncrement(), socket);
     }
 
-    private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
+    private void writeResponse(FramedStream stream, MockResponse response) throws IOException {
       if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
         return;
       }
@@ -899,19 +927,19 @@
       if (body != null) {
         BufferedSink sink = Okio.buffer(stream.getSink());
         sleepIfDelayed(response);
-        throttledTransfer(response, socket, body, sink, bodyLimit);
+        throttledTransfer(response, socket, body, sink, bodyLimit, false);
         sink.close();
       } else if (closeStreamAfterHeaders) {
         stream.close(ErrorCode.NO_ERROR);
       }
     }
 
-    private void pushPromises(SpdyStream stream, List<PushPromise> promises) throws IOException {
+    private void pushPromises(FramedStream stream, List<PushPromise> promises) throws IOException {
       for (PushPromise pushPromise : promises) {
         List<Header> pushedHeaders = new ArrayList<>();
         pushedHeaders.add(new Header(stream.getConnection().getProtocol() == Protocol.SPDY_3
             ? Header.TARGET_HOST
-            : Header.TARGET_AUTHORITY, getUrl(pushPromise.getPath()).getHost()));
+            : Header.TARGET_AUTHORITY, url(pushPromise.getPath()).host()));
         pushedHeaders.add(new Header(Header.TARGET_METHOD, pushPromise.getMethod()));
         pushedHeaders.add(new Header(Header.TARGET_PATH, pushPromise.getPath()));
         Headers pushPromiseHeaders = pushPromise.getHeaders();
@@ -923,7 +951,7 @@
         requestQueue.add(new RecordedRequest(requestLine, pushPromise.getHeaders(), chunkSizes, 0,
             new Buffer(), sequenceNumber.getAndIncrement(), socket));
         boolean hasBody = pushPromise.getResponse().getBody() != null;
-        SpdyStream pushedStream =
+        FramedStream pushedStream =
             stream.getConnection().pushStream(stream.getId(), pushedHeaders, hasBody);
         writeResponse(pushedStream, pushPromise.getResponse());
       }
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
index e2d5f28..4583621 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -50,6 +50,9 @@
    */
   DISCONNECT_AFTER_REQUEST,
 
+  /** Close connection after writing half of the response body (if present). */
+  DISCONNECT_DURING_RESPONSE_BODY,
+
   /** Don't trust the client during the SSL handshake. */
   FAIL_HANDSHAKE,
 
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java
deleted file mode 100644
index 01df8e2..0000000
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2014 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.mockwebserver.rule;
-
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import java.io.IOException;
-import java.net.URL;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import org.junit.rules.ExternalResource;
-
-/**
- * Allows you to use {@link MockWebServer} as a JUnit test rule.
- *
- * <p>This rule starts {@link MockWebServer} on an available port before your test runs, and shuts
- * it down after it completes.
- */
-public class MockWebServerRule extends ExternalResource {
-  private static final Logger logger = Logger.getLogger(MockWebServerRule.class.getName());
-
-  private final MockWebServer server = new MockWebServer();
-  private boolean started;
-
-  @Override protected void before() {
-    if (started) return;
-    started = true;
-    try {
-      server.start();
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  @Override protected void after() {
-    try {
-      server.shutdown();
-    } catch (IOException e) {
-      logger.log(Level.WARNING, "MockWebServer shutdown failed", e);
-    }
-  }
-
-  public String getHostName() {
-    if (!started) before();
-    return server.getHostName();
-  }
-
-  public int getPort() {
-    if (!started) before();
-    return server.getPort();
-  }
-
-  public int getRequestCount() {
-    return server.getRequestCount();
-  }
-
-  public void enqueue(MockResponse response) {
-    server.enqueue(response);
-  }
-
-  public RecordedRequest takeRequest() throws InterruptedException {
-    return server.takeRequest();
-  }
-
-  public URL getUrl(String path) {
-    return server.getUrl(path);
-  }
-
-  /** For any other functionality, use the {@linkplain MockWebServer} directly. */
-  public MockWebServer get() {
-    return server;
-  }
-}
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
index a3816d2..e7749e0 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -16,12 +16,13 @@
 package com.squareup.okhttp.mockwebserver;
 
 import com.squareup.okhttp.Headers;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.net.ConnectException;
 import java.net.HttpURLConnection;
+import java.net.ProtocolException;
 import java.net.SocketTimeoutException;
 import java.net.URL;
 import java.net.URLConnection;
@@ -29,17 +30,23 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public final class MockWebServerTest {
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
+  @Rule public final MockWebServer server = new MockWebServer();
 
   @Test public void defaultMockResponse() {
     MockResponse response = new MockResponse();
@@ -230,9 +237,7 @@
     assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
   }
 
-  /**
-   * Delay the response body by sleeping 1s.
-   */
+  /** Delay the response body by sleeping 1s. */
   @Test public void delayResponse() throws IOException {
     server.enqueue(new MockResponse()
         .setBody("ABCDEF")
@@ -242,17 +247,30 @@
     URLConnection connection = server.getUrl("/").openConnection();
     InputStream in = connection.getInputStream();
     assertEquals('A', in.read());
-    assertEquals('B', in.read());
-    assertEquals('C', in.read());
-    assertEquals('D', in.read());
-    assertEquals('E', in.read());
-    assertEquals('F', in.read());
-    assertEquals(-1, in.read());
     long elapsedNanos = System.nanoTime() - startNanos;
     long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
-
     assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 1000);
-    assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis <= 1100);
+
+    in.close();
+  }
+
+  @Test public void disconnectHalfway() throws IOException {
+    server.enqueue(new MockResponse()
+        .setBody("ab")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
+
+    URLConnection connection = server.getUrl("/").openConnection();
+    assertEquals(2, connection.getContentLength());
+    InputStream in = connection.getInputStream();
+    assertEquals('a', in.read());
+    try {
+      int byteRead = in.read();
+      // OpenJDK behavior: end of stream.
+      assertEquals(-1, byteRead);
+    } catch (ProtocolException e) {
+      // On Android, HttpURLConnection is implemented by OkHttp v2. OkHttp
+      // treats an incomplete response body as a ProtocolException.
+    }
   }
 
   private List<String> headersToList(MockResponse response) {
@@ -267,11 +285,7 @@
 
   @Test public void shutdownWithoutStart() throws IOException {
     MockWebServer server = new MockWebServer();
-    try {
-      server.shutdown();
-      fail();
-    } catch (IllegalStateException expected) {
-    }
+    server.shutdown();
   }
 
   @Test public void shutdownWithoutEnqueue() throws IOException {
@@ -279,4 +293,45 @@
     server.start();
     server.shutdown();
   }
+
+  @After public void tearDown() throws IOException {
+    server.shutdown();
+  }
+
+  @Test public void portImplicitlyStarts() throws IOException {
+    assertTrue(server.getPort() > 0);
+  }
+
+  @Test public void hostNameImplicitlyStarts() throws IOException {
+    assertNotNull(server.getHostName());
+  }
+
+  @Test public void toProxyAddressImplicitlyStarts() throws IOException {
+    assertNotNull(server.toProxyAddress());
+  }
+
+  @Test public void differentInstancesGetDifferentPorts() throws IOException {
+    MockWebServer other = new MockWebServer();
+    assertNotEquals(server.getPort(), other.getPort());
+    other.shutdown();
+  }
+
+  @Test public void statementStartsAndStops() throws Throwable {
+    final AtomicBoolean called = new AtomicBoolean();
+    Statement statement = server.apply(new Statement() {
+      @Override public void evaluate() throws Throwable {
+        called.set(true);
+        server.getUrl("/").openConnection().connect();
+      }
+    }, Description.EMPTY);
+
+    statement.evaluate();
+
+    assertTrue(called.get());
+    try {
+      server.getUrl("/").openConnection().connect();
+      fail();
+    } catch (ConnectException expected) {
+    }
+  }
 }
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java
deleted file mode 100644
index 4c94efb..0000000
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2014 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.mockwebserver.rule;
-
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.ConnectException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import org.junit.After;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class MockWebServerRuleTest {
-
-  private MockWebServerRule server = new MockWebServerRule();
-
-  @After public void tearDown() {
-    server.after();
-  }
-
-  @Test public void whenRuleCreatedPortIsAvailable() throws IOException {
-    assertTrue(server.getPort() > 0);
-  }
-
-  @Test public void differentRulesGetDifferentPorts() throws IOException {
-    // ANDROID-BEGIN
-    assertTrue(server.getPort() != new MockWebServerRule().getPort());
-    // ANDROID-END
-  }
-
-  @Test public void beforePlaysServer() throws Exception {
-    server.before();
-    assertEquals(server.getPort(), server.get().getPort());
-    server.getUrl("/").openConnection().connect();
-  }
-
-  @Test public void afterStopsServer() throws Exception {
-    server.before();
-    server.after();
-
-    try {
-      server.getUrl("/").openConnection().connect();
-      fail();
-    } catch (ConnectException e) {
-    }
-  }
-
-  @Test public void typicalUsage() throws Exception {
-    server.before(); // Implicitly called when @Rule.
-
-    server.enqueue(new MockResponse().setBody("hello world"));
-
-    URL url = server.getUrl("/aaa");
-    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-    InputStream in = connection.getInputStream();
-    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
-    assertEquals("hello world", reader.readLine());
-
-    assertEquals(1, server.getRequestCount());
-    assertEquals("GET /aaa HTTP/1.1", server.takeRequest().getRequestLine());
-
-    server.after(); // Implicitly called when @Rule.
-  }
-}
-
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
index d167ce1..e80d121 100644
--- a/okcurl/pom.xml
+++ b/okcurl/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okcurl</artifactId>
diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
index c6a85e1..dbc51f3 100644
--- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -25,7 +25,7 @@
 import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.http.StatusLine;
-import com.squareup.okhttp.internal.spdy.Http2;
+import com.squareup.okhttp.internal.framed.Http2;
 
 import io.airlift.command.Arguments;
 import io.airlift.command.Command;
diff --git a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
index 80b8665..0e2e3ae 100644
--- a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -29,14 +29,14 @@
   @Test public void simple() {
     Request request = fromArgs("http://example.com").createRequest();
     assertEquals("GET", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertNull(request.body());
   }
 
   @Test public void put() throws IOException {
     Request request = fromArgs("-X", "PUT", "-d", "foo", "http://example.com").createRequest();
     assertEquals("PUT", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals(3, request.body().contentLength());
   }
 
@@ -44,7 +44,7 @@
     Request request = fromArgs("-d", "foo", "http://example.com").createRequest();
     RequestBody body = request.body();
     assertEquals("POST", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
     assertEquals("foo", bodyAsString(body));
   }
@@ -53,7 +53,7 @@
     Request request = fromArgs("-d", "foo", "-X", "PUT", "http://example.com").createRequest();
     RequestBody body = request.body();
     assertEquals("PUT", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
     assertEquals("foo", bodyAsString(body));
   }
@@ -63,7 +63,7 @@
         "http://example.com").createRequest();
     RequestBody body = request.body();
     assertEquals("POST", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals("application/json; charset=utf-8", body.contentType().toString());
     assertEquals("foo", bodyAsString(body));
   }
@@ -71,7 +71,7 @@
   @Test public void referer() {
     Request request = fromArgs("-e", "foo", "http://example.com").createRequest();
     assertEquals("GET", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals("foo", request.header("Referer"));
     assertNull(request.body());
   }
@@ -79,7 +79,7 @@
   @Test public void userAgent() {
     Request request = fromArgs("-A", "foo", "http://example.com").createRequest();
     assertEquals("GET", request.method());
-    assertEquals("http://example.com", request.urlString());
+    assertEquals("http://example.com/", request.urlString());
     assertEquals("foo", request.header("User-Agent"));
     assertNull(request.body());
   }
diff --git a/okhttp-android-support/pom.xml b/okhttp-android-support/pom.xml
index 3bf11e9..f514808 100644
--- a/okhttp-android-support/pom.xml
+++ b/okhttp-android-support/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-android-support</artifactId>
diff --git a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
index 026c45f..89570cc 100644
--- a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
@@ -210,7 +210,7 @@
     }
 
     Request cacheRequest = new Request.Builder()
-        .url(request.url())
+        .url(request.httpUrl())
         .method(request.method(), null)
         .headers(varyHeaders)
         .build();
@@ -821,21 +821,17 @@
       throw throwRequestSslAccessException();
     }
 
-    // ANDROID-BEGIN
-    // @Override public long getContentLengthLong() {
-    //   return delegate.getContentLengthLong();
-    // }
-    // ANDROID-END
+    @Override public long getContentLengthLong() {
+      return delegate.getContentLengthLong();
+    }
 
     @Override public void setFixedLengthStreamingMode(long contentLength) {
       delegate.setFixedLengthStreamingMode(contentLength);
     }
 
-    // ANDROID-BEGIN
-    // @Override public long getHeaderFieldLong(String field, long defaultValue) {
-    //   return delegate.getHeaderFieldLong(field, defaultValue);
-    // }
-    // ANDROID-END
+    @Override public long getHeaderFieldLong(String field, long defaultValue) {
+      return delegate.getHeaderFieldLong(field, defaultValue);
+    }
   }
 
   private static RuntimeException throwRequestModificationException() {
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java
index c349790..8515017 100644
--- a/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java
@@ -17,28 +17,25 @@
 package com.squareup.okhttp.android;
 
 import com.squareup.okhttp.AndroidInternal;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.OkUrlFactory;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
 import java.io.File;
 import java.io.InputStream;
 import java.net.CacheRequest;
 import java.net.CacheResponse;
 import java.net.ResponseCache;
 import java.net.URI;
-import java.net.URL;
 import java.net.URLConnection;
 import java.util.List;
 import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -53,14 +50,12 @@
 public final class HttpResponseCacheTest {
 
   @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
-  @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+  @Rule public MockWebServer server = new MockWebServer();
 
   private File cacheDir;
-  private MockWebServer server;
   private OkUrlFactory client;
 
   @Before public void setUp() throws Exception {
-    server = serverRule.get();
     cacheDir = cacheRule.getRoot();
     client = new OkUrlFactory(new OkHttpClient());
   }
@@ -148,7 +143,7 @@
         .addHeader("Cache-Control: max-age=60")
         .setBody("A"));
 
-    URLConnection c1 = openUrl(server.getUrl("/"));
+    URLConnection c1 = openUrl(server.url("/"));
 
     InputStream inputStream = c1.getInputStream();
     assertEquals('A', inputStream.read());
@@ -157,10 +152,10 @@
     assertEquals(1, cache.getNetworkCount());
     assertEquals(0, cache.getHitCount());
 
-    URLConnection c2 = openUrl(server.getUrl("/"));
+    URLConnection c2 = openUrl(server.url("/"));
     assertEquals('A', c2.getInputStream().read());
 
-    URLConnection c3 = openUrl(server.getUrl("/"));
+    URLConnection c3 = openUrl(server.url("/"));
     assertEquals('A', c3.getInputStream().read());
     assertEquals(3, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
@@ -168,10 +163,10 @@
   }
 
   // This mimics the Android HttpHandler, which is found in the com.squareup.okhttp package.
-  private URLConnection openUrl(URL url) {
+  private URLConnection openUrl(HttpUrl url) {
     ResponseCache responseCache = ResponseCache.getDefault();
     AndroidInternal.setResponseCache(client, responseCache);
-    return client.open(url);
+    return client.open(url.url());
   }
 
   private void initializeCache(HttpResponseCache cache) {
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
index 4cca79e..97593d5 100644
--- a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
@@ -22,6 +22,7 @@
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.IOException;
 import java.net.CacheRequest;
 import java.net.CacheResponse;
@@ -37,13 +38,11 @@
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
+import okio.Buffer;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import okio.Buffer;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -59,17 +58,10 @@
  * </ul>
  */
 public class CacheAdapterTest {
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
-    public boolean verify(String hostname, SSLSession session) {
-      return true;
-    }
-  };
-
+  private SSLContext sslContext = SslContextBuilder.localhost();
+  private HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
   private MockWebServer server;
-
   private OkHttpClient client;
-
   private HttpURLConnection connection;
 
   @Before public void setUp() throws Exception {
@@ -123,7 +115,7 @@
     };
     Internal.instance.setCache(client, new CacheAdapter(responseCache));
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.setHostnameVerifier(hostnameVerifier);
 
     connection = new OkUrlFactory(client).open(serverUrl);
     connection.setRequestProperty("key1", "value1");
@@ -238,7 +230,7 @@
     };
     Internal.instance.setCache(client, new CacheAdapter(responseCache));
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.setHostnameVerifier(hostnameVerifier);
 
     connection = new OkUrlFactory(client).open(serverUrl);
     executeGet(connection);
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
index 227765a..7255372 100644
--- a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
@@ -25,7 +25,7 @@
 import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -93,7 +93,7 @@
       + "fl2WRY8hb4x+zRrwsFaLEpdEvqcjOQ==\n"
       + "-----END CERTIFICATE-----");
 
-  @Rule public MockWebServerRule server = new MockWebServerRule();
+  @Rule public MockWebServer server = new MockWebServer();
 
   @Before public void setUp() throws Exception {
     Internal.initializeInstanceForTests();
@@ -118,7 +118,7 @@
 
     Response response = JavaApiConverter.createOkResponseForCacheGet(request, cacheResponse);
     Request cacheRequest = response.request();
-    assertEquals(request.url(), cacheRequest.url());
+    assertEquals(request.httpUrl(), cacheRequest.httpUrl());
     assertEquals(request.method(), cacheRequest.method());
     assertEquals(0, request.headers().size());
 
@@ -198,7 +198,7 @@
 
     Response response = JavaApiConverter.createOkResponseForCacheGet(request, cacheResponse);
     Request cacheRequest = response.request();
-    assertEquals(request.url(), cacheRequest.url());
+    assertEquals(request.httpUrl(), cacheRequest.httpUrl());
     assertEquals(request.method(), cacheRequest.method());
     assertEquals(0, request.headers().size());
 
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
index 83d1f64..1dbf78f 100644
--- a/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
@@ -27,7 +27,7 @@
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.FileNotFoundException;
@@ -67,7 +67,6 @@
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.GzipSink;
@@ -91,28 +90,18 @@
  * Based on com.squareup.okhttp.CacheTest with changes for ResponseCache and HttpURLConnection.
  */
 public final class ResponseCacheTest {
-  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
-    @Override public boolean verify(String s, SSLSession sslSession) {
-      return true;
-    }
-  };
-
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-
   @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
-  @Rule public MockWebServerRule serverRule = new MockWebServerRule();
-  @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
+  @Rule public MockWebServer server = new MockWebServer();
+  @Rule public MockWebServer server2 = new MockWebServer();
 
+  private HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+  private SSLContext sslContext = SslContextBuilder.localhost();
   private OkHttpClient client;
-  private MockWebServer server;
-  private MockWebServer server2;
   private ResponseCache cache;
   private CookieManager cookieManager;
 
   @Before public void setUp() throws Exception {
-    server = serverRule.get();
     server.setProtocolNegotiationEnabled(false);
-    server2 = server2Rule.get();
 
     client = new OkHttpClient();
 
@@ -275,7 +264,7 @@
 
     HttpsURLConnection c1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     c1.setSSLSocketFactory(sslContext.getSocketFactory());
-    c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    c1.setHostnameVerifier(hostnameVerifier);
     assertEquals("ABC", readAscii(c1));
 
     // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
@@ -287,7 +276,7 @@
 
     HttpsURLConnection c2 = (HttpsURLConnection) openConnection(server.getUrl("/")); // cached!
     c2.setSSLSocketFactory(sslContext.getSocketFactory());
-    c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    c2.setHostnameVerifier(hostnameVerifier);
     assertEquals("ABC", readAscii(c2));
 
     assertEquals(suite, c2.getCipherSuite());
@@ -359,7 +348,7 @@
         .setBody("DEF"));
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.setHostnameVerifier(hostnameVerifier);
 
     HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection1));
@@ -397,7 +386,7 @@
         .addHeader("Location: " + server2.getUrl("/")));
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.setHostnameVerifier(hostnameVerifier);
 
     HttpURLConnection connection1 = openConnection(server.getUrl("/"));
     assertEquals("ABC", readAscii(connection1));
@@ -1466,7 +1455,7 @@
         .setBody("B"));
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
-    client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.setHostnameVerifier(hostnameVerifier);
 
     URL url = server.getUrl("/");
     HttpURLConnection connection1 = openConnection(url);
@@ -2001,13 +1990,13 @@
 
     HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     connection1.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    connection1.setHostnameVerifier(hostnameVerifier);
     assertEquals("ABC", readAscii(connection1));
 
     // Not cached!
     HttpsURLConnection connection2 = (HttpsURLConnection) openConnection(server.getUrl("/"));
     connection2.setSSLSocketFactory(sslContext.getSocketFactory());
-    connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    connection2.setHostnameVerifier(hostnameVerifier);
     assertEquals("DEF", readAscii(connection2));
   }
 
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index 6bc872b..74ff837 100644
--- a/okhttp-apache/pom.xml
+++ b/okhttp-apache/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-apache</artifactId>
diff --git a/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
index d307a33..3a9174a 100644
--- a/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
+++ b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
@@ -67,6 +67,8 @@
         if (encoding != null) {
           builder.header(encoding.getName(), encoding.getValue());
         }
+      } else {
+        body = RequestBody.create(null, new byte[0]);
       }
     }
     builder.method(method, body);
diff --git a/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java b/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
index fd66fda..105f22f 100644
--- a/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
+++ b/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
@@ -16,6 +16,7 @@
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.InputStreamEntity;
 import org.apache.http.entity.StringEntity;
@@ -113,6 +114,24 @@
     assertEquals("Hello, world!", request.getBody().readUtf8());
     assertEquals(request.getHeader("Content-Length"), "13");
   }
+  @Test public void postEmptyEntity() throws Exception {
+    server.enqueue(new MockResponse());
+    final HttpPost post = new HttpPost(server.getUrl("/").toURI());
+    client.execute(post);
+    
+    RecordedRequest request = server.takeRequest();
+    assertEquals(0, request.getBodySize());
+    assertNotNull(request.getBody());
+  }
+  @Test public void putEmptyEntity() throws Exception {
+    server.enqueue(new MockResponse());
+    final HttpPut put = new HttpPut(server.getUrl("/").toURI());
+    client.execute(put);
+    
+    RecordedRequest request = server.takeRequest();
+    assertEquals(0, request.getBodySize());
+    assertNotNull(request.getBody());
+  }
 
   @Test public void postOverrideContentType() throws Exception {
     server.enqueue(new MockResponse());
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java
index 30e1a7b..6cb7a86 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java
@@ -13,15 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
-import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import com.squareup.okhttp.internal.framed.hpackjson.Story;
 import java.util.Collection;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import static com.squareup.okhttp.internal.spdy.hpackjson.HpackJsonUtil.storiesForCurrentDraft;
+import static com.squareup.okhttp.internal.framed.hpackjson.HpackJsonUtil.storiesForCurrentDraft;
 
 @RunWith(Parameterized.class)
 public class HpackDecodeInteropTest extends HpackDecodeTestBase {
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java
index e26b669..fe57319 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
-import com.squareup.okhttp.internal.spdy.hpackjson.Case;
-import com.squareup.okhttp.internal.spdy.hpackjson.HpackJsonUtil;
-import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import com.squareup.okhttp.internal.framed.hpackjson.Case;
+import com.squareup.okhttp.internal.framed.hpackjson.HpackJsonUtil;
+import com.squareup.okhttp.internal.framed.hpackjson.Story;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java
index 4491672..3d34759 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
-import com.squareup.okhttp.internal.spdy.hpackjson.Case;
-import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import com.squareup.okhttp.internal.framed.hpackjson.Case;
+import com.squareup.okhttp.internal.framed.hpackjson.Story;
 import okio.Buffer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java
index d5d2728..b62c9f5 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy.hpackjson;
+package com.squareup.okhttp.internal.framed.hpackjson;
 
-import com.squareup.okhttp.internal.spdy.Header;
+import com.squareup.okhttp.internal.framed.Header;
 import okio.ByteString;
 
 import java.util.ArrayList;
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java
index f643024..fa52d24 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy.hpackjson;
+package com.squareup.okhttp.internal.framed.hpackjson;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
@@ -86,4 +86,4 @@
   }
 
   private HpackJsonUtil() { } // Utilities only.
-}
\ No newline at end of file
+}
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java
index 5ff2b07..cf6a9a0 100644
--- a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy.hpackjson;
+package com.squareup.okhttp.internal.framed.hpackjson;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/okhttp-testing-support/pom.xml b/okhttp-testing-support/pom.xml
index ad016c8..654b0e3 100644
--- a/okhttp-testing-support/pom.xml
+++ b/okhttp-testing-support/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-testing-support</artifactId>
@@ -18,5 +18,10 @@
       <artifactId>junit</artifactId>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
similarity index 100%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
rename to okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/okhttp-testing-support/src/main/java/com/squareup/okhttp/testing/RecordingHostnameVerifier.java
similarity index 95%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
rename to okhttp-testing-support/src/main/java/com/squareup/okhttp/testing/RecordingHostnameVerifier.java
index c9d914f..d4d343a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
+++ b/okhttp-testing-support/src/main/java/com/squareup/okhttp/testing/RecordingHostnameVerifier.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal;
+package com.squareup.okhttp.testing;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
index bcf1268..2bb1982 100644
--- a/okhttp-tests/pom.xml
+++ b/okhttp-tests/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-tests</artifactId>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
index af0f506..b9e1d50 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
@@ -19,10 +19,11 @@
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
 import java.io.File;
 import java.io.IOException;
 import java.net.CookieHandler;
@@ -30,7 +31,6 @@
 import java.net.HttpCookie;
 import java.net.HttpURLConnection;
 import java.net.ResponseCache;
-import java.net.URL;
 import java.security.Principal;
 import java.security.cert.Certificate;
 import java.text.DateFormat;
@@ -57,7 +57,6 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
 import static org.junit.Assert.assertEquals;
@@ -75,23 +74,18 @@
     }
   };
 
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  @Rule public MockWebServer server = new MockWebServer();
+  @Rule public MockWebServer server2 = new MockWebServer();
 
-  @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
-  @Rule public MockWebServerRule serverRule = new MockWebServerRule();
-  @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
-
+  private final SSLContext sslContext = SslContextBuilder.localhost();
+  private final FileSystem fileSystem = new InMemoryFileSystem();
   private final OkHttpClient client = new OkHttpClient();
-  private MockWebServer server;
-  private MockWebServer server2;
   private Cache cache;
   private final CookieManager cookieManager = new CookieManager();
 
   @Before public void setUp() throws Exception {
-    server = serverRule.get();
     server.setProtocolNegotiationEnabled(false);
-    server2 = server2Rule.get();
-    cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE);
+    cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
     client.setCache(cache);
     CookieHandler.setDefault(cookieManager);
   }
@@ -171,12 +165,15 @@
       mockResponse.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"");
     } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
       mockResponse.addHeader("WWW-Authenticate: Basic realm=\"protected area\"");
+    } else if (responseCode == HttpURLConnection.HTTP_NO_CONTENT
+        || responseCode == HttpURLConnection.HTTP_RESET) {
+      mockResponse.setBody(""); // We forbid bodies for 204 and 205.
     }
     server.enqueue(mockResponse);
     server.start();
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     Response response = client.newCall(request).execute();
     assertEquals(responseCode, response.code());
@@ -219,7 +216,7 @@
     server.enqueue(mockResponse);
 
     // Make sure that calling skip() doesn't omit bytes from the cache.
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request).execute();
 
     BufferedSource in1 = response1.body().source();
@@ -256,7 +253,7 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request).execute();
     BufferedSource in = response1.body().source();
     assertEquals("ABC", in.readUtf8());
@@ -295,7 +292,7 @@
     server.enqueue(new MockResponse()
         .setBody("DEF"));
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request).execute();
     assertEquals("ABC", response1.body().string());
 
@@ -317,14 +314,14 @@
     server.enqueue(new MockResponse()
         .setBody("DEF"));
 
-    Request request1 = new Request.Builder().url(server.getUrl("/foo")).build();
+    Request request1 = new Request.Builder().url(server.url("/foo")).build();
     Response response1 = client.newCall(request1).execute();
     assertEquals("ABC", response1.body().string());
     RecordedRequest recordedRequest1 = server.takeRequest();
     assertEquals("GET /foo HTTP/1.1", recordedRequest1.getRequestLine());
     assertEquals(0, recordedRequest1.getSequenceNumber());
 
-    Request request2 = new Request.Builder().url(server.getUrl("/bar")).build();
+    Request request2 = new Request.Builder().url(server.url("/bar")).build();
     Response response2 = client.newCall(request2).execute();
     assertEquals("ABC", response2.body().string());
     RecordedRequest recordedRequest2 = server.takeRequest();
@@ -332,7 +329,7 @@
     assertEquals(1, recordedRequest2.getSequenceNumber());
 
     // an unrelated request should reuse the pooled connection
-    Request request3 = new Request.Builder().url(server.getUrl("/baz")).build();
+    Request request3 = new Request.Builder().url(server.url("/baz")).build();
     Response response3 = client.newCall(request3).execute();
     assertEquals("DEF", response3.body().string());
     RecordedRequest recordedRequest3 = server.takeRequest();
@@ -357,12 +354,12 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("ABC", response1.body().string());
     assertNotNull(response1.handshake().cipherSuite());
 
     // Cached!
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("ABC", response2.body().string());
     assertNotNull(response2.handshake().cipherSuite());
 
@@ -392,16 +389,16 @@
         .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
         .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
         .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
-        .addHeader("Location: " + server2.getUrl("/")));
+        .addHeader("Location: " + server2.url("/")));
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("ABC", response1.body().string());
 
     // Cached!
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("ABC", response2.body().string());
 
     assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
@@ -446,7 +443,7 @@
     server.enqueue(new MockResponse()
         .setBody("c"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("a", get(url).body().string());
     assertEquals("a", get(url).body().string());
   }
@@ -460,7 +457,7 @@
     server.enqueue(new MockResponse()
         .setBody("b"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("a", get(url).body().string());
     assertEquals("b", get(url).body().string());
   }
@@ -486,7 +483,7 @@
     server.enqueue(new MockResponse()
         .setBody("Request #2"));
 
-    BufferedSource bodySource = get(server.getUrl("/")).body().source();
+    BufferedSource bodySource = get(server.url("/")).body().source();
     assertEquals("ABCDE", bodySource.readUtf8Line());
     try {
       bodySource.readUtf8Line();
@@ -498,7 +495,7 @@
 
     assertEquals(1, cache.getWriteAbortCount());
     assertEquals(0, cache.getWriteSuccessCount());
-    Response response = get(server.getUrl("/"));
+    Response response = get(server.url("/"));
     assertEquals("Request #2", response.body().string());
     assertEquals(1, cache.getWriteAbortCount());
     assertEquals(1, cache.getWriteSuccessCount());
@@ -525,7 +522,7 @@
     server.enqueue(new MockResponse()
         .setBody("Request #2"));
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     BufferedSource in = response1.body().source();
     assertEquals("ABCDE", in.readUtf8(5));
     in.close();
@@ -537,7 +534,7 @@
 
     assertEquals(1, cache.getWriteAbortCount());
     assertEquals(0, cache.getWriteSuccessCount());
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("Request #2", response2.body().string());
     assertEquals(1, cache.getWriteAbortCount());
     assertEquals(1, cache.getWriteSuccessCount());
@@ -553,7 +550,7 @@
         .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
         .setBody("A"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Response response1 = get(url);
     assertEquals("A", response1.body().string());
 
@@ -584,8 +581,8 @@
         .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
         .setBody("A"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    Response response = get(server.getUrl("/"));
+    assertEquals("A", get(server.url("/")).body().string());
+    Response response = get(server.url("/"));
     assertEquals("A", response.body().string());
     assertEquals("113 HttpURLConnection \"Heuristic expiration\"", response.header("Warning"));
   }
@@ -598,7 +595,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/?foo=bar");
+    HttpUrl url = server.url("/").newBuilder().addQueryParameter("foo", "bar").build();
     assertEquals("A", get(url).body().string());
     assertEquals("B", get(url).body().string());
   }
@@ -722,7 +719,7 @@
     server.enqueue(new MockResponse()
         .addHeader("X-Response-ID: 2"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     Request request = new Request.Builder()
         .url(url)
@@ -771,7 +768,7 @@
     server.enqueue(new MockResponse()
         .setBody("C"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     assertEquals("A", get(url).body().string());
 
@@ -798,7 +795,7 @@
     server.enqueue(new MockResponse()
         .setBody("C"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     assertEquals("A", get(url).body().string());
 
@@ -887,7 +884,7 @@
     server.enqueue(new MockResponse()
         .setBody("BB"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     Request request = new Request.Builder()
         .url(url)
@@ -908,7 +905,7 @@
         .setBody("B")
         .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
 
     assertEquals("A", get(url).body().string());
     assertEquals("A", get(url).body().string());
@@ -923,14 +920,14 @@
         .setBody("B"));
 
     Request request1 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .cacheControl(new CacheControl.Builder().noStore().build())
         .build();
     Response response1 = client.newCall(request1).execute();
     assertEquals("A", response1.body().string());
 
     Request request2 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     Response response2 = client.newCall(request2).execute();
     assertEquals("B", response2.body().string());
@@ -960,9 +957,9 @@
     // At least three request/response pairs are required because after the first request is cached
     // a different execution path might be taken. Thus modifications to the cache applied during
     // the second request might not be visible until another request is performed.
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
   }
 
   @Test public void notModifiedSpecifiesEncoding() throws Exception {
@@ -977,9 +974,9 @@
     server.enqueue(new MockResponse()
         .setBody("DEFDEFDEF"));
 
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
-    assertEquals("DEFDEFDEF", get(server.getUrl("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
+    assertEquals("DEFDEFDEF", get(server.url("/")).body().string());
   }
 
   /** https://github.com/square/okhttp/issues/947 */
@@ -992,8 +989,8 @@
     server.enqueue(new MockResponse()
         .setBody("FAIL"));
 
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
-    assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
+    assertEquals("ABCABCABC", get(server.url("/")).body().string());
   }
 
   @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
@@ -1008,8 +1005,8 @@
     pool.evictAll();
     client.setConnectionPool(pool);
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(1, client.getConnectionPool().getConnectionCount());
   }
 
@@ -1028,10 +1025,10 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "max-age=30")
         .build();
     Response response = client.newCall(request).execute();
@@ -1046,10 +1043,10 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "min-fresh=120")
         .build();
     Response response = client.newCall(request).execute();
@@ -1064,10 +1061,10 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "max-stale=180")
         .build();
     Response response = client.newCall(request).execute();
@@ -1084,11 +1081,11 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     // With max-stale, we'll return that stale response.
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "max-stale")
         .build();
     Response response = client.newCall(request).execute();
@@ -1104,10 +1101,10 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "max-stale=180")
         .build();
     Response response = client.newCall(request).execute();
@@ -1118,7 +1115,7 @@
     // (no responses enqueued)
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "only-if-cached")
         .build();
     Response response = client.newCall(request).execute();
@@ -1135,9 +1132,9 @@
         .addHeader("Cache-Control: max-age=30")
         .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "only-if-cached")
         .build();
     Response response = client.newCall(request).execute();
@@ -1153,9 +1150,9 @@
         .addHeader("Cache-Control: max-age=30")
         .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "only-if-cached")
         .build();
     Response response = client.newCall(request).execute();
@@ -1170,9 +1167,9 @@
     server.enqueue(new MockResponse()
         .setBody("A"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "only-if-cached")
         .build();
     Response response = client.newCall(request).execute();
@@ -1192,7 +1189,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     Request request = new Request.Builder()
         .url(url)
@@ -1211,7 +1208,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     Request request = new Request.Builder()
         .url(url)
@@ -1249,7 +1246,7 @@
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
 
     Request request = new Request.Builder()
@@ -1285,8 +1282,8 @@
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
 
     // The first request has no conditions.
     RecordedRequest request1 = server.takeRequest();
@@ -1302,7 +1299,7 @@
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("If-Modified-Since", formatDate(-24, TimeUnit.HOURS))
         .build();
     Response response = client.newCall(request).execute();
@@ -1317,7 +1314,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request = new Request.Builder()
         .url(url)
         .header("Authorization", "password")
@@ -1335,8 +1332,8 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/foo")).body().string());
-    assertEquals("B", get(server.getUrl("/bar")).body().string());
+    assertEquals("A", get(server.url("/foo")).body().string());
+    assertEquals("B", get(server.url("/bar")).body().string());
   }
 
   @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception {
@@ -1349,9 +1346,9 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/a")).body().string());
-    assertEquals("A", get(server.getUrl("/a")).body().string());
-    assertEquals("B", get(server.getUrl("/b")).body().string());
+    assertEquals("A", get(server.url("/a")).body().string());
+    assertEquals("A", get(server.url("/a")).body().string());
+    assertEquals("B", get(server.url("/b")).body().string());
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
     assertEquals(1, server.takeRequest().getSequenceNumber());
@@ -1368,12 +1365,12 @@
     server.enqueue(new MockResponse()
         .setBody("C"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(1, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
     assertEquals(0, cache.getHitCount());
-    assertEquals("B", get(server.getUrl("/")).body().string());
-    assertEquals("C", get(server.getUrl("/")).body().string());
+    assertEquals("B", get(server.url("/")).body().string());
+    assertEquals("C", get(server.url("/")).body().string());
     assertEquals(3, cache.getRequestCount());
     assertEquals(3, cache.getNetworkCount());
     assertEquals(0, cache.getHitCount());
@@ -1389,12 +1386,12 @@
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(1, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
     assertEquals(0, cache.getHitCount());
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(3, cache.getRequestCount());
     assertEquals(3, cache.getNetworkCount());
     assertEquals(2, cache.getHitCount());
@@ -1405,12 +1402,12 @@
         .addHeader("Cache-Control: max-age=60")
         .setBody("A"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(1, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
     assertEquals(0, cache.getHitCount());
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     assertEquals(3, cache.getRequestCount());
     assertEquals(1, cache.getNetworkCount());
     assertEquals(2, cache.getHitCount());
@@ -1424,7 +1421,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request frRequest = new Request.Builder()
         .url(url)
         .header("Accept-Language", "fr-CA")
@@ -1448,7 +1445,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request = new Request.Builder()
         .url(url)
         .header("Accept-Language", "fr-CA")
@@ -1471,8 +1468,8 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
   }
 
   @Test public void varyMatchesAddedRequestHeaderField() throws Exception {
@@ -1483,10 +1480,9 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
-        .header("Foo", "bar")
+        .url(server.url("/")).header("Foo", "bar")
         .build();
     Response response = client.newCall(request).execute();
     assertEquals("B", response.body().string());
@@ -1501,12 +1497,11 @@
         .setBody("B"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
-        .header("Foo", "bar")
+        .url(server.url("/")).header("Foo", "bar")
         .build();
     Response fooresponse = client.newCall(request).execute();
     assertEquals("A", fooresponse.body().string());
-    assertEquals("B", get(server.getUrl("/")).body().string());
+    assertEquals("B", get(server.url("/")).body().string());
   }
 
   @Test public void varyFieldsAreCaseInsensitive() throws Exception {
@@ -1517,7 +1512,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request = new Request.Builder()
         .url(url)
         .header("Accept-Language", "fr-CA")
@@ -1541,7 +1536,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request = new Request.Builder()
         .url(url)
         .header("Accept-Language", "fr-CA")
@@ -1569,7 +1564,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request frRequest = new Request.Builder()
         .url(url)
         .header("Accept-Language", "fr-CA")
@@ -1596,7 +1591,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request1 = new Request.Builder()
         .url(url)
         .addHeader("Accept-Language", "fr-CA, fr-FR")
@@ -1622,7 +1617,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request1 = new Request.Builder()
         .url(url)
         .addHeader("Accept-Language", "fr-CA, fr-FR")
@@ -1641,15 +1636,15 @@
   }
 
   @Test public void varyAsterisk() throws Exception {
-    server.enqueue( new MockResponse()
+    server.enqueue(new MockResponse()
         .addHeader("Cache-Control: max-age=60")
         .addHeader("Vary: *")
         .setBody("A"));
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    assertEquals("B", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
+    assertEquals("B", get(server.url("/")).body().string());
   }
 
   @Test public void varyAndHttps() throws Exception {
@@ -1664,7 +1659,7 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request request1 = new Request.Builder()
         .url(url)
         .header("Accept-Language", "en-US")
@@ -1690,7 +1685,7 @@
         .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     assertCookies(url, "a=FIRST");
     assertEquals("A", get(url).body().string());
@@ -1707,11 +1702,11 @@
         .addHeader("Allow: GET, HEAD, PUT")
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("A", response1.body().string());
     assertEquals("GET, HEAD", response1.header("Allow"));
 
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("A", response2.body().string());
     assertEquals("GET, HEAD, PUT", response2.header("Allow"));
   }
@@ -1726,11 +1721,11 @@
         .addHeader("Transfer-Encoding: none")
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("A", response1.body().string());
     assertEquals("identity", response1.header("Transfer-Encoding"));
 
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("A", response2.body().string());
     assertEquals("identity", response2.header("Transfer-Encoding"));
   }
@@ -1744,11 +1739,11 @@
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("A", response1.body().string());
     assertEquals("199 test danger", response1.header("Warning"));
 
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("A", response2.body().string());
     assertEquals(null, response2.header("Warning"));
   }
@@ -1762,18 +1757,18 @@
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    Response response1 = get(server.getUrl("/"));
+    Response response1 = get(server.url("/"));
     assertEquals("A", response1.body().string());
     assertEquals("299 test danger", response1.header("Warning"));
 
-    Response response2 = get(server.getUrl("/"));
+    Response response2 = get(server.url("/"));
     assertEquals("A", response2.body().string());
     assertEquals("299 test danger", response2.header("Warning"));
   }
 
-  public void assertCookies(URL url, String... expectedCookies) throws Exception {
+  public void assertCookies(HttpUrl url, String... expectedCookies) throws Exception {
     List<String> actualCookies = new ArrayList<>();
-    for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
+    for (HttpCookie cookie : cookieManager.getCookieStore().get(url.uri())) {
       actualCookies.add(cookie.toString());
     }
     assertEquals(Arrays.asList(expectedCookies), actualCookies);
@@ -1781,10 +1776,10 @@
 
   @Test public void doNotCachePartialResponse() throws Exception  {
     assertNotCached(new MockResponse()
-            .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
-            .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
-            .addHeader("Content-Range: bytes 100-100/200")
-            .addHeader("Cache-Control: max-age=60"));
+        .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
+        .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+        .addHeader("Content-Range: bytes 100-100/200")
+        .addHeader("Cache-Control: max-age=60"));
   }
 
   @Test public void conditionalHitUpdatesCache() throws Exception {
@@ -1800,18 +1795,18 @@
         .setBody("B"));
 
     // cache miss; seed the cache
-    Response response1 = get(server.getUrl("/a"));
+    Response response1 = get(server.url("/a"));
     assertEquals("A", response1.body().string());
     assertEquals(null, response1.header("Allow"));
 
     // conditional cache hit; update the cache
-    Response response2 = get(server.getUrl("/a"));
+    Response response2 = get(server.url("/a"));
     assertEquals(HttpURLConnection.HTTP_OK, response2.code());
     assertEquals("A", response2.body().string());
     assertEquals("GET, HEAD", response2.header("Allow"));
 
     // full cache hit
-    Response response3 = get(server.getUrl("/a"));
+    Response response3 = get(server.url("/a"));
     assertEquals("A", response3.body().string());
     assertEquals("GET, HEAD", response3.header("Allow"));
 
@@ -1824,10 +1819,9 @@
         .addHeader("Cache-Control: max-age=30")
         .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
+    assertEquals("A", get(server.url("/")).body().string());
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
-        .header("Cache-Control", "only-if-cached")
+        .url(server.url("/")).header("Cache-Control", "only-if-cached")
         .build();
     Response response = client.newCall(request).execute();
     assertEquals("A", response.body().string());
@@ -1843,8 +1837,8 @@
         .addHeader("Cache-Control: max-age=30")
         .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    Response response = get(server.getUrl("/"));
+    assertEquals("A", get(server.url("/")).body().string());
+    Response response = get(server.url("/"));
     assertEquals("B", response.body().string());
   }
 
@@ -1856,8 +1850,8 @@
     server.enqueue(new MockResponse()
         .setResponseCode(304));
 
-    assertEquals("A", get(server.getUrl("/")).body().string());
-    Response response = get(server.getUrl("/"));
+    assertEquals("A", get(server.url("/")).body().string());
+    Response response = get(server.url("/"));
     assertEquals("A", response.body().string());
   }
 
@@ -1865,7 +1859,7 @@
     server.enqueue(new MockResponse()
         .setBody("A"));
 
-    Response response = get(server.getUrl("/"));
+    Response response = get(server.url("/"));
     assertEquals("A", response.body().string());
   }
 
@@ -1877,7 +1871,7 @@
         .setHeaders(headers.build())
         .setBody("body"));
 
-    Response response = get(server.getUrl("/"));
+    Response response = get(server.url("/"));
     assertEquals("A", response.header(""));
   }
 
@@ -1895,7 +1889,7 @@
         .clearHeaders()
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     String urlKey = Util.md5Hex(url.toString());
     String entryMetadata = ""
         + "" + url + "\n"
@@ -1932,7 +1926,7 @@
     writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata);
     writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
     writeFile(cache.getDirectory(), "journal", journalBody);
-    cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE);
+    cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE, fileSystem);
     client.setCache(cache);
 
     Response response = get(url);
@@ -1948,7 +1942,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     client.getCache().evictAll();
     assertEquals(0, client.getCache().getSize());
@@ -1963,7 +1957,7 @@
         .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
 
     // Seed the cache.
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
 
     final AtomicReference<String> ifNoneMatch = new AtomicReference<>();
@@ -1985,7 +1979,7 @@
         .setBody("A"));
 
     // Seed the cache.
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
 
     // Confirm the interceptor isn't exercised.
@@ -2001,17 +1995,17 @@
     // Put some responses in the cache.
     server.enqueue(new MockResponse()
         .setBody("a"));
-    URL urlA = server.getUrl("/a");
+    HttpUrl urlA = server.url("/a");
     assertEquals("a", get(urlA).body().string());
 
     server.enqueue(new MockResponse()
         .setBody("b"));
-    URL urlB = server.getUrl("/b");
+    HttpUrl urlB = server.url("/b");
     assertEquals("b", get(urlB).body().string());
 
     server.enqueue(new MockResponse()
         .setBody("c"));
-    URL urlC = server.getUrl("/c");
+    HttpUrl urlC = server.url("/c");
     assertEquals("c", get(urlC).body().string());
 
     // Confirm the iterator returns those responses...
@@ -2037,7 +2031,7 @@
     server.enqueue(new MockResponse()
         .addHeader("Cache-Control: max-age=60")
         .setBody("a"));
-    URL url = server.getUrl("/a");
+    HttpUrl url = server.url("/a");
     assertEquals("a", get(url).body().string());
 
     // Remove it with iteration.
@@ -2055,7 +2049,7 @@
     // Put a response in the cache.
     server.enqueue(new MockResponse()
         .setBody("a"));
-    URL url = server.getUrl("/a");
+    HttpUrl url = server.url("/a");
     assertEquals("a", get(url).body().string());
 
     Iterator<String> i = cache.urls();
@@ -2071,7 +2065,7 @@
     // Put a response in the cache.
     server.enqueue(new MockResponse()
         .setBody("a"));
-    URL url = server.getUrl("/a");
+    HttpUrl url = server.url("/a");
     assertEquals("a", get(url).body().string());
 
     Iterator<String> i = cache.urls();
@@ -2090,7 +2084,7 @@
     // Put a response in the cache.
     server.enqueue(new MockResponse()
         .setBody("a"));
-    URL url = server.getUrl("/a");
+    HttpUrl url = server.url("/a");
     assertEquals("a", get(url).body().string());
 
     // The URL will remain available if hasNext() returned true...
@@ -2109,7 +2103,7 @@
     // Put a response in the cache.
     server.enqueue(new MockResponse()
         .setBody("a"));
-    URL url = server.getUrl("/a");
+    HttpUrl url = server.url("/a");
     assertEquals("a", get(url).body().string());
 
     Iterator<String> i = cache.urls();
@@ -2124,7 +2118,32 @@
     }
   }
 
-  private Response get(URL url) throws IOException {
+  /** Test https://github.com/square/okhttp/issues/1712. */
+  @Test public void conditionalMissUpdatesCache() throws Exception {
+    server.enqueue(new MockResponse()
+        .addHeader("ETag: v1")
+        .setBody("A"));
+    server.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+    server.enqueue(new MockResponse()
+        .addHeader("ETag: v2")
+        .setBody("B"));
+    server.enqueue(new MockResponse()
+        .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+    HttpUrl url = server.url("/");
+    assertEquals("A", get(url).body().string());
+    assertEquals("A", get(url).body().string());
+    assertEquals("B", get(url).body().string());
+    assertEquals("B", get(url).body().string());
+
+    assertEquals(null, server.takeRequest().getHeader("If-None-Match"));
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+    assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+    assertEquals("v2", server.takeRequest().getHeader("If-None-Match"));
+  }
+
+  private Response get(HttpUrl url) throws IOException {
     Request request = new Request.Builder()
         .url(url)
         .build();
@@ -2133,7 +2152,7 @@
 
 
   private void writeFile(File directory, String file, String content) throws IOException {
-    BufferedSink sink = Okio.buffer(Okio.sink(new File(directory, file)));
+    BufferedSink sink = Okio.buffer(fileSystem.sink(new File(directory, file)));
     sink.writeUtf8(content);
     sink.close();
   }
@@ -2158,7 +2177,7 @@
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     assertEquals("B", get(url).body().string());
   }
@@ -2177,7 +2196,7 @@
         .setStatus("HTTP/1.1 200 C-OK")
         .setBody("C"));
 
-    URL valid = server.getUrl("/valid");
+    HttpUrl valid = server.url("/valid");
     Response response1 = get(valid);
     assertEquals("A", response1.body().string());
     assertEquals(HttpURLConnection.HTTP_OK, response1.code());
@@ -2187,7 +2206,7 @@
     assertEquals(HttpURLConnection.HTTP_OK, response2.code());
     assertEquals("A-OK", response2.message());
 
-    URL invalid = server.getUrl("/invalid");
+    HttpUrl invalid = server.url("/invalid");
     Response response3 = get(invalid);
     assertEquals("B", response3.body().string());
     assertEquals(HttpURLConnection.HTTP_OK, response3.code());
@@ -2205,7 +2224,7 @@
     server.enqueue(response.setBody("A"));
     server.enqueue(response.setBody("B"));
 
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     assertEquals("A", get(url).body().string());
     assertEquals("A", get(url).body().string());
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index 9b908cc..051eae4 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -17,23 +17,30 @@
 
 import com.squareup.okhttp.internal.DoubleInetAddressNetwork;
 import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.RecordingOkAuthenticator;
 import com.squareup.okhttp.internal.SingleInetAddressNetwork;
 import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.Version;
+import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.Dispatcher;
 import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
 import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.net.CookieManager;
 import java.net.HttpCookie;
 import java.net.HttpURLConnection;
-import java.net.URL;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.ServerSocket;
 import java.net.UnknownServiceException;
 import java.security.cert.Certificate;
 import java.util.ArrayList;
@@ -49,6 +56,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import javax.net.ServerSocketFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLPeerUnverifiedException;
@@ -62,10 +70,8 @@
 import okio.Okio;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
 
@@ -73,36 +79,31 @@
 import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public final class CallTest {
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-
-  @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
   @Rule public final TestRule timeout = new Timeout(30_000);
+  @Rule public final MockWebServer server = new MockWebServer();
+  @Rule public final MockWebServer server2 = new MockWebServer();
 
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
-  @Rule public final MockWebServerRule server2 = new MockWebServerRule();
+  private SSLContext sslContext = SslContextBuilder.localhost();
+  private FileSystem fileSystem = new InMemoryFileSystem();
   private OkHttpClient client = new OkHttpClient();
   private RecordingCallback callback = new RecordingCallback();
   private TestLogHandler logHandler = new TestLogHandler();
-  private Cache cache;
+  private Cache cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
+  private ServerSocket nullServer;
 
   @Before public void setUp() throws Exception {
-    client = new OkHttpClient();
-    callback = new RecordingCallback();
-    logHandler = new TestLogHandler();
-
-    cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE);
     logger.addHandler(logHandler);
   }
 
   @After public void tearDown() throws Exception {
     cache.delete();
+    Util.closeQuietly(nullServer);
     logger.removeHandler(logHandler);
   }
 
@@ -110,7 +111,7 @@
     server.enqueue(new MockResponse().setBody("abc").addHeader("Content-Type: text/plain"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "SyncApiTest")
         .build();
 
@@ -127,47 +128,43 @@
     assertNull(recordedRequest.getHeader("Content-Length"));
   }
 
-  @Test public void lazilyEvaluateRequestUrl() throws Exception {
-    server.enqueue(new MockResponse().setBody("abc"));
+  @Test public void buildRequestUsingHttpUrl() throws Exception {
+    server.enqueue(new MockResponse());
 
-    Request request1 = new Request.Builder()
-        .url("foo://bar?baz")
+    HttpUrl httpUrl = server.url("/");
+    Request request = new Request.Builder()
+        .url(httpUrl)
         .build();
-    Request request2 = request1.newBuilder()
-        .url(server.getUrl("/"))
-        .build();
-    executeSynchronously(request2)
-        .assertCode(200)
-        .assertSuccessful()
-        .assertBody("abc");
+    assertEquals(httpUrl, request.httpUrl());
+
+    executeSynchronously(request).assertSuccessful();
   }
 
-  @Ignore // TODO(jwilson): fix.
   @Test public void invalidScheme() throws Exception {
+    Request.Builder requestBuilder = new Request.Builder();
     try {
-      Request request = new Request.Builder()
-          .url("ftp://hostname/path")
-          .build();
-      executeSynchronously(request);
+      requestBuilder.url("ftp://hostname/path");
       fail();
     } catch (IllegalArgumentException expected) {
+      assertEquals(expected.getMessage(), "unexpected url: ftp://hostname/path");
     }
   }
 
   @Test public void invalidPort() throws Exception {
-    Request request = new Request.Builder()
-        .url("http://localhost:65536/")
-        .build();
-    client.newCall(request).enqueue(callback);
-    callback.await(request.url())
-        .assertFailure("No route to localhost:65536; port is out of range");
+    Request.Builder requestBuilder = new Request.Builder();
+    try {
+      requestBuilder.url("http://localhost:65536/");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertEquals(expected.getMessage(), "unexpected url: http://localhost:65536/");
+    }
   }
 
   @Test public void getReturns500() throws Exception {
     server.enqueue(new MockResponse().setResponseCode(500));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     executeSynchronously(request)
@@ -199,7 +196,7 @@
     server.enqueue(new MockResponse().addHeader("Content-Type: text/plain"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .head()
         .header("User-Agent", "SyncApiTest")
         .build();
@@ -229,7 +226,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .post(RequestBody.create(MediaType.parse("text/plain"), "def"))
         .build();
 
@@ -258,7 +255,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("POST", RequestBody.create(null, new byte[0]))
         .build();
 
@@ -317,7 +314,7 @@
     server.enqueue(new MockResponse());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("POST", RequestBody.create(null, body))
         .build();
 
@@ -347,7 +344,7 @@
     String credential = Credentials.basic("jesse", "secret");
     client.setAuthenticator(new RecordingOkAuthenticator(credential));
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     executeSynchronously(request)
         .assertCode(200)
         .assertBody("Success!");
@@ -362,7 +359,7 @@
     client.setAuthenticator(new RecordingOkAuthenticator(credential));
 
     try {
-      client.newCall(new Request.Builder().url(server.getUrl("/0")).build()).execute();
+      client.newCall(new Request.Builder().url(server.url("/0")).build()).execute();
       fail();
     } catch (IOException expected) {
       assertEquals("Too many follow-up requests: 21", expected.getMessage());
@@ -373,7 +370,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .delete()
         .build();
 
@@ -402,7 +399,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("DELETE", RequestBody.create(MediaType.parse("text/plain"), "def"))
         .build();
 
@@ -419,7 +416,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .put(RequestBody.create(MediaType.parse("text/plain"), "def"))
         .build();
 
@@ -448,7 +445,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .patch(RequestBody.create(MediaType.parse("text/plain"), "def"))
         .build();
 
@@ -477,7 +474,7 @@
     server.enqueue(new MockResponse());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("POST", RequestBody.create(null, "abc"))
         .build();
 
@@ -495,7 +492,7 @@
         .addHeader("Content-Type: text/plain"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "SyncApiTest")
         .build();
 
@@ -525,7 +522,7 @@
         .addHeader("Content-Type: text/plain"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "SyncApiTest")
         .build();
 
@@ -555,12 +552,12 @@
         .addHeader("Content-Type: text/plain"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "AsyncApiTest")
         .build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(request.url())
+    callback.await(request.httpUrl())
         .assertCode(200)
         .assertHeader("Content-Type", "text/plain")
         .assertBody("abc");
@@ -572,7 +569,7 @@
     server.enqueue(new MockResponse());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/secret"))
+        .url(server.url("/secret"))
         .build();
 
     client.newCall(request).enqueue(new Callback() {
@@ -585,7 +582,7 @@
       }
     });
 
-    assertEquals("INFO: Callback failure for call to " + server.getUrl("/") + "...",
+    assertEquals("INFO: Callback failure for call to " + server.url("/") + "...",
         logHandler.take());
   }
 
@@ -594,13 +591,13 @@
     server.enqueue(new MockResponse().setBody("def"));
     server.enqueue(new MockResponse().setBody("ghi"));
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/a")).build())
         .assertBody("abc");
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/b")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/b")).build())
         .assertBody("def");
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/c")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/c")).build())
         .assertBody("ghi");
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
@@ -613,14 +610,14 @@
     server.enqueue(new MockResponse().setBody("def"));
     server.enqueue(new MockResponse().setBody("ghi"));
 
-    client.newCall(new Request.Builder().url(server.getUrl("/a")).build()).enqueue(callback);
-    callback.await(server.getUrl("/a")).assertBody("abc");
+    client.newCall(new Request.Builder().url(server.url("/a")).build()).enqueue(callback);
+    callback.await(server.url("/a")).assertBody("abc");
 
-    client.newCall(new Request.Builder().url(server.getUrl("/b")).build()).enqueue(callback);
-    callback.await(server.getUrl("/b")).assertBody("def");
+    client.newCall(new Request.Builder().url(server.url("/b")).build()).enqueue(callback);
+    callback.await(server.url("/b")).assertBody("def");
 
-    client.newCall(new Request.Builder().url(server.getUrl("/c")).build()).enqueue(callback);
-    callback.await(server.getUrl("/c")).assertBody("ghi");
+    client.newCall(new Request.Builder().url(server.url("/c")).build()).enqueue(callback);
+    callback.await(server.url("/c")).assertBody("ghi");
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
     assertEquals(1, server.takeRequest().getSequenceNumber());
@@ -631,7 +628,7 @@
     server.enqueue(new MockResponse().setBody("abc"));
     server.enqueue(new MockResponse().setBody("def"));
 
-    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    Request request = new Request.Builder().url(server.url("/a")).build();
     client.newCall(request).enqueue(new Callback() {
       @Override public void onFailure(Request request, IOException e) {
         throw new AssertionError();
@@ -644,11 +641,11 @@
         assertEquals('c', bytes.read());
 
         // This request will share a connection with 'A' cause it's all done.
-        client.newCall(new Request.Builder().url(server.getUrl("/b")).build()).enqueue(callback);
+        client.newCall(new Request.Builder().url(server.url("/b")).build()).enqueue(callback);
       }
     });
 
-    callback.await(server.getUrl("/b")).assertCode(200).assertBody("def");
+    callback.await(server.url("/b")).assertCode(200).assertBody("def");
     assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
     assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reuse!
   }
@@ -659,11 +656,11 @@
 
     // First request: time out after 1000ms.
     client.setReadTimeout(1000, TimeUnit.MILLISECONDS);
-    executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build()).assertBody("abc");
+    executeSynchronously(new Request.Builder().url(server.url("/a")).build()).assertBody("abc");
 
     // Second request: time out after 250ms.
     client.setReadTimeout(250, TimeUnit.MILLISECONDS);
-    Request request = new Request.Builder().url(server.getUrl("/b")).build();
+    Request request = new Request.Builder().url(server.url("/b")).build();
     Response response = client.newCall(request).execute();
     BufferedSource bodySource = response.body().source();
     assertEquals('d', bodySource.readByte());
@@ -691,7 +688,7 @@
     Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
     client.setReadTimeout(100, TimeUnit.MILLISECONDS);
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       // If this succeeds, too many requests were made.
       client.newCall(request).execute();
@@ -715,7 +712,7 @@
       }
     };
     Request request1 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("POST", requestBody1)
         .build();
     Response response1 = client.newCall(request1).execute();
@@ -732,7 +729,7 @@
       }
     };
     Request request2 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .method("POST", requestBody2)
         .build();
     Response response2 = client.newCall(request2).execute();
@@ -748,14 +745,14 @@
     server.enqueue(new MockResponse().setBody("def"));
 
     // Call 1: set a deadline on the response body.
-    Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request1 = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request1).execute();
     BufferedSource body1 = response1.body().source();
     assertEquals("abc", body1.readUtf8());
     body1.timeout().deadline(5, TimeUnit.SECONDS);
 
     // Call 2: check for the absence of a deadline on the request body.
-    Request request2 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request2 = new Request.Builder().url(server.url("/")).build();
     Response response2 = client.newCall(request2).execute();
     BufferedSource body2 = response2.body().source();
     assertEquals("def", body2.readUtf8());
@@ -767,7 +764,7 @@
   }
 
   @Test public void tls() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse()
         .setBody("abc")
         .addHeader("Content-Type: text/plain"));
@@ -775,12 +772,12 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/")).build())
         .assertHandshake();
   }
 
   @Test public void tls_Async() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse()
         .setBody("abc")
         .addHeader("Content-Type: text/plain"));
@@ -789,11 +786,11 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(request.url()).assertHandshake();
+    callback.await(request.httpUrl()).assertHandshake();
   }
 
   @Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception {
@@ -803,7 +800,7 @@
     Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
     assertTrue(client.getRetryOnConnectionFailure());
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response = client.newCall(request).execute();
     assertEquals("retry success", response.body().string());
   }
@@ -815,7 +812,7 @@
     Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
     client.setRetryOnConnectionFailure(false);
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       // If this succeeds, too many requests were made.
       client.newCall(request).execute();
@@ -825,7 +822,7 @@
   }
 
   @Test public void recoverFromTlsHandshakeFailure() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -833,7 +830,7 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/")).build())
         .assertBody("abc");
   }
 
@@ -846,7 +843,7 @@
       return;
     }
 
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
 
     RecordingSSLSocketFactory clientSocketFactory =
@@ -855,7 +852,7 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       client.newCall(request).execute();
       fail();
@@ -870,7 +867,7 @@
   }
 
   @Test public void recoverFromTlsHandshakeFailure_Async() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
     server.enqueue(new MockResponse().setBody("abc"));
 
@@ -878,24 +875,24 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(request.url()).assertBody("abc");
+    callback.await(request.httpUrl()).assertBody("abc");
   }
 
   @Test public void noRecoveryFromTlsHandshakeFailureWhenTlsFallbackIsDisabled() throws Exception {
     client.setConnectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
 
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
 
     suppressTlsFallbackScsv(client);
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       client.newCall(request).execute();
       fail();
@@ -913,7 +910,7 @@
 
     server.enqueue(new MockResponse());
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       client.newCall(request).execute();
       fail();
@@ -923,20 +920,20 @@
   }
 
   @Test public void setFollowSslRedirectsFalse() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location: http://square.com"));
 
     client.setFollowSslRedirects(false);
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response = client.newCall(request).execute();
     assertEquals(301, response.code());
   }
 
   @Test public void matchingPinnedCertificate() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse());
     server.enqueue(new MockResponse());
 
@@ -944,22 +941,22 @@
     client.setHostnameVerifier(new RecordingHostnameVerifier());
 
     // Make a first request without certificate pinning. Use it to collect certificates to pin.
-    Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request1 = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request1).execute();
     CertificatePinner.Builder certificatePinnerBuilder = new CertificatePinner.Builder();
     for (Certificate certificate : response1.handshake().peerCertificates()) {
-      certificatePinnerBuilder.add(server.get().getHostName(), CertificatePinner.pin(certificate));
+      certificatePinnerBuilder.add(server.getHostName(), CertificatePinner.pin(certificate));
     }
 
     // Make another request with certificate pinning. It should complete normally.
     client.setCertificatePinner(certificatePinnerBuilder.build());
-    Request request2 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request2 = new Request.Builder().url(server.url("/")).build();
     Response response2 = client.newCall(request2).execute();
     assertNotSame(response2.handshake(), response1.handshake());
   }
 
   @Test public void unmatchingPinnedCertificate() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse());
 
     client.setSslSocketFactory(sslContext.getSocketFactory());
@@ -967,11 +964,11 @@
 
     // Pin publicobject.com's cert.
     client.setCertificatePinner(new CertificatePinner.Builder()
-        .add(server.get().getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+        .add(server.getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
         .build());
 
     // When we pin the wrong certificate, connectivity fails.
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     try {
       client.newCall(request).execute();
       fail();
@@ -984,12 +981,12 @@
     server.enqueue(new MockResponse().setBody("abc"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .post(RequestBody.create(MediaType.parse("text/plain"), "def"))
         .build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(request.url())
+    callback.await(request.httpUrl())
         .assertCode(200)
         .assertBody("abc");
 
@@ -1005,12 +1002,12 @@
     server.enqueue(new MockResponse().setBody("def"));
 
     // Seed the connection pool so we have something that can fail.
-    Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request1 = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request1).execute();
     assertEquals("abc", response1.body().string());
 
     Request request2 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .post(RequestBody.create(MediaType.parse("text/plain"), "body!"))
         .build();
     Response response2 = client.newCall(request2).execute();
@@ -1038,7 +1035,7 @@
     client.setCache(cache);
 
     // Store a response in the cache.
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request cacheStoreRequest = new Request.Builder()
         .url(url)
         .addHeader("Accept-Language", "fr-CA")
@@ -1090,7 +1087,7 @@
     client.setCache(cache);
 
     // Store a response in the cache.
-    URL url = server.getUrl("/");
+    HttpUrl url = server.url("/");
     Request cacheStoreRequest = new Request.Builder()
         .url(url)
         .addHeader("Accept-Language", "fr-CA")
@@ -1148,17 +1145,17 @@
     client.setCache(cache);
 
     Request request1 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request1).enqueue(callback);
-    callback.await(request1.url()).assertCode(200).assertBody("A");
+    callback.await(request1.httpUrl()).assertCode(200).assertBody("A");
     assertNull(server.takeRequest().getHeader("If-None-Match"));
 
     Request request2 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request2).enqueue(callback);
-    callback.await(request2.url()).assertCode(200).assertBody("A");
+    callback.await(request2.httpUrl()).assertCode(200).assertBody("A");
     assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
   }
 
@@ -1175,7 +1172,7 @@
     client.setCache(cache);
 
     Request cacheStoreRequest = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .addHeader("Accept-Language", "fr-CA")
         .addHeader("Accept-Charset", "UTF-8")
         .build();
@@ -1185,7 +1182,7 @@
     assertNull(server.takeRequest().getHeader("If-None-Match"));
 
     Request cacheMissRequest = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .addHeader("Accept-Language", "en-US") // Different, but Vary says it doesn't matter.
         .addHeader("Accept-Charset", "UTF-8")
         .build();
@@ -1220,23 +1217,23 @@
     client.setCache(cache);
 
     Request request1 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request1).enqueue(callback);
-    callback.await(request1.url()).assertCode(200).assertBody("A");
+    callback.await(request1.httpUrl()).assertCode(200).assertBody("A");
     assertNull(server.takeRequest().getHeader("If-None-Match"));
 
     Request request2 = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request2).enqueue(callback);
-    callback.await(request2.url()).assertCode(200).assertBody("B");
+    callback.await(request2.httpUrl()).assertCode(200).assertBody("B");
     assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
   }
 
   @Test public void onlyIfCachedReturns504WhenNotCached() throws Exception {
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Cache-Control", "only-if-cached")
         .build();
 
@@ -1260,7 +1257,7 @@
         .setBody("/b has moved!"));
     server.enqueue(new MockResponse().setBody("C"));
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/a")).build())
         .assertCode(200)
         .assertBody("C")
         .priorResponse()
@@ -1283,7 +1280,7 @@
     server.enqueue(new MockResponse().setBody("Page 2"));
 
     Response response = client.newCall(new Request.Builder()
-        .url(server.getUrl("/page1"))
+        .url(server.url("/page1"))
         .post(RequestBody.create(MediaType.parse("text/plain"), "Request Body"))
         .build()).execute();
     assertEquals("Page 2", response.body().string());
@@ -1300,25 +1297,25 @@
     server2.enqueue(new MockResponse().setBody("Page 2"));
     server.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-        .addHeader("Location: " + server2.getUrl("/")));
+        .addHeader("Location: " + server2.url("/")));
 
     CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
     HttpCookie cookie = new HttpCookie("c", "cookie");
-    cookie.setDomain(server.get().getCookieDomain());
+    cookie.setDomain(server.getCookieDomain());
     cookie.setPath("/");
     String portList = Integer.toString(server.getPort());
     cookie.setPortlist(portList);
-    cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookie);
+    cookieManager.getCookieStore().add(server.url("/").uri(), cookie);
     client.setCookieHandler(cookieManager);
 
     Response response = client.newCall(new Request.Builder()
-        .url(server.getUrl("/page1"))
+        .url(server.url("/page1"))
         .build()).execute();
     assertEquals("Page 2", response.body().string());
 
     RecordedRequest request1 = server.takeRequest();
     assertEquals("$Version=\"1\"; c=\"cookie\";$Path=\"/\";$Domain=\""
-        + server.get().getCookieDomain()
+        + server.getCookieDomain()
         + "\";$Port=\""
         + portList
         + "\"", request1.getHeader("Cookie"));
@@ -1333,11 +1330,11 @@
         .setResponseCode(401));
     server.enqueue(new MockResponse()
         .setResponseCode(302)
-        .addHeader("Location: " + server2.getUrl("/b")));
+        .addHeader("Location: " + server2.url("/b")));
 
     client.setAuthenticator(new RecordingOkAuthenticator(Credentials.basic("jesse", "secret")));
 
-    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    Request request = new Request.Builder().url(server.url("/a")).build();
     Response response = client.newCall(request).execute();
     assertEquals("Page 2", response.body().string());
 
@@ -1359,10 +1356,10 @@
         .setBody("/b has moved!"));
     server.enqueue(new MockResponse().setBody("C"));
 
-    Request request = new Request.Builder().url(server.getUrl("/a")).build();
+    Request request = new Request.Builder().url(server.url("/a")).build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(server.getUrl("/c"))
+    callback.await(server.url("/c"))
         .assertCode(200)
         .assertBody("C")
         .priorResponse()
@@ -1386,7 +1383,7 @@
     }
     server.enqueue(new MockResponse().setBody("Success!"));
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/0")).build())
+    executeSynchronously(new Request.Builder().url(server.url("/0")).build())
         .assertCode(200)
         .assertBody("Success!");
   }
@@ -1400,9 +1397,9 @@
     }
     server.enqueue(new MockResponse().setBody("Success!"));
 
-    Request request = new Request.Builder().url(server.getUrl("/0")).build();
+    Request request = new Request.Builder().url(server.url("/0")).build();
     client.newCall(request).enqueue(callback);
-    callback.await(server.getUrl("/20"))
+    callback.await(server.url("/20"))
         .assertCode(200)
         .assertBody("Success!");
   }
@@ -1416,7 +1413,7 @@
     }
 
     try {
-      client.newCall(new Request.Builder().url(server.getUrl("/0")).build()).execute();
+      client.newCall(new Request.Builder().url(server.url("/0")).build()).execute();
       fail();
     } catch (IOException expected) {
       assertEquals("Too many follow-up requests: 21", expected.getMessage());
@@ -1431,13 +1428,39 @@
           .setBody("Redirecting to /" + (i + 1)));
     }
 
-    Request request = new Request.Builder().url(server.getUrl("/0")).build();
+    Request request = new Request.Builder().url(server.url("/0")).build();
     client.newCall(request).enqueue(callback);
-    callback.await(server.getUrl("/20")).assertFailure("Too many follow-up requests: 21");
+    callback.await(server.url("/20")).assertFailure("Too many follow-up requests: 21");
+  }
+
+  @Test public void http204WithBodyDisallowed() throws IOException {
+    server.enqueue(new MockResponse()
+        .setResponseCode(204)
+        .setBody("I'm not even supposed to be here today."));
+
+    try {
+      executeSynchronously(new Request.Builder().url(server.url("/")).build());
+      fail();
+    } catch (ProtocolException e) {
+      assertEquals("HTTP 204 had non-zero Content-Length: 39", e.getMessage());
+    }
+  }
+
+  @Test public void http205WithBodyDisallowed() throws IOException {
+    server.enqueue(new MockResponse()
+        .setResponseCode(205)
+        .setBody("I'm not even supposed to be here today."));
+
+    try {
+      executeSynchronously(new Request.Builder().url(server.url("/")).build());
+      fail();
+    } catch (ProtocolException e) {
+      assertEquals("HTTP 205 had non-zero Content-Length: 39", e.getMessage());
+    }
   }
 
   @Test public void canceledBeforeExecute() throws Exception {
-    Call call = client.newCall(new Request.Builder().url(server.getUrl("/a")).build());
+    Call call = client.newCall(new Request.Builder().url(server.url("/a")).build());
     call.cancel();
 
     try {
@@ -1448,21 +1471,60 @@
     assertEquals(0, server.getRequestCount());
   }
 
+  @Test public void cancelDuringHttpConnect() throws Exception {
+    cancelDuringConnect("http");
+  }
+
+  @Test public void cancelDuringHttpsConnect() throws Exception {
+    cancelDuringConnect("https");
+  }
+
+  /** Cancel a call that's waiting for connect to complete. */
+  private void cancelDuringConnect(String scheme) throws Exception {
+    InetSocketAddress socketAddress = startNullServer();
+
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme(scheme)
+        .host(socketAddress.getHostName())
+        .port(socketAddress.getPort())
+        .build();
+
+    long cancelDelayMillis = 300L;
+    Call call = client.newCall(new Request.Builder().url(url).build());
+    cancelLater(call, cancelDelayMillis);
+
+    long startNanos = System.nanoTime();
+    try {
+      call.execute();
+      fail();
+    } catch (IOException expected) {
+    }
+    long elapsedNanos = System.nanoTime() - startNanos;
+    assertEquals(cancelDelayMillis, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 100f);
+  }
+
+  private InetSocketAddress startNullServer() throws IOException {
+    InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("localhost"), 0);
+    nullServer = ServerSocketFactory.getDefault().createServerSocket();
+    nullServer.bind(address);
+    return new InetSocketAddress(address.getAddress(), nullServer.getLocalPort());
+  }
+
   @Test public void cancelTagImmediatelyAfterEnqueue() throws Exception {
     Call call = client.newCall(new Request.Builder()
-        .url(server.getUrl("/a"))
+        .url(server.url("/a"))
         .tag("request")
         .build());
     call.enqueue(callback);
     client.cancel("request");
     assertEquals(0, server.getRequestCount());
-    callback.await(server.getUrl("/a")).assertFailure("Canceled");
+    callback.await(server.url("/a")).assertFailure("Canceled");
   }
 
   @Test public void cancelBeforeBodyIsRead() throws Exception {
     server.enqueue(new MockResponse().setBody("def").throttleBody(1, 750, TimeUnit.MILLISECONDS));
 
-    final Call call = client.newCall(new Request.Builder().url(server.getUrl("/a")).build());
+    final Call call = client.newCall(new Request.Builder().url(server.url("/a")).build());
     ExecutorService executor = Executors.newSingleThreadExecutor();
     Future<Response> result = executor.submit(new Callable<Response>() {
       @Override public Response call() throws Exception {
@@ -1482,14 +1544,14 @@
   }
 
   @Test public void cancelInFlightBeforeResponseReadThrowsIOE() throws Exception {
-    server.get().setDispatcher(new Dispatcher() {
+    server.setDispatcher(new Dispatcher() {
       @Override public MockResponse dispatch(RecordedRequest request) {
         client.cancel("request");
         return new MockResponse().setBody("A");
       }
     });
 
-    Request request = new Request.Builder().url(server.getUrl("/a")).tag("request").build();
+    Request request = new Request.Builder().url(server.url("/a")).tag("request").build();
     try {
       client.newCall(request).execute();
       fail();
@@ -1513,7 +1575,7 @@
    */
   @Test public void canceledBeforeIOSignalsOnFailure() throws Exception {
     client.getDispatcher().setMaxRequests(1); // Force requests to be executed serially.
-    server.get().setDispatcher(new Dispatcher() {
+    server.setDispatcher(new Dispatcher() {
       char nextResponse = 'A';
 
       @Override public MockResponse dispatch(RecordedRequest request) {
@@ -1522,16 +1584,16 @@
       }
     });
 
-    Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+    Request requestA = new Request.Builder().url(server.url("/a")).tag("request A").build();
     client.newCall(requestA).enqueue(callback);
     assertEquals("/a", server.takeRequest().getPath());
 
-    Request requestB = new Request.Builder().url(server.getUrl("/b")).tag("request B").build();
+    Request requestB = new Request.Builder().url(server.url("/b")).tag("request B").build();
     client.newCall(requestB).enqueue(callback);
 
-    callback.await(requestA.url()).assertBody("A");
+    callback.await(requestA.httpUrl()).assertBody("A");
     // At this point we know the callback is ready, and that it will receive a cancel failure.
-    callback.await(requestB.url()).assertFailure("Canceled");
+    callback.await(requestB.httpUrl()).assertFailure("Canceled");
   }
 
   @Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception {
@@ -1545,9 +1607,9 @@
   }
 
   @Test public void canceledBeforeResponseReadSignalsOnFailure() throws Exception {
-    Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+    Request requestA = new Request.Builder().url(server.url("/a")).tag("request A").build();
     final Call call = client.newCall(requestA);
-    server.get().setDispatcher(new Dispatcher() {
+    server.setDispatcher(new Dispatcher() {
       @Override public MockResponse dispatch(RecordedRequest request) {
         call.cancel();
         return new MockResponse().setBody("A");
@@ -1557,7 +1619,7 @@
     call.enqueue(callback);
     assertEquals("/a", server.takeRequest().getPath());
 
-    callback.await(requestA.url()).assertFailure("Canceled", "stream was reset: CANCEL",
+    callback.await(requestA.httpUrl()).assertFailure("Canceled", "stream was reset: CANCEL",
         "Socket closed");
   }
 
@@ -1582,7 +1644,7 @@
     final AtomicReference<String> bodyRef = new AtomicReference<>();
     final AtomicBoolean failureRef = new AtomicBoolean();
 
-    Request request = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+    Request request = new Request.Builder().url(server.url("/a")).tag("request A").build();
     final Call call = client.newCall(request);
     call.enqueue(new Callback() {
       @Override public void onFailure(Request request, IOException e) {
@@ -1628,7 +1690,7 @@
       }
     });
 
-    Call call = client.newCall(new Request.Builder().url(server.getUrl("/a")).build());
+    Call call = client.newCall(new Request.Builder().url(server.url("/a")).build());
     call.cancel();
 
     try {
@@ -1648,7 +1710,7 @@
         .addHeader("Content-Encoding: gzip"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     // Confirm that the user request doesn't have Accept-Encoding, and the user
@@ -1672,7 +1734,7 @@
     server.enqueue(new MockResponse().setBody("def"));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "SyncApiTest")
         .build();
 
@@ -1696,7 +1758,7 @@
     assertEquals("abc", response.body().string());
 
     // Make another request just to confirm that that connection can be reused...
-    executeSynchronously(new Request.Builder().url(server.getUrl("/")).build()).assertBody("def");
+    executeSynchronously(new Request.Builder().url(server.url("/")).build()).assertBody("def");
     assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
     assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
 
@@ -1707,7 +1769,7 @@
   @Test public void userAgentIsIncludedByDefault() throws Exception {
     server.enqueue(new MockResponse());
 
-    executeSynchronously(new Request.Builder().url(server.getUrl("/")).build());
+    executeSynchronously(new Request.Builder().url(server.url("/")).build());
 
     RecordedRequest recordedRequest = server.takeRequest();
     assertTrue(recordedRequest.getHeader("User-Agent")
@@ -1723,7 +1785,7 @@
 
     client.setFollowRedirects(false);
     RecordedResponse recordedResponse = executeSynchronously(
-        new Request.Builder().url(server.getUrl("/a")).build());
+        new Request.Builder().url(server.url("/a")).build());
 
     recordedResponse
         .assertBody("A")
@@ -1734,7 +1796,7 @@
     server.enqueue(new MockResponse());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Expect", "100-continue")
         .post(RequestBody.create(MediaType.parse("text/plain"), "abc"))
         .build();
@@ -1743,14 +1805,14 @@
         .assertCode(200)
         .assertSuccessful();
 
-    assertEquals("abc", server.takeRequest().getUtf8Body());
+    assertEquals("abc", server.takeRequest().getBody().readUtf8());
   }
 
   @Test public void expect100ContinueEmptyRequestBody() throws Exception {
     server.enqueue(new MockResponse());
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("Expect", "100-continue")
         .post(RequestBody.create(MediaType.parse("text/plain"), ""))
         .build();
@@ -1760,6 +1822,26 @@
         .assertSuccessful();
   }
 
+  /** We forbid non-ASCII characters in outgoing request headers, but accept UTF-8. */
+  @Test public void responseHeaderParsingIsLenient() throws Exception {
+    Headers headers = new Headers.Builder()
+        .add("Content-Length", "0")
+        .addLenient("a\tb: c\u007fd")
+        .addLenient(": ef")
+        .addLenient("\ud83c\udf69: \u2615\ufe0f")
+        .build();
+    server.enqueue(new MockResponse().setHeaders(headers));
+
+    Request request = new Request.Builder()
+        .url(server.url("/"))
+        .build();
+
+    executeSynchronously(request)
+        .assertHeader("a\tb", "c\u007fd")
+        .assertHeader("\ud83c\udf69", "\u2615\ufe0f")
+        .assertHeader("", "ef");
+  }
+
   private RecordedResponse executeSynchronously(Request request) throws IOException {
     Response response = client.newCall(request).execute();
     return new RecordedResponse(request, response, null, response.body().string(), null);
@@ -1773,8 +1855,8 @@
     client.setSslSocketFactory(sslContext.getSocketFactory());
     client.setHostnameVerifier(new RecordingHostnameVerifier());
     client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
-    server.get().useHttps(sslContext.getSocketFactory(), false);
-    server.get().setProtocols(client.getProtocols());
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.setProtocols(client.getProtocols());
   }
 
   private Buffer gzip(String data) throws IOException {
@@ -1785,17 +1867,31 @@
     return result;
   }
 
+  private void cancelLater(final Call call, final long delay) {
+    new Thread("canceler") {
+      @Override public void run() {
+        try {
+          Thread.sleep(delay);
+        } catch (InterruptedException e) {
+          throw new AssertionError();
+        }
+        call.cancel();
+      }
+    }.start();
+  }
+
   private static class RecordingSSLSocketFactory extends DelegatingSSLSocketFactory {
 
-    private List<SSLSocket> socketsCreated = new ArrayList<SSLSocket>();
+    private List<SSLSocket> socketsCreated = new ArrayList<>();
 
     public RecordingSSLSocketFactory(SSLSocketFactory delegate) {
       super(delegate);
     }
 
     @Override
-    protected void configureSocket(SSLSocket sslSocket) throws IOException {
+    protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException {
       socketsCreated.add(sslSocket);
+      return sslSocket;
     }
 
     public List<SSLSocket> getSocketsCreated() {
@@ -1808,7 +1904,7 @@
    * TLS_FALLBACK_SCSV cipher on fallback connections. See
    * {@link com.squareup.okhttp.FallbackTestClientSocketFactory} for details.
    */
-  private static void suppressTlsFallbackScsv(OkHttpClient client) {
+  private void suppressTlsFallbackScsv(OkHttpClient client) {
     FallbackTestClientSocketFactory clientSocketFactory =
         new FallbackTestClientSocketFactory(sslContext.getSocketFactory());
     client.setSslSocketFactory(clientSocketFactory);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
index c5cea28..91b5a59 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
@@ -19,10 +19,15 @@
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.cert.X509Certificate;
+import java.util.Set;
 import javax.net.ssl.SSLPeerUnverifiedException;
+import okio.ByteString;
 import org.junit.Test;
 
+import static com.squareup.okhttp.TestUtil.setOf;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -32,10 +37,16 @@
   static KeyPair keyPairA;
   static X509Certificate keypairACertificate1;
   static String keypairACertificate1Pin;
+  static ByteString keypairACertificate1PinBase64;
 
   static KeyPair keyPairB;
   static X509Certificate keypairBCertificate1;
   static String keypairBCertificate1Pin;
+  static ByteString keypairBCertificate1PinBase64;
+
+  static KeyPair keyPairC;
+  static X509Certificate keypairCCertificate1;
+  static String keypairCCertificate1Pin;
 
   static {
     try {
@@ -44,15 +55,25 @@
       keyPairA = sslContextBuilder.generateKeyPair();
       keypairACertificate1 = sslContextBuilder.selfSignedCertificate(keyPairA, "1");
       keypairACertificate1Pin = CertificatePinner.pin(keypairACertificate1);
+      keypairACertificate1PinBase64 = pinToBase64(keypairACertificate1Pin);
 
       keyPairB = sslContextBuilder.generateKeyPair();
       keypairBCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairB, "1");
       keypairBCertificate1Pin = CertificatePinner.pin(keypairBCertificate1);
+      keypairBCertificate1PinBase64 = pinToBase64(keypairBCertificate1Pin);
+
+      keyPairC = sslContextBuilder.generateKeyPair();
+      keypairCCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairC, "1");
+      keypairCCertificate1Pin = CertificatePinner.pin(keypairCCertificate1);
     } catch (GeneralSecurityException e) {
       throw new AssertionError(e);
     }
   }
 
+  static ByteString pinToBase64(String pin) {
+    return ByteString.decodeBase64(pin.substring("sha1/".length()));
+  }
+
   @Test public void malformedPin() throws Exception {
     CertificatePinner.Builder builder = new CertificatePinner.Builder();
     try {
@@ -135,4 +156,98 @@
     CertificatePinner certificatePinner = new CertificatePinner.Builder().build();
     certificatePinner.check("example.com", keypairACertificate1);
   }
+
+  @Test public void successfulCheckForWildcardHostname() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .build();
+
+    certificatePinner.check("a.example.com", keypairACertificate1);
+  }
+
+  @Test public void successfulMatchAcceptsAnyMatchingCertificateForWildcardHostname() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairBCertificate1Pin)
+        .build();
+
+    certificatePinner.check("a.example.com", keypairACertificate1, keypairBCertificate1);
+  }
+
+  @Test public void unsuccessfulCheckForWildcardHostname() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .build();
+
+    try {
+      certificatePinner.check("a.example.com", keypairBCertificate1);
+      fail();
+    } catch (SSLPeerUnverifiedException expected) {
+    }
+  }
+
+  @Test public void multipleCertificatesForOneWildcardHostname() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+        .build();
+
+    certificatePinner.check("a.example.com", keypairACertificate1);
+    certificatePinner.check("a.example.com", keypairBCertificate1);
+  }
+
+  @Test public void successfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .add("a.example.com", keypairBCertificate1Pin)
+        .build();
+
+    certificatePinner.check("a.example.com", keypairACertificate1);
+    certificatePinner.check("a.example.com", keypairBCertificate1);
+  }
+
+  @Test public void unsuccessfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .add("a.example.com", keypairBCertificate1Pin)
+        .build();
+
+    try {
+      certificatePinner.check("a.example.com", keypairCCertificate1);
+      fail();
+    } catch (SSLPeerUnverifiedException expected) {
+    }
+  }
+
+  @Test public void successfulFindMatchingPins() {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("first.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+        .add("second.com", keypairCCertificate1Pin)
+        .build();
+
+    Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
+    Set<ByteString> matchedPins  = certificatePinner.findMatchingPins("first.com");
+
+    assertEquals(expectedPins, matchedPins);
+  }
+
+  @Test public void successfulFindMatchingPinsForWildcardAndDirectCertificates() {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .add("a.example.com", keypairBCertificate1Pin)
+        .add("b.example.com", keypairCCertificate1Pin)
+        .build();
+
+    Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
+    Set<ByteString> matchedPins  = certificatePinner.findMatchingPins("a.example.com");
+
+    assertEquals(expectedPins, matchedPins);
+  }
+
+  @Test public void wildcardHostnameShouldNotMatchThroughDot() throws Exception {
+    CertificatePinner certificatePinner = new CertificatePinner.Builder()
+        .add("*.example.com", keypairACertificate1Pin)
+        .build();
+
+    assertNull(certificatePinner.findMatchingPins("example.com"));
+    assertNull(certificatePinner.findMatchingPins("a.b.example.com"));
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index 4e8ec7a..d528c7a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -16,12 +16,12 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
 import com.squareup.okhttp.internal.http.RecordingProxySelector;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -52,8 +52,8 @@
       ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
 
   private static final int KEEP_ALIVE_DURATION_MS = 5000;
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
 
+  private SSLContext sslContext = SslContextBuilder.localhost();
   private MockWebServer spdyServer;
   private InetSocketAddress spdySocketAddress;
   private Address spdyAddress;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
index a14db22..3fe6eb4 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
@@ -90,7 +90,8 @@
     return sslSocket;
   }
 
-  protected void configureSocket(SSLSocket sslSocket) throws IOException {
+  protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException {
     // No-op by default.
+    return sslSocket;
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java
index ef24aaa..7116fa3 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java
@@ -35,33 +35,30 @@
   @Override
   public ServerSocket createServerSocket() throws IOException {
     ServerSocket serverSocket = delegate.createServerSocket();
-    configureServerSocket(serverSocket);
-    return serverSocket;
+    return configureServerSocket(serverSocket);
   }
 
   @Override
   public ServerSocket createServerSocket(int port) throws IOException {
     ServerSocket serverSocket = delegate.createServerSocket(port);
-    configureServerSocket(serverSocket);
-    return serverSocket;
+    return configureServerSocket(serverSocket);
   }
 
   @Override
   public ServerSocket createServerSocket(int port, int backlog) throws IOException {
     ServerSocket serverSocket = delegate.createServerSocket(port, backlog);
-    configureServerSocket(serverSocket);
-    return serverSocket;
+    return configureServerSocket(serverSocket);
   }
 
   @Override
   public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress)
       throws IOException {
     ServerSocket serverSocket = delegate.createServerSocket(port, backlog, ifAddress);
-    configureServerSocket(serverSocket);
-    return serverSocket;
+    return configureServerSocket(serverSocket);
   }
 
-  protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+  protected ServerSocket configureServerSocket(ServerSocket serverSocket) throws IOException {
     // No-op by default.
+    return serverSocket;
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java
index e8fdfe8..e673fdf 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java
@@ -36,41 +36,37 @@
   @Override
   public Socket createSocket() throws IOException {
     Socket socket = delegate.createSocket();
-    configureSocket(socket);
-    return socket;
+    return configureSocket(socket);
   }
 
   @Override
   public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
     Socket socket = delegate.createSocket(host, port);
-    configureSocket(socket);
-    return socket;
+    return configureSocket(socket);
   }
 
   @Override
   public Socket createSocket(String host, int port, InetAddress localAddress, int localPort)
       throws IOException, UnknownHostException {
     Socket socket = delegate.createSocket(host, port, localAddress, localPort);
-    configureSocket(socket);
-    return socket;
+    return configureSocket(socket);
   }
 
   @Override
   public Socket createSocket(InetAddress host, int port) throws IOException {
     Socket socket = delegate.createSocket(host, port);
-    configureSocket(socket);
-    return socket;
+    return configureSocket(socket);
   }
 
   @Override
   public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
       throws IOException {
     Socket socket = delegate.createSocket(host, port, localAddress, localPort);
-    configureSocket(socket);
-    return socket;
+    return configureSocket(socket);
   }
 
-  protected void configureSocket(Socket socket) throws IOException {
+  protected Socket configureSocket(Socket socket) throws IOException {
     // No-op by default.
+    return socket;
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
index 5f9e623..5504e77 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
@@ -40,37 +40,8 @@
     super(delegate);
   }
 
-  @Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
-      throws IOException {
-    SSLSocket socket = super.createSocket(s, host, port, autoClose);
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
-  }
-
-  @Override public SSLSocket createSocket() throws IOException {
-    SSLSocket socket = super.createSocket();
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
-  }
-
-  @Override public SSLSocket createSocket(String host,int port) throws IOException {
-    SSLSocket socket = super.createSocket(host, port);
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
-  }
-
-  @Override public SSLSocket createSocket(String host,int port, InetAddress localHost,
-      int localPort) throws IOException {
-    SSLSocket socket = super.createSocket(host, port, localHost, localPort);
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
-  }
-
-  @Override public SSLSocket createSocket(InetAddress host,int port) throws IOException {
-    SSLSocket socket = super.createSocket(host, port);
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
-  }
-
-  @Override public SSLSocket createSocket(InetAddress address,int port,
-      InetAddress localAddress, int localPort) throws IOException {
-    SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
-    return new TlsFallbackScsvDisabledSSLSocket(socket);
+  @Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException {
+    return new TlsFallbackScsvDisabledSSLSocket(sslSocket);
   }
 
   private static class TlsFallbackScsvDisabledSSLSocket extends DelegatingSSLSocket {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java
index a9533bf..04e74a4 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java
@@ -15,6 +15,7 @@
  */
 package com.squareup.okhttp;
 
+import java.io.IOException;
 import okio.Buffer;
 import org.junit.Test;
 
@@ -25,11 +26,12 @@
     RequestBody formEncoding = new FormEncodingBuilder()
         .add("a&b", "c=d")
         .add("space, the", "final frontier")
+        .add("%25", "%25")
         .build();
 
     assertEquals("application/x-www-form-urlencoded", formEncoding.contentType().toString());
 
-    String expected = "a%26b=c%3Dd&space%2C+the=final+frontier";
+    String expected = "a%26b=c%3Dd&space%2C%20the=final%20frontier&%2525=%2525";
     assertEquals(expected.length(), formEncoding.contentLength());
 
     Buffer out = new Buffer();
@@ -37,6 +39,19 @@
     assertEquals(expected, out.readUtf8());
   }
 
+  @Test public void addEncoded() throws Exception {
+    RequestBody formEncoding = new FormEncodingBuilder()
+        .addEncoded("a+=& b", "c+=& d")
+        .addEncoded("e+=& f", "g+=& h")
+        .addEncoded("%25", "%25")
+        .build();
+
+    String expected = "a%20%3D%26%20b=c%20%3D%26%20d&e%20%3D%26%20f=g%20%3D%26%20h&%25=%25";
+    Buffer out = new Buffer();
+    formEncoding.writeTo(out);
+    assertEquals(expected, out.readUtf8());
+  }
+
   @Test public void encodedPair() throws Exception {
     RequestBody formEncoding = new FormEncodingBuilder()
         .add("sim", "ple")
@@ -64,4 +79,103 @@
     formEncoding.writeTo(buffer);
     assertEquals(expected, buffer.readUtf8());
   }
+
+  @Test public void buildEmptyForm() throws Exception {
+    RequestBody formEncoding = new FormEncodingBuilder().build();
+
+    String expected = "";
+    assertEquals(expected.length(), formEncoding.contentLength());
+
+    Buffer buffer = new Buffer();
+    formEncoding.writeTo(buffer);
+    assertEquals(expected, buffer.readUtf8());
+  }
+
+  @Test public void characterEncoding() throws Exception {
+    assertEquals("%00", formEncode(0)); // Browsers convert '\u0000' to '%EF%BF%BD'.
+    assertEquals("%01", formEncode(1));
+    assertEquals("%02", formEncode(2));
+    assertEquals("%03", formEncode(3));
+    assertEquals("%04", formEncode(4));
+    assertEquals("%05", formEncode(5));
+    assertEquals("%06", formEncode(6));
+    assertEquals("%07", formEncode(7));
+    assertEquals("%08", formEncode(8));
+    assertEquals("%09", formEncode(9));
+    assertEquals("%0A", formEncode(10)); // Browsers convert '\n' to '\r\n'
+    assertEquals("%0B", formEncode(11));
+    assertEquals("%0C", formEncode(12));
+    assertEquals("%0D", formEncode(13)); // Browsers convert '\r' to '\r\n'
+    assertEquals("%0E", formEncode(14));
+    assertEquals("%0F", formEncode(15));
+    assertEquals("%10", formEncode(16));
+    assertEquals("%11", formEncode(17));
+    assertEquals("%12", formEncode(18));
+    assertEquals("%13", formEncode(19));
+    assertEquals("%14", formEncode(20));
+    assertEquals("%15", formEncode(21));
+    assertEquals("%16", formEncode(22));
+    assertEquals("%17", formEncode(23));
+    assertEquals("%18", formEncode(24));
+    assertEquals("%19", formEncode(25));
+    assertEquals("%1A", formEncode(26));
+    assertEquals("%1B", formEncode(27));
+    assertEquals("%1C", formEncode(28));
+    assertEquals("%1D", formEncode(29));
+    assertEquals("%1E", formEncode(30));
+    assertEquals("%1F", formEncode(31));
+    assertEquals("%20", formEncode(32)); // Browsers use '+' for space.
+    assertEquals("%21", formEncode(33));
+    assertEquals("%22", formEncode(34));
+    assertEquals("%23", formEncode(35));
+    assertEquals("%24", formEncode(36));
+    assertEquals("%25", formEncode(37));
+    assertEquals("%26", formEncode(38));
+    assertEquals("%27", formEncode(39));
+    assertEquals("%28", formEncode(40));
+    assertEquals("%29", formEncode(41));
+    assertEquals("*", formEncode(42));
+    assertEquals("%2B", formEncode(43));
+    assertEquals("%2C", formEncode(44));
+    assertEquals("-", formEncode(45));
+    assertEquals(".", formEncode(46));
+    assertEquals("%2F", formEncode(47));
+    assertEquals("0", formEncode(48));
+    assertEquals("9", formEncode(57));
+    assertEquals("%3A", formEncode(58));
+    assertEquals("%3B", formEncode(59));
+    assertEquals("%3C", formEncode(60));
+    assertEquals("%3D", formEncode(61));
+    assertEquals("%3E", formEncode(62));
+    assertEquals("%3F", formEncode(63));
+    assertEquals("%40", formEncode(64));
+    assertEquals("A", formEncode(65));
+    assertEquals("Z", formEncode(90));
+    assertEquals("%5B", formEncode(91));
+    assertEquals("%5C", formEncode(92));
+    assertEquals("%5D", formEncode(93));
+    assertEquals("%5E", formEncode(94));
+    assertEquals("_", formEncode(95));
+    assertEquals("%60", formEncode(96));
+    assertEquals("a", formEncode(97));
+    assertEquals("z", formEncode(122));
+    assertEquals("%7B", formEncode(123));
+    assertEquals("%7C", formEncode(124));
+    assertEquals("%7D", formEncode(125));
+    assertEquals("%7E", formEncode(126));
+    assertEquals("%7F", formEncode(127));
+    assertEquals("%C2%80", formEncode(128));
+    assertEquals("%C3%BF", formEncode(255));
+  }
+
+  private String formEncode(int codePoint) throws IOException {
+    // Wrap the codepoint with regular printable characters to prevent trimming.
+    RequestBody body = new FormEncodingBuilder()
+        .add("a", new String(new int[] { 'b', codePoint, 'c' }, 0, 3))
+        .build();
+    Buffer buffer = new Buffer();
+    body.writeTo(buffer);
+    buffer.skip(3); // Skip "a=b" prefix.
+    return buffer.readUtf8(buffer.size() - 1); // Skip the "c" suffix.
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
index 4dd7f83..3f5a708 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
@@ -17,10 +17,20 @@
 
 import com.squareup.okhttp.UrlComponentEncodingTester.Component;
 import com.squareup.okhttp.UrlComponentEncodingTester.Encoding;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.UnknownHostException;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import org.junit.Ignore;
 import org.junit.Test;
 
+import static java.util.Collections.singletonList;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 
 public final class HttpUrlTest {
   @Test public void parseTrimsAsciiWhitespace() throws Exception {
@@ -33,38 +43,43 @@
     assertEquals(expected, HttpUrl.parse("http://host/").resolve("  .  "));
   }
 
+  @Test public void parseHostAsciiNonPrintable() throws Exception {
+    String host = "host\u0001";
+    assertNull(HttpUrl.parse("http://" + host + "/"));
+  }
+
   @Test public void parseDoesNotTrimOtherWhitespaceCharacters() throws Exception {
     // Whitespace characters list from Google's Guava team: http://goo.gl/IcR9RD
-    assertEquals("/%0B", HttpUrl.parse("http://h/\u000b").path()); // line tabulation
-    assertEquals("/%1C", HttpUrl.parse("http://h/\u001c").path()); // information separator 4
-    assertEquals("/%1D", HttpUrl.parse("http://h/\u001d").path()); // information separator 3
-    assertEquals("/%1E", HttpUrl.parse("http://h/\u001e").path()); // information separator 2
-    assertEquals("/%1F", HttpUrl.parse("http://h/\u001f").path()); // information separator 1
-    assertEquals("/%C2%85", HttpUrl.parse("http://h/\u0085").path()); // next line
-    assertEquals("/%C2%A0", HttpUrl.parse("http://h/\u00a0").path()); // non-breaking space
-    assertEquals("/%E1%9A%80", HttpUrl.parse("http://h/\u1680").path()); // ogham space mark
-    assertEquals("/%E1%A0%8E", HttpUrl.parse("http://h/\u180e").path()); // mongolian vowel separator
-    assertEquals("/%E2%80%80", HttpUrl.parse("http://h/\u2000").path()); // en quad
-    assertEquals("/%E2%80%81", HttpUrl.parse("http://h/\u2001").path()); // em quad
-    assertEquals("/%E2%80%82", HttpUrl.parse("http://h/\u2002").path()); // en space
-    assertEquals("/%E2%80%83", HttpUrl.parse("http://h/\u2003").path()); // em space
-    assertEquals("/%E2%80%84", HttpUrl.parse("http://h/\u2004").path()); // three-per-em space
-    assertEquals("/%E2%80%85", HttpUrl.parse("http://h/\u2005").path()); // four-per-em space
-    assertEquals("/%E2%80%86", HttpUrl.parse("http://h/\u2006").path()); // six-per-em space
-    assertEquals("/%E2%80%87", HttpUrl.parse("http://h/\u2007").path()); // figure space
-    assertEquals("/%E2%80%88", HttpUrl.parse("http://h/\u2008").path()); // punctuation space
-    assertEquals("/%E2%80%89", HttpUrl.parse("http://h/\u2009").path()); // thin space
-    assertEquals("/%E2%80%8A", HttpUrl.parse("http://h/\u200a").path()); // hair space
-    assertEquals("/%E2%80%8B", HttpUrl.parse("http://h/\u200b").path()); // zero-width space
-    assertEquals("/%E2%80%8C", HttpUrl.parse("http://h/\u200c").path()); // zero-width non-joiner
-    assertEquals("/%E2%80%8D", HttpUrl.parse("http://h/\u200d").path()); // zero-width joiner
-    assertEquals("/%E2%80%8E", HttpUrl.parse("http://h/\u200e").path()); // left-to-right mark
-    assertEquals("/%E2%80%8F", HttpUrl.parse("http://h/\u200f").path()); // right-to-left mark
-    assertEquals("/%E2%80%A8", HttpUrl.parse("http://h/\u2028").path()); // line separator
-    assertEquals("/%E2%80%A9", HttpUrl.parse("http://h/\u2029").path()); // paragraph separator
-    assertEquals("/%E2%80%AF", HttpUrl.parse("http://h/\u202f").path()); // narrow non-breaking space
-    assertEquals("/%E2%81%9F", HttpUrl.parse("http://h/\u205f").path()); // medium mathematical space
-    assertEquals("/%E3%80%80", HttpUrl.parse("http://h/\u3000").path()); // ideographic space
+    assertEquals("/%0B", HttpUrl.parse("http://h/\u000b").encodedPath()); // line tabulation
+    assertEquals("/%1C", HttpUrl.parse("http://h/\u001c").encodedPath()); // information separator 4
+    assertEquals("/%1D", HttpUrl.parse("http://h/\u001d").encodedPath()); // information separator 3
+    assertEquals("/%1E", HttpUrl.parse("http://h/\u001e").encodedPath()); // information separator 2
+    assertEquals("/%1F", HttpUrl.parse("http://h/\u001f").encodedPath()); // information separator 1
+    assertEquals("/%C2%85", HttpUrl.parse("http://h/\u0085").encodedPath()); // next line
+    assertEquals("/%C2%A0", HttpUrl.parse("http://h/\u00a0").encodedPath()); // non-breaking space
+    assertEquals("/%E1%9A%80", HttpUrl.parse("http://h/\u1680").encodedPath()); // ogham space mark
+    assertEquals("/%E1%A0%8E", HttpUrl.parse("http://h/\u180e").encodedPath()); // mongolian vowel separator
+    assertEquals("/%E2%80%80", HttpUrl.parse("http://h/\u2000").encodedPath()); // en quad
+    assertEquals("/%E2%80%81", HttpUrl.parse("http://h/\u2001").encodedPath()); // em quad
+    assertEquals("/%E2%80%82", HttpUrl.parse("http://h/\u2002").encodedPath()); // en space
+    assertEquals("/%E2%80%83", HttpUrl.parse("http://h/\u2003").encodedPath()); // em space
+    assertEquals("/%E2%80%84", HttpUrl.parse("http://h/\u2004").encodedPath()); // three-per-em space
+    assertEquals("/%E2%80%85", HttpUrl.parse("http://h/\u2005").encodedPath()); // four-per-em space
+    assertEquals("/%E2%80%86", HttpUrl.parse("http://h/\u2006").encodedPath()); // six-per-em space
+    assertEquals("/%E2%80%87", HttpUrl.parse("http://h/\u2007").encodedPath()); // figure space
+    assertEquals("/%E2%80%88", HttpUrl.parse("http://h/\u2008").encodedPath()); // punctuation space
+    assertEquals("/%E2%80%89", HttpUrl.parse("http://h/\u2009").encodedPath()); // thin space
+    assertEquals("/%E2%80%8A", HttpUrl.parse("http://h/\u200a").encodedPath()); // hair space
+    assertEquals("/%E2%80%8B", HttpUrl.parse("http://h/\u200b").encodedPath()); // zero-width space
+    assertEquals("/%E2%80%8C", HttpUrl.parse("http://h/\u200c").encodedPath()); // zero-width non-joiner
+    assertEquals("/%E2%80%8D", HttpUrl.parse("http://h/\u200d").encodedPath()); // zero-width joiner
+    assertEquals("/%E2%80%8E", HttpUrl.parse("http://h/\u200e").encodedPath()); // left-to-right mark
+    assertEquals("/%E2%80%8F", HttpUrl.parse("http://h/\u200f").encodedPath()); // right-to-left mark
+    assertEquals("/%E2%80%A8", HttpUrl.parse("http://h/\u2028").encodedPath()); // line separator
+    assertEquals("/%E2%80%A9", HttpUrl.parse("http://h/\u2029").encodedPath()); // paragraph separator
+    assertEquals("/%E2%80%AF", HttpUrl.parse("http://h/\u202f").encodedPath()); // narrow non-breaking space
+    assertEquals("/%E2%81%9F", HttpUrl.parse("http://h/\u205f").encodedPath()); // medium mathematical space
+    assertEquals("/%E3%80%80", HttpUrl.parse("http://h/\u3000").encodedPath()); // ideographic space
   }
 
   @Test public void scheme() throws Exception {
@@ -74,6 +89,8 @@
     assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("HTTP://host/"));
     assertEquals(HttpUrl.parse("https://host/"), HttpUrl.parse("https://host/"));
     assertEquals(HttpUrl.parse("https://host/"), HttpUrl.parse("HTTPS://host/"));
+    assertEquals(HttpUrl.Builder.ParseResult.UNSUPPORTED_SCHEME,
+        new HttpUrl.Builder().parse(null, "image640://480.png"));
     assertEquals(null, HttpUrl.parse("httpp://host/"));
     assertEquals(null, HttpUrl.parse("0ttp://host/"));
     assertEquals(null, HttpUrl.parse("ht+tp://host/"));
@@ -196,19 +213,20 @@
   @Test public void passwordWithEmptyUsername() throws Exception {
     // Chrome doesn't mind, but Firefox rejects URLs with empty usernames and non-empty passwords.
     assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http://:@host/path"));
-    assertEquals("password%40", HttpUrl.parse("http://:password@@host/path").password());
+    assertEquals("password%40", HttpUrl.parse("http://:password@@host/path").encodedPassword());
   }
 
   @Test public void unprintableCharactersArePercentEncoded() throws Exception {
-    assertEquals("/%00", HttpUrl.parse("http://host/\u0000").path());
-    assertEquals("/%08", HttpUrl.parse("http://host/\u0008").path());
-    assertEquals("/%EF%BF%BD", HttpUrl.parse("http://host/\ufffd").path());
+    assertEquals("/%00", HttpUrl.parse("http://host/\u0000").encodedPath());
+    assertEquals("/%08", HttpUrl.parse("http://host/\u0008").encodedPath());
+    assertEquals("/%EF%BF%BD", HttpUrl.parse("http://host/\ufffd").encodedPath());
   }
 
   @Test public void usernameCharacters() throws Exception {
     new UrlComponentEncodingTester()
         .override(Encoding.PERCENT, '[', ']', '{', '}', '|', '^', '\'', ';', '=', '@')
         .override(Encoding.SKIP, ':', '/', '\\', '?', '#')
+        .skipForUri('%')
         .test(Component.USER);
   }
 
@@ -216,6 +234,7 @@
     new UrlComponentEncodingTester()
         .override(Encoding.PERCENT, '[', ']', '{', '}', '|', '^', '\'', ':', ';', '=', '@')
         .override(Encoding.SKIP, '/', '\\', '?', '#')
+        .skipForUri('%')
         .test(Component.PASSWORD);
   }
 
@@ -225,6 +244,39 @@
     assertEquals(null, HttpUrl.parse("http://%20/"));
   }
 
+  @Test public void hostnameLowercaseCharactersMappedDirectly() throws Exception {
+    assertEquals("abcd", HttpUrl.parse("http://abcd").host());
+    assertEquals("xn--4xa", HttpUrl.parse("http://σ").host());
+  }
+
+  @Test public void hostnameUppercaseCharactersConvertedToLowercase() throws Exception {
+    assertEquals("abcd", HttpUrl.parse("http://ABCD").host());
+    assertEquals("xn--4xa", HttpUrl.parse("http://Σ").host());
+  }
+
+  @Test public void hostnameIgnoredCharacters() throws Exception {
+    // The soft hyphen (­) should be ignored.
+    assertEquals("abcd", HttpUrl.parse("http://AB\u00adCD").host());
+  }
+
+  @Test public void hostnameMultipleCharacterMapping() throws Exception {
+    // Map the single character telephone symbol (℡) to the string "tel".
+    assertEquals("tel", HttpUrl.parse("http://\u2121").host());
+  }
+
+  @Test public void hostnameMappingLastMappedCodePoint() throws Exception {
+    assertEquals("xn--pu5l", HttpUrl.parse("http://\uD87E\uDE1D").host());
+  }
+
+  @Ignore("The java.net.IDN implementation doesn't ignore characters that it should.")
+  @Test public void hostnameMappingLastIgnoredCodePoint() throws Exception {
+    assertEquals("abcd", HttpUrl.parse("http://ab\uDB40\uDDEFcd").host());
+  }
+
+  @Test public void hostnameMappingLastDisallowedCodePoint() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://\uDBFF\uDFFF"));
+  }
+
   @Test public void hostIpv6() throws Exception {
     // Square braces are absent from host()...
     assertEquals("::1", HttpUrl.parse("http://[::1]/").host());
@@ -244,9 +296,138 @@
     assertEquals("::1", HttpUrl.parse("http://%5B%3A%3A1%5D/").host());
   }
 
+  @Test public void hostIpv6AddressDifferentFormats() throws Exception {
+    // Multiple representations of the same address; see http://tools.ietf.org/html/rfc5952.
+    String a3 = "2001:db8::1:0:0:1";
+    assertEquals(a3, HttpUrl.parse("http://[2001:db8:0:0:1:0:0:1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:0db8:0:0:1:0:0:1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:db8::1:0:0:1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:db8::0:1:0:0:1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:0db8::1:0:0:1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:db8:0:0:1::1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:db8:0000:0:1::1]").host());
+    assertEquals(a3, HttpUrl.parse("http://[2001:DB8:0:0:1::1]").host());
+  }
+
+  @Test public void hostIpv6AddressLeadingCompression() throws Exception {
+    assertEquals("::1", HttpUrl.parse("http://[::0001]").host());
+    assertEquals("::1", HttpUrl.parse("http://[0000::0001]").host());
+    assertEquals("::1", HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001]").host());
+    assertEquals("::1", HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000::0001]").host());
+  }
+
+  @Test public void hostIpv6AddressTrailingCompression() throws Exception {
+    assertEquals("1::", HttpUrl.parse("http://[0001:0000::]").host());
+    assertEquals("1::", HttpUrl.parse("http://[0001::0000]").host());
+    assertEquals("1::", HttpUrl.parse("http://[0001::]").host());
+    assertEquals("1::", HttpUrl.parse("http://[1::]").host());
+  }
+
+  @Test public void hostIpv6AddressTooManyDigitsInGroup() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://[00000:0000:0000:0000:0000:0000:0000:0001]"));
+    assertEquals(null, HttpUrl.parse("http://[::00001]"));
+  }
+
+  @Test public void hostIpv6AddressMisplacedColons() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://[:0000:0000:0000:0000:0000:0000:0000:0001]"));
+    assertEquals(null, HttpUrl.parse("http://[:::0000:0000:0000:0000:0000:0000:0000:0001]"));
+    assertEquals(null, HttpUrl.parse("http://[:1]"));
+    assertEquals(null, HttpUrl.parse("http://[:::1]"));
+    assertEquals(null, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0001:]"));
+    assertEquals(null, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001:]"));
+    assertEquals(null, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001::]"));
+    assertEquals(null, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001:::]"));
+    assertEquals(null, HttpUrl.parse("http://[1:]"));
+    assertEquals(null, HttpUrl.parse("http://[1:::]"));
+    assertEquals(null, HttpUrl.parse("http://[1:::1]"));
+    assertEquals(null, HttpUrl.parse("http://[00000:0000:0000:0000::0000:0000:0000:0001]"));
+  }
+
+  @Test public void hostIpv6AddressTooManyGroups() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://[00000:0000:0000:0000:0000:0000:0000:0000:0001]"));
+  }
+
+  @Test public void hostIpv6AddressTooMuchCompression() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://[0000::0000:0000:0000:0000::0001]"));
+    assertEquals(null, HttpUrl.parse("http://[::0000:0000:0000:0000::0001]"));
+  }
+
+  @Test public void hostIpv6ScopedAddress() throws Exception {
+    // java.net.InetAddress parses scoped addresses. These aren't valid in URLs.
+    assertEquals(null, HttpUrl.parse("http://[::1%2544]"));
+  }
+
+  @Test public void hostIpv6WithIpv4Suffix() throws Exception {
+    assertEquals("::1:ffff:ffff", HttpUrl.parse("http://[::1:255.255.255.255]/").host());
+    assertEquals("::1:0:0", HttpUrl.parse("http://[0:0:0:0:0:1:0.0.0.0]/").host());
+  }
+
+  @Test public void hostIpv6WithIpv4SuffixWithOctalPrefix() throws Exception {
+    // Chrome interprets a leading '0' as octal; Firefox rejects them. (We reject them.)
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.0.0.000000]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.010.0.010]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.0.0.000001]/"));
+  }
+
+  @Test public void hostIpv6WithIpv4SuffixWithHexadecimalPrefix() throws Exception {
+    // Chrome interprets a leading '0x' as hexadecimal; Firefox rejects them. (We reject them.)
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.0x10.0.0x10]/"));
+  }
+
+  @Test public void hostIpv6WithMalformedIpv4Suffix() throws Exception {
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.0:0.0]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:0.0-0.0]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:.255.255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:255..255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:255.255..255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:0:1:255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:256.255.255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:ff.255.255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:0:1:255.255.255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:1:255.255.255.255]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:1:0.0.0.0:1]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0.0.0.0:1:0:0:0:0:1]/"));
+    assertEquals(null, HttpUrl.parse("http://[0.0.0.0:0:0:0:0:0:1]/"));
+  }
+
+  @Test public void hostIpv6WithIncompleteIpv4Suffix() throws Exception {
+    // To Chrome & Safari these are well-formed; Firefox disagrees. (We're consistent with Firefox).
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:255.255.255.]/"));
+    assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:255.255.255]/"));
+  }
+
+  @Test public void hostIpv6CanonicalForm() throws Exception {
+    assertEquals("abcd:ef01:2345:6789:abcd:ef01:2345:6789",
+        HttpUrl.parse("http://[abcd:ef01:2345:6789:abcd:ef01:2345:6789]/").host());
+    assertEquals("a::b:0:0:0", HttpUrl.parse("http://[a:0:0:0:b:0:0:0]/").host());
+    assertEquals("a:b:0:0:c::", HttpUrl.parse("http://[a:b:0:0:c:0:0:0]/").host());
+    assertEquals("a:b::c:0:0", HttpUrl.parse("http://[a:b:0:0:0:c:0:0]/").host());
+    assertEquals("a::b:0:0:0", HttpUrl.parse("http://[a:0:0:0:b:0:0:0]/").host());
+    assertEquals("::a:b:0:0:0", HttpUrl.parse("http://[0:0:0:a:b:0:0:0]/").host());
+    assertEquals("::a:0:0:0:b", HttpUrl.parse("http://[0:0:0:a:0:0:0:b]/").host());
+    assertEquals("::a:b:c:d:e:f:1", HttpUrl.parse("http://[0:a:b:c:d:e:f:1]/").host());
+    assertEquals("a:b:c:d:e:f:1::", HttpUrl.parse("http://[a:b:c:d:e:f:1:0]/").host());
+    assertEquals("ff01::101", HttpUrl.parse("http://[FF01:0:0:0:0:0:0:101]/").host());
+    assertEquals("1::", HttpUrl.parse("http://[1:0:0:0:0:0:0:0]/").host());
+    assertEquals("::1", HttpUrl.parse("http://[0:0:0:0:0:0:0:1]/").host());
+    assertEquals("::", HttpUrl.parse("http://[0:0:0:0:0:0:0:0]/").host());
+  }
+
+  @Test public void hostIpv4CanonicalForm() throws Exception {
+    assertEquals("255.255.255.255", HttpUrl.parse("http://255.255.255.255/").host());
+    assertEquals("1.2.3.4", HttpUrl.parse("http://1.2.3.4/").host());
+    assertEquals("0.0.0.0", HttpUrl.parse("http://0.0.0.0/").host());
+  }
+
+  @Ignore("java.net.IDN strips trailing trailing dots on Java 7, but not on Java 8.")
+  @Test public void hostWithTrailingDot() throws Exception {
+    assertEquals("host.", HttpUrl.parse("http://host./").host());
+  }
+
   @Test public void port() throws Exception {
     assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("http://host:80/"));
     assertEquals(HttpUrl.parse("http://host:99/"), HttpUrl.parse("http://host:99/"));
+    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("http://host:/"));
     assertEquals(65535, HttpUrl.parse("http://host:65535/").port());
     assertEquals(null, HttpUrl.parse("http://host:0/"));
     assertEquals(null, HttpUrl.parse("http://host:65536/"));
@@ -259,6 +440,7 @@
     new UrlComponentEncodingTester()
         .override(Encoding.PERCENT, '^', '{', '}', '|')
         .override(Encoding.SKIP, '\\', '?', '#')
+        .skipForUri('%', '[', ']')
         .test(Component.PATH);
   }
 
@@ -266,13 +448,15 @@
     new UrlComponentEncodingTester()
         .override(Encoding.IDENTITY, '?', '`')
         .override(Encoding.PERCENT, '\'')
-        .override(Encoding.SKIP, '#')
+        .override(Encoding.SKIP, '#', '+')
+        .skipForUri('%', '\\', '^', '`', '{', '|', '}')
         .test(Component.QUERY);
   }
 
   @Test public void fragmentCharacters() throws Exception {
     new UrlComponentEncodingTester()
         .override(Encoding.IDENTITY, ' ', '"', '#', '<', '>', '?', '`')
+        .skipForUri('%', ' ', '"', '#', '<', '>', '\\', '^', '`', '{', '|', '}')
         .test(Component.FRAGMENT);
     // TODO(jwilson): don't percent-encode non-ASCII characters. (But do encode control characters!)
   }
@@ -299,6 +483,21 @@
     assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve("%2e"));
   }
 
+  @Test public void relativePathWithTrailingSlash() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c/");
+    assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve(".."));
+    assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve("../"));
+    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("../.."));
+    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("../../"));
+    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../.."));
+    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../../"));
+    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../../.."));
+    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../../../"));
+    assertEquals(HttpUrl.parse("http://host/a"), base.resolve("../../../../a"));
+    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../../../a/.."));
+    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("../../../../a/b/.."));
+  }
+
   @Test public void pathWithBackslash() throws Exception {
     HttpUrl base = HttpUrl.parse("http://host/a/b/c");
     assertEquals(HttpUrl.parse("http://host/a/b/d/e/f"), base.resolve("d\\e\\f"));
@@ -313,53 +512,658 @@
   }
 
   @Test public void decodeUsername() {
-    assertEquals("user", HttpUrl.parse("http://user@host/").decodeUsername());
-    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://%F0%9F%8D%A9@host/").decodeUsername());
+    assertEquals("user", HttpUrl.parse("http://user@host/").username());
+    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://%F0%9F%8D%A9@host/").username());
   }
 
   @Test public void decodePassword() {
-    assertEquals("password", HttpUrl.parse("http://user:password@host/").decodePassword());
-    assertEquals(null, HttpUrl.parse("http://user:@host/").decodePassword());
-    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://user:%F0%9F%8D%A9@host/").decodePassword());
+    assertEquals("password", HttpUrl.parse("http://user:password@host/").password());
+    assertEquals("", HttpUrl.parse("http://user:@host/").password());
+    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://user:%F0%9F%8D%A9@host/").password());
   }
 
   @Test public void decodeSlashCharacterInDecodedPathSegment() {
     assertEquals(Arrays.asList("a/b/c"),
-        HttpUrl.parse("http://host/a%2Fb%2Fc").decodePathSegments());
+        HttpUrl.parse("http://host/a%2Fb%2Fc").pathSegments());
   }
 
   @Test public void decodeEmptyPathSegments() {
     assertEquals(Arrays.asList(""),
-        HttpUrl.parse("http://host/").decodePathSegments());
+        HttpUrl.parse("http://host/").pathSegments());
   }
 
   @Test public void percentDecode() throws Exception {
     assertEquals(Arrays.asList("\u0000"),
-        HttpUrl.parse("http://host/%00").decodePathSegments());
+        HttpUrl.parse("http://host/%00").pathSegments());
     assertEquals(Arrays.asList("a", "\u2603", "c"),
-        HttpUrl.parse("http://host/a/%E2%98%83/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%E2%98%83/c").pathSegments());
     assertEquals(Arrays.asList("a", "\uD83C\uDF69", "c"),
-        HttpUrl.parse("http://host/a/%F0%9F%8D%A9/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%F0%9F%8D%A9/c").pathSegments());
     assertEquals(Arrays.asList("a", "b", "c"),
-        HttpUrl.parse("http://host/a/%62/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%62/c").pathSegments());
     assertEquals(Arrays.asList("a", "z", "c"),
-        HttpUrl.parse("http://host/a/%7A/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%7A/c").pathSegments());
     assertEquals(Arrays.asList("a", "z", "c"),
-        HttpUrl.parse("http://host/a/%7a/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%7a/c").pathSegments());
   }
 
   @Test public void malformedPercentEncoding() {
     assertEquals(Arrays.asList("a%f", "b"),
-        HttpUrl.parse("http://host/a%f/b").decodePathSegments());
+        HttpUrl.parse("http://host/a%f/b").pathSegments());
     assertEquals(Arrays.asList("%", "b"),
-        HttpUrl.parse("http://host/%/b").decodePathSegments());
+        HttpUrl.parse("http://host/%/b").pathSegments());
     assertEquals(Arrays.asList("%"),
-        HttpUrl.parse("http://host/%").decodePathSegments());
+        HttpUrl.parse("http://host/%").pathSegments());
   }
 
   @Test public void malformedUtf8Encoding() {
     // Replace a partial UTF-8 sequence with the Unicode replacement character.
     assertEquals(Arrays.asList("a", "\ufffdx", "c"),
-        HttpUrl.parse("http://host/a/%E2%98x/c").decodePathSegments());
+        HttpUrl.parse("http://host/a/%E2%98x/c").pathSegments());
+  }
+
+  @Test public void incompleteUrlComposition() throws Exception {
+    try {
+      new HttpUrl.Builder().scheme("http").build();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertEquals("host == null", expected.getMessage());
+    }
+    try {
+      new HttpUrl.Builder().host("host").build();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertEquals("scheme == null", expected.getMessage());
+    }
+  }
+
+  @Test public void minimalUrlComposition() throws Exception {
+    HttpUrl url = new HttpUrl.Builder().scheme("http").host("host").build();
+    assertEquals("http://host/", url.toString());
+    assertEquals("http", url.scheme());
+    assertEquals("", url.username());
+    assertEquals("", url.password());
+    assertEquals("host", url.host());
+    assertEquals(80, url.port());
+    assertEquals("/", url.encodedPath());
+    assertEquals(null, url.query());
+    assertEquals(null, url.fragment());
+  }
+
+  @Test public void fullUrlComposition() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .username("username")
+        .password("password")
+        .host("host")
+        .port(8080)
+        .addPathSegment("path")
+        .query("query")
+        .fragment("fragment")
+        .build();
+    assertEquals("http://username:password@host:8080/path?query#fragment", url.toString());
+    assertEquals("http", url.scheme());
+    assertEquals("username", url.username());
+    assertEquals("password", url.password());
+    assertEquals("host", url.host());
+    assertEquals(8080, url.port());
+    assertEquals("/path", url.encodedPath());
+    assertEquals("query", url.query());
+    assertEquals("fragment", url.fragment());
+  }
+
+  @Test public void changingSchemeChangesDefaultPort() throws Exception {
+    assertEquals(443, HttpUrl.parse("http://example.com")
+        .newBuilder()
+        .scheme("https")
+        .build().port());
+
+    assertEquals(80, HttpUrl.parse("https://example.com")
+        .newBuilder()
+        .scheme("http")
+        .build().port());
+
+    assertEquals(1234, HttpUrl.parse("https://example.com:1234")
+        .newBuilder()
+        .scheme("http")
+        .build().port());
+  }
+
+  @Test public void composeEncodesWhitespace() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .username("a\r\n\f\t b")
+        .password("c\r\n\f\t d")
+        .host("host")
+        .addPathSegment("e\r\n\f\t f")
+        .query("g\r\n\f\t h")
+        .fragment("i\r\n\f\t j")
+        .build();
+    assertEquals("http://a%0D%0A%0C%09%20b:c%0D%0A%0C%09%20d@host"
+        + "/e%0D%0A%0C%09%20f?g%0D%0A%0C%09%20h#i%0D%0A%0C%09 j", url.toString());
+    assertEquals("a\r\n\f\t b", url.username());
+    assertEquals("c\r\n\f\t d", url.password());
+    assertEquals("e\r\n\f\t f", url.pathSegments().get(0));
+    assertEquals("g\r\n\f\t h", url.query());
+    assertEquals("i\r\n\f\t j", url.fragment());
+  }
+
+  @Test public void composeFromUnencodedComponents() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .username("a:\u0001@/\\?#%b")
+        .password("c:\u0001@/\\?#%d")
+        .host("ef")
+        .port(8080)
+        .addPathSegment("g:\u0001@/\\?#%h")
+        .query("i:\u0001@/\\?#%j")
+        .fragment("k:\u0001@/\\?#%l")
+        .build();
+    assertEquals("http://a%3A%01%40%2F%5C%3F%23%25b:c%3A%01%40%2F%5C%3F%23%25d@ef:8080/"
+        + "g:%01@%2F%5C%3F%23%25h?i:%01@/\\?%23%25j#k:%01@/\\?#%25l", url.toString());
+    assertEquals("http", url.scheme());
+    assertEquals("a:\u0001@/\\?#%b", url.username());
+    assertEquals("c:\u0001@/\\?#%d", url.password());
+    assertEquals(Arrays.asList("g:\u0001@/\\?#%h"), url.pathSegments());
+    assertEquals("i:\u0001@/\\?#%j", url.query());
+    assertEquals("k:\u0001@/\\?#%l", url.fragment());
+    assertEquals("a%3A%01%40%2F%5C%3F%23%25b", url.encodedUsername());
+    assertEquals("c%3A%01%40%2F%5C%3F%23%25d", url.encodedPassword());
+    assertEquals("/g:%01@%2F%5C%3F%23%25h", url.encodedPath());
+    assertEquals("i:%01@/\\?%23%25j", url.encodedQuery());
+    assertEquals("k:%01@/\\?#%25l", url.encodedFragment());
+  }
+
+  @Test public void composeFromEncodedComponents() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .encodedUsername("a:\u0001@/\\?#%25b")
+        .encodedPassword("c:\u0001@/\\?#%25d")
+        .host("ef")
+        .port(8080)
+        .addEncodedPathSegment("g:\u0001@/\\?#%25h")
+        .encodedQuery("i:\u0001@/\\?#%25j")
+        .encodedFragment("k:\u0001@/\\?#%25l")
+        .build();
+    assertEquals("http://a%3A%01%40%2F%5C%3F%23%25b:c%3A%01%40%2F%5C%3F%23%25d@ef:8080/"
+        + "g:%01@%2F%5C%3F%23%25h?i:%01@/\\?%23%25j#k:%01@/\\?#%25l", url.toString());
+    assertEquals("http", url.scheme());
+    assertEquals("a:\u0001@/\\?#%b", url.username());
+    assertEquals("c:\u0001@/\\?#%d", url.password());
+    assertEquals(Arrays.asList("g:\u0001@/\\?#%h"), url.pathSegments());
+    assertEquals("i:\u0001@/\\?#%j", url.query());
+    assertEquals("k:\u0001@/\\?#%l", url.fragment());
+    assertEquals("a%3A%01%40%2F%5C%3F%23%25b", url.encodedUsername());
+    assertEquals("c%3A%01%40%2F%5C%3F%23%25d", url.encodedPassword());
+    assertEquals("/g:%01@%2F%5C%3F%23%25h", url.encodedPath());
+    assertEquals("i:%01@/\\?%23%25j", url.encodedQuery());
+    assertEquals("k:%01@/\\?#%25l", url.encodedFragment());
+  }
+
+  @Test public void composeWithEncodedPath() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .host("host")
+        .encodedPath("/a%2Fb/c")
+        .build();
+    assertEquals("http://host/a%2Fb/c", url.toString());
+    assertEquals("/a%2Fb/c", url.encodedPath());
+    assertEquals(Arrays.asList("a/b", "c"), url.pathSegments());
+  }
+
+  @Test public void composeMixingPathSegments() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .host("host")
+        .encodedPath("/a%2fb/c")
+        .addPathSegment("d%25e")
+        .addEncodedPathSegment("f%25g")
+        .build();
+    assertEquals("http://host/a%2fb/c/d%2525e/f%25g", url.toString());
+    assertEquals("/a%2fb/c/d%2525e/f%25g", url.encodedPath());
+    assertEquals(Arrays.asList("a%2fb", "c", "d%2525e", "f%25g"), url.encodedPathSegments());
+    assertEquals(Arrays.asList("a/b", "c", "d%25e", "f%g"), url.pathSegments());
+  }
+
+  @Test public void composeWithAddSegment() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/c/", base.newBuilder().addPathSegment("").build().encodedPath());
+    assertEquals("/a/b/c/d",
+        base.newBuilder().addPathSegment("").addPathSegment("d").build().encodedPath());
+    assertEquals("/a/b/", base.newBuilder().addPathSegment("..").build().encodedPath());
+    assertEquals("/a/b/", base.newBuilder().addPathSegment("").addPathSegment("..").build()
+        .encodedPath());
+    assertEquals("/a/b/c/", base.newBuilder().addPathSegment("").addPathSegment("").build()
+        .encodedPath());
+  }
+
+  @Test public void pathSize() throws Exception {
+    assertEquals(1, HttpUrl.parse("http://host/").pathSize());
+    assertEquals(3, HttpUrl.parse("http://host/a/b/c").pathSize());
+  }
+
+  @Test public void addPathSegmentDotDoesNothing() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/c", base.newBuilder().addPathSegment(".").build().encodedPath());
+  }
+
+  @Test public void addPathSegmentEncodes() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/c/%252e",
+        base.newBuilder().addPathSegment("%2e").build().encodedPath());
+    assertEquals("/a/b/c/%252e%252e",
+        base.newBuilder().addPathSegment("%2e%2e").build().encodedPath());
+  }
+
+  @Test public void addPathSegmentDotDotPopsDirectory() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/", base.newBuilder().addPathSegment("..").build().encodedPath());
+  }
+
+  @Test public void addPathSegmentDotAndIgnoredCharacter() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/c/.%0A", base.newBuilder().addPathSegment(".\n").build().encodedPath());
+  }
+
+  @Test public void addEncodedPathSegmentDotAndIgnoredCharacter() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/c", base.newBuilder().addEncodedPathSegment(".\n").build().encodedPath());
+  }
+
+  @Test public void addEncodedPathSegmentDotDotAndIgnoredCharacter() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/a/b/", base.newBuilder().addEncodedPathSegment("..\n").build().encodedPath());
+  }
+
+  @Test public void setPathSegment() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/d/b/c", base.newBuilder().setPathSegment(0, "d").build().encodedPath());
+    assertEquals("/a/d/c", base.newBuilder().setPathSegment(1, "d").build().encodedPath());
+    assertEquals("/a/b/d", base.newBuilder().setPathSegment(2, "d").build().encodedPath());
+  }
+
+  @Test public void setPathSegmentEncodes() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/%2525/b/c", base.newBuilder().setPathSegment(0, "%25").build().encodedPath());
+    assertEquals("/.%0A/b/c", base.newBuilder().setPathSegment(0, ".\n").build().encodedPath());
+    assertEquals("/%252e/b/c", base.newBuilder().setPathSegment(0, "%2e").build().encodedPath());
+  }
+
+  @Test public void setPathSegmentAcceptsEmpty() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("//b/c", base.newBuilder().setPathSegment(0, "").build().encodedPath());
+    assertEquals("/a/b/", base.newBuilder().setPathSegment(2, "").build().encodedPath());
+  }
+
+  @Test public void setPathSegmentRejectsDot() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setPathSegment(0, ".");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setPathSegmentRejectsDotDot() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setPathSegment(0, "..");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setPathSegmentWithSlash() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    HttpUrl url = base.newBuilder().setPathSegment(1, "/").build();
+    assertEquals("/a/%2F/c", url.encodedPath());
+  }
+
+  @Test public void setPathSegmentOutOfBounds() throws Exception {
+    try {
+      new HttpUrl.Builder().setPathSegment(1, "a");
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void setEncodedPathSegmentEncodes() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    assertEquals("/%25/b/c",
+        base.newBuilder().setEncodedPathSegment(0, "%25").build().encodedPath());
+  }
+
+  @Test public void setEncodedPathSegmentRejectsDot() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setEncodedPathSegment(0, ".");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setEncodedPathSegmentRejectsDotAndIgnoredCharacter() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setEncodedPathSegment(0, ".\n");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setEncodedPathSegmentRejectsDotDot() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setEncodedPathSegment(0, "..");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setEncodedPathSegmentRejectsDotDotAndIgnoredCharacter() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    try {
+      base.newBuilder().setEncodedPathSegment(0, "..\n");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void setEncodedPathSegmentWithSlash() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    HttpUrl url = base.newBuilder().setEncodedPathSegment(1, "/").build();
+    assertEquals("/a/%2F/c", url.encodedPath());
+  }
+
+  @Test public void setEncodedPathSegmentOutOfBounds() throws Exception {
+    try {
+      new HttpUrl.Builder().setEncodedPathSegment(1, "a");
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void removePathSegment() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    HttpUrl url = base.newBuilder()
+        .removePathSegment(0)
+        .build();
+    assertEquals("/b/c", url.encodedPath());
+  }
+
+  @Test public void removePathSegmentDoesntRemovePath() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
+    HttpUrl url = base.newBuilder()
+        .removePathSegment(0)
+        .removePathSegment(0)
+        .removePathSegment(0)
+        .build();
+    assertEquals(Arrays.asList(""), url.pathSegments());
+    assertEquals("/", url.encodedPath());
+  }
+
+  @Test public void removePathSegmentOutOfBounds() throws Exception {
+    try {
+      new HttpUrl.Builder().removePathSegment(1);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test public void toJavaNetUrl() throws Exception {
+    HttpUrl httpUrl = HttpUrl.parse("http://username:password@host/path?query#fragment");
+    URL javaNetUrl = httpUrl.url();
+    assertEquals("http://username:password@host/path?query#fragment", javaNetUrl.toString());
+  }
+
+  @Test public void toUri() throws Exception {
+    HttpUrl httpUrl = HttpUrl.parse("http://username:password@host/path?query#fragment");
+    URI uri = httpUrl.uri();
+    assertEquals("http://username:password@host/path?query#fragment", uri.toString());
+  }
+
+  @Test public void toUriSpecialQueryCharacters() throws Exception {
+    HttpUrl httpUrl = HttpUrl.parse("http://host/?d=abc!@[]^`{}|\\");
+    URI uri = httpUrl.uri();
+    assertEquals("http://host/?d=abc!@[]%5E%60%7B%7D%7C%5C", uri.toString());
+  }
+
+  @Test public void toUriForbiddenCharacter() throws Exception {
+    HttpUrl httpUrl = HttpUrl.parse("http://host/a[b");
+    try {
+      httpUrl.uri();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertEquals("not valid as a java.net.URI: http://host/a[b", expected.getMessage());
+    }
+  }
+
+  @Test public void fromJavaNetUrl() throws Exception {
+    URL javaNetUrl = new URL("http://username:password@host/path?query#fragment");
+    HttpUrl httpUrl = HttpUrl.get(javaNetUrl);
+    assertEquals("http://username:password@host/path?query#fragment", httpUrl.toString());
+  }
+
+  @Test public void fromJavaNetUrlUnsupportedScheme() throws Exception {
+    URL javaNetUrl = new URL("mailto:user@example.com");
+    assertEquals(null, HttpUrl.get(javaNetUrl));
+  }
+
+  @Test public void fromUri() throws Exception {
+    URI uri = new URI("http://username:password@host/path?query#fragment");
+    HttpUrl httpUrl = HttpUrl.get(uri);
+    assertEquals("http://username:password@host/path?query#fragment", httpUrl.toString());
+  }
+
+  @Test public void fromUriUnsupportedScheme() throws Exception {
+    URI uri = new URI("mailto:user@example.com");
+    assertEquals(null, HttpUrl.get(uri));
+  }
+
+  @Test public void fromUriPartial() throws Exception {
+    URI uri = new URI("/path");
+    assertEquals(null, HttpUrl.get(uri));
+  }
+
+  @Test public void fromJavaNetUrl_checked() throws Exception {
+    HttpUrl httpUrl = HttpUrl.getChecked("http://username:password@host/path?query#fragment");
+    assertEquals("http://username:password@host/path?query#fragment", httpUrl.toString());
+  }
+
+  @Test public void fromJavaNetUrlUnsupportedScheme_checked() throws Exception {
+    try {
+      HttpUrl.getChecked("mailto:user@example.com");
+      fail();
+    } catch (MalformedURLException e) {
+    }
+  }
+
+  @Test public void fromJavaNetUrlBadHost_checked() throws Exception {
+    try {
+      HttpUrl.getChecked("http://hostw ithspace/");
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+  }
+
+  @Test public void composeQueryWithComponents() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/");
+    HttpUrl url = base.newBuilder().addQueryParameter("a+=& b", "c+=& d").build();
+    assertEquals("http://host/?a%2B%3D%26%20b=c%2B%3D%26%20d", url.toString());
+    assertEquals("c+=& d", url.queryParameterValue(0));
+    assertEquals("a+=& b", url.queryParameterName(0));
+    assertEquals("c+=& d", url.queryParameter("a+=& b"));
+    assertEquals(Collections.singleton("a+=& b"), url.queryParameterNames());
+    assertEquals(singletonList("c+=& d"), url.queryParameterValues("a+=& b"));
+    assertEquals(1, url.querySize());
+    assertEquals("a+=& b=c+=& d", url.query()); // Ambiguous! (Though working as designed.)
+    assertEquals("a%2B%3D%26%20b=c%2B%3D%26%20d", url.encodedQuery());
+  }
+
+  @Test public void composeQueryWithEncodedComponents() throws Exception {
+    HttpUrl base = HttpUrl.parse("http://host/");
+    HttpUrl url = base.newBuilder().addEncodedQueryParameter("a+=& b", "c+=& d").build();
+    assertEquals("http://host/?a%20%3D%26%20b=c%20%3D%26%20d", url.toString());
+    assertEquals("c =& d", url.queryParameter("a =& b"));
+  }
+
+  @Test public void composeQueryRemoveQueryParameter() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .addQueryParameter("a+=& b", "c+=& d")
+        .removeAllQueryParameters("a+=& b")
+        .build();
+    assertEquals("http://host/", url.toString());
+    assertEquals(null, url.queryParameter("a+=& b"));
+  }
+
+  @Test public void composeQueryRemoveEncodedQueryParameter() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .addEncodedQueryParameter("a+=& b", "c+=& d")
+        .removeAllEncodedQueryParameters("a+=& b")
+        .build();
+    assertEquals("http://host/", url.toString());
+    assertEquals(null, url.queryParameter("a =& b"));
+  }
+
+  @Test public void composeQuerySetQueryParameter() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .addQueryParameter("a+=& b", "c+=& d")
+        .setQueryParameter("a+=& b", "ef")
+        .build();
+    assertEquals("http://host/?a%2B%3D%26%20b=ef", url.toString());
+    assertEquals("ef", url.queryParameter("a+=& b"));
+  }
+
+  @Test public void composeQuerySetEncodedQueryParameter() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .addEncodedQueryParameter("a+=& b", "c+=& d")
+        .setEncodedQueryParameter("a+=& b", "ef")
+        .build();
+    assertEquals("http://host/?a%20%3D%26%20b=ef", url.toString());
+    assertEquals("ef", url.queryParameter("a =& b"));
+  }
+
+  @Test public void composeQueryMultipleEncodedValuesForParameter() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .addQueryParameter("a+=& b", "c+=& d")
+        .addQueryParameter("a+=& b", "e+=& f")
+        .build();
+    assertEquals("http://host/?a%2B%3D%26%20b=c%2B%3D%26%20d&a%2B%3D%26%20b=e%2B%3D%26%20f",
+        url.toString());
+    assertEquals(2, url.querySize());
+    assertEquals(Collections.singleton("a+=& b"), url.queryParameterNames());
+    assertEquals(Arrays.asList("c+=& d", "e+=& f"), url.queryParameterValues("a+=& b"));
+  }
+
+  @Test public void absentQueryIsZeroNameValuePairs() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .query(null)
+        .build();
+    assertEquals(0, url.querySize());
+  }
+
+  @Test public void emptyQueryIsSingleNameValuePairWithEmptyKey() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .query("")
+        .build();
+    assertEquals(1, url.querySize());
+    assertEquals("", url.queryParameterName(0));
+    assertEquals(null, url.queryParameterValue(0));
+  }
+
+  @Test public void ampersandQueryIsTwoNameValuePairsWithEmptyKeys() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .query("&")
+        .build();
+    assertEquals(2, url.querySize());
+    assertEquals("", url.queryParameterName(0));
+    assertEquals(null, url.queryParameterValue(0));
+    assertEquals("", url.queryParameterName(1));
+    assertEquals(null, url.queryParameterValue(1));
+  }
+
+  @Test public void removeAllDoesNotRemoveQueryIfNoParametersWereRemoved() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/").newBuilder()
+        .query("")
+        .removeAllQueryParameters("a")
+        .build();
+    assertEquals("http://host/?", url.toString());
+  }
+
+  @Test public void queryParametersWithoutValues() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/?foo&bar&baz");
+    assertEquals(3, url.querySize());
+    assertEquals(new LinkedHashSet<>(Arrays.asList("foo", "bar", "baz")),
+        url.queryParameterNames());
+    assertEquals(null, url.queryParameterValue(0));
+    assertEquals(null, url.queryParameterValue(1));
+    assertEquals(null, url.queryParameterValue(2));
+    assertEquals(singletonList((String) null), url.queryParameterValues("foo"));
+    assertEquals(singletonList((String) null), url.queryParameterValues("bar"));
+    assertEquals(singletonList((String) null), url.queryParameterValues("baz"));
+  }
+
+  @Test public void queryParametersWithEmptyValues() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/?foo=&bar=&baz=");
+    assertEquals(3, url.querySize());
+    assertEquals(new LinkedHashSet<>(Arrays.asList("foo", "bar", "baz")),
+        url.queryParameterNames());
+    assertEquals("", url.queryParameterValue(0));
+    assertEquals("", url.queryParameterValue(1));
+    assertEquals("", url.queryParameterValue(2));
+    assertEquals(singletonList(""), url.queryParameterValues("foo"));
+    assertEquals(singletonList(""), url.queryParameterValues("bar"));
+    assertEquals(singletonList(""), url.queryParameterValues("baz"));
+  }
+
+  @Test public void queryParametersWithRepeatedName() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/?foo[]=1&foo[]=2&foo[]=3");
+    assertEquals(3, url.querySize());
+    assertEquals(Collections.singleton("foo[]"), url.queryParameterNames());
+    assertEquals("1", url.queryParameterValue(0));
+    assertEquals("2", url.queryParameterValue(1));
+    assertEquals("3", url.queryParameterValue(2));
+    assertEquals(Arrays.asList("1", "2", "3"), url.queryParameterValues("foo[]"));
+  }
+
+  @Test public void queryParameterLookupWithNonCanonicalEncoding() throws Exception {
+    HttpUrl url = HttpUrl.parse("http://host/?%6d=m&+=%20");
+    assertEquals("m", url.queryParameterName(0));
+    assertEquals(" ", url.queryParameterName(1));
+    assertEquals("m", url.queryParameter("m"));
+    assertEquals(" ", url.queryParameter(" "));
+  }
+
+  @Test public void roundTripBuilder() throws Exception {
+    HttpUrl url = new HttpUrl.Builder()
+        .scheme("http")
+        .username("%")
+        .password("%")
+        .host("host")
+        .addPathSegment("%")
+        .query("%")
+        .fragment("%")
+        .build();
+    assertEquals("http://%25:%25@host/%25?%25#%25", url.toString());
+    assertEquals("http://%25:%25@host/%25?%25#%25", url.newBuilder().build().toString());
+    assertEquals("http://%25:%25@host/%25?%25", url.resolve("").toString());
+  }
+
+  /**
+   * Although HttpUrl prefers percent-encodings in uppercase, it should preserve the exact
+   * structure of the original encoding.
+   */
+  @Test public void rawEncodingRetained() throws Exception {
+    String urlString = "http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D#%6d%6D";
+    HttpUrl url = HttpUrl.parse(urlString);
+    assertEquals("%6d%6D", url.encodedUsername());
+    assertEquals("%6d%6D", url.encodedPassword());
+    assertEquals("/%6d%6D", url.encodedPath());
+    assertEquals(Arrays.asList("%6d%6D"), url.encodedPathSegments());
+    assertEquals("%6d%6D", url.encodedQuery());
+    assertEquals("%6d%6D", url.encodedFragment());
+    assertEquals(urlString, url.toString());
+    assertEquals(urlString, url.newBuilder().build().toString());
+    assertEquals("http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D", url.resolve("").toString());
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
index 2546c8c..054343c 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
@@ -16,10 +16,9 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
 import java.io.IOException;
-import java.net.URL;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
@@ -46,13 +45,13 @@
 import static org.junit.Assert.fail;
 
 public final class InterceptorTest {
-  @Rule public MockWebServerRule server = new MockWebServerRule();
+  @Rule public MockWebServer server = new MockWebServer();
 
   private OkHttpClient client = new OkHttpClient();
   private RecordingCallback callback = new RecordingCallback();
 
   @Test public void applicationInterceptorsCanShortCircuitResponses() throws Exception {
-    server.get().shutdown(); // Accept no connections.
+    server.shutdown(); // Accept no connections.
 
     Request request = new Request.Builder()
         .url("https://localhost:1/")
@@ -93,7 +92,7 @@
     client.networkInterceptors().add(interceptor);
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     try {
@@ -118,7 +117,7 @@
     client.networkInterceptors().add(interceptor);
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     try {
@@ -139,14 +138,14 @@
         String sameHost = address.getUriHost();
         int differentPort = address.getUriPort() + 1;
         return chain.proceed(chain.request().newBuilder()
-            .url(new URL("http://" + sameHost + ":" + differentPort + "/"))
+            .url(HttpUrl.parse("http://" + sameHost + ":" + differentPort + "/"))
             .build());
       }
     };
     client.networkInterceptors().add(interceptor);
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     try {
@@ -170,7 +169,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request).execute();
   }
@@ -185,7 +184,7 @@
         // The network request has everything: User-Agent, Host, Accept-Encoding.
         Request networkRequest = chain.request();
         assertNotNull(networkRequest.header("User-Agent"));
-        assertEquals(server.get().getHostName() + ":" + server.get().getPort(),
+        assertEquals(server.getHostName() + ":" + server.getPort(),
             networkRequest.header("Host"));
         assertNotNull(networkRequest.header("Accept-Encoding"));
 
@@ -197,7 +196,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     // No extra headers in the application's request.
@@ -233,7 +232,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .addHeader("Original-Header", "foo")
         .method("PUT", RequestBody.create(MediaType.parse("text/plain"), "abc"))
         .build();
@@ -271,7 +270,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     Response response = client.newCall(request).execute();
@@ -315,7 +314,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     Response response = client.newCall(request).execute();
@@ -348,11 +347,11 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request).enqueue(callback);
 
-    callback.await(request.url())
+    callback.await(request.httpUrl())
         .assertCode(200)
         .assertHeader("OkHttp-Intercepted", "yep");
   }
@@ -369,7 +368,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     Response response = client.newCall(request).execute();
@@ -385,7 +384,7 @@
       @Override public Response intercept(Chain chain) throws IOException {
         if (chain.request().url().getPath().equals("/b")) {
           Request requestA = new Request.Builder()
-              .url(server.getUrl("/a"))
+              .url(server.url("/a"))
               .build();
           Response responseA = client.newCall(requestA).execute();
           assertEquals("a", responseA.body().string());
@@ -396,7 +395,7 @@
     });
 
     Request requestB = new Request.Builder()
-        .url(server.getUrl("/b"))
+        .url(server.url("/b"))
         .build();
     Response responseB = client.newCall(requestB).execute();
     assertEquals("b", responseB.body().string());
@@ -411,13 +410,13 @@
       @Override public Response intercept(Chain chain) throws IOException {
         if (chain.request().url().getPath().equals("/b")) {
           Request requestA = new Request.Builder()
-              .url(server.getUrl("/a"))
+              .url(server.url("/a"))
               .build();
 
           try {
             RecordingCallback callbackA = new RecordingCallback();
             client.newCall(requestA).enqueue(callbackA);
-            callbackA.await(requestA.url()).assertBody("a");
+            callbackA.await(requestA.httpUrl()).assertBody("a");
           } catch (Exception e) {
             throw new RuntimeException(e);
           }
@@ -428,11 +427,11 @@
     });
 
     Request requestB = new Request.Builder()
-        .url(server.getUrl("/b"))
+        .url(server.url("/b"))
         .build();
     RecordingCallback callbackB = new RecordingCallback();
     client.newCall(requestB).enqueue(callbackB);
-    callbackB.await(requestB.url()).assertBody("b");
+    callbackB.await(requestB.httpUrl()).assertBody("b");
   }
 
   @Test public void applicationkInterceptorThrowsRuntimeExceptionSynchronous() throws Exception {
@@ -458,7 +457,7 @@
     });
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
 
     try {
@@ -491,7 +490,7 @@
     client.networkInterceptors().add(modifyHeaderInterceptor);
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .header("User-Agent", "user request")
         .build();
 
@@ -519,7 +518,7 @@
     client.setDispatcher(new Dispatcher(executor));
 
     Request request = new Request.Builder()
-        .url(server.getUrl("/"))
+        .url(server.url("/"))
         .build();
     client.newCall(request).enqueue(callback);
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java
index aae4295..7f2635b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java
@@ -57,6 +57,13 @@
     Authenticator.setDefault(DEFAULT_AUTHENTICATOR);
   }
 
+  @Test public void timeoutDefaults() {
+    OkHttpClient client = new OkHttpClient();
+    assertEquals(10_000, client.getConnectTimeout());
+    assertEquals(10_000, client.getReadTimeout());
+    assertEquals(10_000, client.getWriteTimeout());
+  }
+
   @Test public void timeoutValidRange() {
     OkHttpClient client = new OkHttpClient();
     try {
@@ -89,9 +96,9 @@
   @Test public void copyWithDefaultsWhenDefaultIsAConstant() throws Exception {
     OkHttpClient client = new OkHttpClient().copyWithDefaults();
     assertNull(client.internalCache());
-    assertEquals(0, client.getConnectTimeout());
-    assertEquals(0, client.getReadTimeout());
-    assertEquals(0, client.getWriteTimeout());
+    assertEquals(10_000, client.getConnectTimeout());
+    assertEquals(10_000, client.getReadTimeout());
+    assertEquals(10_000, client.getWriteTimeout());
     assertTrue(client.getFollowSslRedirects());
     assertNull(client.getProxy());
     assertEquals(Arrays.asList(Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1),
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
index 73e38f0..9d65147 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
@@ -16,7 +16,6 @@
 package com.squareup.okhttp;
 
 import java.io.IOException;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -49,12 +48,12 @@
    * Returns the recorded response triggered by {@code request}. Throws if the
    * response isn't enqueued before the timeout.
    */
-  public synchronized RecordedResponse await(URL url) throws Exception {
+  public synchronized RecordedResponse await(HttpUrl url) throws Exception {
     long timeoutMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + TIMEOUT_MILLIS;
     while (true) {
       for (Iterator<RecordedResponse> i = responses.iterator(); i.hasNext(); ) {
         RecordedResponse recordedResponse = i.next();
-        if (recordedResponse.request.url().equals(url)) {
+        if (recordedResponse.request.httpUrl().equals(url)) {
           i.remove();
           return recordedResponse;
         }
@@ -68,9 +67,9 @@
     throw new AssertionError("Timed out waiting for response to " + url);
   }
 
-  public synchronized void assertNoResponse(URL url) throws Exception {
+  public synchronized void assertNoResponse(HttpUrl url) throws Exception {
     for (RecordedResponse recordedResponse : responses) {
-      if (recordedResponse.request.url().equals(url)) {
+      if (recordedResponse.request.httpUrl().equals(url)) {
         throw new AssertionError("Expected no response for " + url);
       }
     }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
index a1249e5..39da500 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -28,6 +28,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 
 public final class RequestTest {
   @Test public void string() throws Exception {
@@ -131,7 +132,8 @@
     Request requestWithCache = new Request.Builder().url("http://localhost/api").build();
     // cache url object
     requestWithCache.url();
-    Request builtRequestWithCache = requestWithCache.newBuilder().url("http://localhost/api/foo").build();
+    Request builtRequestWithCache = requestWithCache.newBuilder().url(
+        "http://localhost/api/foo").build();
     assertEquals(new URL("http://localhost/api/foo"), builtRequestWithCache.url());
   }
 
@@ -152,6 +154,62 @@
     assertEquals(Collections.<String>emptyList(), request.headers("Cache-Control"));
   }
 
+  @Test public void headerAcceptsPermittedCharacters() throws Exception {
+    Request.Builder builder = new Request.Builder();
+    builder.header("AZab09 ~", "AZab09 ~");
+    builder.addHeader("AZab09 ~", "AZab09 ~");
+  }
+
+  @Test public void emptyNameForbidden() throws Exception {
+    Request.Builder builder = new Request.Builder();
+    try {
+      builder.header("", "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      builder.addHeader("", "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void headerForbidsControlCharacters() throws Exception {
+    assertForbiddenHeader(null);
+    assertForbiddenHeader("\u0000");
+    assertForbiddenHeader("\r");
+    assertForbiddenHeader("\n");
+    assertForbiddenHeader("\t");
+    assertForbiddenHeader("\u001f");
+    assertForbiddenHeader("\u007f");
+    assertForbiddenHeader("\u0080");
+    assertForbiddenHeader("\ud83c\udf69");
+  }
+
+  private void assertForbiddenHeader(String s) {
+    Request.Builder builder = new Request.Builder();
+    try {
+      builder.header(s, "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      builder.addHeader(s, "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      builder.header("Name", s);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      builder.addHeader("Name", s);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
   private String bodyToHex(RequestBody body) throws IOException {
     Buffer buffer = new Buffer();
     body.writeTo(buffer);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
index 9b10213..377ff83 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
@@ -51,11 +51,11 @@
     OkHttpClient client = new OkHttpClient()
         .setProxy(socksProxy.proxy());
 
-    Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request1 = new Request.Builder().url(server.url("/")).build();
     Response response1 = client.newCall(request1).execute();
     assertEquals("abc", response1.body().string());
 
-    Request request2 = new Request.Builder().url(server.getUrl("/")).build();
+    Request request2 = new Request.Builder().url(server.url("/")).build();
     Response response2 = client.newCall(request2).execute();
     assertEquals("def", response2.body().string());
 
@@ -79,7 +79,7 @@
     OkHttpClient client = new OkHttpClient()
         .setProxySelector(proxySelector);
 
-    Request request = new Request.Builder().url(server.getUrl("/")).build();
+    Request request = new Request.Builder().url(server.url("/")).build();
     Response response = client.newCall(request).execute();
     assertEquals("abc", response.body().string());
 
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java b/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java
index 10f0d4d..bf2ed4a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java
@@ -1,8 +1,12 @@
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.spdy.Header;
+import com.squareup.okhttp.internal.framed.Header;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 public final class TestUtil {
   private TestUtil() {
@@ -15,4 +19,18 @@
     }
     return result;
   }
+
+  public static <T> Set<T> setOf(T... elements) {
+    return setOf(Arrays.asList(elements));
+  }
+
+  public static <T> Set<T> setOf(Collection<T> elements) {
+    return new LinkedHashSet<>(elements);
+  }
+
+  public static String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
similarity index 93%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
index 3be5a2d..1b75090 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
@@ -14,35 +14,20 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp;
 
-import com.squareup.okhttp.Cache;
-import com.squareup.okhttp.Challenge;
-import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.ConnectionSpec;
-import com.squareup.okhttp.Credentials;
-import com.squareup.okhttp.DelegatingServerSocketFactory;
-import com.squareup.okhttp.DelegatingSocketFactory;
-import com.squareup.okhttp.FallbackTestClientSocketFactory;
-import com.squareup.okhttp.Headers;
-import com.squareup.okhttp.Interceptor;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.OkUrlFactory;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.TlsVersion;
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.RecordingAuthenticator;
-import com.squareup.okhttp.internal.RecordingHostnameVerifier;
 import com.squareup.okhttp.internal.RecordingOkAuthenticator;
 import com.squareup.okhttp.internal.SingleInetAddressNetwork;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
 import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -68,7 +53,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -114,18 +99,17 @@
 
 /** Android's URLConnectionTest. */
 public final class URLConnectionTest {
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
-  @Rule public final MockWebServerRule server2 = new MockWebServerRule();
+  @Rule public final MockWebServer server = new MockWebServer();
+  @Rule public final MockWebServer server2 = new MockWebServer();
   @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
 
+  private SSLContext sslContext = SslContextBuilder.localhost();
   private OkUrlFactory client;
   private HttpURLConnection connection;
   private Cache cache;
 
   @Before public void setUp() throws Exception {
-    server.get().setProtocolNegotiationEnabled(false);
+    server.setProtocolNegotiationEnabled(false);
     client = new OkUrlFactory(new OkHttpClient());
   }
 
@@ -152,8 +136,8 @@
     assertEquals("f", connection.getRequestProperty("D"));
     assertEquals("f", connection.getRequestProperty("d"));
     Map<String, List<String>> requestHeaders = connection.getRequestProperties();
-    assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
-    assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
+    assertEquals(newSet("e", "f"), new LinkedHashSet<>(requestHeaders.get("D")));
+    assertEquals(newSet("e", "f"), new LinkedHashSet<>(requestHeaders.get("d")));
     try {
       requestHeaders.put("G", Arrays.asList("h"));
       fail("Modified an unmodifiable view.");
@@ -224,8 +208,8 @@
     assertEquals("HTTP/1.0 200 Fantastic", connection.getHeaderField(null));
     Map<String, List<String>> responseHeaders = connection.getHeaderFields();
     assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
-    assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
-    assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a")));
+    assertEquals(newSet("c", "e"), new LinkedHashSet<>(responseHeaders.get("A")));
+    assertEquals(newSet("c", "e"), new LinkedHashSet<>(responseHeaders.get("a")));
     try {
       responseHeaders.put("N", Arrays.asList("o"));
       fail("Modified an unmodifiable view.");
@@ -290,7 +274,7 @@
 
   @Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
     URL url = server.getUrl("/foo");
-    server.get().shutdown();
+    server.shutdown();
 
     connection = client.open(url);
     try {
@@ -317,9 +301,9 @@
 
     // Use a misconfigured proxy to guarantee that the request is retried.
     FakeProxySelector proxySelector = new FakeProxySelector();
-    proxySelector.proxies.add(server2.get().toProxyAddress());
+    proxySelector.proxies.add(server2.toProxyAddress());
     client.client().setProxySelector(proxySelector);
-    server2.get().shutdown();
+    server2.shutdown();
 
     connection = client.open(server.getUrl("/def"));
     connection.setDoOutput(true);
@@ -425,11 +409,6 @@
     HttpURLConnection connection1 = client.open(server.getUrl("/a"));
     connection1.setReadTimeout(100);
     assertContent("This connection won't pool properly", connection1);
-
-    // Give the server time to enact the socket policy if it's one that could happen after the
-    // client has received the response.
-    Thread.sleep(500);
-
     assertEquals(0, server.takeRequest().getSequenceNumber());
     HttpURLConnection connection2 = client.open(server.getUrl("/b"));
     connection2.setReadTimeout(100);
@@ -472,7 +451,7 @@
 
   private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
     int n = 512 * 1024;
-    server.get().setBodyLimit(0);
+    server.setBodyLimit(0);
     server.enqueue(new MockResponse());
 
     HttpURLConnection conn = client.open(server.getUrl("/"));
@@ -522,7 +501,7 @@
   }
 
   @Test public void connectViaHttps() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
 
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -536,7 +515,7 @@
   }
 
   @Test public void inspectHandshakeThroughoutRequestLifecycle() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse());
 
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -565,7 +544,7 @@
   }
 
   @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
     server.enqueue(new MockResponse().setBody("another response via HTTPS"));
 
@@ -587,7 +566,7 @@
 
   @Test public void connectViaHttpsReusingConnectionsDifferentFactories()
       throws IOException, InterruptedException {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
     server.enqueue(new MockResponse().setBody("another response via HTTPS"));
 
@@ -607,7 +586,7 @@
   }
 
   @Test public void connectViaHttpsWithSSLFallback() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
     server.enqueue(new MockResponse().setBody("this response comes via SSL"));
 
@@ -623,7 +602,7 @@
   }
 
   @Test public void connectViaHttpsWithSSLFallbackFailuresRecorded() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
 
@@ -635,8 +614,9 @@
 
     try {
       connection.getResponseCode();
-    } catch (IOException e) {
-      assertEquals(1, e.getSuppressed().length);
+      fail();
+    } catch (IOException expected) {
+      assertEquals(1, expected.getSuppressed().length);
     }
   }
 
@@ -647,7 +627,7 @@
    * https://github.com/square/okhttp/issues/515
    */
   @Test public void sslFallbackNotUsedWhenRecycledConnectionFails() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse()
         .setBody("abc")
         .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
@@ -657,10 +637,6 @@
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
 
     assertContent("abc", client.open(server.getUrl("/")));
-
-    // Give the server time to disconnect.
-    Thread.sleep(500);
-
     assertContent("def", client.open(server.getUrl("/")));
 
     Set<TlsVersion> tlsVersions =
@@ -679,7 +655,7 @@
    * http://code.google.com/p/android/issues/detail?id=13178
    */
   @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse()); // unused
 
     connection = client.open(server.getUrl("/foo"));
@@ -709,22 +685,33 @@
     server.enqueue(mockResponse);
 
     URL url = new URL("http://android.com/foo");
-    connection = proxyConfig.connect(server.get(), client, url);
+    connection = proxyConfig.connect(server, client, url);
     assertContent("this response comes via a proxy", connection);
     assertTrue(connection.usingProxy());
 
-    RecordedRequest request = server.get().takeRequest();
+    RecordedRequest request = server.takeRequest();
     assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
     assertEquals("android.com", request.getHeader("Host"));
   }
 
-  @Test public void contentDisagreesWithContentLengthHeader() throws IOException {
+  @Test public void contentDisagreesWithContentLengthHeaderBodyTooLong() throws IOException {
     server.enqueue(new MockResponse().setBody("abc\r\nYOU SHOULD NOT SEE THIS")
         .clearHeaders()
         .addHeader("Content-Length: 3"));
     assertContent("abc", client.open(server.getUrl("/")));
   }
 
+  @Test public void contentDisagreesWithContentLengthHeaderBodyTooShort() throws IOException {
+    server.enqueue(new MockResponse().setBody("abc")
+        .setHeader("Content-Length", "5")
+        .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+    try {
+      readAscii(client.open(server.getUrl("/")).getInputStream(), 5);
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+
   public void testConnectViaSocketFactory(boolean useHttps) throws IOException {
     SocketFactory uselessSocketFactory = new SocketFactory() {
       public Socket createSocket() { throw new IllegalArgumentException("useless"); }
@@ -738,7 +725,7 @@
     };
 
     if (useHttps) {
-      server.get().useHttps(sslContext.getSocketFactory(), false);
+      server.useHttps(sslContext.getSocketFactory(), false);
       client.client().setSslSocketFactory(sslContext.getSocketFactory());
       client.client().setHostnameVerifier(new RecordingHostnameVerifier());
     }
@@ -766,7 +753,7 @@
     testConnectViaSocketFactory(true);
   }
 
-  @Test public void contentDisagreesWithChunkedHeader() throws IOException {
+  @Test public void contentDisagreesWithChunkedHeaderBodyTooLong() throws IOException {
     MockResponse mockResponse = new MockResponse();
     mockResponse.setChunkedBody("abc", 3);
     Buffer buffer = mockResponse.getBody();
@@ -780,6 +767,28 @@
     assertContent("abc", client.open(server.getUrl("/")));
   }
 
+  @Test public void contentDisagreesWithChunkedHeaderBodyTooShort() throws IOException {
+    MockResponse mockResponse = new MockResponse();
+    mockResponse.setChunkedBody("abcde", 5);
+
+    Buffer truncatedBody = new Buffer();
+    Buffer fullBody = mockResponse.getBody();
+    truncatedBody.write(fullBody, fullBody.indexOf((byte) 'e'));
+    mockResponse.setBody(truncatedBody);
+
+    mockResponse.clearHeaders();
+    mockResponse.addHeader("Transfer-encoding: chunked");
+    mockResponse.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END);
+
+    server.enqueue(mockResponse);
+
+    try {
+      readAscii(client.open(server.getUrl("/")).getInputStream(), 5);
+      fail();
+    } catch (ProtocolException expected) {
+    }
+  }
+
   @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
     testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
   }
@@ -790,13 +799,13 @@
   }
 
   private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
 
     URL url = server.getUrl("/foo");
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
-    connection = proxyConfig.connect(server.get(), client, url);
+    connection = proxyConfig.connect(server, client, url);
 
     assertContent("this response comes via HTTPS", connection);
 
@@ -827,7 +836,7 @@
   private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
     RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    server.get().useHttps(sslContext.getSocketFactory(), true);
+    server.useHttps(sslContext.getSocketFactory(), true);
     server.enqueue(
         new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
     server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
@@ -835,7 +844,7 @@
     URL url = new URL("https://android.com/foo");
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
     client.client().setHostnameVerifier(hostnameVerifier);
-    connection = proxyConfig.connect(server.get(), client, url);
+    connection = proxyConfig.connect(server, client, url);
 
     assertContent("this response comes via a secure proxy", connection);
 
@@ -854,7 +863,7 @@
   @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
     initResponseCache();
 
-    server.get().useHttps(sslContext.getSocketFactory(), true);
+    server.useHttps(sslContext.getSocketFactory(), true);
     // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912.
     MockResponse badProxyResponse = new MockResponse()
         .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
@@ -868,7 +877,7 @@
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
     client.client().setConnectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS));
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
-    client.client().setProxy(server.get().toProxyAddress());
+    client.client().setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
     connection = client.open(url);
@@ -889,12 +898,12 @@
       throws IOException, InterruptedException {
     RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    server.get().useHttps(sslContext.getSocketFactory(), true);
+    server.useHttps(sslContext.getSocketFactory(), true);
     server.enqueue(
         new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
     server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
 
-    client.client().setProxy(server.get().toProxyAddress());
+    client.client().setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -919,14 +928,14 @@
 
   @Test public void proxyAuthenticateOnConnect() throws Exception {
     Authenticator.setDefault(new RecordingAuthenticator());
-    server.get().useHttps(sslContext.getSocketFactory(), true);
+    server.useHttps(sslContext.getSocketFactory(), true);
     server.enqueue(new MockResponse().setResponseCode(407)
         .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
     server.enqueue(
         new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
     server.enqueue(new MockResponse().setBody("A"));
 
-    client.client().setProxy(server.get().toProxyAddress());
+    client.client().setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -951,12 +960,12 @@
   // Don't disconnect after building a tunnel with CONNECT
   // http://code.google.com/p/android/issues/detail?id=37221
   @Test public void proxyWithConnectionClose() throws IOException {
-    server.get().useHttps(sslContext.getSocketFactory(), true);
+    server.useHttps(sslContext.getSocketFactory(), true);
     server.enqueue(
         new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
     server.enqueue(new MockResponse().setBody("this response comes via a proxy"));
 
-    client.client().setProxy(server.get().toProxyAddress());
+    client.client().setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -971,13 +980,13 @@
     SSLSocketFactory socketFactory = sslContext.getSocketFactory();
     RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
 
-    server.get().useHttps(socketFactory, true);
+    server.useHttps(socketFactory, true);
     server.enqueue(
         new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
     server.enqueue(new MockResponse().setBody("response 1"));
     server.enqueue(new MockResponse().setBody("response 2"));
 
-    client.client().setProxy(server.get().toProxyAddress());
+    client.client().setProxy(server.toProxyAddress());
 
     URL url = new URL("https://android.com/foo");
     client.client().setSslSocketFactory(socketFactory);
@@ -1213,7 +1222,7 @@
     if (tls) {
       SSLSocketFactory socketFactory = sslContext.getSocketFactory();
       RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
-      server.get().useHttps(socketFactory, false);
+      server.useHttps(socketFactory, false);
       client.client().setSslSocketFactory(socketFactory);
       client.client().setHostnameVerifier(hostnameVerifier);
     }
@@ -1248,9 +1257,6 @@
     // Seed the pool with a bad connection.
     assertContent("a", client.open(server.getUrl("/")));
 
-    // Give the server time to disconnect.
-    Thread.sleep(500);
-
     // This connection will need to be recovered. When it is, transparent gzip should still work!
     assertContent("b", client.open(server.getUrl("/")));
 
@@ -1501,7 +1507,7 @@
     server.enqueue(pleaseAuthenticate);
 
     if (proxy) {
-      client.client().setProxy(server.get().toProxyAddress());
+      client.client().setProxy(server.toProxyAddress());
       connection = client.open(new URL("http://android.com"));
     } else {
       connection = client.open(server.getUrl("/"));
@@ -1638,7 +1644,7 @@
    * http://code.google.com/p/android/issues/detail?id=12860
    */
   private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("Success!"));
 
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
@@ -1809,7 +1815,7 @@
   }
 
   @Test public void redirectedOnHttps() throws IOException, InterruptedException {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
         .addHeader("Location: /foo")
         .setBody("This page has moved!"));
@@ -1829,7 +1835,7 @@
   }
 
   @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
         .addHeader("Location: http://anyhost/foo")
         .setBody("This page has moved!"));
@@ -1854,7 +1860,7 @@
   @Test public void redirectedFromHttpsToHttpFollowingProtocolRedirects() throws Exception {
     server2.enqueue(new MockResponse().setBody("This is insecure HTTP!"));
 
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
         .addHeader("Location: " + server2.getUrl("/"))
         .setBody("This page has moved!"));
@@ -1872,7 +1878,7 @@
   }
 
   @Test public void redirectedFromHttpToHttpsFollowingProtocolRedirects() throws Exception {
-    server2.get().useHttps(sslContext.getSocketFactory(), false);
+    server2.useHttps(sslContext.getSocketFactory(), false);
     server2.enqueue(new MockResponse().setBody("This is secure HTTPS!"));
 
     server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
@@ -1897,9 +1903,9 @@
 
   private void redirectToAnotherOriginServer(boolean https) throws Exception {
     if (https) {
-      server.get().useHttps(sslContext.getSocketFactory(), false);
-      server2.get().useHttps(sslContext.getSocketFactory(), false);
-      server2.get().setProtocolNegotiationEnabled(false);
+      server.useHttps(sslContext.getSocketFactory(), false);
+      server2.useHttps(sslContext.getSocketFactory(), false);
+      server2.setProtocolNegotiationEnabled(false);
       client.client().setSslSocketFactory(sslContext.getSocketFactory());
       client.client().setHostnameVerifier(new RecordingHostnameVerifier());
     }
@@ -1920,8 +1926,8 @@
     assertContent("This is the first server again!", client.open(server.getUrl("/")));
     assertContent("This is the 2nd server, again!", client.open(server2.getUrl("/")));
 
-    String server1Host = server.get().getHostName() + ":" + server.getPort();
-    String server2Host = server2.get().getHostName() + ":" + server2.getPort();
+    String server1Host = server.getHostName() + ":" + server.getPort();
+    String server2Host = server2.getHostName() + ":" + server2.getPort();
     assertEquals(server1Host, server.takeRequest().getHeader("Host"));
     assertEquals(server2Host, server2.takeRequest().getHeader("Host"));
     assertEquals("Expected connection reuse", 1, server.takeRequest().getSequenceNumber());
@@ -1933,9 +1939,9 @@
     client.client().setProxySelector(new ProxySelector() {
       @Override public List<Proxy> select(URI uri) {
         proxySelectionRequests.add(uri);
-        MockWebServer proxyServer = (uri.getPort() == server.get().getPort())
-            ? server.get()
-            : server2.get();
+        MockWebServer proxyServer = (uri.getPort() == server.getPort())
+            ? server
+            : server2;
         return Arrays.asList(proxyServer.toProxyAddress());
       }
 
@@ -2180,7 +2186,7 @@
 
     client.client().setHostnameVerifier(hostnameVerifier);
     client.client().setSslSocketFactory(sc.getSocketFactory());
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     server.enqueue(new MockResponse().setBody("ABC"));
     server.enqueue(new MockResponse().setBody("DEF"));
     server.enqueue(new MockResponse().setBody("GHI"));
@@ -2190,9 +2196,9 @@
     assertContent("DEF", client.open(url));
     assertContent("GHI", client.open(url));
 
-    assertEquals(Arrays.asList("verify " + server.get().getHostName()),
+    assertEquals(Arrays.asList("verify " + server.getHostName()),
         hostnameVerifier.calls);
-    assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.get().getHostName() + " 1]"),
+    assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.getHostName() + " 1]"),
         trustManager.calls);
   }
 
@@ -2223,18 +2229,21 @@
     // Sockets on some platforms can have large buffers that mean writes do not block when
     // required. These socket factories explicitly set the buffer sizes on sockets created.
     final int SOCKET_BUFFER_SIZE = 256 * 1024;
-    server.get().setServerSocketFactory(
+    server.setServerSocketFactory(
         new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
           @Override
-          protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+          protected ServerSocket configureServerSocket(ServerSocket serverSocket)
+              throws IOException {
             serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+            return serverSocket;
           }
         });
     client.client().setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
       @Override
-      protected void configureSocket(Socket socket) throws IOException {
+      protected Socket configureSocket(Socket socket) throws IOException {
         socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
         socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+        return socket;
       }
     });
 
@@ -2430,11 +2439,11 @@
     server.enqueue(new MockResponse().setBody("ABC"));
 
     // The request should work once and then fail
-    HttpURLConnection connection1 = client.open(server.getUrl(""));
+    HttpURLConnection connection1 = client.open(server.getUrl("/"));
     connection1.setReadTimeout(100);
     InputStream input = connection1.getInputStream();
     assertEquals("ABC", readAscii(input, Integer.MAX_VALUE));
-    server.get().shutdown();
+    server.shutdown();
     try {
       HttpURLConnection connection2 = client.open(server.getUrl(""));
       connection2.setReadTimeout(100);
@@ -2478,7 +2487,7 @@
       fail();
     } catch (NullPointerException expected) {
     }
-    assertNull(connection.getContent(new Class[]{getClass()}));
+    assertNull(connection.getContent(new Class[] { getClass() }));
   }
 
   @Test public void getOutputStreamOnGetFails() throws Exception {
@@ -2553,7 +2562,7 @@
   @Test public void urlContainsQueryButNoPath() throws Exception {
     server.enqueue(new MockResponse().setBody("A"));
 
-    URL url = new URL("http", server.get().getHostName(), server.getPort(), "?query");
+    URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
     assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE));
     RecordedRequest request = server.takeRequest();
     assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
@@ -2630,9 +2639,6 @@
 
     assertContent("A", client.open(server.getUrl("/a")));
 
-    // Give the server time to disconnect.
-    Thread.sleep(500);
-
     // If the request body is larger than OkHttp's replay buffer, the failure may still occur.
     byte[] requestBody = new byte[requestSize];
     new Random(0).nextBytes(requestBody);
@@ -2781,6 +2787,51 @@
     assertEquals("A", connection.getHeaderField(""));
   }
 
+  @Test public void requestHeaderValidationIsStrict() throws Exception {
+    connection = client.open(server.getUrl("/"));
+    try {
+      connection.addRequestProperty("a\tb", "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      connection.addRequestProperty("Name", "c\u007fd");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      connection.addRequestProperty("", "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      connection.addRequestProperty("\ud83c\udf69", "Value");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      connection.addRequestProperty("Name", "\u2615\ufe0f");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test public void responseHeaderParsingIsLenient() throws Exception {
+    Headers headers = new Headers.Builder()
+        .add("Content-Length", "0")
+        .addLenient("a\tb: c\u007fd")
+        .addLenient(": ef")
+        .addLenient("\ud83c\udf69: \u2615\ufe0f")
+        .build();
+    server.enqueue(new MockResponse().setHeaders(headers));
+
+    connection = client.open(server.getUrl("/"));
+    connection.getResponseCode();
+    assertEquals("c\u007fd", connection.getHeaderField("a\tb"));
+    assertEquals("\u2615\ufe0f", connection.getHeaderField("\ud83c\udf69"));
+    assertEquals("ef", connection.getHeaderField(""));
+  }
+
   @Test @Ignore public void deflateCompression() {
     fail("TODO");
   }
@@ -2999,7 +3050,7 @@
   }
 
   @Test public void veryLargeFixedLengthRequest() throws Exception {
-    server.get().setBodyLimit(0);
+    server.setBodyLimit(0);
     server.enqueue(new MockResponse());
 
     connection = client.open(server.getUrl("/"));
@@ -3097,13 +3148,24 @@
     assertEquals("foo", request.getHeader("User-Agent"));
   }
 
-  @Test public void userAgentDefaultsToJavaVersion() throws Exception {
+  /** https://github.com/square/okhttp/issues/891 */
+  @Test public void userAgentSystemPropertyIsNotAscii() throws Exception {
+    server.enqueue(new MockResponse().setBody("abc"));
+
+    System.setProperty("http.agent", "a\nb\ud83c\udf69c\ud83c\udf68d\u007fe");
+    assertContent("abc", client.open(server.getUrl("/")));
+
+    RecordedRequest request = server.takeRequest();
+    assertEquals("a?b?c?d?e", request.getHeader("User-Agent"));
+  }
+
+  @Test public void userAgentDefaultsToOkHttpVersion() throws Exception {
     server.enqueue(new MockResponse().setBody("abc"));
 
     assertContent("abc", client.open(server.getUrl("/")));
 
     RecordedRequest request = server.takeRequest();
-    assertTrue(request.getHeader("User-Agent").startsWith("Java"));
+    assertEquals(Version.userAgent(), request.getHeader("User-Agent"));
   }
 
   @Test public void interceptorsNotInvoked() throws Exception {
@@ -3119,6 +3181,56 @@
     assertContent("abc", client.open(server.getUrl("/")));
   }
 
+  @Test public void urlWithSpaceInHost() throws Exception {
+    URLConnection urlConnection = client.open(new URL("http://and roid.com/"));
+    try {
+      urlConnection.getInputStream();
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+  }
+
+  @Test public void urlWithSpaceInHostViaHttpProxy() throws Exception {
+    server.enqueue(new MockResponse());
+    URLConnection urlConnection =
+        client.open(new URL("http://and roid.com/"), server.toProxyAddress());
+
+    try {
+      // This test is to check that a NullPointerException is not thrown.
+      urlConnection.getInputStream();
+      fail(); // the RI makes a bogus proxy request for "GET http://and roid.com/ HTTP/1.1"
+    } catch (UnknownHostException expected) {
+    }
+  }
+
+  @Test public void urlHostWithNul() throws Exception {
+    URLConnection urlConnection = client.open(new URL("http://host\u0000/"));
+    try {
+      urlConnection.getInputStream();
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+  }
+
+  @Test public void urlRedirectToHostWithNul() throws Exception {
+    String redirectUrl = "http://host\u0000/";
+    server.enqueue(new MockResponse().setResponseCode(302)
+        .addHeaderLenient("Location", redirectUrl));
+
+    HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+    assertEquals(302, urlConnection.getResponseCode());
+    assertEquals(redirectUrl, urlConnection.getHeaderField("Location"));
+  }
+
+  @Test public void urlWithBadAsciiHost() throws Exception {
+    URLConnection urlConnection = client.open(new URL("http://host\u0001/"));
+    try {
+      urlConnection.getInputStream();
+      fail();
+    } catch (UnknownHostException expected) {
+    }
+  }
+
   /** Returns a gzipped copy of {@code bytes}. */
   public Buffer gzip(String data) throws IOException {
     Buffer result = new Buffer();
@@ -3143,7 +3255,7 @@
   }
 
   private Set<String> newSet(String... elements) {
-    return new HashSet<String>(Arrays.asList(elements));
+    return new LinkedHashSet<>(Arrays.asList(elements));
   }
 
   enum TransferKind {
@@ -3284,9 +3396,9 @@
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
     client.client().setHostnameVerifier(new RecordingHostnameVerifier());
     client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
-    server.get().useHttps(sslContext.getSocketFactory(), false);
-    server.get().setProtocolNegotiationEnabled(true);
-    server.get().setProtocols(client.client().getProtocols());
+    server.useHttps(sslContext.getSocketFactory(), false);
+    server.setProtocolNegotiationEnabled(true);
+    server.setProtocols(client.client().getProtocols());
   }
 
   /**
@@ -3294,7 +3406,7 @@
    * TLS_FALLBACK_SCSV cipher on fallback connections. See
    * {@link com.squareup.okhttp.FallbackTestClientSocketFactory} for details.
    */
-  private static void suppressTlsFallbackScsv(OkHttpClient client) {
+  private void suppressTlsFallbackScsv(OkHttpClient client) {
     FallbackTestClientSocketFactory clientSocketFactory =
         new FallbackTestClientSocketFactory(sslContext.getSocketFactory());
     client.setSslSocketFactory(clientSocketFactory);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/UrlComponentEncodingTester.java b/okhttp-tests/src/test/java/com/squareup/okhttp/UrlComponentEncodingTester.java
index d602bcc..199279f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/UrlComponentEncodingTester.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/UrlComponentEncodingTester.java
@@ -15,13 +15,15 @@
  */
 package com.squareup.okhttp;
 
+import java.net.URI;
+import java.net.URL;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import okio.Buffer;
 import okio.ByteString;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 /** Tests how each code point is encoded and decoded in the context of each URL component. */
 class UrlComponentEncodingTester {
@@ -166,6 +168,7 @@
   }
 
   private final Map<Integer, Encoding> encodings;
+  private final StringBuilder skipForUri = new StringBuilder();
 
   public UrlComponentEncodingTester() {
     this.encodings = new LinkedHashMap<>(defaultEncodings);
@@ -178,13 +181,31 @@
     return this;
   }
 
+  /**
+   * Configure a character to be skipped but only for conversion to and from {@code java.net.URI}.
+   * That class is more strict than the others.
+   */
+  public UrlComponentEncodingTester skipForUri(int... codePoints) {
+    skipForUri.append(new String(codePoints, 0, codePoints.length));
+    return this;
+  }
+
   public UrlComponentEncodingTester test(Component component) {
     for (Map.Entry<Integer, Encoding> entry : encodings.entrySet()) {
-      if (entry.getValue() == Encoding.SKIP) continue;
+      Encoding encoding = entry.getValue();
+      int codePoint = entry.getKey();
+      testEncodeAndDecode(codePoint, component);
+      if (encoding == Encoding.SKIP) continue;
 
-      testParseOriginal(entry.getKey(), entry.getValue(), component);
-      testParseAlreadyEncoded(entry.getKey(), entry.getValue(), component);
-      testSerialize(entry.getKey(), entry.getValue(), component);
+      testParseOriginal(codePoint, encoding, component);
+      testParseAlreadyEncoded(codePoint, encoding, component);
+      testToUrl(codePoint, encoding, component);
+      testFromUrl(codePoint, encoding, component);
+
+      if (skipForUri.indexOf(Encoding.IDENTITY.encode(codePoint)) == -1) {
+        testToUri(codePoint, encoding, component);
+        testFromUri(codePoint, encoding, component);
+      }
     }
     return this;
   }
@@ -193,9 +214,19 @@
     String encoded = encoding.encode(codePoint);
     String urlString = component.urlString(encoded);
     HttpUrl url = HttpUrl.parse(urlString);
-    if (!component.decodedValue(url).equals(encoded)) {
-      assertEquals(String.format("Encoding %s %#x using %s", component, codePoint, encoding),
-          encoded, component.decodedValue(url));
+    if (!component.encodedValue(url).equals(encoded)) {
+      fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
+    }
+  }
+
+  private void testEncodeAndDecode(int codePoint, Component component) {
+    String expected = Encoding.IDENTITY.encode(codePoint);
+    HttpUrl.Builder builder = HttpUrl.parse("http://host/").newBuilder();
+    component.set(builder, expected);
+    HttpUrl url = builder.build();
+    String actual = component.get(url);
+    if (!expected.equals(actual)) {
+      fail(String.format("Roundtrip %s %#x %s", component, codePoint, url));
     }
   }
 
@@ -206,15 +237,46 @@
     String urlString = component.urlString(identity);
     HttpUrl url = HttpUrl.parse(urlString);
 
-    String s = component.decodedValue(url);
+    String s = component.encodedValue(url);
     if (!s.equals(encoded)) {
-      assertEquals(String.format("Encoding %s %#02x using %s", component, codePoint, encoding),
-          encoded, component.decodedValue(url));
+      fail(String.format("Encoding %s %#02x using %s", component, codePoint, encoding));
     }
   }
 
-  private void testSerialize(int codePoint, Encoding encoding, Component component) {
-    // TODO.
+  private void testToUrl(int codePoint, Encoding encoding, Component component) {
+    String encoded = encoding.encode(codePoint);
+    HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
+    URL javaNetUrl = httpUrl.url();
+    if (!javaNetUrl.toString().equals(javaNetUrl.toString())) {
+      fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
+    }
+  }
+
+  private void testFromUrl(int codePoint, Encoding encoding, Component component) {
+    String encoded = encoding.encode(codePoint);
+    HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
+    HttpUrl toAndFromJavaNetUrl = HttpUrl.get(httpUrl.url());
+    if (!toAndFromJavaNetUrl.equals(httpUrl)) {
+      fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
+    }
+  }
+
+  private void testToUri(int codePoint, Encoding encoding, Component component) {
+    String encoded = encoding.encode(codePoint);
+    HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
+    URI uri = httpUrl.uri();
+    if (!uri.toString().equals(uri.toString())) {
+      fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
+    }
+  }
+
+  private void testFromUri(int codePoint, Encoding encoding, Component component) {
+    String encoded = encoding.encode(codePoint);
+    HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
+    HttpUrl toAndFromUri = HttpUrl.get(httpUrl.uri());
+    if (!toAndFromUri.equals(httpUrl)) {
+      fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
+    }
   }
 
   public enum Encoding {
@@ -235,14 +297,11 @@
       }
     },
 
-    SKIP {
-      public String encode(int codePoint) {
-        throw new UnsupportedOperationException();
-      }
-    };
+    SKIP;
 
-    public abstract String encode(int codePoint);
-
+    public String encode(int codePoint) {
+      throw new UnsupportedOperationException();
+    }
   }
 
   public enum Component {
@@ -250,7 +309,13 @@
       @Override public String urlString(String value) {
         return "http://" + value + "@example.com/";
       }
-      @Override public String decodedValue(HttpUrl url) {
+      @Override public String encodedValue(HttpUrl url) {
+        return url.encodedUsername();
+      }
+      @Override public void set(HttpUrl.Builder builder, String value) {
+        builder.username(value);
+      }
+      @Override public String get(HttpUrl url) {
         return url.username();
       }
     },
@@ -258,60 +323,69 @@
       @Override public String urlString(String value) {
         return "http://:" + value + "@example.com/";
       }
-      @Override public String decodedValue(HttpUrl url) {
+      @Override public String encodedValue(HttpUrl url) {
+        return url.encodedPassword();
+      }
+      @Override public void set(HttpUrl.Builder builder, String value) {
+        builder.password(value);
+      }
+      @Override public String get(HttpUrl url) {
         return url.password();
       }
     },
-    HOST {
-      @Override public String urlString(String value) {
-        throw new UnsupportedOperationException("TODO");
-      }
-
-      @Override public String decodedValue(HttpUrl url) {
-        throw new UnsupportedOperationException("TODO");
-      }
-    },
-    PORT {
-      @Override public String urlString(String value) {
-        throw new UnsupportedOperationException("TODO");
-      }
-
-      @Override public String decodedValue(HttpUrl url) {
-        throw new UnsupportedOperationException("TODO");
-      }
-    },
     PATH {
       @Override public String urlString(String value) {
         return "http://example.com/a" + value + "z/";
       }
-      @Override public String decodedValue(HttpUrl url) {
-        String path = url.path();
+      @Override public String encodedValue(HttpUrl url) {
+        String path = url.encodedPath();
         return path.substring(2, path.length() - 2);
       }
+      @Override public void set(HttpUrl.Builder builder, String value) {
+        builder.addPathSegment("a" + value + "z");
+      }
+      @Override public String get(HttpUrl url) {
+        String pathSegment = url.pathSegments().get(0);
+        return pathSegment.substring(1, pathSegment.length() - 1);
+      }
     },
     QUERY {
       @Override public String urlString(String value) {
         return "http://example.com/?a" + value + "z";
       }
-
-      @Override public String decodedValue(HttpUrl url) {
-        String query = url.query();
+      @Override public String encodedValue(HttpUrl url) {
+        String query = url.encodedQuery();
         return query.substring(1, query.length() - 1);
       }
+      @Override public void set(HttpUrl.Builder builder, String value) {
+        builder.query(value);
+      }
+      @Override public String get(HttpUrl url) {
+        return url.query();
+      }
     },
     FRAGMENT {
       @Override public String urlString(String value) {
         return "http://example.com/#a" + value + "z";
       }
-
-      @Override public String decodedValue(HttpUrl url) {
-        String fragment = url.fragment();
+      @Override public String encodedValue(HttpUrl url) {
+        String fragment = url.encodedFragment();
         return fragment.substring(1, fragment.length() - 1);
       }
+      @Override public void set(HttpUrl.Builder builder, String value) {
+        builder.fragment(value);
+      }
+      @Override public String get(HttpUrl url) {
+        return url.fragment();
+      }
     };
 
     public abstract String urlString(String value);
 
-    public abstract String decodedValue(HttpUrl url);
+    public abstract String encodedValue(HttpUrl url);
+
+    public abstract void set(HttpUrl.Builder builder, String value);
+
+    public abstract String get(HttpUrl url);
   }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
deleted file mode 100644
index da71661..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2015 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import com.google.gson.Gson;
-import com.squareup.okhttp.internal.Util;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.List;
-
-/**
- * The result of a test run from the <a href="https://github.com/w3c/web-platform-tests">W3C web
- * platform tests</a>. This class serves as a Gson model for browser test results.
- *
- * <p><strong>Note:</strong> When extracting the .json file from the browser after a test run, be
- * careful to avoid text encoding problems. In one environment, Safari was corrupting UTF-8 data
- * for download (but the clipboard was fine), and Firefox was corrupting UTF-8 data copied to the
- * clipboard (but the download was fine).
- */
-public final class WebPlatformTestRun {
-  List<TestResult> results;
-
-  public SubtestResult get(String testName, String subtestName) {
-    for (TestResult result : results) {
-      if (testName.equals(result.test)) {
-        for (SubtestResult subtestResult : result.subtests) {
-          if (subtestName.equals(subtestResult.name)) {
-            return subtestResult;
-          }
-        }
-      }
-    }
-    return null;
-  }
-
-  public static WebPlatformTestRun load(InputStream in) throws IOException {
-    try {
-      return new Gson().getAdapter(WebPlatformTestRun.class)
-          .fromJson(new InputStreamReader(in, Util.UTF_8));
-    } finally {
-      Util.closeQuietly(in);
-    }
-  }
-
-  public static class TestResult {
-    String test;
-    List<SubtestResult> subtests;
-  }
-
-  public static class SubtestResult {
-    String name;
-    Status status;
-    String message;
-
-    public boolean isPass() {
-      return status == Status.PASS;
-    }
-  }
-
-  public enum Status {
-    PASS, FAIL
-  }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java
index 72c1d3c..e45761c 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java
@@ -15,16 +15,12 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.WebPlatformTestRun.SubtestResult;
 import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import okio.BufferedSource;
 import okio.Okio;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,25 +36,9 @@
   @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> parameters() {
     try {
-      List<WebPlatformUrlTestData> tests = loadTests();
-
-      // The web platform tests are run in both HTML and XHTML variants. Major browsers pass more
-      // tests in HTML mode, so that's what we'll attempt to match.
-      String testName = "/url/a-element.html";
-      WebPlatformTestRun firefoxTestRun
-          = loadTestRun("/web-platform-test-results-url-firefox-37.0.json");
-      WebPlatformTestRun chromeTestRun
-          = loadTestRun("/web-platform-test-results-url-chrome-42.0.json");
-      WebPlatformTestRun safariTestRun
-          = loadTestRun("/web-platform-test-results-url-safari-7.1.json");
-
       List<Object[]> result = new ArrayList<>();
-      for (WebPlatformUrlTestData urlTestData : tests) {
-        String subtestName = urlTestData.toString();
-        SubtestResult firefoxResult = firefoxTestRun.get(testName, subtestName);
-        SubtestResult chromeResult = chromeTestRun.get(testName, subtestName);
-        SubtestResult safariResult = safariTestRun.get(testName, subtestName);
-        result.add(new Object[] { urlTestData, firefoxResult, chromeResult, safariResult });
+      for (WebPlatformUrlTestData urlTestData : loadTests()) {
+        result.add(new Object[] { urlTestData });
       }
       return result;
     } catch (IOException e) {
@@ -69,89 +49,49 @@
   @Parameter(0)
   public WebPlatformUrlTestData testData;
 
-  @Parameter(1)
-  public SubtestResult firefoxResult;
-
-  @Parameter(2)
-  public SubtestResult chromeResultResult;
-
-  @Parameter(3)
-  public SubtestResult safariResult;
-
-  private static final List<String> JAVA_NET_URL_SCHEMES
-      = Util.immutableList("file", "ftp", "http", "https", "mailto");
   private static final List<String> HTTP_URL_SCHEMES
       = Util.immutableList("http", "https");
-
-  /** Test how {@link URL} does against the web platform test suite. */
-  @Ignore // java.net.URL is broken. Not much we can do about that.
-  @Test public void javaNetUrl() throws Exception {
-    if (!testData.scheme.isEmpty() && !JAVA_NET_URL_SCHEMES.contains(testData.scheme)) {
-      System.out.println("Ignoring unsupported scheme " + testData.scheme);
-      return;
-    }
-
-    try {
-      testJavaNetUrl();
-    } catch (AssertionError e) {
-      if (tolerateFailure()) {
-        System.out.println("Tolerable failure: " + e.getMessage());
-        return;
-      }
-      throw e;
-    }
-  }
-
-  private void testJavaNetUrl() {
-    URL url = null;
-    String failureMessage = "";
-    try {
-      if (testData.base.equals("about:blank")) {
-        url = new URL(testData.input);
-      } else {
-        URL baseUrl = new URL(testData.base);
-        url = new URL(baseUrl, testData.input);
-      }
-    } catch (MalformedURLException e) {
-      failureMessage = e.getMessage();
-    }
-
-    if (testData.expectParseFailure()) {
-      assertNull("Expected URL to fail parsing", url);
-    } else {
-      assertNotNull("Expected URL to parse successfully, but was " + failureMessage, url);
-      String effectivePort = url.getPort() != -1 ? Integer.toString(url.getPort()) : "";
-      String effectiveQuery = url.getQuery() != null ? "?" + url.getQuery() : "";
-      String effectiveFragment = url.getRef() != null ? "#" + url.getRef() : "";
-      assertEquals("scheme", testData.scheme, url.getProtocol());
-      assertEquals("host", testData.host, url.getHost());
-      assertEquals("port", testData.port, effectivePort);
-      assertEquals("path", testData.path, url.getPath());
-      assertEquals("query", testData.query, effectiveQuery);
-      assertEquals("fragment", testData.fragment, effectiveFragment);
-    }
-  }
+  private static final List<String> KNOWN_FAILURES = Util.immutableList(
+      "Parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
+      "Parsing: <http://f:0/c> against <http://example.org/foo/bar>",
+      "Parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
+      "Parsing: <http://f:\n/c> against <http://example.org/foo/bar>",
+      "Parsing: <http://f:999999/c> against <http://example.org/foo/bar>",
+      "Parsing: <#β> against <http://example.org/foo/bar>",
+      "Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>",
+      "Parsing: <http://192.0x00A80001> against <about:blank>",
+      // This test fails on Java 7 but passes on Java 8. See HttpUrlTest.hostWithTrailingDot().
+      "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01%2e> against <http://other.com/>",
+      "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01> against <http://other.com/>",
+      "Parsing: <http://192.168.0.257> against <http://other.com/>",
+      "Parsing: <http://0Xc0.0250.01> against <http://other.com/>"
+  );
 
   /** Test how {@link HttpUrl} does against the web platform test suite. */
-  @Ignore // TODO(jwilson): implement character encoding.
   @Test public void httpUrl() throws Exception {
     if (!testData.scheme.isEmpty() && !HTTP_URL_SCHEMES.contains(testData.scheme)) {
-      System.out.println("Ignoring unsupported scheme " + testData.scheme);
+      System.err.println("Ignoring unsupported scheme " + testData.scheme);
       return;
     }
-    if (!testData.base.startsWith("https:") && !testData.base.startsWith("http:")) {
-      System.out.println("Ignoring unsupported base " + testData.base);
+    if (!testData.base.startsWith("https:")
+        && !testData.base.startsWith("http:")
+        && !testData.base.equals("about:blank")) {
+      System.err.println("Ignoring unsupported base " + testData.base);
       return;
     }
 
     try {
       testHttpUrl();
-    } catch (AssertionError e) {
-      if (tolerateFailure()) {
-        System.out.println("Tolerable failure: " + e.getMessage());
-        return;
+      if (KNOWN_FAILURES.contains(testData.toString())) {
+        System.err.println("Expected failure but was success: " + testData);
       }
-      throw e;
+    } catch (Throwable e) {
+      if (KNOWN_FAILURES.contains(testData.toString())) {
+        System.err.println("Ignoring known failure: " + testData);
+        e.printStackTrace();
+      } else {
+        throw e;
+      }
     }
   }
 
@@ -171,34 +111,23 @@
       String effectivePort = url.port() != HttpUrl.defaultPort(url.scheme())
           ? Integer.toString(url.port())
           : "";
-      String effectiveQuery = url.query() != null ? "?" + url.query() : "";
-      String effectiveFragment = url.fragment() != null ? "#" + url.fragment() : "";
+      String effectiveQuery = url.encodedQuery() != null ? "?" + url.encodedQuery() : "";
+      String effectiveFragment = url.encodedFragment() != null ? "#" + url.encodedFragment() : "";
+      String effectiveHost = url.host().contains(":")
+          ? ("[" + url.host() + "]")
+          : url.host();
       assertEquals("scheme", testData.scheme, url.scheme());
-      assertEquals("host", testData.host, url.host());
+      assertEquals("host", testData.host, effectiveHost);
       assertEquals("port", testData.port, effectivePort);
-      assertEquals("path", testData.path, url.path());
+      assertEquals("path", testData.path, url.encodedPath());
       assertEquals("query", testData.query, effectiveQuery);
       assertEquals("fragment", testData.fragment, effectiveFragment);
     }
   }
 
-  /**
-   * Returns true if several major browsers also fail this test, in which case the test itself is
-   * questionable.
-   */
-  private boolean tolerateFailure() {
-    return !firefoxResult.isPass()
-        && !chromeResultResult.isPass()
-        && !safariResult.isPass();
-  }
-
   private static List<WebPlatformUrlTestData> loadTests() throws IOException {
     BufferedSource source = Okio.buffer(Okio.source(
         WebPlatformUrlTest.class.getResourceAsStream("/web-platform-test-urltestdata.txt")));
     return WebPlatformUrlTestData.load(source);
   }
-
-  private static WebPlatformTestRun loadTestRun(String name) throws IOException {
-    return WebPlatformTestRun.load(WebPlatformUrlTest.class.getResourceAsStream(name));
-  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTestData.java b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTestData.java
index 08bc9e3..2ea3693 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTestData.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTestData.java
@@ -29,8 +29,9 @@
  * attempts to be compatible.
  *
  * <p>Each line of the urltestdata.text file specifies a test. Lines look like this: <pre>   {@code
+ *
  *   http://example\t.\norg http://example.org/foo/bar s:http h:example.org p:/
- * }
+ * }</pre>
  */
 public final class WebPlatformUrlTestData {
   String input;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ConnectionSpecSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ConnectionSpecSelectorTest.java
index 6af9c02..c94cc23 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ConnectionSpecSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ConnectionSpecSelectorTest.java
@@ -17,9 +17,6 @@
 
 import com.squareup.okhttp.ConnectionSpec;
 import com.squareup.okhttp.TlsVersion;
-
-import org.junit.Test;
-
 import java.io.IOException;
 import java.security.cert.CertificateException;
 import java.util.Arrays;
@@ -28,22 +25,22 @@
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSocket;
+import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class ConnectionSpecSelectorTest {
-
   static {
     Internal.initializeInstanceForTests();
   }
 
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-
   public static final SSLHandshakeException RETRYABLE_EXCEPTION = new SSLHandshakeException(
       "Simulated handshake exception");
 
+  private SSLContext sslContext = SslContextBuilder.localhost();
+
   @Test
   public void nonRetryableIOException() throws Exception {
     ConnectionSpecSelector connectionSpecSelector =
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/BaseTestHandler.java
similarity index 97%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/BaseTestHandler.java
index d0b5e97..252b4c7 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/BaseTestHandler.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 import java.util.List;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HpackTest.java
similarity index 99%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HpackTest.java
index 1dcbc01..aacddab 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HpackTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 import java.util.Arrays;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
similarity index 81%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
index a13fa53..24c512d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
+import java.net.Socket;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -30,16 +31,17 @@
 import org.junit.Test;
 
 import static com.squareup.okhttp.TestUtil.headerEntries;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
-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_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_SETTINGS;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
+import static com.squareup.okhttp.TestUtil.repeat;
+import static com.squareup.okhttp.internal.framed.ErrorCode.CANCEL;
+import static com.squareup.okhttp.internal.framed.ErrorCode.PROTOCOL_ERROR;
+import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_DATA;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_PING;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_RST_STREAM;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_SETTINGS;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_WINDOW_UPDATE;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -82,7 +84,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, HTTP_2);
+    FramedConnection connection = connection(peer, HTTP_2);
     Ping ping = connection.ping();
     assertTrue(ping.roundTripTime() > 0);
     assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
@@ -110,7 +112,7 @@
     peer.acceptFrame(); // HEADERS
     peer.play();
 
-    SpdyConnection connection = connection(peer, HTTP_2);
+    FramedConnection connection = connection(peer, HTTP_2);
 
     // Default is 64KiB - 1.
     assertEquals(65535, connection.peerSettings.getInitialWindowSize(-1));
@@ -126,7 +128,7 @@
     assertTrue(ackFrame.ack);
 
     // This stream was created *after* the connection settings were adjusted.
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), false, true);
 
     assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
     assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
@@ -139,7 +141,7 @@
     Settings settings = new Settings();
     settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
 
-    SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+    FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
 
     // verify the peer's settings were read and applied.
     assertEquals(0, connection.peerSettings.getHeaderTableSize());
@@ -153,7 +155,7 @@
     Settings settings = new Settings();
     settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
 
-    SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+    FramedConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
 
     // verify the peer's settings were read and applied.
     assertFalse(connection.peerSettings.getEnablePush(true));
@@ -164,7 +166,7 @@
     Settings settings = new Settings();
     settings.set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize);
 
-    SpdyConnection connection = sendHttp2SettingsAndCheckForAck(true, settings);
+    FramedConnection connection = sendHttp2SettingsAndCheckForAck(true, settings);
 
     // verify the peer's settings were read and applied.
     assertEquals(newMaxFrameSize, connection.peerSettings.getMaxFrameSize(-1));
@@ -184,9 +186,9 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, HTTP_2);
-    SpdyStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
-    SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, HTTP_2);
+    FramedStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
     connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
     BufferedSink sink1 = Okio.buffer(stream1.getSink());
     BufferedSink sink2 = Okio.buffer(stream2.getSink());
@@ -244,9 +246,9 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, HTTP_2);
+    FramedConnection connection = connection(peer, HTTP_2);
     connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(0, stream.unacknowledgedBytesRead);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     Source in = stream.getSource();
@@ -284,8 +286,8 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, HTTP_2);
-    SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+    FramedConnection connection = connection(peer, HTTP_2);
+    FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(-1, client.getSource().read(new Buffer(), 1));
 
     // Verify the peer received what was expected.
@@ -304,8 +306,8 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, HTTP_2);
-    SpdyStream client = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, HTTP_2);
+    FramedStream client = connection.newStream(headerEntries("b", "banana"), true, true);
     BufferedSink out = Okio.buffer(client.getSink());
     out.write(Util.EMPTY_BYTE_ARRAY);
     out.flush();
@@ -331,8 +333,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, HTTP_2);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, HTTP_2);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     BufferedSink out = Okio.buffer(stream.getSink());
     out.write(buff);
     out.flush();
@@ -369,9 +371,9 @@
     RecordingPushObserver observer = new RecordingPushObserver();
 
     // play it back
-    SpdyConnection connection = connectionBuilder(peer, HTTP_2)
+    FramedConnection connection = connectionBuilder(peer, HTTP_2)
         .pushObserver(observer).build();
-    SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+    FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(-1, client.getSource().read(new Buffer(), 1));
 
     // verify the peer received what was expected
@@ -392,7 +394,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connectionBuilder(peer, HTTP_2).build();
+    FramedConnection connection = connectionBuilder(peer, HTTP_2).build();
     connection.newStream(headerEntries("b", "banana"), false, true);
 
     // verify the peer received what was expected
@@ -427,7 +429,38 @@
     assertEquals(CANCEL, rstStream.errorCode);
   }
 
-  private SpdyConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
+  /**
+   * When writing a set of headers fails due to an {@code IOException}, make sure the writer is left
+   * in a consistent state so the next writer also gets an {@code IOException} also instead of
+   * something worse (like an {@link IllegalStateException}.
+   *
+   * <p>See https://github.com/square/okhttp/issues/1651
+   */
+  @Test public void socketExceptionWhileWritingHeaders() throws Exception {
+    peer.setVariantAndClient(HTTP_2, false);
+    peer.acceptFrame(); // SYN_STREAM.
+    peer.play();
+
+    String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
+    Socket socket = peer.openSocket();
+    FramedConnection connection = new FramedConnection.Builder(true, socket)
+        .pushObserver(IGNORE)
+        .protocol(HTTP_2.getProtocol())
+        .build();
+    socket.shutdownOutput();
+    try {
+      connection.newStream(headerEntries("a", longString), false, true);
+      fail();
+    } catch (IOException expected) {
+    }
+    try {
+      connection.newStream(headerEntries("b", longString), false, true);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  private FramedConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
       throws IOException, InterruptedException {
     peer.setVariantAndClient(HTTP_2, client);
     peer.sendFrame().settings(settings);
@@ -437,7 +470,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, HTTP_2);
+    FramedConnection connection = connection(peer, HTTP_2);
 
     // verify the peer received the ACK
     MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
@@ -449,13 +482,13 @@
     return connection;
   }
 
-  private SpdyConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
+  private FramedConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
     return connectionBuilder(peer, variant).build();
   }
 
-  private SpdyConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
+  private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
       throws IOException {
-    return new SpdyConnection.Builder(true, peer.openSocket())
+    return new FramedConnection.Builder(true, peer.openSocket())
         .pushObserver(IGNORE)
         .protocol(variant.getProtocol());
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2FrameLoggerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2FrameLoggerTest.java
similarity index 83%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2FrameLoggerTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2FrameLoggerTest.java
index 0a0a9da..12a9e3b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2FrameLoggerTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2FrameLoggerTest.java
@@ -13,26 +13,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import org.junit.Test;
 
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_ACK;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_END_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_END_STREAM;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_NONE;
-import static com.squareup.okhttp.internal.spdy.Http2.FrameLogger.formatFlags;
-import static com.squareup.okhttp.internal.spdy.Http2.FrameLogger.formatHeader;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_CONTINUATION;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_DATA;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_GOAWAY;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_PING;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_PUSH_PROMISE;
-import static com.squareup.okhttp.internal.spdy.Http2.TYPE_SETTINGS;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_ACK;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_HEADERS;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_STREAM;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_NONE;
+import static com.squareup.okhttp.internal.framed.Http2.FrameLogger.formatFlags;
+import static com.squareup.okhttp.internal.framed.Http2.FrameLogger.formatHeader;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_CONTINUATION;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_DATA;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_GOAWAY;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_PING;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_PUSH_PROMISE;
+import static com.squareup.okhttp.internal.framed.Http2.TYPE_SETTINGS;
 import static org.junit.Assert.assertEquals;
 
 public class Http2FrameLoggerTest {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2Test.java
similarity index 98%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2Test.java
index 331514d..8e4f306 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2Test.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
@@ -28,12 +28,12 @@
 import org.junit.Test;
 
 import static com.squareup.okhttp.TestUtil.headerEntries;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_COMPRESSED;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_END_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_END_STREAM;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_NONE;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_PADDED;
-import static com.squareup.okhttp.internal.spdy.Http2.FLAG_PRIORITY;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_COMPRESSED;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_HEADERS;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_STREAM;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_NONE;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_PADDED;
+import static com.squareup.okhttp.internal.framed.Http2.FLAG_PRIORITY;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HuffmanTest.java
similarity index 97%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HuffmanTest.java
index 222d23e..eeddd3e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HuffmanTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
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/framed/MockSpdyPeer.java
similarity index 94%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/MockSpdyPeer.java
index bc5499c..f30d099 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/MockSpdyPeer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.internal.Util;
 import java.io.Closeable;
@@ -30,6 +30,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Logger;
 import okio.Buffer;
 import okio.BufferedSource;
 import okio.ByteString;
@@ -37,6 +38,8 @@
 
 /** Replays prerecorded outgoing frames and records incoming frames. */
 public final class MockSpdyPeer implements Closeable {
+  private static final Logger logger = Logger.getLogger(MockSpdyPeer.class.getName());
+
   private int frameCount = 0;
   private boolean client = false;
   private Variant variant = new Spdy3();
@@ -122,7 +125,7 @@
           readAndWriteFrames();
         } catch (IOException e) {
           Util.closeQuietly(MockSpdyPeer.this);
-          e.printStackTrace();
+          logger.info(MockSpdyPeer.this + " done: " + e.getMessage());
         }
       }
     });
@@ -131,6 +134,15 @@
   private void readAndWriteFrames() throws IOException {
     if (socket != null) throw new IllegalStateException();
     socket = serverSocket.accept();
+
+    // Bail out now if this instance was closed while waiting for the socket to accept.
+    synchronized (this) {
+      if (executor.isShutdown()) {
+        socket.close();
+        return;
+      }
+    }
+
     OutputStream out = socket.getOutputStream();
     InputStream in = socket.getInputStream();
     FrameReader reader = variant.newReader(Okio.buffer(Okio.source(in)), client);
@@ -180,16 +192,12 @@
 
   @Override public synchronized void close() throws IOException {
     executor.shutdown();
-    Socket socket = this.socket;
-    if (socket != null) {
-      Util.closeQuietly(socket);
-      this.socket = null;
-    }
-    ServerSocket serverSocket = this.serverSocket;
-    if (serverSocket != null) {
-      Util.closeQuietly(serverSocket);
-      this.serverSocket = null;
-    }
+    Util.closeQuietly(socket);
+    Util.closeQuietly(serverSocket);
+  }
+
+  @Override public String toString() {
+    return "MockSpdyPeer[" + port + "]";
   }
 
   private static class OutFrame {
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/framed/SettingsTest.java
similarity index 91%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/SettingsTest.java
index f9f9efa..be5f8ec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/SettingsTest.java
@@ -13,17 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 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;
-import static com.squareup.okhttp.internal.spdy.Settings.PERSISTED;
-import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
-import static com.squareup.okhttp.internal.spdy.Settings.UPLOAD_BANDWIDTH;
+import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.framed.Settings.DOWNLOAD_BANDWIDTH;
+import static com.squareup.okhttp.internal.framed.Settings.DOWNLOAD_RETRANS_RATE;
+import static com.squareup.okhttp.internal.framed.Settings.MAX_CONCURRENT_STREAMS;
+import static com.squareup.okhttp.internal.framed.Settings.PERSISTED;
+import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE;
+import static com.squareup.okhttp.internal.framed.Settings.UPLOAD_BANDWIDTH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
similarity index 87%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
index 959f1e9..26d4986 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
@@ -13,14 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.net.Socket;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -35,20 +37,20 @@
 import org.junit.Test;
 
 import static com.squareup.okhttp.TestUtil.headerEntries;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.INTERNAL_ERROR;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.INVALID_STREAM;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.REFUSED_STREAM;
-import static com.squareup.okhttp.internal.spdy.ErrorCode.STREAM_IN_USE;
-import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
-import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_DATA;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_GOAWAY;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_HEADERS;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
+import static com.squareup.okhttp.internal.framed.ErrorCode.CANCEL;
+import static com.squareup.okhttp.internal.framed.ErrorCode.INTERNAL_ERROR;
+import static com.squareup.okhttp.internal.framed.ErrorCode.INVALID_STREAM;
+import static com.squareup.okhttp.internal.framed.ErrorCode.PROTOCOL_ERROR;
+import static com.squareup.okhttp.internal.framed.ErrorCode.REFUSED_STREAM;
+import static com.squareup.okhttp.internal.framed.ErrorCode.STREAM_IN_USE;
+import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.framed.Settings.PERSIST_VALUE;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_DATA;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_GOAWAY;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_PING;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_RST_STREAM;
+import static com.squareup.okhttp.internal.framed.Spdy3.TYPE_WINDOW_UPDATE;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -72,8 +74,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     assertStreamData("robot", stream.getSource());
     BufferedSink out = Okio.buffer(stream.getSink());
@@ -101,8 +103,8 @@
     peer.sendFrame().ping(true, 1, 0);
     peer.play();
 
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, false);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), false, false);
     assertEquals(1, connection.openStreamCount());
     assertEquals(headerEntries("b", "banana"), stream.getResponseHeaders());
     connection.ping().roundTripTime(); // Ensure that inFinished has been received.
@@ -118,7 +120,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(1, connection.openStreamCount());
     connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
@@ -149,14 +151,14 @@
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
     IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
+      @Override public void receive(FramedStream stream) throws IOException {
         receiveCount.incrementAndGet();
         assertEquals(pushHeaders, stream.getRequestHeaders());
         assertEquals(null, stream.getErrorCode());
         stream.reply(headerEntries("b", "banana"), true);
       }
     };
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
+    new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
 
     // verify the peer received what was expected
     MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -177,7 +179,7 @@
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
     IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
+      @Override public void receive(FramedStream stream) throws IOException {
         stream.reply(headerEntries("b", "banana"), false);
         receiveCount.incrementAndGet();
       }
@@ -218,7 +220,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     Ping ping = connection.ping();
     assertTrue(ping.roundTripTime() > 0);
     assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
@@ -260,7 +262,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
 
     peer.takeFrame(); // Guarantees that the peer Settings frame has been processed.
     synchronized (connection) {
@@ -285,7 +287,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
 
     peer.takeFrame(); // Guarantees that the Settings frame has been processed.
     synchronized (connection) {
@@ -312,7 +314,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
 
     peer.takeFrame(); // Guarantees that the Settings frame has been processed.
 
@@ -380,8 +382,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, false);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, false);
     BufferedSink out = Okio.buffer(stream.getSink());
     out.writeUtf8("square");
     out.flush();
@@ -423,8 +425,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, true);
     BufferedSink out = Okio.buffer(stream.getSink());
     connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received.
     try {
@@ -464,8 +466,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), false, true);
     Source in = stream.getSource();
     BufferedSink out = Okio.buffer(stream.getSink());
     in.close();
@@ -508,8 +510,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, true);
     Source source = stream.getSource();
     BufferedSink out = Okio.buffer(stream.getSink());
     source.close();
@@ -552,8 +554,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), false, true);
     Source source = stream.getSource();
     assertStreamData("square", source);
     connection.ping().roundTripTime(); // Ensure that inFinished has been received.
@@ -578,8 +580,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("c", "cola"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("c", "cola"), true, true);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received.
     try {
@@ -612,14 +614,14 @@
     // play it back
     final AtomicInteger receiveCount = new AtomicInteger();
     IncomingStreamHandler handler = new IncomingStreamHandler() {
-      @Override public void receive(SpdyStream stream) throws IOException {
+      @Override public void receive(FramedStream stream) throws IOException {
         receiveCount.incrementAndGet();
         assertEquals(headerEntries("a", "android"), stream.getRequestHeaders());
         assertEquals(null, stream.getErrorCode());
         stream.reply(headerEntries("c", "cola"), true);
       }
     };
-    new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build();
+    new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
 
     // verify the peer received what was expected
     MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -643,8 +645,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     assertStreamData("robot", stream.getSource());
 
@@ -668,8 +670,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, true);
     assertEquals(headerEntries("b", "banana"), stream.getResponseHeaders());
 
     // verify the peer received what was expected
@@ -690,8 +692,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, true);
     try {
       stream.getResponseHeaders();
       fail();
@@ -722,9 +724,9 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
-    SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
     connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
     BufferedSink sink1 = Okio.buffer(stream1.getSink());
     BufferedSink sink2 = Okio.buffer(stream2.getSink());
@@ -771,7 +773,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     connection.newStream(headerEntries("a", "android"), true, true);
     Ping ping = connection.ping();
     connection.shutdown(PROTOCOL_ERROR);
@@ -795,7 +797,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     connection.shutdown(INTERNAL_ERROR);
     try {
       connection.ping();
@@ -818,8 +820,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("a", "android"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("a", "android"), true, true);
     assertEquals(1, connection.openStreamCount());
     connection.close();
     assertEquals(0, connection.openStreamCount());
@@ -862,7 +864,7 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     Ping ping = connection.ping();
     connection.close();
     assertEquals(-1, ping.roundTripTime());
@@ -875,8 +877,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
     long startNanos = System.nanoTime();
     try {
@@ -902,8 +904,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
     Source source = stream.getSource();
     long startNanos = System.nanoTime();
@@ -937,9 +939,9 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     connection.ping().roundTripTime(); // Make sure settings have been received.
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     Sink sink = stream.getSink();
     sink.write(new Buffer().writeUtf8("abcde"), 5);
     stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS);
@@ -979,8 +981,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     connection.ping().roundTripTime(); // Make sure the window update has been received.
     Sink sink = stream.getSink();
     stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS);
@@ -1011,8 +1013,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
 
     // two outgoing writes
     Sink sink = stream.getSink();
@@ -1038,8 +1040,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
     assertEquals(headerEntries("a", "android", "c", "c3po"), stream.getResponseHeaders());
 
@@ -1061,8 +1063,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
     try {
       stream.getResponseHeaders();
@@ -1103,9 +1105,9 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, SPDY3);
+    FramedConnection connection = connection(peer, SPDY3);
     connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(0, stream.unacknowledgedBytesRead);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     Source in = stream.getSource();
@@ -1143,8 +1145,8 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream client = connection.newStream(headerEntries("b", "banana"), false, true);
     assertEquals(-1, client.getSource().read(new Buffer(), 1));
 
     // Verify the peer received what was expected.
@@ -1163,8 +1165,8 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream client = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream client = connection.newStream(headerEntries("b", "banana"), true, true);
     BufferedSink out = Okio.buffer(client.getSink());
     out.write(Util.EMPTY_BYTE_ARRAY);
     out.flush();
@@ -1185,8 +1187,8 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
     Source in = stream.getSource();
     try {
@@ -1210,8 +1212,8 @@
     peer.play();
 
     // Play it back.
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream1 = connection.newStream(headerEntries("a", "apple"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream1 = connection.newStream(headerEntries("a", "apple"), true, true);
     BufferedSink out1 = Okio.buffer(stream1.getSink());
     out1.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
     out1.flush();
@@ -1227,7 +1229,7 @@
     assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
 
     // Another stream should be able to send data even though 1 is blocked.
-    SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
     BufferedSink out2 = Okio.buffer(stream2.getSink());
     out2.writeUtf8("foo");
     out2.flush();
@@ -1300,20 +1302,48 @@
     peer.play();
 
     // play it back
-    SpdyConnection connection = connection(peer, SPDY3);
-    SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+    FramedConnection connection = connection(peer, SPDY3);
+    FramedStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
     assertEquals("a", stream.getResponseHeaders().get(0).name.utf8());
     assertEquals(length, stream.getResponseHeaders().get(0).value.size());
     assertStreamData("robot", stream.getSource());
   }
 
-  private SpdyConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
+  @Test public void socketExceptionWhileWritingHeaders() throws Exception {
+    peer.acceptFrame(); // SYN_STREAM.
+    peer.play();
+
+    String longString = ByteString.of(randomBytes(2048)).base64();
+    Socket socket = peer.openSocket();
+    FramedConnection connection = new FramedConnection.Builder(true, socket)
+        .protocol(SPDY3.getProtocol())
+        .build();
+    socket.shutdownOutput();
+    try {
+      connection.newStream(headerEntries("a", longString), false, true);
+      fail();
+    } catch (IOException expected) {
+    }
+    try {
+      connection.newStream(headerEntries("b", longString), false, true);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  private byte[] randomBytes(int length) {
+    byte[] bytes = new byte[length];
+    new Random(0).nextBytes(bytes);
+    return bytes;
+  }
+
+  private FramedConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
     return connectionBuilder(peer, variant).build();
   }
 
-  private SpdyConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
+  private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
       throws IOException {
-    return new SpdyConnection.Builder(true, peer.openSocket())
+    return new FramedConnection.Builder(true, peer.openSocket())
         .protocol(variant.getProtocol());
   }
 
@@ -1322,15 +1352,6 @@
     assertEquals(expected, actual);
   }
 
-  private void assertFlushBlocks(BufferedSink out) throws IOException {
-    interruptAfterDelay(500);
-    try {
-      out.flush();
-      fail();
-    } catch (InterruptedIOException expected) {
-    }
-  }
-
   /** Interrupts the current thread after {@code delayMillis}. */
   private void interruptAfterDelay(final long delayMillis) {
     final Thread toInterrupt = Thread.currentThread();
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3Test.java
similarity index 98%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3Test.java
index c902773..2627959 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3Test.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.internal.Util;
 import java.io.IOException;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
index d0fa1b2..043234e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
@@ -195,11 +195,11 @@
     HttpCookie cookieA = new HttpCookie("a", "android");
     cookieA.setDomain(server.getCookieDomain());
     cookieA.setPath("/");
-    cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookieA);
+    cookieManager.getCookieStore().add(server.url("/").uri(), cookieA);
     HttpCookie cookieB = new HttpCookie("b", "banana");
     cookieB.setDomain(server.getCookieDomain());
     cookieB.setPath("/");
-    cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookieB);
+    cookieManager.getCookieStore().add(server.url("/").uri(), cookieB);
     CookieHandler.setDefault(cookieManager);
 
     get(server, "/");
@@ -222,7 +222,7 @@
     MockWebServer redirectSource = new MockWebServer();
     redirectSource.enqueue(new MockResponse()
         .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
-        .addHeader("Location: " + redirectTarget.getUrl("/")));
+        .addHeader("Location: " + redirectTarget.url("/")));
     redirectSource.start();
 
     CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
@@ -231,7 +231,7 @@
     cookie.setPath("/");
     String portList = Integer.toString(redirectSource.getPort());
     cookie.setPortlist(portList);
-    cookieManager.getCookieStore().add(redirectSource.getUrl("/").toURI(), cookie);
+    cookieManager.getCookieStore().add(redirectSource.url("/").uri(), cookie);
     CookieHandler.setDefault(cookieManager);
 
     get(redirectSource, "/");
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
index 5c0f814..d64badb 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
@@ -55,21 +55,24 @@
     server.setServerSocketFactory(
         new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
           @Override
-          protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+          protected ServerSocket configureServerSocket(ServerSocket serverSocket)
+              throws IOException {
             serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+            return serverSocket;
           }
         });
     client.setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
       @Override
-      protected void configureSocket(Socket socket) throws IOException {
+      protected Socket configureSocket(Socket socket) throws IOException {
         socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
         socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+        return socket;
       }
     });
   }
 
   @Test public void interruptWritingRequestBody() throws Exception {
-    int requestBodySize = 10 * 1024 * 1024; // 10 MiB
+    int requestBodySize = 2 * 1024 * 1024; // 2 MiB
 
     server.enqueue(new MockResponse()
         .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
@@ -95,7 +98,7 @@
   }
 
   @Test public void interruptReadingResponseBody() throws Exception {
-    int responseBodySize = 10 * 1024 * 1024; // 10 MiB
+    int responseBodySize = 2 * 1024 * 1024; // 2 MiB
 
     server.enqueue(new MockResponse()
         .setBody(new Buffer().write(new byte[responseBodySize]))
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
index 1d94622..1f5ad6d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
@@ -19,7 +19,7 @@
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
-import com.squareup.okhttp.internal.spdy.Header;
+import com.squareup.okhttp.internal.framed.Header;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
@@ -43,7 +43,7 @@
         ":version", "HTTP/1.1");
     Request request = new Request.Builder().url("http://square.com/").build();
     Response response =
-        SpdyTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+        FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
     Headers headers = response.headers();
     assertEquals(4, headers.size());
     assertEquals(Protocol.SPDY_3, response.protocol());
@@ -71,7 +71,7 @@
         "connection", "close");
     Request request = new Request.Builder().url("http://square.com/").build();
     Response response =
-        SpdyTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+        FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
     Headers headers = response.headers();
     assertEquals(1, headers.size());
     assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
@@ -84,7 +84,7 @@
         ":version", "HTTP/1.1",
         "connection", "close");
     Request request = new Request.Builder().url("http://square.com/").build();
-    Response response = SpdyTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
+    Response response = FramedTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
         .request(request).build();
     Headers headers = response.headers();
     assertEquals(1, headers.size());
@@ -101,7 +101,7 @@
         .header(":status", "200 OK")
         .build();
     List<Header> headerBlock =
-        SpdyTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
+        FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
     List<Header> expected = headerEntries(
         ":method", "GET",
         ":path", "/",
@@ -126,7 +126,7 @@
         ":version", "HTTP/1.1",
         ":host", "square.com",
         ":scheme", "http");
-    assertEquals(expected, SpdyTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
+    assertEquals(expected, FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
   }
 
   @Test public void toNameValueBlockDropsForbiddenHeadersHttp2() {
@@ -141,7 +141,7 @@
         ":authority", "square.com",
         ":scheme", "http");
     assertEquals(expected,
-        SpdyTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
+        FramedTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
   }
 
   @Test public void ofTrims() {
@@ -302,4 +302,14 @@
     } catch (IllegalArgumentException expected) {
     }
   }
+
+  @Test public void toMultimapGroupsHeaders() {
+    Headers headers = Headers.of(
+        "cache-control", "no-cache",
+        "cache-control", "no-store",
+        "user-agent", "OkHttp");
+    Map<String, List<String>> headerMap = headers.toMultimap();
+    assertEquals(2, headerMap.get("cache-control").size());
+    assertEquals(1, headerMap.get("user-agent").size());
+  }
 }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
index be9f10e..2d52eee 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
@@ -17,6 +17,7 @@
 
 import com.squareup.okhttp.Cache;
 import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.OkUrlFactory;
 import com.squareup.okhttp.Protocol;
@@ -24,16 +25,16 @@
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
 import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.Authenticator;
 import java.net.CookieManager;
 import java.net.HttpURLConnection;
 import java.net.SocketTimeoutException;
-import java.net.URL;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -44,7 +45,6 @@
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.GzipSink;
@@ -64,21 +64,15 @@
 
 /** Test how SPDY interacts with HTTP features. */
 public abstract class HttpOverSpdyTest {
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
-
-  private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
-    public boolean verify(String hostname, SSLSession session) {
-      return true;
-    }
-  };
-
   @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
+  @Rule public final MockWebServer server = new MockWebServer();
 
   /** Protocol to test, for example {@link com.squareup.okhttp.Protocol#SPDY_3} */
   private final Protocol protocol;
   protected String hostHeader = ":host";
 
+  protected SSLContext sslContext = SslContextBuilder.localhost();
+  protected HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
   protected final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
   protected HttpURLConnection connection;
   protected Cache cache;
@@ -88,10 +82,10 @@
   }
 
   @Before public void setUp() throws Exception {
-    server.get().useHttps(sslContext.getSocketFactory(), false);
+    server.useHttps(sslContext.getSocketFactory(), false);
     client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
     client.client().setSslSocketFactory(sslContext.getSocketFactory());
-    client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+    client.client().setHostnameVerifier(hostnameVerifier);
     cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE);
   }
 
@@ -160,8 +154,8 @@
     connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length));
     connection.setDoOutput(true);
     connection.getOutputStream().write(postBytes); // push bytes into SpdyDataOutputStream.buffer
-    connection.getOutputStream().flush(); // SpdyConnection.writeData subject to write window
-    connection.getOutputStream().close(); // SpdyConnection.writeData empty frame
+    connection.getOutputStream().flush(); // FramedConnection.writeData subject to write window
+    connection.getOutputStream().close(); // FramedConnection.writeData empty frame
     assertContent("ABCDE", connection, Integer.MAX_VALUE);
 
     RecordedRequest request = server.takeRequest();
@@ -308,7 +302,7 @@
     try {
       readAscii(connection.getInputStream(), Integer.MAX_VALUE);
       fail("Should have timed out!");
-    } catch (SocketTimeoutException expected){
+    } catch (SocketTimeoutException expected) {
       assertEquals("timeout", expected.getMessage());
     }
   }
@@ -381,18 +375,18 @@
     client.client().setCookieHandler(cookieManager);
 
     server.enqueue(new MockResponse()
-        .addHeader("set-cookie: c=oreo; domain=" + server.get().getCookieDomain())
+        .addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain())
         .setBody("A"));
     server.enqueue(new MockResponse()
         .setBody("B"));
 
-    URL url = server.getUrl("/");
-    assertContent("A", client.open(url), Integer.MAX_VALUE);
+    HttpUrl url = server.url("/");
+    assertContent("A", client.open(url.url()), Integer.MAX_VALUE);
     Map<String, List<String>> requestHeaders = Collections.emptyMap();
     assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")),
-        cookieManager.get(url.toURI(), requestHeaders));
+        cookieManager.get(url.uri(), requestHeaders));
 
-    assertContent("B", client.open(url), Integer.MAX_VALUE);
+    assertContent("B", client.open(url.url()), Integer.MAX_VALUE);
     RecordedRequest requestA = server.takeRequest();
     assertNull(requestA.getHeader("Cookie"));
     RecordedRequest requestB = server.takeRequest();
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteExceptionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteExceptionTest.java
index efd0d7a..eeb9564 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteExceptionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteExceptionTest.java
@@ -21,9 +21,6 @@
 
 import static org.junit.Assert.assertSame;
 
-/**
- * Tests for {@link RouteException}.
- */
 public class RouteExceptionTest {
 
   @Test public void getConnectionIOException_single() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
index 63f55e1..a7e6007 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
@@ -57,15 +57,18 @@
     server.setServerSocketFactory(
         new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
           @Override
-          protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+          protected ServerSocket configureServerSocket(ServerSocket serverSocket)
+              throws IOException {
             serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+            return serverSocket;
           }
         });
     client.setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
       @Override
-      protected void configureSocket(Socket socket) throws IOException {
+      protected Socket configureSocket(Socket socket) throws IOException {
         socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
         socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+        return socket;
       }
     });
   }
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
index fcaa9c0..d7f1c78 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/HostnameVerifierTest.java
@@ -293,7 +293,6 @@
     assertTrue(verifier.verify("www.foo.com", session));
     assertTrue(verifier.verify("\u82b1\u5b50.foo.com", session));
     assertFalse(verifier.verify("a.b.foo.com", session));
-    assertFalse(verifier.verify("foo.com.au", session));
   }
 
   @Test public void verifyWilcardCnOnTld() throws Exception {
diff --git a/okhttp-tests/src/test/resources/web-platform-test-results-url-chrome-42.0.json b/okhttp-tests/src/test/resources/web-platform-test-results-url-chrome-42.0.json
deleted file mode 100644
index 60adf69..0000000
--- a/okhttp-tests/src/test/resources/web-platform-test-results-url-chrome-42.0.json
+++ /dev/null
@@ -1,1341 +0,0 @@
-{
-  "results": [
-    {
-      "test": "/url/a-element.html",
-      "subtests": [
-        {
-          "name": "Loading data…",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://user:pass@foo:21/bar;par?b#c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:foo.com> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <\t   :foo.com   \n> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: < foo.com  > against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <a:\t foo.com> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:21/ b ? d # e > against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:0/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000000000080/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:b/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://f: /c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://f:\n/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:fifty-two/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://f:999999/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://f: 21 / b ? d # e > against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <  \t> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:a> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#;?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@c:29/d> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::@c:29> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://&a:foo(b]c@d:2/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://&a:foo(b]c@d:2/\" but got \"http://&a:foo(b%5Dc@d:2/\""
-        },
-        {
-          "name": "Parsing: <http://::@c@d:2> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://::%40c@d:2/\" but got \"http://:%3A%40c@d:2/\""
-        },
-        {
-          "name": "Parsing: <http://foo.com:b@d/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo.com/\\@> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\a\\b:c\\d@foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:/bar.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://///////> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://///////bar.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:////://///> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <c:/foo> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//foo/bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/path;a??e#f#g> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd?efgh?ijkl> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd#foo?bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <[61:24:74]:98> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:[61:27]/:foo> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[1::2]:3:4> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://2001::1> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://2001::1]> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://2001::1]\" but got \"http://2001::1]/\""
-        },
-        {
-          "name": "Parsing: <http://2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://2001::1]:80\" but got \"http://2001::1]/\""
-        },
-        {
-          "name": "Parsing: <http://[2001::1]> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/b/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/ /c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a%2fc> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/%2f/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#β> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:text/html,test#test> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:c:\\foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:/foo/bar.html\" but got \"/tmp/mock/c:/foo/bar.html\""
-        },
-        {
-          "name": "Parsing: <  File:c|////foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:////foo/bar.html\" but got \"/tmp/mock/c%7C////foo/bar.html\""
-        },
-        {
-          "name": "Parsing: <C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/tmp/mock/C%7C/foo/bar\""
-        },
-        {
-          "name": "Parsing: </C|\\foo\\bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/C%7C/foo/bar\""
-        },
-        {
-          "name": "Parsing: <//C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"\" but got \"c%7C\""
-        },
-        {
-          "name": "Parsing: <//server/file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <\\\\server\\file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </\\server/file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:///foo/bar.txt> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:///home/me> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://localhost> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://localhost/> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://localhost/test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/././foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/./.foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/.> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/./> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/..bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton/../../a> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e%2> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/%2e%2\" but got \"/foo/.%2\""
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/%2e.bar\" but got \"/..bar\""
-        },
-        {
-          "name": "Parsing: <http://example.com////../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%20foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2©zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%41%7a> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo%41%7a\" but got \"/fooAz\""
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\t‘%91> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%00%51> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://example.com/(%28:%3A%29)> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%3A%3a%3C%3c> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\tbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com\\\\foo\\\\bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/@asdf%40> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/你好你好> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‥/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com//foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‮/foo/‭/bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz#> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:test# »> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: hash expected \"# »\" but got \"# %C2%BB\""
-        },
-        {
-          "name": "Parsing: <http://[www.google.com]/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://192.0x00A80001> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www/foo%2Ehtml> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo%2Ehtml\" but got \"/foo.html\""
-        },
-        {
-          "name": "Parsing: <http://www/foo/%2E/html> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://user:pass@/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%25DOMAIN:foobar@foodomain.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\www.google.com\\foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <httpa://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:-80/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <https://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:21/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher://foo:70/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@pple.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http:/:@/www.example.com\" but got \"http:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <http://user@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http:@/www.example.com\" but got \"http:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <http:/@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http:/@/www.example.com\" but got \"http:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <http://@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://@/www.example.com\" but got \"http:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <https:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"https:@/www.example.com\" but got \"https:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <http:a:b@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http:a:b@/www.example.com\" but got \"http://a:b@/www.example.com\""
-        },
-        {
-          "name": "Parsing: <http:/a:b@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http:/a:b@/www.example.com\" but got \"http://a:b@/www.example.com\""
-        },
-        {
-          "name": "Parsing: <http://a:b@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http::@/www.example.com\" but got \"http:///www.example.com\""
-        },
-        {
-          "name": "Parsing: <http:a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http:/a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http://a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http://www.@pple.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http:/@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"0\""
-        },
-        {
-          "name": "Parsing: <http://:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://:@www.example.com/\" but got \"http://www.example.com/\""
-        },
-        {
-          "name": "Parsing: </> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <.> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <..> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <./test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../aaa/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <中/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://ExAmPlE.CoM> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example example.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://Goo%20 goo%7C|.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://GOO  goo.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://GOO​⁠goo.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.foo。bar.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://﷐zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://﷐zyx.com\" but got \"http://%EF%BF%BDzyx.com/\""
-        },
-        {
-          "name": "Parsing: <http://%ef%b7%90zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%ef%b7%90zyx.com\" but got \"http://%EF%BF%BDzyx.com/\""
-        },
-        {
-          "name": "Parsing: <http://Go.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%41.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%94%ef%bc%91.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%00.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%00.com\" but got \"http://%00.com/\""
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%90%ef%bc%90.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%ef%bc%85%ef%bc%90%ef%bc%90.com\" but got \"http://%00.com/\""
-        },
-        {
-          "name": "Parsing: <http://你好你好> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%zz%66%a.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%zz%66%a.com\" but got \"http://%25zzf%25a.com/\""
-        },
-        {
-          "name": "Parsing: <http://%25> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%25\" but got \"http://%25/\""
-        },
-        {
-          "name": "Parsing: <http://hello%00> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://hello%00\" but got \"http://hello%00/\""
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01%2e> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"0xc0.0250.01.\" but got \"192.168.0.1\""
-        },
-        {
-          "name": "Parsing: <http://192.168.0.257> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://192.168.0.257\" but got \"http://192.168.0.257/\""
-        },
-        {
-          "name": "Parsing: <http://%3g%78%63%30%2e%30%32%35%30%2E.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://%3g%78%63%30%2e%30%32%35%30%2E.01\" but got \"http://%253gxc0.0250..01/\""
-        },
-        {
-          "name": "Parsing: <http://192.168.0.1 hello> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://0Xc0.0250.01> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[google.com]> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://[google.com]\" but got \"http://[google.com]/\""
-        },
-        {
-          "name": "Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <x> against <test:test>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"x\" but got \"\""
-        }
-      ],
-      "status": "OK",
-      "message": null
-    }
-  ]
-}
diff --git a/okhttp-tests/src/test/resources/web-platform-test-results-url-firefox-37.0.json b/okhttp-tests/src/test/resources/web-platform-test-results-url-firefox-37.0.json
deleted file mode 100644
index 750ad4e..0000000
--- a/okhttp-tests/src/test/resources/web-platform-test-results-url-firefox-37.0.json
+++ /dev/null
@@ -1,1341 +0,0 @@
-{
-  "results": [
-    {
-      "test": "/url/a-element.html",
-      "subtests": [
-        {
-          "name": "Loading data…",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://user:pass@foo:21/bar;par?b#c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:foo.com> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <\t   :foo.com   \n> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: < foo.com  > against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <a:\t foo.com> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \" foo.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http://f:21/ b ? d # e > against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://f:21/%20b%20?%20d%20# e\" but got \"http://f:21/%20b%20?%20d%20#%20e\""
-        },
-        {
-          "name": "Parsing: <http://f:/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:0/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000000000080/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:b/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://f: /c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://f:\n/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:fifty-two/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://f:999999/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f: 21 / b ? d # e > against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <  \t> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com\\> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/:foo.com/\" but got \"/foo/:foo.com%5C\""
-        },
-        {
-          "name": "Parsing: <:> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:a> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:\\> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/:/\" but got \"/foo/:%5C\""
-        },
-        {
-          "name": "Parsing: <:#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#;?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"//\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http://a:b@c:29/d> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::@c:29> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/:@c:29\" but got \"/foo/http::@c:29\""
-        },
-        {
-          "name": "Parsing: <http://&a:foo(b]c@d:2/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://&a:foo(b]c@d:2/\" but got \"http://&a:foo(b%5Dc@d:2/\""
-        },
-        {
-          "name": "Parsing: <http://::@c@d:2> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"d\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http://foo.com:b@d/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://foo.com:b@d/\" but got \"http://foo%2Ecom:b@d/\""
-        },
-        {
-          "name": "Parsing: <http://foo.com/\\@> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"//@\" but got \"/%5C@\""
-        },
-        {
-          "name": "Parsing: <http:\\\\foo.com\\> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"foo.com\" but got \"example.org\""
-        },
-        {
-          "name": "Parsing: <http:\\\\a\\b:c\\d@foo.com\\> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"a\" but got \"example.org\""
-        },
-        {
-          "name": "Parsing: <foo:/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <foo:/bar.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/bar.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <foo://///////> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/////////\" but got \"\""
-        },
-        {
-          "name": "Parsing: <foo://///////bar.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/////////bar.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <foo:////://///> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"////://///\" but got \"\""
-        },
-        {
-          "name": "Parsing: <c:/foo> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo\" but got \"\""
-        },
-        {
-          "name": "Parsing: <//foo/bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/path;a??e#f#g> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd?efgh?ijkl> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd#foo?bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <[61:24:74]:98> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/[61:24:74]:98\" but got \"/foo/%5B61:24:74%5D:98\""
-        },
-        {
-          "name": "Parsing: <http:[61:27]/:foo> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/[61:27]/:foo\" but got \"/foo/%5B61:27%5D/:foo\""
-        },
-        {
-          "name": "Parsing: <http://[1::2]:3:4> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://2001::1> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://2001::1]> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://[2001::1]> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"data:\" but got \"http:\""
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"data:\" but got \"http:\""
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: </a/b/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/ /c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a%2fc> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/%2f/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#β> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://example.org/foo/bar#β\" but got \"http://example.org/foo/bar#%CE%B2\""
-        },
-        {
-          "name": "Parsing: <data:text/html,test#test> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"text/html,test\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file:c:\\foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:/foo/bar.html\" but got \"/tmp/mock/c:%5Cfoo%5Cbar.html\""
-        },
-        {
-          "name": "Parsing: <  File:c|////foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:////foo/bar.html\" but got \"/tmp/mock/c|////foo%5Cbar.html\""
-        },
-        {
-          "name": "Parsing: <C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/tmp/mock/C|/foo/bar\""
-        },
-        {
-          "name": "Parsing: </C|\\foo\\bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/C|%5Cfoo%5Cbar\""
-        },
-        {
-          "name": "Parsing: <//C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/foo/bar\""
-        },
-        {
-          "name": "Parsing: <//server/file> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"server\" but got \"\""
-        },
-        {
-          "name": "Parsing: <\\\\server\\file> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"server\" but got \"\""
-        },
-        {
-          "name": "Parsing: </\\server/file> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"server\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file:///foo/bar.txt> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:///home/me> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://test> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"test\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file://localhost> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file://localhost/> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file://localhost/test> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/././foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/./.foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/.> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/./> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/..bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton/../../a> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/\" but got \"/foo/%2e\""
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e%2> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com////../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%20foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2©zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%41%7a> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\t‘%91> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%00%51> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/(%28:%3A%29)> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%3A%3a%3C%3c> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\tbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com\\\\foo\\\\bar> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.com\" but got \"example.com\\\\\\foo\\\\bar\""
-        },
-        {
-          "name": "Parsing: <http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/@asdf%40> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/你好你好> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‥/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com//foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‮/foo/‭/bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz#> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://www.google.com/foo?bar=baz# »\" but got \"http://www.google.com/foo?bar=baz#%20%C2%BB\""
-        },
-        {
-          "name": "Parsing: <data:test# »> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"data:\" but got \"http:\""
-        },
-        {
-          "name": "Parsing: <http://[www.google.com]/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://www.google.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://192.0x00A80001> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"192.0x00a80001\""
-        },
-        {
-          "name": "Parsing: <http://www/foo%2Ehtml> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www/foo/%2E/html> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://user:pass@/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%25DOMAIN:foobar@foodomain.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\www.google.com\\foo> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"www.google.com\" but got \"\\\\\\www.google.com\\foo\""
-        },
-        {
-          "name": "Parsing: <http://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <httpa://foo:80/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"//foo:80/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http://foo:-80/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <https://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:21/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher://foo:70/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"foo\" but got \"\""
-        },
-        {
-          "name": "Parsing: <gopher://foo:443/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"foo\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ws://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"data:\" but got \"http:\""
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"data:\" but got \"http:\""
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"example.com/\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@pple.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::b@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"www.example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http:/:b@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"www.example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http://:b@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"www.example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: <http:/:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://user@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:/@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <https:@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:a:b@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:/a:b@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://a:b@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http::@/www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:a:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/a:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.@pple.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://www.@pple.com/\" but got \"http://www%2E@pple.com/\""
-        },
-        {
-          "name": "Parsing: <http:@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http:/@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://@:www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"www.example.com\" but got \"\""
-        },
-        {
-          "name": "Parsing: </> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <.> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <..> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <./test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../aaa/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <中/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://ExAmPlE.CoM> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example example.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://Goo%20 goo%7C|.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://GOO  goo.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://GOO​⁠goo.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.foo。bar.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://﷐zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%b7%90zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://Go.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%41.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%94%ef%bc%91.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%00.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%90%ef%bc%90.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://你好你好> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"xn--6qqa088eba\" but got \"你好你好\""
-        },
-        {
-          "name": "Parsing: <http://%zz%66%a.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%25> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://hello%00> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"%30%78%63%30%2e%30%32%35%30.01\""
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01%2e> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"0xc0.0250.01.\" but got \"%30%78%63%30%2e%30%32%35%30.01%2e\""
-        },
-        {
-          "name": "Parsing: <http://192.168.0.257> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%3g%78%63%30%2e%30%32%35%30%2E.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://192.168.0.1 hello> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://0Xc0.0250.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"0xc0.0250.01\""
-        },
-        {
-          "name": "Parsing: <http://[google.com]> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <x> against <test:test>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        }
-      ],
-      "status": "OK",
-      "message": null
-    }
-  ]
-}
\ No newline at end of file
diff --git a/okhttp-tests/src/test/resources/web-platform-test-results-url-safari-7.1.json b/okhttp-tests/src/test/resources/web-platform-test-results-url-safari-7.1.json
deleted file mode 100644
index de3b5d3..0000000
--- a/okhttp-tests/src/test/resources/web-platform-test-results-url-safari-7.1.json
+++ /dev/null
@@ -1,1341 +0,0 @@
-{
-  "results": [
-    {
-      "test": "/url/a-element.html",
-      "subtests": [
-        {
-          "name": "Loading data…",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example\t.\norg> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://user:pass@foo:21/bar;par?b#c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:foo.com> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <\t   :foo.com   \n> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: < foo.com  > against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <a:\t foo.com> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:21/ b ? d # e > against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:0/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://f:0/c\" but got \"http://f:00000000000000/c\""
-        },
-        {
-          "name": "Parsing: <http://f:00000000000000000000080/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"\" but got \"80\""
-        },
-        {
-          "name": "Parsing: <http://f:b/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f: /c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:\n/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://f:fifty-two/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://f:999999/c> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: port expected \"999999\" but got \"65535\""
-        },
-        {
-          "name": "Parsing: <http://f: 21 / b ? d # e > against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://f: 21 / b ? d # e \" but got \"http://f: 21 / b ? d # e\""
-        },
-        {
-          "name": "Parsing: <> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <  \t> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:a> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#;?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <?> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </:23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <::23> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@c:29/d> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::@c:29> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://&a:foo(b]c@d:2/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://::@c@d:2> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <http://foo.com:b@d/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo.com/\\@> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\a\\b:c\\d@foo.com\\> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:/bar.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://///////> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo://///////bar.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <foo:////://///> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <c:/foo> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//foo/bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/path;a??e#f#g> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd?efgh?ijkl> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo/abcd#foo?bar> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <[61:24:74]:98> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:[61:27]/:foo> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[1::2]:3:4> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://2001::1> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://2001::1]> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[2001::1]> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://[2001::1]:80> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"example.org\" but got \"example.com\""
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/b/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/ /c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a%2fc> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </a/%2f/c> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <#β> against <http://example.org/foo/bar>",
-          "status": "FAIL",
-          "message": "assert_equals: hash expected \"#β\" but got \"#%CE%B2\""
-        },
-        {
-          "name": "Parsing: <data:text/html,test#test> against <http://example.org/foo/bar>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:c:\\foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:/foo/bar.html\" but got \"/tmp/mock/c:/foo/bar.html\""
-        },
-        {
-          "name": "Parsing: <  File:c|////foo\\bar.html> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/c:////foo/bar.html\" but got \"/tmp/mock/c|////foo/bar.html\""
-        },
-        {
-          "name": "Parsing: <C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/tmp/mock/C|/foo/bar\""
-        },
-        {
-          "name": "Parsing: </C|\\foo\\bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/C:/foo/bar\" but got \"/C|/foo/bar\""
-        },
-        {
-          "name": "Parsing: <//C|/foo/bar> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"file:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <//server/file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <\\\\server\\file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </\\server/file> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:///foo/bar.txt> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:///home/me> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <///test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file://localhost> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file://localhost/> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <file://localhost/test> against <file:///tmp/mock/path>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"localhost\" but got \"\""
-        },
-        {
-          "name": "Parsing: <test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:test> against <file:///tmp/mock/path>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/././foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/./.foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/.> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/./> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/..bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar/../ton/../../a> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/../../../ton> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/\" but got \"/foo/%2e\""
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e%2> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/%2e.bar\" but got \"/foo/%2e./%2e%2e/.%2e/%2e.bar\""
-        },
-        {
-          "name": "Parsing: <http://example.com////../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//../..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo/bar//..> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%20foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%2©zbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%41%7a> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\t‘%91> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo%00%51> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/(%28:%3A%29)> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%3A%3a%3C%3c> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/foo\tbar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com\\\\foo\\\\bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/@asdf%40> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/你好你好> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‥/foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com//foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example.com/‮/foo/‭/bar> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz#> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com/foo?bar=baz# »> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: hash expected \"# »\" but got \"# %C2%BB\""
-        },
-        {
-          "name": "Parsing: <data:test# »> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: hash expected \"# »\" but got \"# %C2%BB\""
-        },
-        {
-          "name": "Parsing: <http://[www.google.com]/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.google.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://192.0x00A80001> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"192.0x00a80001\""
-        },
-        {
-          "name": "Parsing: <http://www/foo%2Ehtml> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www/foo/%2E/html> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: path expected \"/foo/html\" but got \"/foo/%2E/html\""
-        },
-        {
-          "name": "Parsing: <http://user:pass@/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%25DOMAIN:foobar@foodomain.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:\\\\www.google.com\\foo> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <httpa://foo:80/> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"\" but got \"foo\""
-        },
-        {
-          "name": "Parsing: <http://foo:-80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:21/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher://foo:70/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:80/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:81/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:443/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss://foo:815/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <file:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:/example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftp:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <madeupscheme:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ftps:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <gopher:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <ws:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <wss:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <data:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <javascript:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <mailto:example.com/> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@pple.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://:b@www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/:@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://user@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <https:@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:a:b@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/a:b@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://a:b@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http::@/www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http:/a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http://a:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://a:@www.example.com/\" but got \"http://a@www.example.com/\""
-        },
-        {
-          "name": "Parsing: <http://www.@pple.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:@:www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http:/@:www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://@:www.example.com> against <about:blank>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://:@www.example.com> against <about:blank>",
-          "status": "FAIL",
-          "message": "assert_equals: href expected \"http://:@www.example.com/\" but got \"http://www.example.com/\""
-        },
-        {
-          "name": "Parsing: </> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: </test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <.> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <..> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <./test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../aaa/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <../../test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <中/test.txt> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <//www.example2.com> against <http://www.example.com/test>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://ExAmPlE.CoM> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://example example.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://Goo%20 goo%7C|.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://GOO  goo.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://GOO​⁠goo.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://www.foo。bar.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://﷐zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%b7%90zyx.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://Go.com> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%41.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%94%ef%bc%91.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%00.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%ef%bc%85%ef%bc%90%ef%bc%90.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://你好你好> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://%zz%66%a.com> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%25> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://hello%00> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"%30%78%63%30%2e%30%32%35%30.01\""
-        },
-        {
-          "name": "Parsing: <http://%30%78%63%30%2e%30%32%35%30.01%2e> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"0xc0.0250.01.\" but got \"%30%78%63%30%2e%30%32%35%30.01%2e\""
-        },
-        {
-          "name": "Parsing: <http://192.168.0.257> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://%3g%78%63%30%2e%30%32%35%30%2E.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_unreached: Expected URL to fail parsing Reached unreachable code"
-        },
-        {
-          "name": "Parsing: <http://192.168.0.1 hello> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://0Xc0.0250.01> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: host expected \"192.168.0.1\" but got \"0xc0.0250.01\""
-        },
-        {
-          "name": "Parsing: <http://[google.com]> against <http://other.com/>",
-          "status": "PASS",
-          "message": null
-        },
-        {
-          "name": "Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>",
-          "status": "FAIL",
-          "message": "assert_equals: scheme expected \"http:\" but got \":\""
-        },
-        {
-          "name": "Parsing: <x> against <test:test>",
-          "status": "PASS",
-          "message": null
-        }
-      ],
-      "status": "OK",
-      "message": null
-    }
-  ]
-}
diff --git a/okhttp-urlconnection/pom.xml b/okhttp-urlconnection/pom.xml
index a093827..be60560 100644
--- a/okhttp-urlconnection/pom.xml
+++ b/okhttp-urlconnection/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-urlconnection</artifactId>
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
index d09e971..1cddd3e 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
@@ -20,6 +20,7 @@
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.Handshake;
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
@@ -28,14 +29,15 @@
 import com.squareup.okhttp.Route;
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.http.RouteException;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
 import com.squareup.okhttp.internal.http.HttpDate;
 import com.squareup.okhttp.internal.http.HttpEngine;
 import com.squareup.okhttp.internal.http.HttpMethod;
 import com.squareup.okhttp.internal.http.OkHeaders;
 import com.squareup.okhttp.internal.http.RequestException;
 import com.squareup.okhttp.internal.http.RetryableSink;
+import com.squareup.okhttp.internal.http.RouteException;
 import com.squareup.okhttp.internal.http.StatusLine;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -44,10 +46,12 @@
 import java.net.HttpRetryException;
 import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
 import java.net.ProtocolException;
 import java.net.Proxy;
 import java.net.SocketPermission;
 import java.net.URL;
+import java.net.UnknownHostException;
 import java.security.Permission;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -255,8 +259,11 @@
   }
 
   @Override public final Permission getPermission() throws IOException {
-    String hostName = getURL().getHost();
-    int hostPort = Util.getEffectivePort(getURL());
+    URL url = getURL();
+    String hostName = url.getHost();
+    int hostPort = url.getPort() != -1
+        ? url.getPort()
+        : HttpUrl.defaultPort(url.getProtocol());
     if (usingProxy()) {
       InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address();
       hostName = proxyAddress.getHostName();
@@ -316,14 +323,16 @@
     }
   }
 
-  private HttpEngine newHttpEngine(String method, Connection connection,
-      RetryableSink requestBody, Response priorResponse) {
+  private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody,
+      Response priorResponse) throws MalformedURLException, UnknownHostException {
     // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
     RequestBody placeholderBody = HttpMethod.requiresRequestBody(method)
         ? EMPTY_REQUEST_BODY
         : null;
+    URL url = getURL();
+    HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString());
     Request.Builder builder = new Request.Builder()
-        .url(getURL())
+        .url(httpUrl)
         .method(method, placeholderBody);
     Headers headers = requestHeaders.build();
     for (int i = 0, size = headers.size(); i < size; i++) {
@@ -365,7 +374,7 @@
 
   private String defaultUserAgent() {
     String agent = System.getProperty("http.agent");
-    return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+    return agent != null ? Util.toHumanReadableAscii(agent) : Version.userAgent();
   }
 
   /**
@@ -413,7 +422,7 @@
         throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
       }
 
-      if (!httpEngine.sameConnection(followUp.url())) {
+      if (!httpEngine.sameConnection(followUp.httpUrl())) {
         httpEngine.releaseConnection();
       }
 
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
index 75f7158..2aba087 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
@@ -63,19 +63,15 @@
     return delegate.client.getSslSocketFactory();
   }
 
-  // ANDROID-BEGIN
-  //  @Override public long getContentLengthLong() {
-  //    return delegate.getContentLengthLong();
-  //  }
-  // ANDROID-END
+  @Override public long getContentLengthLong() {
+    return delegate.getContentLengthLong();
+  }
 
   @Override public void setFixedLengthStreamingMode(long contentLength) {
     delegate.setFixedLengthStreamingMode(contentLength);
   }
 
-  // ANDROID-BEGIN
-  // @Override public long getHeaderFieldLong(String field, long defaultValue) {
-  //   return delegate.getHeaderFieldLong(field, defaultValue);
-  // }
-  // ANDROID-END
+  @Override public long getHeaderFieldLong(String field, long defaultValue) {
+    return delegate.getHeaderFieldLong(field, defaultValue);
+  }
 }
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
index a7dc44b..ab792b9 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
@@ -1,9 +1,11 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import java.io.File;
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.text.DateFormat;
@@ -15,7 +17,6 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
 import static java.nio.charset.StandardCharsets.US_ASCII;
 import static okio.Okio.buffer;
@@ -24,17 +25,14 @@
 import static org.junit.Assert.fail;
 
 public class OkUrlFactoryTest {
-  @Rule public MockWebServerRule serverRule = new MockWebServerRule();
-  @Rule public TemporaryFolder cacheFolder = new TemporaryFolder();
+  @Rule public MockWebServer server = new MockWebServer();
 
-  private MockWebServer server;
+  private FileSystem fileSystem = new InMemoryFileSystem();
   private OkUrlFactory factory;
 
   @Before public void setUp() throws IOException {
-    server = serverRule.get();
-
     OkHttpClient client = new OkHttpClient();
-    client.setCache(new Cache(cacheFolder.getRoot(), 10 * 1024 * 1024));
+    client.setCache(new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem));
     factory = new OkUrlFactory(client);
   }
 
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
index db0ed8f..0af815b 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
@@ -19,24 +19,11 @@
 import com.squareup.okhttp.internal.Internal;
 import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.io.InMemoryFileSystem;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
-import okio.Buffer;
-import okio.BufferedSink;
-import okio.GzipSink;
-import okio.Okio;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -62,6 +49,18 @@
 import java.util.Locale;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
 
 import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
 import static org.junit.Assert.assertEquals;
@@ -80,23 +79,18 @@
     }
   };
 
-  private static final SSLContext sslContext = SslContextBuilder.localhost();
+  @Rule public MockWebServer server = new MockWebServer();
+  @Rule public MockWebServer server2 = new MockWebServer();
 
-  @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
-  @Rule public MockWebServerRule serverRule = new MockWebServerRule();
-  @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
-
+  private final SSLContext sslContext = SslContextBuilder.localhost();
+  private final FileSystem fileSystem = new InMemoryFileSystem();
   private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
-  private MockWebServer server;
-  private MockWebServer server2;
   private Cache cache;
   private final CookieManager cookieManager = new CookieManager();
 
   @Before public void setUp() throws Exception {
-    server = serverRule.get();
     server.setProtocolNegotiationEnabled(false);
-    server2 = server2Rule.get();
-    cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE);
+    cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
     client.client().setCache(cache);
     CookieHandler.setDefault(cookieManager);
   }
@@ -1645,7 +1639,7 @@
     writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata);
     writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
     writeFile(cache.getDirectory(), "journal", journalBody);
-    cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE);
+    cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE, fileSystem);
     client.client().setCache(cache);
 
     HttpURLConnection connection = client.open(url);
@@ -1655,7 +1649,7 @@
   }
 
   private void writeFile(File directory, String file, String content) throws IOException {
-    BufferedSink sink = Okio.buffer(Okio.sink(new File(directory, file)));
+    BufferedSink sink = Okio.buffer(fileSystem.sink(new File(directory, file)));
     sink.writeUtf8(content);
     sink.close();
   }
diff --git a/okhttp-ws-tests/pom.xml b/okhttp-ws-tests/pom.xml
index af4ea7e..c7d1778 100644
--- a/okhttp-ws-tests/pom.xml
+++ b/okhttp-ws-tests/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-ws-tests</artifactId>
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
index 037903c..a592624 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
@@ -69,8 +69,7 @@
           private final ExecutorService sendExecutor = Executors.newSingleThreadExecutor();
           private WebSocket webSocket;
 
-          @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-              throws IOException {
+          @Override public void onOpen(WebSocket webSocket, Response response) {
             System.out.println("Executing test case " + number + "/" + count);
             this.webSocket = webSocket;
           }
@@ -100,7 +99,7 @@
             latch.countDown();
           }
 
-          @Override public void onFailure(IOException e) {
+          @Override public void onFailure(IOException e, Response response) {
             latch.countDown();
           }
         });
@@ -118,8 +117,7 @@
     final AtomicLong countRef = new AtomicLong();
     final AtomicReference<IOException> failureRef = new AtomicReference<>();
     newWebSocket("/getCaseCount").enqueue(new WebSocketListener() {
-      @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-          throws IOException {
+      @Override public void onOpen(WebSocket webSocket, Response response) {
       }
 
       @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
@@ -135,7 +133,7 @@
         latch.countDown();
       }
 
-      @Override public void onFailure(IOException e) {
+      @Override public void onFailure(IOException e, Response response) {
         failureRef.set(e);
         latch.countDown();
       }
@@ -157,8 +155,7 @@
   private void updateReports() {
     final CountDownLatch latch = new CountDownLatch(1);
     newWebSocket("/updateReports?agent=" + Version.userAgent()).enqueue(new WebSocketListener() {
-      @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-          throws IOException {
+      @Override public void onOpen(WebSocket webSocket, Response response) {
       }
 
       @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
@@ -172,7 +169,7 @@
         latch.countDown();
       }
 
-      @Override public void onFailure(IOException e) {
+      @Override public void onFailure(IOException e, Response response) {
         latch.countDown();
       }
     });
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
index 63d21cb..895eb1f 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
@@ -18,14 +18,17 @@
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.SslContextBuilder;
 import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
 import java.io.IOException;
 import java.net.ProtocolException;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import javax.net.ssl.SSLContext;
 import okio.Buffer;
 import okio.BufferedSink;
 import okio.BufferedSource;
@@ -36,8 +39,9 @@
 import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
 
 public final class WebSocketCallTest {
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
+  @Rule public final MockWebServer server = new MockWebServer();
 
+  private final SSLContext sslContext = SslContextBuilder.localhost();
   private final WebSocketRecorder listener = new WebSocketRecorder();
   private final OkHttpClient client = new OkHttpClient();
   private final Random random = new Random(0);
@@ -66,9 +70,16 @@
 
   @Test public void serverMessage() throws IOException {
     WebSocketListener serverListener = new EmptyWebSocketListener() {
-      @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-          throws IOException {
-        webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+      @Override public void onOpen(final WebSocket webSocket, Response response) {
+        new Thread() {
+          @Override public void run() {
+            try {
+              webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+            } catch (IOException e) {
+              throw new AssertionError(e);
+            }
+          }
+        }.start();
       }
     };
     server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
@@ -92,12 +103,19 @@
 
   @Test public void serverStreamingMessage() throws IOException {
     WebSocketListener serverListener = new EmptyWebSocketListener() {
-      @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-          throws IOException {
-        BufferedSink sink = webSocket.newMessageSink(TEXT);
-        sink.writeUtf8("Hello, ").flush();
-        sink.writeUtf8("WebSockets!").flush();
-        sink.close();
+      @Override public void onOpen(final WebSocket webSocket, Response response) {
+        new Thread() {
+          @Override public void run() {
+            try {
+              BufferedSink sink = webSocket.newMessageSink(TEXT);
+              sink.writeUtf8("Hello, ").flush();
+              sink.writeUtf8("WebSockets!").flush();
+              sink.close();
+            } catch (IOException e) {
+              throw new AssertionError(e);
+            }
+          }
+        }.start();
       }
     };
     server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
@@ -130,7 +148,8 @@
   }
 
   @Test public void wrongConnectionHeader() {
-    server.enqueue(new MockResponse().setResponseCode(101)
+    server.enqueue(new MockResponse()
+        .setResponseCode(101)
         .setHeader("Upgrade", "websocket")
         .setHeader("Connection", "Downgrade")
         .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
@@ -181,8 +200,48 @@
         "Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'magic'");
   }
 
+  @Test public void wsScheme() throws IOException {
+    websocketScheme("ws");
+  }
+
+  @Test public void wsUppercaseScheme() throws IOException {
+    websocketScheme("WS");
+  }
+
+  @Test public void wssScheme() throws IOException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    websocketScheme("wss");
+  }
+
+  @Test public void httpsScheme() throws IOException {
+    server.useHttps(sslContext.getSocketFactory(), false);
+    client.setSslSocketFactory(sslContext.getSocketFactory());
+    client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+    websocketScheme("https");
+  }
+
+  private void websocketScheme(String scheme) throws IOException {
+    WebSocketRecorder serverListener = new WebSocketRecorder();
+    server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+    Request request1 = new Request.Builder()
+        .url(scheme + "://" + server.getHostName() + ":" + server.getPort() + "/")
+        .build();
+
+    WebSocket webSocket = awaitWebSocket(request1);
+    webSocket.sendMessage(TEXT, new Buffer().writeUtf8("abc"));
+    serverListener.assertTextMessage("abc");
+  }
+
   private WebSocket awaitWebSocket() {
-    Request request = new Request.Builder().get().url(server.getUrl("/")).build();
+    return awaitWebSocket(new Request.Builder().get().url(server.url("/")).build());
+  }
+
+  private WebSocket awaitWebSocket(Request request) {
     WebSocketCall call = new WebSocketCall(client, request, random);
 
     final AtomicReference<Response> responseRef = new AtomicReference<>();
@@ -190,8 +249,7 @@
     final AtomicReference<IOException> failureRef = new AtomicReference<>();
     final CountDownLatch latch = new CountDownLatch(1);
     call.enqueue(new WebSocketListener() {
-      @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-          throws IOException {
+      @Override public void onOpen(WebSocket webSocket, Response response) {
         webSocketRef.set(webSocket);
         responseRef.set(response);
         latch.countDown();
@@ -210,8 +268,8 @@
         listener.onClose(code, reason);
       }
 
-      @Override public void onFailure(IOException e) {
-        listener.onFailure(e);
+      @Override public void onFailure(IOException e, Response response) {
+        listener.onFailure(e, null);
         failureRef.set(e);
         latch.countDown();
       }
@@ -229,8 +287,7 @@
   }
 
   private static class EmptyWebSocketListener implements WebSocketListener {
-    @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-        throws IOException {
+    @Override public void onOpen(WebSocket webSocket, Response response) {
     }
 
     @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
@@ -243,7 +300,7 @@
     @Override public void onClose(int code, String reason) {
     }
 
-    @Override public void onFailure(IOException e) {
+    @Override public void onFailure(IOException e, Response response) {
     }
   }
 }
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
index 551cd91..56b3810 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
@@ -15,7 +15,6 @@
  */
 package com.squareup.okhttp.ws;
 
-import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.ws.WebSocketReader;
 import java.io.IOException;
@@ -44,7 +43,7 @@
     this.delegate = delegate;
   }
 
-  @Override public void onOpen(WebSocket webSocket, Request request, Response response) {
+  @Override public void onOpen(WebSocket webSocket, Response response) {
   }
 
   @Override public void onMessage(BufferedSource source, WebSocket.PayloadType type)
@@ -72,7 +71,7 @@
     events.add(new Close(code, reason));
   }
 
-  @Override public void onFailure(IOException e) {
+  @Override public void onFailure(IOException e, Response response) {
     events.add(e);
   }
 
@@ -109,7 +108,7 @@
   }
 
   public void assertClose(int code, String reason) {
-      assertEquals(new Close(code, reason), nextEvent());
+    assertEquals(new Close(code, reason), nextEvent());
   }
 
   public void assertFailure(Class<? extends IOException> cls, String message) {
diff --git a/okhttp-ws/pom.xml b/okhttp-ws/pom.xml
index ae34464..81f8afd 100644
--- a/okhttp-ws/pom.xml
+++ b/okhttp-ws/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp-ws</artifactId>
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
index 07d763b..8d6b7c4 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
@@ -181,7 +181,7 @@
     } catch (IOException ignored) {
     }
 
-    listener.onFailure(e);
+    listener.onFailure(e, null);
   }
 
   /** Perform any tear-down work on the connection (close the socket, recycle, etc.). */
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
index b499485..46ee8a1 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
@@ -22,7 +22,6 @@
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.NamedRunnable;
 import com.squareup.okhttp.internal.Util;
 import com.squareup.okhttp.internal.ws.RealWebSocket;
 import com.squareup.okhttp.internal.ws.WebSocketProtocol;
@@ -61,19 +60,6 @@
     if (!"GET".equals(request.method())) {
       throw new IllegalArgumentException("Request must be GET: " + request.method());
     }
-    String url = request.urlString();
-    String httpUrl;
-    if (url.startsWith("ws://")) {
-      httpUrl = "http://" + url.substring(5);
-    } else if (url.startsWith("wss://")) {
-      httpUrl = "https://" + url.substring(6);
-    } else if (url.startsWith("http://") || url.startsWith("https://")) {
-      httpUrl = url;
-    } else {
-      throw new IllegalArgumentException(
-          "Request url must use 'ws', 'wss', 'http', or 'https' scheme: " + url);
-    }
-
     this.random = random;
 
     byte[] nonce = new byte[16];
@@ -87,7 +73,6 @@
     client.setProtocols(Collections.singletonList(com.squareup.okhttp.Protocol.HTTP_1_1));
 
     request = request.newBuilder()
-        .url(httpUrl)
         .header("Upgrade", "websocket")
         .header("Connection", "Upgrade")
         .header("Sec-WebSocket-Key", key)
@@ -116,12 +101,12 @@
         try {
           createWebSocket(response, listener);
         } catch (IOException e) {
-          listener.onFailure(e);
+          listener.onFailure(e, response);
         }
       }
 
       @Override public void onFailure(Request request, IOException e) {
-        listener.onFailure(e);
+        listener.onFailure(e, null);
       }
     };
     // TODO call.enqueue(responseCallback, true);
@@ -181,15 +166,10 @@
     // TODO connection.setOwner(webSocket);
     Internal.instance.connectionSetOwner(connection, webSocket);
 
-    listener.onOpen(webSocket, request, response);
+    listener.onOpen(webSocket, response);
 
-    // Start a dedicated thread for reading the web socket.
-    new Thread(new NamedRunnable("OkHttp WebSocket reader %s", request.urlString()) {
-      @Override protected void execute() {
-        while (webSocket.readMessage()) {
-        }
-      }
-    }).start();
+    while (webSocket.readMessage()) {
+    }
   }
 
   // Keep static so that the WebSocketCall instance can be garbage collected.
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
index a113eed..8941b74 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
@@ -15,7 +15,6 @@
  */
 package com.squareup.okhttp.ws;
 
-import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import java.io.IOException;
 import okio.Buffer;
@@ -25,7 +24,24 @@
 
 /** Listener for server-initiated messages on a connected {@link WebSocket}. */
 public interface WebSocketListener {
-  void onOpen(WebSocket webSocket, Request request, Response response) throws IOException;
+  /**
+   * Called when the request has successfully been upgraded to a web socket. This method is called
+   * on the message reading thread to allow setting up any state before the
+   * {@linkplain #onMessage message}, {@linkplain #onPong pong}, and {@link #onClose close}
+   * callbacks start.
+   * <p>
+   * <b>Do not</b> use this callback to write to the web socket. Start a new thread or use
+   * another thread in your application.
+   */
+  void onOpen(WebSocket webSocket, Response response);
+
+  /**
+   * Called when the transport or protocol layer of this web socket errors during communication.
+   *
+   * @param response Present when the failure is a direct result of the response (e.g., failed
+   * upgrade, non-101 response code, etc.). {@code null} otherwise.
+   */
+  void onFailure(IOException e, Response response);
 
   /**
    * Called when a server message is received. The {@code type} indicates whether the
@@ -53,7 +69,4 @@
    * @param reason Reason for close or an empty string.
    */
   void onClose(int code, String reason);
-
-  /** Called when the transport or protocol layer of this web socket errors during communication. */
-  void onFailure(IOException e);
 }
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 1cd007c..5cd1187 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okhttp</artifactId>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Cache.java b/okhttp/src/main/java/com/squareup/okhttp/Cache.java
index 03c37a5..dcf3634 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Cache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Cache.java
@@ -165,7 +165,11 @@
   private int requestCount;
 
   public Cache(File directory, long maxSize) {
-    cache = DiskLruCache.create(FileSystem.SYSTEM, directory, VERSION, ENTRY_COUNT, maxSize);
+    this(directory, maxSize, FileSystem.SYSTEM);
+  }
+
+  Cache(File directory, long maxSize, FileSystem fileSystem) {
+    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
   }
 
   private static String urlToKey(Request request) {
@@ -270,6 +274,23 @@
   }
 
   /**
+   * Initialize the cache. This will include reading the journal files from
+   * the storage and building up the necessary in-memory cache information.
+   * <p>
+   * The initialization time may vary depending on the journal file size and
+   * the current actual cache size. The application needs to be aware of calling
+   * this function during the initialization phase and preferably in a background
+   * worker thread.
+   * <p>
+   * Note that if the application chooses to not call this method to initialize
+   * the cache. By default, the okhttp will perform lazy initialization upon the
+   * first usage of the cache.
+   */
+  public void initialize() throws IOException {
+    cache.initialize();
+  }
+
+  /**
    * Closes the cache and deletes all of its stored values. This will delete
    * all files in the cache directory including files that weren't created by
    * the cache.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index 99393cf..33561ba 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -16,13 +16,11 @@
 package com.squareup.okhttp;
 
 import com.squareup.okhttp.internal.NamedRunnable;
-import com.squareup.okhttp.internal.http.RouteException;
 import com.squareup.okhttp.internal.http.HttpEngine;
 import com.squareup.okhttp.internal.http.RequestException;
+import com.squareup.okhttp.internal.http.RouteException;
 import java.io.IOException;
-import java.net.MalformedURLException;
 import java.net.ProtocolException;
-import java.net.URL;
 import java.util.logging.Level;
 
 import static com.squareup.okhttp.internal.Internal.logger;
@@ -44,7 +42,7 @@
   Request originalRequest;
   HttpEngine engine;
 
-  Call(OkHttpClient client, Request originalRequest) {
+  protected Call(OkHttpClient client, Request originalRequest) {
     // Copy the client. Otherwise changes (socket factory, redirect policy,
     // etc.) may incorrectly be reflected in the request when it is executed.
     this.client = client.copyWithDefaults();
@@ -139,7 +137,7 @@
     }
 
     String host() {
-      return originalRequest.url().getHost();
+      return originalRequest.httpUrl().host();
     }
 
     Request request() {
@@ -188,12 +186,8 @@
    */
   private String toLoggableString() {
     String string = canceled ? "canceled call" : "call";
-    try {
-      String redactedUrl = new URL(originalRequest.url(), "/...").toString();
-      return string + " to " + redactedUrl;
-    } catch (MalformedURLException e) {
-      return string;
-    }
+    HttpUrl redactedUrl = originalRequest.httpUrl().resolve("/...");
+    return string + " to " + redactedUrl;
   }
 
   private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
@@ -310,7 +304,7 @@
         throw new ProtocolException("Too many follow-up requests: " + followUpCount);
       }
 
-      if (!engine.sameConnection(followUp.url())) {
+      if (!engine.sameConnection(followUp.httpUrl())) {
         engine.releaseConnection();
       }
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
index 49221b7..15a2952 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
@@ -18,15 +18,16 @@
 import com.squareup.okhttp.internal.Util;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import okio.ByteString;
 
-import static java.util.Collections.unmodifiableList;
+import static java.util.Collections.unmodifiableSet;
 
 /**
  * Constrains which certificates are trusted. Pinning certificates defends
@@ -91,8 +92,28 @@
  *       .build();
  * }</pre>
  *
- * Pinning is per-hostname. To pin both {@code publicobject.com} and {@code
- * www.publicobject.com}, you must configure both hostnames.
+ * Pinning is per-hostname and/or per-wildcard pattern. To pin both
+ * {@code publicobject.com} and {@code www.publicobject.com}, you must
+ * configure both hostnames.
+ *
+ * <p>Wildcard pattern rules:
+ * <ol>
+ *   <li>Asterisk {@code *} is only permitted in the left-most
+ *       domain name label and must be the only character in that label
+ *       (i.e., must match the whole left-most label). For example,
+ *       {@code *.example.com} is permitted, while {@code *a.example.com},
+ *       {@code a*.example.com}, {@code a*b.example.com}, {@code a.*.example.com}
+ *       are not permitted.
+ *   <li>Asterisk {@code *} cannot match across domain name labels.
+ *       For example, {@code *.example.com} matches {@code test.example.com}
+ *       but does not match {@code sub.test.example.com}.
+ *   <li>Wildcard patterns for single-label domain names are not permitted.
+ * </ol>
+ *
+ * If hostname pinned directly and via wildcard pattern, both
+ * direct and wildcard pins will be used. For example: {@code *.example.com} pinned
+ * with {@code pin1} and {@code a.example.com} pinned with {@code pin2},
+ * to check {@code a.example.com} both {@code pin1} and {@code pin2} will be used.
  *
  * <h3>Warning: Certificate Pinning is Dangerous!</h3>
  * Pinning certificates limits your server team's abilities to update their TLS
@@ -111,7 +132,7 @@
 public final class CertificatePinner {
   public static final CertificatePinner DEFAULT = new Builder().build();
 
-  private final Map<String, List<ByteString>> hostnameToPins;
+  private final Map<String, Set<ByteString>> hostnameToPins;
 
   private CertificatePinner(Builder builder) {
     hostnameToPins = Util.immutableMap(builder.hostnameToPins);
@@ -128,7 +149,9 @@
    */
   public void check(String hostname, List<Certificate> peerCertificates)
       throws SSLPeerUnverifiedException {
-    List<ByteString> pins = hostnameToPins.get(hostname);
+
+    Set<ByteString> pins = findMatchingPins(hostname);
+
     if (pins == null) return;
 
     for (int i = 0, size = peerCertificates.size(); i < size; i++) {
@@ -146,8 +169,7 @@
           .append(": ").append(x509Certificate.getSubjectDN().getName());
     }
     message.append("\n  Pinned certificates for ").append(hostname).append(":");
-    for (int i = 0, size = pins.size(); i < size; i++) {
-      ByteString pin = pins.get(i);
+    for (ByteString pin : pins) {
       message.append("\n    sha1/").append(pin.base64());
     }
     throw new SSLPeerUnverifiedException(message.toString());
@@ -160,6 +182,39 @@
   }
 
   /**
+   * Returns list of matching certificates' pins for the hostname
+   * or {@code null} if hostname does not have pinned certificates.
+   */
+  Set<ByteString> findMatchingPins(String hostname) {
+    Set<ByteString> directPins   = hostnameToPins.get(hostname);
+    Set<ByteString> wildcardPins = null;
+
+    int indexOfFirstDot = hostname.indexOf('.');
+    int indexOfLastDot  = hostname.lastIndexOf('.');
+
+    // Skip hostnames with one dot symbol for wildcard pattern search
+    //   example.com   will  be skipped
+    //   a.example.com won't be skipped
+    if (indexOfFirstDot != indexOfLastDot) {
+      // a.example.com -> search for wildcard pattern *.example.com
+      wildcardPins = hostnameToPins.get("*." + hostname.substring(indexOfFirstDot + 1));
+    }
+
+    if (directPins == null && wildcardPins == null) return null;
+
+    if (directPins != null && wildcardPins != null) {
+      Set<ByteString> pins = new LinkedHashSet<>();
+      pins.addAll(directPins);
+      pins.addAll(wildcardPins);
+      return pins;
+    }
+
+    if (directPins != null) return directPins;
+
+    return wildcardPins;
+  }
+
+  /**
    * Returns the SHA-1 of {@code certificate}'s public key. This uses the
    * mechanism Moxie Marlinspike describes in <a
    * href="https://github.com/moxie0/AndroidPinning">Android Pinning</a>.
@@ -177,18 +232,21 @@
 
   /** Builds a configured certificate pinner. */
   public static final class Builder {
-    private final Map<String, List<ByteString>> hostnameToPins = new LinkedHashMap<>();
+    private final Map<String, Set<ByteString>> hostnameToPins = new LinkedHashMap<>();
 
     /**
-     * Pins certificates for {@code hostname}. Each pin is a SHA-1 hash of a
-     * certificate's Subject Public Key Info, base64-encoded and prefixed with
-     * "sha1/".
+     * Pins certificates for {@code hostname}.
+     *
+     * @param hostname lower-case host name or wildcard pattern such as {@code *.example.com}.
+     * @param pins SHA-1 hashes. Each pin is a SHA-1 hash of a
+     *     certificate's Subject Public Key Info, base64-encoded and prefixed with
+     *     {@code sha1/}.
      */
     public Builder add(String hostname, String... pins) {
       if (hostname == null) throw new IllegalArgumentException("hostname == null");
 
-      List<ByteString> hostPins = new ArrayList<>();
-      List<ByteString> previousPins = hostnameToPins.put(hostname, unmodifiableList(hostPins));
+      Set<ByteString> hostPins = new LinkedHashSet<>();
+      Set<ByteString> previousPins = hostnameToPins.put(hostname, unmodifiableSet(hostPins));
       if (previousPins != null) {
         hostPins.addAll(previousPins);
       }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index 6819952..2a3614e 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -16,20 +16,35 @@
  */
 package com.squareup.okhttp;
 
+import com.squareup.okhttp.internal.ConnectionSpecSelector;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.http.FramedTransport;
 import com.squareup.okhttp.internal.http.HttpConnection;
 import com.squareup.okhttp.internal.http.HttpEngine;
 import com.squareup.okhttp.internal.http.HttpTransport;
+import com.squareup.okhttp.internal.http.OkHeaders;
 import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.SocketConnector;
-import com.squareup.okhttp.internal.http.SpdyTransport;
 import com.squareup.okhttp.internal.http.Transport;
-import com.squareup.okhttp.internal.spdy.SpdyConnection;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
 import java.io.IOException;
+import java.net.Proxy;
 import java.net.Socket;
 import java.net.UnknownServiceException;
+import java.security.cert.X509Certificate;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
 import okio.BufferedSink;
 import okio.BufferedSource;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.Util.closeQuietly;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
 
 /**
  * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be
@@ -64,7 +79,7 @@
   private Socket socket;
   private boolean connected = false;
   private HttpConnection httpConnection;
-  private SpdyConnection spdyConnection;
+  private FramedConnection framedConnection;
   private Protocol protocol = Protocol.HTTP_1_1;
   private long idleStartTimeNs;
   private Handshake handshake;
@@ -89,7 +104,7 @@
   }
 
   void setOwner(Object owner) {
-    if (isSpdy()) return; // SPDY connections are shared.
+    if (isFramed()) return; // Framed connections are shared.
     synchronized (pool) {
       if (this.owner != null) throw new IllegalStateException("Connection already has an owner!");
       this.owner = owner;
@@ -119,7 +134,7 @@
    * strips the ownership of the connection so it cannot be pooled or reused.
    */
   void closeIfOwnedBy(Object owner) throws IOException {
-    if (isSpdy()) throw new IllegalStateException();
+    if (isFramed()) throw new IllegalStateException();
     synchronized (pool) {
       if (this.owner != owner) {
         return; // Wrong owner. Perhaps a late disconnect?
@@ -129,47 +144,214 @@
     }
 
     // Don't close() inside the synchronized block.
-    socket.close();
+    if (socket != null) {
+      socket.close();
+    }
   }
 
   void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
       List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
     if (connected) throw new IllegalStateException("already connected");
 
-    SocketConnector socketConnector = new SocketConnector(this, pool);
-    SocketConnector.ConnectedSocket connectedSocket;
+    RouteException routeException = null;
+    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
+    Proxy proxy = route.getProxy();
+    Address address = route.getAddress();
+
+    if (route.address.getSslSocketFactory() == null
+        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
+      throw new RouteException(new UnknownServiceException(
+          "CLEARTEXT communication not supported: " + connectionSpecs));
+    }
+
+    while (!connected) {
+      try {
+        socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
+            ? address.getSocketFactory().createSocket()
+            : new Socket(proxy);
+        connectSocket(connectTimeout, readTimeout, writeTimeout, request,
+            connectionSpecSelector);
+        connected = true; // Success!
+      } catch (IOException e) {
+        Util.closeQuietly(socket);
+        socket = null;
+
+        if (routeException == null) {
+          routeException = new RouteException(e);
+        } else {
+          routeException.addConnectException(e);
+        }
+
+        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
+          throw routeException;
+        }
+      }
+    }
+  }
+
+  /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
+  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
+      Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
+    socket.setSoTimeout(readTimeout);
+    Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
+
     if (route.address.getSslSocketFactory() != null) {
-      // https:// communication
-      connectedSocket = socketConnector.connectTls(connectTimeout, readTimeout, writeTimeout,
-          request, route, connectionSpecs, connectionRetryEnabled);
+      connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
+    }
+
+    if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
+      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
+      framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket)
+          .protocol(protocol).build();
+      framedConnection.sendConnectionPreface();
     } else {
-      // http:// communication.
-      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
-        throw new RouteException(
-            new UnknownServiceException(
-                "CLEARTEXT communication not supported: " + connectionSpecs));
-      }
-      connectedSocket = socketConnector.connectCleartext(connectTimeout, readTimeout, route);
+      httpConnection = new HttpConnection(pool, this, socket);
+    }
+  }
+
+  private void connectTls(int readTimeout, int writeTimeout, Request request,
+      ConnectionSpecSelector connectionSpecSelector) throws IOException {
+    if (route.requiresTunnel()) {
+      createTunnel(readTimeout, writeTimeout, request);
     }
 
-    socket = connectedSocket.socket;
-    handshake = connectedSocket.handshake;
-    protocol = connectedSocket.alpnProtocol == null
-        ? Protocol.HTTP_1_1 : connectedSocket.alpnProtocol;
-
+    Address address = route.getAddress();
+    SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
+    boolean success = false;
+    SSLSocket sslSocket = null;
     try {
-      if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
-        socket.setSoTimeout(0); // SPDY timeouts are set per-stream.
-        spdyConnection = new SpdyConnection.Builder(route.address.uriHost, true, socket)
-            .protocol(protocol).build();
-        spdyConnection.sendConnectionPreface();
-      } else {
-        httpConnection = new HttpConnection(pool, this, socket);
+      // Create the wrapper over the connected socket.
+      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
+          socket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
+
+      // Configure the socket's ciphers, TLS versions, and extensions.
+      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
+      if (connectionSpec.supportsTlsExtensions()) {
+        Platform.get().configureTlsExtensions(
+            sslSocket, address.getUriHost(), address.getProtocols());
       }
-    } catch (IOException e) {
-      throw new RouteException(e);
+
+      // Force handshake. This can throw!
+      sslSocket.startHandshake();
+      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
+
+      // Verify that the socket's certificates are acceptable for the target host.
+      if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
+        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
+        throw new SSLPeerUnverifiedException("Hostname " + address.getUriHost() + " not verified:"
+            + "\n    certificate: " + CertificatePinner.pin(cert)
+            + "\n    DN: " + cert.getSubjectDN().getName()
+            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
+      }
+
+      // Check that the certificate pinner is satisfied by the certificates presented.
+      address.getCertificatePinner().check(address.getUriHost(),
+          unverifiedHandshake.peerCertificates());
+
+      // Success! Save the handshake and the ALPN protocol.
+      String maybeProtocol = connectionSpec.supportsTlsExtensions()
+          ? Platform.get().getSelectedProtocol(sslSocket)
+          : null;
+      protocol = maybeProtocol != null
+          ? Protocol.get(maybeProtocol)
+          : Protocol.HTTP_1_1;
+      handshake = unverifiedHandshake;
+      socket = sslSocket;
+      success = true;
+    } catch (AssertionError e) {
+      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+      throw e;
+    } finally {
+      if (sslSocket != null) {
+        Platform.get().afterHandshake(sslSocket);
+      }
+      if (!success) {
+        closeQuietly(sslSocket);
+      }
     }
-    connected = true;
+  }
+
+  /**
+   * To make an HTTPS connection over an HTTP proxy, send an unencrypted
+   * CONNECT request to create the proxy connection. This may need to be
+   * retried if the proxy requires authorization.
+   */
+  private void createTunnel(int readTimeout, int writeTimeout, Request request) throws IOException {
+    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+    Request tunnelRequest = createTunnelRequest(request);
+    HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
+    tunnelConnection.setTimeouts(readTimeout, writeTimeout);
+    HttpUrl url = tunnelRequest.httpUrl();
+    String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1";
+    while (true) {
+      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
+      tunnelConnection.flush();
+      Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
+      // The response body from a CONNECT should be empty, but if it is not then we should consume
+      // it before proceeding.
+      long contentLength = OkHeaders.contentLength(response);
+      if (contentLength == -1L) {
+        contentLength = 0L;
+      }
+      Source body = tunnelConnection.newFixedLengthSource(contentLength);
+      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
+      body.close();
+
+      switch (response.code()) {
+        case HTTP_OK:
+          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
+          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
+          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
+          // that it will almost certainly fail because the proxy has sent unexpected data.
+          if (tunnelConnection.bufferSize() > 0) {
+            throw new IOException("TLS tunnel buffered too many bytes!");
+          }
+          return;
+
+        case HTTP_PROXY_AUTH:
+          tunnelRequest = OkHeaders.processAuthHeader(
+              route.getAddress().getAuthenticator(), response, route.getProxy());
+          if (tunnelRequest != null) continue;
+          throw new IOException("Failed to authenticate with proxy");
+
+        default:
+          throw new IOException(
+              "Unexpected response code for CONNECT: " + response.code());
+      }
+    }
+  }
+
+  /**
+   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
+   * no tunnel is necessary. Everything in the tunnel request is sent
+   * unencrypted to the proxy server, so tunnels include only the minimum set of
+   * headers. This avoids sending potentially sensitive data like HTTP cookies
+   * to the proxy unencrypted.
+   */
+  private Request createTunnelRequest(Request request) throws IOException {
+    HttpUrl tunnelUrl = new HttpUrl.Builder()
+        .scheme("https")
+        .host(request.httpUrl().host())
+        .port(request.httpUrl().port())
+        .build();
+    Request.Builder result = new Request.Builder()
+        .url(tunnelUrl)
+        .header("Host", Util.hostHeader(tunnelUrl))
+        .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
+
+    // Copy over the User-Agent header if it exists.
+    String userAgent = request.header("User-Agent");
+    if (userAgent != null) {
+      result.header("User-Agent", userAgent);
+    }
+
+    // Copy over the Proxy-Authorization header if it exists.
+    String proxyAuthorization = request.header("Proxy-Authorization");
+    if (proxyAuthorization != null) {
+      result.header("Proxy-Authorization", proxyAuthorization);
+    }
+
+    return result.build();
   }
 
   /**
@@ -184,7 +366,7 @@
       List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
       connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
           request, connectionSpecs, client.getRetryOnConnectionFailure());
-      if (isSpdy()) {
+      if (isFramed()) {
         client.getConnectionPool().share(this);
       }
       client.routeDatabase().connected(getRoute());
@@ -233,17 +415,17 @@
    */
   boolean isReadable() {
     if (httpConnection != null) return httpConnection.isReadable();
-    return true; // SPDY connections, and connections before connect() are both optimistic.
+    return true; // Framed connections, and connections before connect() are both optimistic.
   }
 
   void resetIdleStartTime() {
-    if (spdyConnection != null) throw new IllegalStateException("spdyConnection != null");
+    if (framedConnection != null) throw new IllegalStateException("framedConnection != null");
     this.idleStartTimeNs = System.nanoTime();
   }
 
   /** Returns true if this connection is idle. */
   boolean isIdle() {
-    return spdyConnection == null || spdyConnection.isIdle();
+    return framedConnection == null || framedConnection.isIdle();
   }
 
   /**
@@ -251,7 +433,7 @@
    * this connection is not idle.
    */
   long getIdleStartTimeNs() {
-    return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
+    return framedConnection == null ? idleStartTimeNs : framedConnection.getIdleStartTimeNs();
   }
 
   public Handshake getHandshake() {
@@ -260,8 +442,8 @@
 
   /** Returns the transport appropriate for this connection. */
   Transport newTransport(HttpEngine httpEngine) throws IOException {
-    return (spdyConnection != null)
-        ? new SpdyTransport(httpEngine, spdyConnection)
+    return (framedConnection != null)
+        ? new FramedTransport(httpEngine, framedConnection)
         : new HttpTransport(httpEngine, httpConnection);
   }
 
@@ -269,8 +451,8 @@
    * Returns true if this is a SPDY connection. Such connections can be used
    * in multiple HTTP requests simultaneously.
    */
-  boolean isSpdy() {
-    return spdyConnection != null;
+  boolean isFramed() {
+    return framedConnection != null;
   }
 
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index ba664ea..da3ac73 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -122,7 +122,7 @@
   public synchronized int getMultiplexedConnectionCount() {
     int total = 0;
     for (Connection connection : connections) {
-      if (connection.isSpdy()) total++;
+      if (connection.isFramed()) total++;
     }
     return total;
   }
@@ -144,7 +144,7 @@
         continue;
       }
       i.remove();
-      if (!connection.isSpdy()) {
+      if (!connection.isFramed()) {
         try {
           Platform.get().tagSocket(connection.getSocket());
         } catch (SocketException e) {
@@ -158,7 +158,7 @@
       break;
     }
 
-    if (foundConnection != null && foundConnection.isSpdy()) {
+    if (foundConnection != null && foundConnection.isFramed()) {
       connections.addFirst(foundConnection); // Add it back after iteration.
     }
 
@@ -172,7 +172,7 @@
    * <p>It is an error to use {@code connection} after calling this method.
    */
   void recycle(Connection connection) {
-    if (connection.isSpdy()) {
+    if (connection.isFramed()) {
       return;
     }
 
@@ -216,7 +216,7 @@
    * continue to use {@code connection}.
    */
   void share(Connection connection) {
-    if (!connection.isSpdy()) throw new IllegalArgumentException();
+    if (!connection.isFramed()) throw new IllegalArgumentException();
     if (!connection.isAlive()) return;
     synchronized (this) {
       addConnection(connection);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
index 63eac1a..6f4b93c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
@@ -15,8 +15,6 @@
  */
 package com.squareup.okhttp;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
 import okio.Buffer;
 
 /**
@@ -34,20 +32,28 @@
     if (content.size() > 0) {
       content.writeByte('&');
     }
-    try {
-      content.writeUtf8(URLEncoder.encode(name, "UTF-8"));
-      content.writeByte('=');
-      content.writeUtf8(URLEncoder.encode(value, "UTF-8"));
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError(e);
+    HttpUrl.canonicalize(content, name, 0, name.length(),
+        HttpUrl.FORM_ENCODE_SET, false, true);
+    content.writeByte('=');
+    HttpUrl.canonicalize(content, value, 0, value.length(),
+        HttpUrl.FORM_ENCODE_SET, false, true);
+    return this;
+  }
+
+  /** Add new key-value pair. */
+  public FormEncodingBuilder addEncoded(String name, String value) {
+    if (content.size() > 0) {
+      content.writeByte('&');
     }
+    HttpUrl.canonicalize(content, name, 0, name.length(),
+        HttpUrl.FORM_ENCODE_SET, true, true);
+    content.writeByte('=');
+    HttpUrl.canonicalize(content, value, 0, value.length(),
+        HttpUrl.FORM_ENCODE_SET, true, true);
     return this;
   }
 
   public RequestBody build() {
-    if (content.size() == 0) {
-      throw new IllegalStateException("Form encoded body must have at least one part.");
-    }
     return RequestBody.create(CONTENT_TYPE, content.snapshot());
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Headers.java b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
index 475d120..0dca428 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Headers.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -130,6 +131,20 @@
     return result.toString();
   }
 
+  public Map<String, List<String>> toMultimap() {
+    Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
+    for (int i = 0, size = size(); i < size; i++) {
+      String name = name(i);
+      List<String> values = result.get(name);
+      if (values == null) {
+        values = new ArrayList<>(2);
+        result.put(name, values);
+      }
+      values.add(value(i));
+    }
+    return result;
+  }
+
   private static String get(String[] namesAndValues, String name) {
     for (int i = namesAndValues.length - 2; i >= 0; i -= 2) {
       if (name.equalsIgnoreCase(namesAndValues[i])) {
@@ -227,11 +242,7 @@
 
     /** Add a field with the specified value. */
     public Builder add(String name, String value) {
-      if (name == null) throw new IllegalArgumentException("name == null");
-      if (value == null) throw new IllegalArgumentException("value == null");
-      if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
-        throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
-      }
+      checkNameAndValue(name, value);
       return addLenient(name, value);
     }
 
@@ -261,11 +272,32 @@
      * added. If the field is found, the existing values are replaced.
      */
     public Builder set(String name, String value) {
+      checkNameAndValue(name, value);
       removeAll(name);
-      add(name, value);
+      addLenient(name, value);
       return this;
     }
 
+    private void checkNameAndValue(String name, String value) {
+      if (name == null) throw new IllegalArgumentException("name == null");
+      if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
+      for (int i = 0, length = name.length(); i < length; i++) {
+        char c = name.charAt(i);
+        if (c <= '\u001f' || c >= '\u007f') {
+          throw new IllegalArgumentException(String.format(
+              "Unexpected char %#04x at %d in header name: %s", (int) c, i, name));
+        }
+      }
+      if (value == null) throw new IllegalArgumentException("value == null");
+      for (int i = 0, length = value.length(); i < length; i++) {
+        char c = value.charAt(i);
+        if (c <= '\u001f' || c >= '\u007f') {
+          throw new IllegalArgumentException(String.format(
+              "Unexpected char %#04x at %d in header value: %s", (int) c, i, value));
+        }
+      }
+    }
+
     /** Equivalent to {@code build().get(name)}, but potentially faster. */
     public String get(String name) {
       for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
index 0698528..ae80c29 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
@@ -15,76 +15,328 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Util;
-import java.io.IOException;
+import java.net.IDN;
 import java.net.InetAddress;
+import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import okio.Buffer;
 
 /**
- * A <a href="https://url.spec.whatwg.org/">URL</a> with an {@code http} or {@code https} scheme.
+ * A uniform resource locator (URL) with a scheme of either {@code http} or {@code https}. Use this
+ * class to compose and decompose Internet addresses. For example, this code will compose and print
+ * a URL for Google search: <pre>   {@code
  *
- * TODO: discussion on canonicalization
+ *   HttpUrl url = new HttpUrl.Builder()
+ *       .scheme("https")
+ *       .host("www.google.com")
+ *       .addPathSegment("search")
+ *       .addQueryParameter("q", "polar bears")
+ *       .build();
+ *   System.out.println(url);
+ * }</pre>
  *
- * TODO: discussion on encoding-by-parts
+ * which prints: <pre>   {@code
  *
- * TODO: discussion on this vs. java.net.URL vs. java.net.URI
+ *     https://www.google.com/search?q=polar%20bears
+ * }</pre>
+ *
+ * As another example, this code prints the human-readable query parameters of a Twitter search:
+ * <pre>   {@code
+ *
+ *   HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images");
+ *   for (int i = 0, size = url.querySize(); i < size; i++) {
+ *     System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i));
+ *   }
+ * }</pre>
+ *
+ * which prints: <pre>   {@code
+ *
+ *   q: cute #puppies
+ *   f: images
+ * }</pre>
+ *
+ * In addition to composing URLs from their component parts and decomposing URLs into their
+ * component parts, this class implements relative URL resolution: what address you'd reach by
+ * clicking a relative link on a specified page. For example: <pre>   {@code
+ *
+ *   HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos");
+ *   HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc");
+ *   System.out.println(link);
+ * }</pre>
+ *
+ * which prints: <pre>   {@code
+ *
+ *   https://www.youtube.com/watch?v=cbP2N1BQdYc
+ * }</pre>
+ *
+ * <h3>What's in a URL?</h3>
+ *
+ * A URL has several components.
+ *
+ * <h4>Scheme</h4>
+ * Sometimes referred to as <i>protocol</i>, A URL's scheme describes what mechanism should be used
+ * to retrieve the resource. Although URLs have many schemes ({@code mailto}, {@code file}, {@code
+ * ftp}), this class only supports {@code http} and {@code https}. Use {@link URI java.net.URI} for
+ * URLs with arbitrary schemes.
+ *
+ * <h4>Username and Password</h4>
+ * Username and password are either present, or the empty string {@code ""} if absent. This class
+ * offers no mechanism to differentiate empty from absent. Neither of these components are popular
+ * in practice. Typically HTTP applications use other mechanisms for user identification and
+ * authentication.
+ *
+ * <h4>Host</h4>
+ * The host identifies the webserver that serves the URL's resource. It is either a hostname like
+ * {@code square.com} or {@code localhost}, an IPv4 address like {@code 192.168.0.1}, or an IPv6
+ * address like {@code ::1}.
+ *
+ * <p>Usually a webserver is reachable with multiple identifiers: its IP addresses, registered
+ * domain names, and even {@code localhost} when connecting from the server itself. Each of a
+ * webserver's names is a distinct URL and they are not interchangeable. For example, even if
+ * {@code http://square.github.io/dagger} and {@code http://google.github.io/dagger} are served by
+ * the same IP address, the two URLs identify different resources.
+ *
+ * <h4>Port</h4>
+ * The port used to connect to the webserver. By default this is 80 for HTTP and 443 for HTTPS. This
+ * class never returns -1 for the port: if no port is explicitly specified in the URL then the
+ * scheme's default is used.
+ *
+ * <h4>Path</h4>
+ * The path identifies a specific resource on the host. Paths have a hierarchical structure like
+ * "/square/okhttp/issues/1486". Each path segment is prefixed with "/". This class offers methods
+ * to compose and decompose paths by segment. If a path's last segment is the empty string, then the
+ * path ends with "/". This class always builds non-empty paths: if the path is omitted it defaults
+ * to "/", which is a path whose only segment is the empty string.
+ *
+ * <h4>Query</h4>
+ * The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string
+ * is subdivided into a collection of name-value parameters. This class offers methods to set the
+ * query as the single string, or as individual name-value parameters. With name-value parameters
+ * the values are optional and names may be repeated.
+ *
+ * <h4>Fragment</h4>
+ * The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and query
+ * the fragment is not sent to the webserver: it's private to the client.
+ *
+ * <h3>Encoding</h3>
+ * Each component must be encoded before it is embedded in the complete URL. As we saw above, the
+ * string {@code cute #puppies} is encoded as {@code cute%20%23puppies} when used as a query
+ * parameter value.
+ *
+ * <h4>Percent encoding</h4>
+ * Percent encoding replaces a character (like {@code \ud83c\udf69}) with its UTF-8 hex bytes (like
+ * {@code %F0%9F%8D%A9}). This approach works for whitespace characters, control characters,
+ * non-ASCII characters, and characters that already have another meaning in a particular context.
+ *
+ * <p>Percent encoding is used in every URL component except for the hostname. But the set of
+ * characters that need to be encoded is different for each component. For example, the path
+ * component must escape all of its {@code ?} characters, otherwise it could be interpreted as the
+ * start of the URL's query. But within the query and fragment components, the {@code ?} character
+ * doesn't delimit anything and doesn't need to be escaped. <pre>   {@code
+ *
+ *   HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
+ *       .addPathSegment("_Who?_")
+ *       .query("_Who?_")
+ *       .fragment("_Who?_")
+ *       .build();
+ *   System.out.println(url);
+ * }</pre>
+ *
+ * This prints: <pre>   {@code
+ *
+ *   http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_
+ * }</pre>
+ *
+ * When parsing URLs that lack percent encoding where it is required, this class will percent encode
+ * the offending characters.
+ *
+ * <h4>IDNA Mapping and Punycode encoding</h4>
+ * Hostnames have different requirements and use a different encoding scheme. It consists of IDNA
+ * mapping and Punycode encoding.
+ *
+ * <p>In order to avoid confusion and discourage phishing attacks,
+ * <a href="http://www.unicode.org/reports/tr46/#ToASCII">IDNA Mapping</a> transforms names to avoid
+ * confusing characters. This includes basic case folding: transforming shouting {@code SQUARE.COM}
+ * into cool and casual {@code square.com}. It also handles more exotic characters. For example, the
+ * Unicode trademark sign (™) could be confused for the letters "TM" in {@code http://ho™mail.com}.
+ * To mitigate this, the single character (™) maps to the string (tm). There is similar policy for
+ * all of the 1.1 million Unicode code points. Note that some code points such as "\ud83c\udf69" are
+ * not mapped and cannot be used in a hostname.
+ *
+ * <p><a href="http://ietf.org/rfc/rfc3492.txt">Punycode</a> converts a Unicode string to an ASCII
+ * string to make international domain names work everywhere. For example, "σ" encodes as
+ * "xn--4xa". The encoded string is not human readable, but can be used with classes like {@link
+ * InetAddress} to establish connections.
+ *
+ * <h3>Why another URL model?</h3>
+ * Java includes both {@link URL java.net.URL} and {@link URI java.net.URI}. We offer a new URL
+ * model to address problems that the others don't.
+ *
+ * <h4>Different URLs should be different</h4>
+ * Although they have different content, {@code java.net.URL} considers the following two URLs
+ * equal, and the {@link Object#equals equals()} method between them returns true:
+ * <ul>
+ *   <li>http://square.github.io/
+ *   <li>http://google.github.io/
+ * </ul>
+ * This is because those two hosts share the same IP address. This is an old, bad design decision
+ * that makes {@code java.net.URL} unusable for many things. It shouldn't be used as a {@link
+ * java.util.Map Map} key or in a {@link Set}. Doing so is both inefficient because equality may
+ * require a DNS lookup, and incorrect because unequal URLs may be equal because of how they are
+ * hosted.
+ *
+ * <h4>Equal URLs should be equal</h4>
+ * These two URLs are semantically identical, but {@code java.net.URI} disagrees:
+ * <ul>
+ *   <li>http://host:80/
+ *   <li>http://host
+ * </ul>
+ * Both the unnecessary port specification ({@code :80}) and the absent trailing slash ({@code /})
+ * cause URI to bucket the two URLs separately. This harms URI's usefulness in collections. Any
+ * application that stores information-per-URL will need to either canonicalize manually, or suffer
+ * unnecessary redundancy for such URLs.
+ *
+ * <p>Because they don't attempt canonical form, these classes are surprisingly difficult to use
+ * securely. Suppose you're building a webservice that checks that incoming paths are prefixed
+ * "/static/images/" before serving the corresponding assets from the filesystem. <pre>   {@code
+ *
+ *   String attack = "http://example.com/static/images/../../../../../etc/passwd";
+ *   System.out.println(new URL(attack).getPath());
+ *   System.out.println(new URI(attack).getPath());
+ *   System.out.println(HttpUrl.parse(attack).path());
+ * }</pre>
+ *
+ * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that
+ * checks only the path prefix may suffer!
+ * <pre>   {@code
+ *
+ *    /static/images/../../../../../etc/passwd
+ *    /static/images/../../../../../etc/passwd
+ *    /etc/passwd
+ * }</pre>
+ *
+ * <h4>If it works on the web, it should work in your application</h4>
+ * The {@code java.net.URI} class is strict around what URLs it accepts. It rejects URLs like
+ * "http://example.com/abc|def" because the '|' character is unsupported. This class is more
+ * forgiving: it will automatically percent-encode the '|', yielding "http://example.com/abc%7Cdef".
+ * This kind behavior is consistent with web browsers. {@code HttpUrl} prefers consistency with
+ * major web browsers over consistency with obsolete specifications.
+ *
+ * <h4>Paths and Queries should decompose</h4>
+ * Neither of the built-in URL models offer direct access to path segments or query parameters.
+ * Manually using {@code StringBuilder} to assemble these components is cumbersome: do '+'
+ * characters get silently replaced with spaces? If a query parameter contains a '&amp;', does that
+ * get escaped? By offering methods to read and write individual query parameters directly,
+ * application developers are saved from the hassles of encoding and decoding.
+ *
+ * <h4>Plus a modern API</h4>
+ * The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping
+ * constructors. For example, there's no API to compose a URI with a custom port without also
+ * providing a query and fragment.
+ *
+ * <p>Instances of {@link HttpUrl} are well-formed and always have a scheme, host, and path. With
+ * {@code java.net.URL} it's possible to create an awkward URL like {@code http:/} with scheme and
+ * path but no hostname. Building APIs that consume such malformed values is difficult!
+ *
+ * <p>This class has a modern API. It avoids punitive checked exceptions: {@link #parse parse()}
+ * returns null if the input is an invalid URL. You can even be explicit about whether each
+ * component has been encoded already.
  */
 public final class HttpUrl {
   private static final char[] HEX_DIGITS =
       { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+  static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
+  static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
+  static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#";
+  static final String QUERY_ENCODE_SET = " \"'<>#";
+  static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&=";
+  static final String CONVERT_TO_URI_ENCODE_SET = "^`{}|\\";
+  static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~";
+  static final String FRAGMENT_ENCODE_SET = "";
 
   /** Either "http" or "https". */
   private final String scheme;
 
-  /** Encoded username. */
+  /** Decoded username. */
   private final String username;
 
-  /** Encoded password. */
+  /** Decoded password. */
   private final String password;
 
-  /** Encoded hostname. */
-  // TODO(jwilson): implement punycode.
+  /** Canonical hostname. */
   private final String host;
 
   /** Either 80, 443 or a user-specified port. In range [1..65535]. */
   private final int port;
 
-  /** Encoded path. */
-  private final String path;
+  /**
+   * A list of canonical path segments. This list always contains at least one element, which may
+   * be the empty string. Each segment is formatted with a leading '/', so if path segments were
+   * ["a", "b", ""], then the encoded path would be "/a/b/".
+   */
+  private final List<String> pathSegments;
 
-  /** Encoded query. */
-  private final String query;
+  /**
+   * Alternating, decoded query names and values, or null for no query. Names may be empty or
+   * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or
+   * empty, or non-empty.
+   */
+  private final List<String> queryNamesAndValues;
 
-  /** Encoded fragment. */
+  /** Decoded fragment. */
   private final String fragment;
 
   /** Canonical URL. */
   private final String url;
 
-  private HttpUrl(String scheme, String username, String password, String host, int port,
-      String path, String query, String fragment, String url) {
-    this.scheme = scheme;
-    this.username = username;
-    this.password = password;
-    this.host = host;
-    this.port = port;
-    this.path = path;
-    this.query = query;
-    this.fragment = fragment;
-    this.url = url;
+  private HttpUrl(Builder builder) {
+    this.scheme = builder.scheme;
+    this.username = percentDecode(builder.encodedUsername);
+    this.password = percentDecode(builder.encodedPassword);
+    this.host = builder.host;
+    this.port = builder.effectivePort();
+    this.pathSegments = percentDecode(builder.encodedPathSegments);
+    this.queryNamesAndValues = builder.encodedQueryNamesAndValues != null
+        ? percentDecode(builder.encodedQueryNamesAndValues)
+        : null;
+    this.fragment = builder.encodedFragment != null
+        ? percentDecode(builder.encodedFragment)
+        : null;
+    this.url = builder.toString();
   }
 
+  /** Returns this URL as a {@link URL java.net.URL}. */
   public URL url() {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    try {
+      return new URL(url);
+    } catch (MalformedURLException e) {
+      throw new RuntimeException(e); // Unexpected!
+    }
   }
 
-  public URI uri() throws IOException {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+  /**
+   * Attempt to convert this URL to a {@link URI java.net.URI}. This method throws an unchecked
+   * {@link IllegalStateException} if the URL it holds isn't valid by URI's overly-stringent
+   * standard. For example, URI rejects paths containing the '[' character. Consult that class for
+   * the exact rules of what URLs are permitted.
+   */
+  public URI uri() {
+    try {
+      String uriSafeUrl = canonicalize(url, CONVERT_TO_URI_ENCODE_SET, true, false);
+      return new URI(uriSafeUrl);
+    } catch (URISyntaxException e) {
+      throw new IllegalStateException("not valid as a java.net.URI: " + url);
+    }
   }
 
   /** Returns either "http" or "https". */
@@ -96,24 +348,31 @@
     return scheme.equals("https");
   }
 
+  /** Returns the username, or an empty string if none is set. */
+  public String encodedUsername() {
+    if (username.isEmpty()) return "";
+    int usernameStart = scheme.length() + 3; // "://".length() == 3.
+    int usernameEnd = delimiterOffset(url, usernameStart, url.length(), ":@");
+    return url.substring(usernameStart, usernameEnd);
+  }
+
   public String username() {
     return username;
   }
 
-  public String decodeUsername() {
-    return decode(username, 0, username.length());
+  /** Returns the password, or an empty string if none is set. */
+  public String encodedPassword() {
+    if (password.isEmpty()) return "";
+    int passwordStart = url.indexOf(':', scheme.length() + 3) + 1;
+    int passwordEnd = url.indexOf('@');
+    return url.substring(passwordStart, passwordEnd);
   }
 
-  /** Returns the encoded password if one is present; null otherwise. */
+  /** Returns the decoded password, or an empty string if none is present. */
   public String password() {
     return password;
   }
 
-  /** Returns the decoded password if one is present; null otherwise. */
-  public String decodePassword() {
-    return password != null ? decode(password, 0, password.length()) : null;
-  }
-
   /**
    * Returns the host address suitable for use with {@link InetAddress#getAllByName(String)}. May
    * be:
@@ -129,16 +388,6 @@
   }
 
   /**
-   * Returns the decoded (potentially non-ASCII) hostname. The returned string may contain non-ASCII
-   * characters and is <strong>not suitable</strong> for DNS lookups; for that use {@link
-   * #host}. For example, this may return {@code ☃.net} which is a user-displayable IDN that cannot
-   * be used for DNS lookups without encoding.
-   */
-  public String decodeHost() {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
-  }
-
-  /**
    * Returns the explicitly-specified port if one was provided, or the default port for this URL's
    * scheme. For example, this returns 8443 for {@code https://square.com:8443/} and 443 for {@code
    * https://square.com/}. The result is in {@code [1..65535]}.
@@ -161,25 +410,42 @@
     }
   }
 
+  public int pathSize() {
+    return pathSegments.size();
+  }
+
   /**
    * Returns the entire path of this URL, encoded for use in HTTP resource resolution. The
    * returned path is always nonempty and is prefixed with {@code /}.
    */
-  public String path() {
-    return path;
+  public String encodedPath() {
+    int pathStart = url.indexOf('/', scheme.length() + 3); // "://".length() == 3.
+    int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#");
+    return url.substring(pathStart, pathEnd);
   }
 
-  public List<String> decodePathSegments() {
-    List<String> result = new ArrayList<>();
-    int segmentStart = 1; // Path always starts with '/'.
-    for (int i = segmentStart; i < path.length(); i++) {
-      if (path.charAt(i) == '/') {
-        result.add(decode(path, segmentStart, i));
-        segmentStart = i + 1;
-      }
+  static void pathSegmentsToString(StringBuilder out, List<String> pathSegments) {
+    for (int i = 0, size = pathSegments.size(); i < size; i++) {
+      out.append('/');
+      out.append(pathSegments.get(i));
     }
-    result.add(decode(path, segmentStart, path.length()));
-    return Util.immutableList(result);
+  }
+
+  public List<String> encodedPathSegments() {
+    int pathStart = url.indexOf('/', scheme.length() + 3);
+    int pathEnd = delimiterOffset(url, pathStart, url.length(), "?#");
+    List<String> result = new ArrayList<>();
+    for (int i = pathStart; i < pathEnd; ) {
+      i++; // Skip the '/'.
+      int segmentEnd = delimiterOffset(url, i, pathEnd, "/");
+      result.add(url.substring(i, segmentEnd));
+      i = segmentEnd;
+    }
+    return result;
+  }
+
+  public List<String> pathSegments() {
+    return pathSegments;
   }
 
   /**
@@ -187,8 +453,60 @@
    * may be null (for URLs with no query), empty (for URLs with an empty query) or non-empty (all
    * other URLs).
    */
+  public String encodedQuery() {
+    if (queryNamesAndValues == null) return null; // No query.
+    int queryStart = url.indexOf('?') + 1;
+    int queryEnd = delimiterOffset(url, queryStart + 1, url.length(), "#");
+    return url.substring(queryStart, queryEnd);
+  }
+
+  static void namesAndValuesToQueryString(StringBuilder out, List<String> namesAndValues) {
+    for (int i = 0, size = namesAndValues.size(); i < size; i += 2) {
+      String name = namesAndValues.get(i);
+      String value = namesAndValues.get(i + 1);
+      if (i > 0) out.append('&');
+      out.append(name);
+      if (value != null) {
+        out.append('=');
+        out.append(value);
+      }
+    }
+  }
+
+  /**
+   * Cuts {@code encodedQuery} up into alternating parameter names and values. This divides a
+   * query string like {@code subject=math&easy&problem=5-2=3} into the list {@code ["subject",
+   * "math", "easy", null, "problem", "5-2=3"]}. Note that values may be null and may contain
+   * '=' characters.
+   */
+  static List<String> queryStringToNamesAndValues(String encodedQuery) {
+    List<String> result = new ArrayList<>();
+    for (int pos = 0; pos <= encodedQuery.length(); ) {
+      int ampersandOffset = encodedQuery.indexOf('&', pos);
+      if (ampersandOffset == -1) ampersandOffset = encodedQuery.length();
+
+      int equalsOffset = encodedQuery.indexOf('=', pos);
+      if (equalsOffset == -1 || equalsOffset > ampersandOffset) {
+        result.add(encodedQuery.substring(pos, ampersandOffset));
+        result.add(null); // No value for this name.
+      } else {
+        result.add(encodedQuery.substring(pos, equalsOffset));
+        result.add(encodedQuery.substring(equalsOffset + 1, ampersandOffset));
+      }
+      pos = ampersandOffset + 1;
+    }
+    return result;
+  }
+
   public String query() {
-    return query;
+    if (queryNamesAndValues == null) return null; // No query.
+    StringBuilder result = new StringBuilder();
+    namesAndValuesToQueryString(result, queryNamesAndValues);
+    return result.toString();
+  }
+
+  public int querySize() {
+    return queryNamesAndValues != null ? queryNamesAndValues.size() / 2 : 0;
   }
 
   /**
@@ -196,54 +514,120 @@
    * no such query parameter.
    */
   public String queryParameter(String name) {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    if (queryNamesAndValues == null) return null;
+    for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) {
+      if (name.equals(queryNamesAndValues.get(i))) {
+        return queryNamesAndValues.get(i + 1);
+      }
+    }
+    return null;
   }
 
   public Set<String> queryParameterNames() {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    if (queryNamesAndValues == null) return Collections.emptySet();
+    Set<String> result = new LinkedHashSet<>();
+    for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) {
+      result.add(queryNamesAndValues.get(i));
+    }
+    return Collections.unmodifiableSet(result);
   }
 
   public List<String> queryParameterValues(String name) {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    if (queryNamesAndValues == null) return Collections.emptyList();
+    List<String> result = new ArrayList<>();
+    for (int i = 0, size = queryNamesAndValues.size(); i < size; i += 2) {
+      if (name.equals(queryNamesAndValues.get(i))) {
+        result.add(queryNamesAndValues.get(i + 1));
+      }
+    }
+    return Collections.unmodifiableList(result);
   }
 
   public String queryParameterName(int index) {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    return queryNamesAndValues.get(index * 2);
   }
 
   public String queryParameterValue(int index) {
-    throw new UnsupportedOperationException(); // TODO(jwilson).
+    return queryNamesAndValues.get(index * 2 + 1);
+  }
+
+  public String encodedFragment() {
+    if (fragment == null) return null;
+    int fragmentStart = url.indexOf('#') + 1;
+    return url.substring(fragmentStart);
   }
 
   public String fragment() {
     return fragment;
   }
 
-  /**
-   * Returns the URL that would be retrieved by following {@code link} from this URL.
-   *
-   * TODO: explain better.
-   */
+  /** Returns the URL that would be retrieved by following {@code link} from this URL. */
   public HttpUrl resolve(String link) {
-    return new Builder().parse(this, link);
+    Builder builder = new Builder();
+    Builder.ParseResult result = builder.parse(this, link);
+    return result == Builder.ParseResult.SUCCESS ? builder.build() : null;
   }
 
   public Builder newBuilder() {
-    return new Builder(this);
+    Builder result = new Builder();
+    result.scheme = scheme;
+    result.encodedUsername = encodedUsername();
+    result.encodedPassword = encodedPassword();
+    result.host = host;
+    // If we're set to a default port, unset it, in case of a scheme change.
+    if (port == defaultPort(scheme)) {
+      result.port = -1;
+    } else {
+      result.port = port;
+    }
+    result.encodedPathSegments.clear();
+    result.encodedPathSegments.addAll(encodedPathSegments());
+    result.encodedQuery(encodedQuery());
+    result.encodedFragment = encodedFragment();
+    return result;
   }
 
   /**
-   * Returns a new {@code OkUrl} representing {@code url} if it is a well-formed HTTP or HTTPS URL,
-   * or null if it isn't.
+   * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS
+   * URL, or null if it isn't.
    */
   public static HttpUrl parse(String url) {
-    return new Builder().parse(null, url);
+    Builder builder = new Builder();
+    Builder.ParseResult result = builder.parse(null, url);
+    return result == Builder.ParseResult.SUCCESS ? builder.build() : null;
   }
 
+  /**
+   * Returns an {@link HttpUrl} for {@code url} if its protocol is {@code http} or {@code https}, or
+   * null if it has any other protocol.
+   */
   public static HttpUrl get(URL url) {
     return parse(url.toString());
   }
 
+  /**
+   * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS
+   * URL, or throws an exception if it isn't.
+   *
+   * @throws MalformedURLException if there was a non-host related URL issue
+   * @throws UnknownHostException if the host was invalid
+   */
+  static HttpUrl getChecked(String url) throws MalformedURLException, UnknownHostException {
+    Builder builder = new Builder();
+    Builder.ParseResult result = builder.parse(null, url);
+    switch (result) {
+      case SUCCESS:
+        return builder.build();
+      case INVALID_HOST:
+        throw new UnknownHostException("Invalid host: " + url);
+      case UNSUPPORTED_SCHEME:
+      case MISSING_SCHEME:
+      case INVALID_PORT:
+      default:
+        throw new MalformedURLException("Invalid URL: " + result + " for " + url);
+    }
+  }
+
   public static HttpUrl get(URI uri) {
     return parse(uri.toString());
   }
@@ -262,39 +646,53 @@
 
   public static final class Builder {
     String scheme;
-    String username = "";
-    String password;
+    String encodedUsername = "";
+    String encodedPassword = "";
     String host;
     int port = -1;
-    StringBuilder pathBuilder = new StringBuilder();
-    String query;
-    String fragment;
+    final List<String> encodedPathSegments = new ArrayList<>();
+    List<String> encodedQueryNamesAndValues;
+    String encodedFragment;
 
     public Builder() {
-    }
-
-    private Builder(HttpUrl url) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      encodedPathSegments.add(""); // The default path is '/' which needs a trailing space.
     }
 
     public Builder scheme(String scheme) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (scheme == null) {
+        throw new IllegalArgumentException("scheme == null");
+      } else if (scheme.equalsIgnoreCase("http")) {
+        this.scheme = "http";
+      } else if (scheme.equalsIgnoreCase("https")) {
+        this.scheme = "https";
+      } else {
+        throw new IllegalArgumentException("unexpected scheme: " + scheme);
+      }
+      return this;
     }
 
-    public Builder user(String user) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+    public Builder username(String username) {
+      if (username == null) throw new IllegalArgumentException("username == null");
+      this.encodedUsername = canonicalize(username, USERNAME_ENCODE_SET, false, false);
+      return this;
     }
 
-    public Builder encodedUser(String encodedUser) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+    public Builder encodedUsername(String encodedUsername) {
+      if (encodedUsername == null) throw new IllegalArgumentException("encodedUsername == null");
+      this.encodedUsername = canonicalize(encodedUsername, USERNAME_ENCODE_SET, true, false);
+      return this;
     }
 
     public Builder password(String password) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (password == null) throw new IllegalArgumentException("password == null");
+      this.encodedPassword = canonicalize(password, PASSWORD_ENCODE_SET, false, false);
+      return this;
     }
 
     public Builder encodedPassword(String encodedPassword) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedPassword == null) throw new IllegalArgumentException("encodedPassword == null");
+      this.encodedPassword = canonicalize(encodedPassword, PASSWORD_ENCODE_SET, true, false);
+      return this;
     }
 
     /**
@@ -302,124 +700,227 @@
      *     address.
      */
     public Builder host(String host) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (host == null) throw new IllegalArgumentException("host == null");
+      String encoded = canonicalizeHost(host, 0, host.length());
+      if (encoded == null) throw new IllegalArgumentException("unexpected host: " + host);
+      this.host = encoded;
+      return this;
     }
 
     public Builder port(int port) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (port <= 0 || port > 65535) throw new IllegalArgumentException("unexpected port: " + port);
+      this.port = port;
+      return this;
+    }
+
+    int effectivePort() {
+      return port != -1 ? port : defaultPort(scheme);
     }
 
     public Builder addPathSegment(String pathSegment) {
       if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      push(pathSegment, 0, pathSegment.length(), false, false);
+      return this;
     }
 
     public Builder addEncodedPathSegment(String encodedPathSegment) {
       if (encodedPathSegment == null) {
         throw new IllegalArgumentException("encodedPathSegment == null");
       }
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      push(encodedPathSegment, 0, encodedPathSegment.length(), false, true);
+      return this;
+    }
+
+    public Builder setPathSegment(int index, String pathSegment) {
+      if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null");
+      String canonicalPathSegment = canonicalize(
+          pathSegment, 0, pathSegment.length(), PATH_SEGMENT_ENCODE_SET, false, false);
+      if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) {
+        throw new IllegalArgumentException("unexpected path segment: " + pathSegment);
+      }
+      encodedPathSegments.set(index, canonicalPathSegment);
+      return this;
+    }
+
+    public Builder setEncodedPathSegment(int index, String encodedPathSegment) {
+      if (encodedPathSegment == null) {
+        throw new IllegalArgumentException("encodedPathSegment == null");
+      }
+      String canonicalPathSegment = canonicalize(encodedPathSegment,
+          0, encodedPathSegment.length(), PATH_SEGMENT_ENCODE_SET, true, false);
+      encodedPathSegments.set(index, canonicalPathSegment);
+      if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) {
+        throw new IllegalArgumentException("unexpected path segment: " + encodedPathSegment);
+      }
+      return this;
+    }
+
+    public Builder removePathSegment(int index) {
+      encodedPathSegments.remove(index);
+      if (encodedPathSegments.isEmpty()) {
+        encodedPathSegments.add(""); // Always leave at least one '/'.
+      }
+      return this;
     }
 
     public Builder encodedPath(String encodedPath) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedPath == null) throw new IllegalArgumentException("encodedPath == null");
+      if (!encodedPath.startsWith("/")) {
+        throw new IllegalArgumentException("unexpected encodedPath: " + encodedPath);
+      }
+      resolvePath(encodedPath, 0, encodedPath.length());
+      return this;
+    }
+
+    public Builder query(String query) {
+      this.encodedQueryNamesAndValues = query != null
+          ? queryStringToNamesAndValues(canonicalize(query, QUERY_ENCODE_SET, false, true))
+          : null;
+      return this;
     }
 
     public Builder encodedQuery(String encodedQuery) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      this.encodedQueryNamesAndValues = encodedQuery != null
+          ? queryStringToNamesAndValues(canonicalize(encodedQuery, QUERY_ENCODE_SET, true, true))
+          : null;
+      return this;
     }
 
     /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */
     public Builder addQueryParameter(String name, String value) {
       if (name == null) throw new IllegalArgumentException("name == null");
-      if (value == null) throw new IllegalArgumentException("value == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>();
+      encodedQueryNamesAndValues.add(canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true));
+      encodedQueryNamesAndValues.add(value != null
+          ? canonicalize(value, QUERY_COMPONENT_ENCODE_SET, false, true)
+          : null);
+      return this;
     }
 
     /** Adds the pre-encoded query parameter to this URL's query string. */
     public Builder addEncodedQueryParameter(String encodedName, String encodedValue) {
       if (encodedName == null) throw new IllegalArgumentException("encodedName == null");
-      if (encodedValue == null) throw new IllegalArgumentException("encodedValue == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>();
+      encodedQueryNamesAndValues.add(
+          canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true));
+      encodedQueryNamesAndValues.add(encodedValue != null
+          ? canonicalize(encodedValue, QUERY_COMPONENT_ENCODE_SET, true, true)
+          : null);
+      return this;
     }
 
     public Builder setQueryParameter(String name, String value) {
-      if (name == null) throw new IllegalArgumentException("name == null");
-      if (value == null) throw new IllegalArgumentException("value == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      removeAllQueryParameters(name);
+      addQueryParameter(name, value);
+      return this;
     }
 
     public Builder setEncodedQueryParameter(String encodedName, String encodedValue) {
-      if (encodedName == null) throw new IllegalArgumentException("encodedName == null");
-      if (encodedValue == null) throw new IllegalArgumentException("encodedValue == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      removeAllEncodedQueryParameters(encodedName);
+      addEncodedQueryParameter(encodedName, encodedValue);
+      return this;
     }
 
     public Builder removeAllQueryParameters(String name) {
       if (name == null) throw new IllegalArgumentException("name == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedQueryNamesAndValues == null) return this;
+      String nameToRemove = canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true);
+      removeAllCanonicalQueryParameters(nameToRemove);
+      return this;
     }
 
     public Builder removeAllEncodedQueryParameters(String encodedName) {
       if (encodedName == null) throw new IllegalArgumentException("encodedName == null");
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (encodedQueryNamesAndValues == null) return this;
+      removeAllCanonicalQueryParameters(
+          canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true));
+      return this;
+    }
+
+    private void removeAllCanonicalQueryParameters(String canonicalName) {
+      for (int i = encodedQueryNamesAndValues.size() - 2; i >= 0; i -= 2) {
+        if (canonicalName.equals(encodedQueryNamesAndValues.get(i))) {
+          encodedQueryNamesAndValues.remove(i + 1);
+          encodedQueryNamesAndValues.remove(i);
+          if (encodedQueryNamesAndValues.isEmpty()) {
+            encodedQueryNamesAndValues = null;
+            return;
+          }
+        }
+      }
     }
 
     public Builder fragment(String fragment) {
-      throw new UnsupportedOperationException(); // TODO(jwilson)
+      if (fragment == null) throw new IllegalArgumentException("fragment == null");
+      this.encodedFragment = canonicalize(fragment, FRAGMENT_ENCODE_SET, false, false);
+      return this;
+    }
+
+    public Builder encodedFragment(String encodedFragment) {
+      if (encodedFragment == null) throw new IllegalArgumentException("encodedFragment == null");
+      this.encodedFragment = canonicalize(encodedFragment, FRAGMENT_ENCODE_SET, true, false);
+      return this;
     }
 
     public HttpUrl build() {
-      StringBuilder url = new StringBuilder();
-      url.append(scheme);
-      url.append("://");
+      if (scheme == null) throw new IllegalStateException("scheme == null");
+      if (host == null) throw new IllegalStateException("host == null");
+      return new HttpUrl(this);
+    }
 
-      String effectivePassword = (password != null && !password.isEmpty()) ? password : null;
-      if (!username.isEmpty() || effectivePassword != null) {
-        url.append(username);
-        if (effectivePassword != null) {
-          url.append(':');
-          url.append(effectivePassword);
+    @Override public String toString() {
+      StringBuilder result = new StringBuilder();
+      result.append(scheme);
+      result.append("://");
+
+      if (!encodedUsername.isEmpty() || !encodedPassword.isEmpty()) {
+        result.append(encodedUsername);
+        if (!encodedPassword.isEmpty()) {
+          result.append(':');
+          result.append(encodedPassword);
         }
-        url.append('@');
+        result.append('@');
       }
 
       if (host.indexOf(':') != -1) {
         // Host is an IPv6 address.
-        url.append('[');
-        url.append(host);
-        url.append(']');
+        result.append('[');
+        result.append(host);
+        result.append(']');
       } else {
-        url.append(host);
+        result.append(host);
       }
 
-      int defaultPort = defaultPort(scheme);
-      int effectivePort = port != -1 ? port : defaultPort;
-      if (effectivePort != defaultPort) {
-        url.append(':');
-        url.append(port);
+      int effectivePort = effectivePort();
+      if (effectivePort != defaultPort(scheme)) {
+        result.append(':');
+        result.append(effectivePort);
       }
 
-      String effectivePath = pathBuilder.length() > 0
-          ? pathBuilder.toString()
-          : "/";
-      url.append(effectivePath);
+      pathSegmentsToString(result, encodedPathSegments);
 
-      if (query != null) {
-        url.append('?');
-        url.append(query);
+      if (encodedQueryNamesAndValues != null) {
+        result.append('?');
+        namesAndValuesToQueryString(result, encodedQueryNamesAndValues);
       }
 
-      if (fragment != null) {
-        url.append('#');
-        url.append(fragment);
+      if (encodedFragment != null) {
+        result.append('#');
+        result.append(encodedFragment);
       }
 
-      return new HttpUrl(scheme, username, effectivePassword, host, effectivePort, effectivePath,
-          query, fragment, url.toString());
+      return result.toString();
     }
 
-    HttpUrl parse(HttpUrl base, String input) {
+    enum ParseResult {
+      SUCCESS,
+      MISSING_SCHEME,
+      UNSUPPORTED_SCHEME,
+      INVALID_PORT,
+      INVALID_HOST,
+    }
+
+    ParseResult parse(HttpUrl base, String input) {
       int pos = skipLeadingAsciiWhitespace(input, 0, input.length());
       int limit = skipTrailingAsciiWhitespace(input, pos, input.length());
 
@@ -433,16 +934,17 @@
           this.scheme = "http";
           pos += "http:".length();
         } else {
-          return null; // Not an HTTP scheme.
+          return ParseResult.UNSUPPORTED_SCHEME; // Not an HTTP scheme.
         }
       } else if (base != null) {
         this.scheme = base.scheme;
       } else {
-        return null; // No scheme.
+        return ParseResult.MISSING_SCHEME; // No scheme.
       }
 
       // Authority.
       boolean hasUsername = false;
+      boolean hasPassword = false;
       int slashCount = slashCount(input, pos, limit);
       if (slashCount >= 2 || base == null || !base.scheme.equals(this.scheme)) {
         // Read an authority if either:
@@ -464,20 +966,23 @@
           switch (c) {
             case '@':
               // User info precedes.
-              if (this.password == null) {
+              if (!hasPassword) {
                 int passwordColonOffset = delimiterOffset(
                     input, pos, componentDelimiterOffset, ":");
-                this.username = hasUsername
-                    ? (this.username + "%40" + username(input, pos, passwordColonOffset))
-                    : username(input, pos, passwordColonOffset);
+                String canonicalUsername = canonicalize(
+                    input, pos, passwordColonOffset, USERNAME_ENCODE_SET, true, false);
+                this.encodedUsername = hasUsername
+                    ? this.encodedUsername + "%40" + canonicalUsername
+                    : canonicalUsername;
                 if (passwordColonOffset != componentDelimiterOffset) {
-                  this.password = password(
-                      input, passwordColonOffset + 1, componentDelimiterOffset);
+                  hasPassword = true;
+                  this.encodedPassword = canonicalize(input, passwordColonOffset + 1,
+                      componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false);
                 }
                 hasUsername = true;
               } else {
-                this.password = this.password + "%40"
-                    + password(input, pos, componentDelimiterOffset);
+                this.encodedPassword = this.encodedPassword + "%40" + canonicalize(
+                    input, pos, componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false);
               }
               pos = componentDelimiterOffset + 1;
               break;
@@ -490,104 +995,132 @@
               // Host info precedes.
               int portColonOffset = portColonOffset(input, pos, componentDelimiterOffset);
               if (portColonOffset + 1 < componentDelimiterOffset) {
-                this.host = host(input, pos, portColonOffset);
-                this.port = port(input, portColonOffset + 1, componentDelimiterOffset);
-                if (this.port == -1) return null; // Invalid port.
+                this.host = canonicalizeHost(input, pos, portColonOffset);
+                this.port = parsePort(input, portColonOffset + 1, componentDelimiterOffset);
+                if (this.port == -1) return ParseResult.INVALID_PORT; // Invalid port.
               } else {
-                this.host = host(input, pos, portColonOffset);
+                this.host = canonicalizeHost(input, pos, portColonOffset);
                 this.port = defaultPort(this.scheme);
               }
-              if (this.host == null) return null; // Invalid host.
+              if (this.host == null) return ParseResult.INVALID_HOST; // Invalid host.
               pos = componentDelimiterOffset;
               break authority;
           }
         }
       } else {
         // This is a relative link. Copy over all authority components. Also maybe the path & query.
-        this.username = base.username;
-        this.password = base.password;
+        this.encodedUsername = base.encodedUsername();
+        this.encodedPassword = base.encodedPassword();
         this.host = base.host;
         this.port = base.port;
-        int c = pos != limit
-            ? input.charAt(pos)
-            : -1;
-        switch (c) {
-          case -1:
-          case '#':
-            pathBuilder.append(base.path);
-            this.query = base.query;
-            break;
-
-          case '?':
-            pathBuilder.append(base.path);
-            break;
-
-          case '/':
-          case '\\':
-            break;
-
-          default:
-            pathBuilder.append(base.path);
-            pathBuilder.append('/'); // Because pop wants the input to end with '/'.
-            pop();
-            break;
+        this.encodedPathSegments.clear();
+        this.encodedPathSegments.addAll(base.encodedPathSegments());
+        if (pos == limit || input.charAt(pos) == '#') {
+          encodedQuery(base.encodedQuery());
         }
       }
 
       // Resolve the relative path.
       int pathDelimiterOffset = delimiterOffset(input, pos, limit, "?#");
-      while (pos < pathDelimiterOffset) {
-        int pathSegmentDelimiterOffset = delimiterOffset(input, pos, pathDelimiterOffset, "/\\");
-        int segmentLength = pathSegmentDelimiterOffset - pos;
-
-        if ((segmentLength == 2 && input.regionMatches(false, pos, "..", 0, 2))
-            || (segmentLength == 4 && input.regionMatches(true, pos, "%2e.", 0, 4))
-            || (segmentLength == 4 && input.regionMatches(true, pos, ".%2e", 0, 4))
-            || (segmentLength == 6 && input.regionMatches(true, pos, "%2e%2e", 0, 6))) {
-          pop();
-        } else if ((segmentLength == 1 && input.regionMatches(false, pos, ".", 0, 1))
-            || (segmentLength == 3 && input.regionMatches(true, pos, "%2e", 0, 3))) {
-          // Skip '.' path segments.
-        } else if (pathSegmentDelimiterOffset < pathDelimiterOffset) {
-          pathSegment(input, pos, pathSegmentDelimiterOffset);
-          pathBuilder.append('/');
-        } else {
-          pathSegment(input, pos, pathSegmentDelimiterOffset);
-        }
-
-        pos = pathSegmentDelimiterOffset;
-        if (pathSegmentDelimiterOffset < pathDelimiterOffset) {
-          pos++; // Eat '/'.
-        }
-      }
+      resolvePath(input, pos, pathDelimiterOffset);
+      pos = pathDelimiterOffset;
 
       // Query.
       if (pos < limit && input.charAt(pos) == '?') {
         int queryDelimiterOffset = delimiterOffset(input, pos, limit, "#");
-        this.query = query(input, pos + 1, queryDelimiterOffset);
+        this.encodedQueryNamesAndValues = queryStringToNamesAndValues(canonicalize(
+            input, pos + 1, queryDelimiterOffset, QUERY_ENCODE_SET, true, true));
         pos = queryDelimiterOffset;
       }
 
       // Fragment.
       if (pos < limit && input.charAt(pos) == '#') {
-        this.fragment = fragment(input, pos + 1, limit);
+        this.encodedFragment = canonicalize(
+            input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false);
       }
 
-      return build();
+      return ParseResult.SUCCESS;
     }
 
-    /** Remove the last character '/' of path, plus all characters after the preceding '/'. */
-    private void pop() {
-      if (pathBuilder.charAt(pathBuilder.length() - 1) != '/') throw new IllegalStateException();
-
-      for (int i = pathBuilder.length() - 2; i >= 0; i--) {
-        if (pathBuilder.charAt(i) == '/') {
-          pathBuilder.delete(i + 1, pathBuilder.length());
-          return;
-        }
+    private void resolvePath(String input, int pos, int limit) {
+      // Read a delimiter.
+      if (pos == limit) {
+        // Empty path: keep the base path as-is.
+        return;
+      }
+      char c = input.charAt(pos);
+      if (c == '/' || c == '\\') {
+        // Absolute path: reset to the default "/".
+        encodedPathSegments.clear();
+        encodedPathSegments.add("");
+        pos++;
+      } else {
+        // Relative path: clear everything after the last '/'.
+        encodedPathSegments.set(encodedPathSegments.size() - 1, "");
       }
 
-      // If we get this far, there's nothing to pop. Do nothing.
+      // Read path segments.
+      for (int i = pos; i < limit; ) {
+        int pathSegmentDelimiterOffset = delimiterOffset(input, i, limit, "/\\");
+        boolean segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit;
+        push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true);
+        i = pathSegmentDelimiterOffset;
+        if (segmentHasTrailingSlash) i++;
+      }
+    }
+
+    /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */
+    private void push(String input, int pos, int limit, boolean addTrailingSlash,
+        boolean alreadyEncoded) {
+      String segment = canonicalize(
+          input, pos, limit, PATH_SEGMENT_ENCODE_SET, alreadyEncoded, false);
+      if (isDot(segment)) {
+        return; // Skip '.' path segments.
+      }
+      if (isDotDot(segment)) {
+        pop();
+        return;
+      }
+      if (encodedPathSegments.get(encodedPathSegments.size() - 1).isEmpty()) {
+        encodedPathSegments.set(encodedPathSegments.size() - 1, segment);
+      } else {
+        encodedPathSegments.add(segment);
+      }
+      if (addTrailingSlash) {
+        encodedPathSegments.add("");
+      }
+    }
+
+    private boolean isDot(String input) {
+      return input.equals(".") || input.equalsIgnoreCase("%2e");
+    }
+
+    private boolean isDotDot(String input) {
+      return input.equals("..")
+          || input.equalsIgnoreCase("%2e.")
+          || input.equalsIgnoreCase(".%2e")
+          || input.equalsIgnoreCase("%2e%2e");
+    }
+
+    /**
+     * Removes a path segment. When this method returns the last segment is always "", which means
+     * the encoded path will have a trailing '/'.
+     *
+     * <p>Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from
+     * ["a", "b", "c", ""] to ["a", "b", ""].
+     *
+     * <p>Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"]
+     * to ["a", "b", ""].
+     */
+    private void pop() {
+      String removed = encodedPathSegments.remove(encodedPathSegments.size() - 1);
+
+      // Make sure the path ends with a '/' by either adding an empty string or clearing a segment.
+      if (removed.isEmpty() && !encodedPathSegments.isEmpty()) {
+        encodedPathSegments.set(encodedPathSegments.size() - 1, "");
+      } else {
+        encodedPathSegments.add("");
+      }
     }
 
     /**
@@ -645,6 +1178,7 @@
 
         if ((c >= 'a' && c <= 'z')
             || (c >= 'A' && c <= 'Z')
+            || (c >= '0' && c <= '9')
             || c == '+'
             || c == '-'
             || c == '.') {
@@ -674,17 +1208,6 @@
       return slashCount;
     }
 
-    /**
-     * Returns the index of the first character in {@code input} that contains a character in {@code
-     * delimiters}. Returns limit if there is no such character.
-     */
-    private static int delimiterOffset(String input, int pos, int limit, String delimiters) {
-      for (int i = pos; i < limit; i++) {
-        if (delimiters.indexOf(input.charAt(i)) != -1) return i;
-      }
-      return limit;
-    }
-
     /** Finds the first ':' in {@code input}, skipping characters between square braces "[...]". */
     private static int portColonOffset(String input, int pos, int limit) {
       for (int i = pos; i < limit; i++) {
@@ -701,47 +1224,209 @@
       return limit; // No colon.
     }
 
-    private String username(String input, int pos, int limit) {
-      return encode(input, pos, limit, " \"';<=>@[]^`{}|");
-    }
-
-    private String password(String input, int pos, int limit) {
-      return encode(input, pos, limit, " \"':;<=>@[]\\^`{}|");
-    }
-
-    private static String host(String input, int pos, int limit) {
+    private static String canonicalizeHost(String input, int pos, int limit) {
       // Start by percent decoding the host. The WHATWG spec suggests doing this only after we've
       // checked for IPv6 square braces. But Chrome does it first, and that's more lenient.
-      String percentDecoded = decode(input, pos, limit);
+      String percentDecoded = percentDecode(input, pos, limit);
 
       // If the input is encased in square braces "[...]", drop 'em. We have an IPv6 address.
       if (percentDecoded.startsWith("[") && percentDecoded.endsWith("]")) {
-        return decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1);
+        InetAddress inetAddress = decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1);
+        if (inetAddress == null) return null;
+        byte[] address = inetAddress.getAddress();
+        if (address.length == 16) return inet6AddressToAscii(address);
+        throw new AssertionError();
       }
 
-      // Do IDN decoding. This converts {@code ☃.net} to {@code xn--n3h.net}.
-      String idnDecoded = domainToAscii(percentDecoded);
+      return domainToAscii(percentDecoded);
+    }
 
-      // Confirm that the decoded result doesn't contain any illegal characters.
-      int length = idnDecoded.length();
-      if (delimiterOffset(idnDecoded, 0, length, "\u0000\t\n\r #%/:?@[\\]") != length) {
+    /** Decodes an IPv6 address like 1111:2222:3333:4444:5555:6666:7777:8888 or ::1. */
+    private static InetAddress decodeIpv6(String input, int pos, int limit) {
+      byte[] address = new byte[16];
+      int b = 0;
+      int compress = -1;
+      int groupOffset = -1;
+
+      for (int i = pos; i < limit; ) {
+        if (b == address.length) return null; // Too many groups.
+
+        // Read a delimiter.
+        if (i + 2 <= limit && input.regionMatches(i, "::", 0, 2)) {
+          // Compression "::" delimiter, which is anywhere in the input, including its prefix.
+          if (compress != -1) return null; // Multiple "::" delimiters.
+          i += 2;
+          b += 2;
+          compress = b;
+          if (i == limit) break;
+        } else if (b != 0) {
+          // Group separator ":" delimiter.
+          if (input.regionMatches(i, ":", 0, 1)) {
+            i++;
+          } else if (input.regionMatches(i, ".", 0, 1)) {
+            // If we see a '.', rewind to the beginning of the previous group and parse as IPv4.
+            if (!decodeIpv4Suffix(input, groupOffset, limit, address, b - 2)) return null;
+            b += 2; // We rewound two bytes and then added four.
+            break;
+          } else {
+            return null; // Wrong delimiter.
+          }
+        }
+
+        // Read a group, one to four hex digits.
+        int value = 0;
+        groupOffset = i;
+        for (; i < limit; i++) {
+          char c = input.charAt(i);
+          int hexDigit = decodeHexDigit(c);
+          if (hexDigit == -1) break;
+          value = (value << 4) + hexDigit;
+        }
+        int groupLength = i - groupOffset;
+        if (groupLength == 0 || groupLength > 4) return null; // Group is the wrong size.
+
+        // We've successfully read a group. Assign its value to our byte array.
+        address[b++] = (byte) ((value >>> 8) & 0xff);
+        address[b++] = (byte) (value & 0xff);
+      }
+
+      // All done. If compression happened, we need to move bytes to the right place in the
+      // address. Here's a sample:
+      //
+      //      input: "1111:2222:3333::7777:8888"
+      //     before: { 11, 11, 22, 22, 33, 33, 00, 00, 77, 77, 88, 88, 00, 00, 00, 00  }
+      //   compress: 6
+      //          b: 10
+      //      after: { 11, 11, 22, 22, 33, 33, 00, 00, 00, 00, 00, 00, 77, 77, 88, 88 }
+      //
+      if (b != address.length) {
+        if (compress == -1) return null; // Address didn't have compression or enough groups.
+        System.arraycopy(address, compress, address, address.length - (b - compress), b - compress);
+        Arrays.fill(address, compress, compress + (address.length - b), (byte) 0);
+      }
+
+      try {
+        return InetAddress.getByAddress(address);
+      } catch (UnknownHostException e) {
+        throw new AssertionError();
+      }
+    }
+
+    /** Decodes an IPv4 address suffix of an IPv6 address, like 1111::5555:6666:192.168.0.1. */
+    private static boolean decodeIpv4Suffix(
+        String input, int pos, int limit, byte[] address, int addressOffset) {
+      int b = addressOffset;
+
+      for (int i = pos; i < limit; ) {
+        if (b == address.length) return false; // Too many groups.
+
+        // Read a delimiter.
+        if (b != addressOffset) {
+          if (input.charAt(i) != '.') return false; // Wrong delimiter.
+          i++;
+        }
+
+        // Read 1 or more decimal digits for a value in 0..255.
+        int value = 0;
+        int groupOffset = i;
+        for (; i < limit; i++) {
+          char c = input.charAt(i);
+          if (c < '0' || c > '9') break;
+          if (value == 0 && groupOffset != i) return false; // Reject unnecessary leading '0's.
+          value = (value * 10) + c - '0';
+          if (value > 255) return false; // Value out of range.
+        }
+        int groupLength = i - groupOffset;
+        if (groupLength == 0) return false; // No digits.
+
+        // We've successfully read a byte.
+        address[b++] = (byte) value;
+      }
+
+      if (b != addressOffset + 4) return false; // Too few groups. We wanted exactly four.
+      return true; // Success.
+    }
+
+    /**
+     * Performs IDN ToASCII encoding and canonicalize the result to lowercase. e.g. This converts
+     * {@code ☃.net} to {@code xn--n3h.net}, and {@code WwW.GoOgLe.cOm} to {@code www.google.com}.
+     * {@code null} will be returned if the input cannot be ToASCII encoded or if the result
+     * contains unsupported ASCII characters.
+     */
+    private static String domainToAscii(String input) {
+      try {
+        String result = IDN.toASCII(input).toLowerCase(Locale.US);
+        if (result.isEmpty()) return null;
+
+        if (result == null) return null;
+
+        // Confirm that the IDN ToASCII result doesn't contain any illegal characters.
+        if (containsInvalidHostnameAsciiCodes(result)) {
+          return null;
+        }
+        // TODO: implement all label limits.
+        return result;
+      } catch (IllegalArgumentException e) {
         return null;
       }
-
-      return idnDecoded;
     }
 
-    private static String decodeIpv6(String input, int pos, int limit) {
-      return input.substring(pos, limit); // TODO(jwilson) implement IPv6 decoding.
+    private static boolean containsInvalidHostnameAsciiCodes(String hostnameAscii) {
+      for (int i = 0; i < hostnameAscii.length(); i++) {
+        char c = hostnameAscii.charAt(i);
+        // The WHATWG Host parsing rules accepts some character codes which are invalid by
+        // definition for OkHttp's host header checks (and the WHATWG Host syntax definition). Here
+        // we rule out characters that would cause problems in host headers.
+        if (c <= '\u001f' || c >= '\u007f') {
+          return true;
+        }
+        // Check for the characters mentioned in the WHATWG Host parsing spec:
+        // U+0000, U+0009, U+000A, U+000D, U+0020, "#", "%", "/", ":", "?", "@", "[", "\", and "]"
+        // (excluding the characters covered above).
+        if (" #%/:?@[\\]".indexOf(c) != -1) {
+          return true;
+        }
+      }
+      return false;
     }
 
-    private static String domainToAscii(String input) {
-      return input; // TODO(jwilson): implement IDN decoding.
+    private static String inet6AddressToAscii(byte[] address) {
+      // Go through the address looking for the longest run of 0s. Each group is 2-bytes.
+      int longestRunOffset = -1;
+      int longestRunLength = 0;
+      for (int i = 0; i < address.length; i += 2) {
+        int currentRunOffset = i;
+        while (i < 16 && address[i] == 0 && address[i + 1] == 0) {
+          i += 2;
+        }
+        int currentRunLength = i - currentRunOffset;
+        if (currentRunLength > longestRunLength) {
+          longestRunOffset = currentRunOffset;
+          longestRunLength = currentRunLength;
+        }
+      }
+
+      // Emit each 2-byte group in hex, separated by ':'. The longest run of zeroes is "::".
+      Buffer result = new Buffer();
+      for (int i = 0; i < address.length; ) {
+        if (i == longestRunOffset) {
+          result.writeByte(':');
+          i += longestRunLength;
+          if (i == 16) result.writeByte(':');
+        } else {
+          if (i > 0) result.writeByte(':');
+          int group = (address[i] & 0xff) << 8 | address[i + 1] & 0xff;
+          result.writeHexadecimalUnsignedLong(group);
+          i += 2;
+        }
+      }
+      return result.readUtf8();
     }
 
-    private int port(String input, int pos, int limit) {
+    private static int parsePort(String input, int pos, int limit) {
       try {
-        String portString = encode(input, pos, limit, ""); // To skip '\n' etc.
+        // Canonicalize the port string to skip '\n' etc.
+        String portString = canonicalize(input, pos, limit, "", false, false);
         int i = Integer.parseInt(portString);
         if (i > 0 && i <= 65535) return i;
         return -1;
@@ -749,27 +1434,40 @@
         return -1; // Invalid port.
       }
     }
-
-    private void pathSegment(String input, int pos, int limit) {
-      encode(pathBuilder, input, pos, limit, " \"<>^`{}|");
-    }
-
-    private String query(String input, int pos, int limit) {
-      return encode(input, pos, limit, " \"'<>");
-    }
-
-    private String fragment(String input, int pos, int limit) {
-      return encode(input, pos, limit, ""); // To skip '\n' etc.
-    }
   }
 
-  private static String decode(String encoded, int pos, int limit) {
+  /**
+   * Returns the index of the first character in {@code input} that contains a character in {@code
+   * delimiters}. Returns limit if there is no such character.
+   */
+  private static int delimiterOffset(String input, int pos, int limit, String delimiters) {
     for (int i = pos; i < limit; i++) {
-      if (encoded.charAt(i) == '%') {
+      if (delimiters.indexOf(input.charAt(i)) != -1) return i;
+    }
+    return limit;
+  }
+
+  static String percentDecode(String encoded) {
+    return percentDecode(encoded, 0, encoded.length());
+  }
+
+  private List<String> percentDecode(List<String> list) {
+    List<String> result = new ArrayList<>(list.size());
+    for (String s : list) {
+      result.add(s != null ? percentDecode(s) : null);
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  static String percentDecode(String encoded, int pos, int limit) {
+    for (int i = pos; i < limit; i++) {
+      char c = encoded.charAt(i);
+      if (c == '%') {
         // Slow path: the character at i requires decoding!
         Buffer out = new Buffer();
         out.writeUtf8(encoded, pos, i);
-        return decode(out, encoded, i, limit);
+        percentDecode(out, encoded, i, limit);
+        return out.readUtf8();
       }
     }
 
@@ -777,7 +1475,7 @@
     return encoded.substring(pos, limit);
   }
 
-  private static String decode(Buffer out, String encoded, int pos, int limit) {
+  static void percentDecode(Buffer out, String encoded, int pos, int limit) {
     int codePoint;
     for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
       codePoint = encoded.codePointAt(i);
@@ -792,10 +1490,9 @@
       }
       out.writeUtf8CodePoint(codePoint);
     }
-    return out.readUtf8();
   }
 
-  private static int decodeHexDigit(char c) {
+  static int decodeHexDigit(char c) {
     if (c >= '0' && c <= '9') return c - '0';
     if (c >= 'a' && c <= 'f') return c - 'a' + 10;
     if (c >= 'A' && c <= 'F') return c - 'A' + 10;
@@ -807,23 +1504,30 @@
    * transformations:
    * <ul>
    *   <li>Tabs, newlines, form feeds and carriage returns are skipped.
+   *   <li>In queries, ' ' is encoded to '+' and '+' is encoded to "%2B".
    *   <li>Characters in {@code encodeSet} are percent-encoded.
    *   <li>Control characters and non-ASCII characters are percent-encoded.
    *   <li>All other characters are copied without transformation.
    * </ul>
+   *
+   * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
+   * @param query true if to encode ' ' as '+', and '+' as "%2B".
    */
-  static String encode(String input, int pos, int limit, String encodeSet) {
+  static String canonicalize(String input, int pos, int limit, String encodeSet,
+      boolean alreadyEncoded, boolean query) {
     int codePoint;
     for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
       codePoint = input.codePointAt(i);
       if (codePoint < 0x20
           || codePoint >= 0x7f
-          || encodeSet.indexOf(codePoint) != -1) {
+          || encodeSet.indexOf(codePoint) != -1
+          || (codePoint == '%' && !alreadyEncoded)
+          || (query && codePoint == '+')) {
         // Slow path: the character at i requires encoding!
-        StringBuilder out = new StringBuilder();
-        out.append(input, pos, i);
-        encode(out, input, i, limit, encodeSet);
-        return out.toString();
+        Buffer out = new Buffer();
+        out.writeUtf8(input, pos, i);
+        canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, query);
+        return out.readUtf8();
       }
     }
 
@@ -831,19 +1535,22 @@
     return input.substring(pos, limit);
   }
 
-  static void encode(StringBuilder out, String input, int pos, int limit, String encodeSet) {
+  static void canonicalize(Buffer out, String input, int pos, int limit,
+      String encodeSet, boolean alreadyEncoded, boolean query) {
     Buffer utf8Buffer = null; // Lazily allocated.
     int codePoint;
     for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
       codePoint = input.codePointAt(i);
-      if (codePoint == '\t'
-          || codePoint == '\n'
-          || codePoint == '\f'
-          || codePoint == '\r') {
+      if (alreadyEncoded
+          && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) {
         // Skip this character.
+      } else if (query && codePoint == '+') {
+        // HTML permits space to be encoded as '+'. We use '%20' to avoid special cases.
+        out.writeUtf8(alreadyEncoded ? "%20" : "%2B");
       } else if (codePoint < 0x20
           || codePoint >= 0x7f
-          || encodeSet.indexOf(codePoint) != -1) {
+          || encodeSet.indexOf(codePoint) != -1
+          || (codePoint == '%' && !alreadyEncoded)) {
         // Percent encode this character.
         if (utf8Buffer == null) {
           utf8Buffer = new Buffer();
@@ -851,14 +1558,19 @@
         utf8Buffer.writeUtf8CodePoint(codePoint);
         while (!utf8Buffer.exhausted()) {
           int b = utf8Buffer.readByte() & 0xff;
-          out.append('%');
-          out.append(HEX_DIGITS[(b >> 4) & 0xf]);
-          out.append(HEX_DIGITS[b & 0xf]);
+          out.writeByte('%');
+          out.writeByte(HEX_DIGITS[(b >> 4) & 0xf]);
+          out.writeByte(HEX_DIGITS[b & 0xf]);
         }
       } else {
         // This character doesn't need encoding. Just copy it over.
-        out.append((char) codePoint);
+        out.writeUtf8CodePoint(codePoint);
       }
     }
   }
+
+  static String canonicalize(
+      String input, String encodeSet, boolean alreadyEncoded, boolean query) {
+    return canonicalize(input, 0, input.length(), encodeSet, alreadyEncoded, query);
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index ed0811e..4ed8000 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -27,9 +27,11 @@
 import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
 import java.io.IOException;
 import java.net.CookieHandler;
+import java.net.MalformedURLException;
 import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.URLConnection;
+import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.List;
@@ -157,6 +159,11 @@
       public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) {
         tlsConfiguration.apply(sslSocket, isFallback);
       }
+
+      @Override public HttpUrl getHttpUrlChecked(String url)
+          throws MalformedURLException, UnknownHostException {
+        return HttpUrl.getChecked(url);
+      }
     };
   }
 
@@ -187,9 +194,9 @@
   private boolean followSslRedirects = true;
   private boolean followRedirects = true;
   private boolean retryOnConnectionFailure = true;
-  private int connectTimeout;
-  private int readTimeout;
-  private int writeTimeout;
+  private int connectTimeout = 10_000;
+  private int readTimeout = 10_000;
+  private int writeTimeout = 10_000;
 
   public OkHttpClient() {
     routeDatabase = new RouteDatabase();
@@ -351,7 +358,10 @@
   }
 
   /**
-   * Sets the socket factory used to create connections.
+   * Sets the socket factory used to create connections. OkHttp only uses
+   * the parameterless {@link SocketFactory#createSocket() createSocket()}
+   * method to create unconnected sockets. Overriding this method,
+   * e. g., allows the socket to be bound to a specific local address.
    *
    * <p>If unset, the {@link SocketFactory#getDefault() system-wide default}
    * socket factory will be used.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 84fd045..2417c13 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -15,12 +15,9 @@
  */
 package com.squareup.okhttp;
 
-import com.squareup.okhttp.internal.Platform;
 import com.squareup.okhttp.internal.http.HttpMethod;
 import java.io.IOException;
-import java.net.MalformedURLException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.List;
 
@@ -29,45 +26,44 @@
  * is null or itself immutable.
  */
 public final class Request {
-  private final String urlString;
+  private final HttpUrl url;
   private final String method;
   private final Headers headers;
   private final RequestBody body;
   private final Object tag;
 
-  private volatile URL url; // Lazily initialized.
-  private volatile URI uri; // Lazily initialized.
+  private volatile URL javaNetUrl; // Lazily initialized.
+  private volatile URI javaNetUri; // Lazily initialized.
   private volatile CacheControl cacheControl; // Lazily initialized.
 
   private Request(Builder builder) {
-    this.urlString = builder.urlString;
+    this.url = builder.url;
     this.method = builder.method;
     this.headers = builder.headers.build();
     this.body = builder.body;
     this.tag = builder.tag != null ? builder.tag : this;
-    this.url = builder.url;
+  }
+
+  public HttpUrl httpUrl() {
+    return url;
   }
 
   public URL url() {
-    try {
-      URL result = url;
-      return result != null ? result : (url = new URL(urlString));
-    } catch (MalformedURLException e) {
-      throw new RuntimeException("Malformed URL: " + urlString, e);
-    }
+    URL result = javaNetUrl;
+    return result != null ? result : (javaNetUrl = url.url());
   }
 
   public URI uri() throws IOException {
     try {
-      URI result = uri;
-      return result != null ? result : (uri = Platform.get().toUriLenient(url()));
-    } catch (URISyntaxException e) {
+      URI result = javaNetUri;
+      return result != null ? result : (javaNetUri = url.uri());
+    } catch (IllegalStateException e) {
       throw new IOException(e.getMessage());
     }
   }
 
   public String urlString() {
-    return urlString;
+    return url.toString();
   }
 
   public String method() {
@@ -108,22 +104,21 @@
   }
 
   public boolean isHttps() {
-    return url().getProtocol().equals("https");
+    return url.isHttps();
   }
 
   @Override public String toString() {
     return "Request{method="
         + method
         + ", url="
-        + urlString
+        + url
         + ", tag="
         + (tag != this ? tag : null)
         + '}';
   }
 
   public static class Builder {
-    private String urlString;
-    private URL url;
+    private HttpUrl url;
     private String method;
     private Headers.Builder headers;
     private RequestBody body;
@@ -135,7 +130,6 @@
     }
 
     private Builder(Request request) {
-      this.urlString = request.urlString;
       this.url = request.url;
       this.method = request.method;
       this.body = request.body;
@@ -143,18 +137,44 @@
       this.headers = request.headers.newBuilder();
     }
 
-    public Builder url(String url) {
+    public Builder url(HttpUrl url) {
       if (url == null) throw new IllegalArgumentException("url == null");
-      this.urlString = url;
-      this.url = null;
+      this.url = url;
       return this;
     }
 
+    /**
+     * Sets the URL target of this request.
+     *
+     * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this
+     *     exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs.
+     */
+    public Builder url(String url) {
+      if (url == null) throw new IllegalArgumentException("url == null");
+
+      // Silently replace websocket URLs with HTTP URLs.
+      if (url.regionMatches(true, 0, "ws:", 0, 3)) {
+        url = "http:" + url.substring(3);
+      } else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
+        url = "https:" + url.substring(4);
+      }
+
+      HttpUrl parsed = HttpUrl.parse(url);
+      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
+      return url(parsed);
+    }
+
+    /**
+     * Sets the URL target of this request.
+     *
+     * @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code
+     *     https}.
+     */
     public Builder url(URL url) {
       if (url == null) throw new IllegalArgumentException("url == null");
-      this.url = url;
-      this.urlString = url.toString();
-      return this;
+      HttpUrl parsed = HttpUrl.get(url);
+      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
+      return url(parsed);
     }
 
     /**
@@ -251,7 +271,7 @@
     }
 
     public Request build() {
-      if (urlString == null) throw new IllegalStateException("url == null");
+      if (url == null) throw new IllegalStateException("url == null");
       return new Request(this);
     }
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ConnectionSpecSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ConnectionSpecSelector.java
index ac9de11..1b2dc03 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/ConnectionSpecSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ConnectionSpecSelector.java
@@ -17,7 +17,6 @@
 package com.squareup.okhttp.internal;
 
 import com.squareup.okhttp.ConnectionSpec;
-
 import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.net.ProtocolException;
@@ -92,19 +91,18 @@
     // Any future attempt to connect using this strategy will be a fallback attempt.
     isFallback = true;
 
-    // TODO(nfuller): This is the same logic as in HttpEngine.
+    if (!isFallbackPossible) {
+      return false;
+    }
+
     // If there was a protocol problem, don't recover.
     if (e instanceof ProtocolException) {
       return false;
     }
 
-    // If there was an interruption or timeout, don't recover.
-    //
-    // NOTE: This code (unlike the rest of this method) is not shared with HttpEngine.
-    // In HttpEngine, we would like to retry if we see an interruption or timeout because
-    // we might potentially be connecting to a different address family (IPV4 vs IPV6, say).
-    // That consideration is not relevant here, since the SocketConnector will always connect
-    // via the same Route even if the connection failed.
+    // If there was an interruption or timeout (SocketTimeoutException), don't recover.
+    // For the socket connect timeout case we do not try the same host with a different
+    // ConnectionSpec: we assume it is unreachable.
     if (e instanceof InterruptedIOException) {
       return false;
     }
@@ -122,13 +120,11 @@
       // e.g. a certificate pinning error.
       return false;
     }
-    // TODO(nfuller): End of common code.
 
 
     // On Android, SSLProtocolExceptions can be caused by TLS_FALLBACK_SCSV failures, which means we
     // retry those when we probably should not.
-    return ((e instanceof SSLHandshakeException || e instanceof SSLProtocolException))
-        && isFallbackPossible;
+    return (e instanceof SSLHandshakeException || e instanceof SSLProtocolException);
   }
 
   /**
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 284771f..b940168 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -197,8 +197,7 @@
     this.executor = executor;
   }
 
-  // Visible for testing.
-  void initialize() throws IOException {
+  public synchronized void initialize() throws IOException {
     assert Thread.holdsLock(this);
 
     if (initialized) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
index 1e583ba..21bcbf5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
@@ -21,6 +21,7 @@
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.ConnectionSpec;
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
@@ -28,6 +29,8 @@
 import com.squareup.okhttp.internal.http.RouteException;
 import com.squareup.okhttp.internal.http.Transport;
 import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.UnknownHostException;
 import java.util.logging.Logger;
 import javax.net.ssl.SSLSocket;
 import okio.BufferedSink;
@@ -85,6 +88,9 @@
   public abstract void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket,
       boolean isFallback);
 
+  public abstract HttpUrl getHttpUrlChecked(String url)
+      throws MalformedURLException, UnknownHostException;
+
   // TODO delete the following when web sockets move into the main package.
   public abstract void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket);
   public abstract void callEngineReleaseConnection(Call call) throws IOException;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
index 073f75f..b906495 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -25,9 +25,6 @@
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
@@ -76,10 +73,6 @@
   public void untagSocket(Socket socket) throws SocketException {
   }
 
-  public URI toUriLenient(URL url) throws URISyntaxException {
-    return url.toURI(); // this isn't as good as the built-in toUriLenient
-  }
-
   /**
    * Configure TLS extensions on {@code sslSocket} for {@code route}.
    *
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 eee686f..efc26ec 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -16,6 +16,7 @@
 
 package com.squareup.okhttp.internal;
 
+import com.squareup.okhttp.HttpUrl;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InterruptedIOException;
@@ -23,8 +24,6 @@
 import java.lang.reflect.Array;
 import java.net.ServerSocket;
 import java.net.Socket;
-import java.net.URI;
-import java.net.URL;
 import java.nio.charset.Charset;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -51,24 +50,6 @@
   private Util() {
   }
 
-  public static int getEffectivePort(URI uri) {
-    return getEffectivePort(uri.getScheme(), uri.getPort());
-  }
-
-  public static int getEffectivePort(URL url) {
-    return getEffectivePort(url.getProtocol(), url.getPort());
-  }
-
-  private static int getEffectivePort(String scheme, int specifiedPort) {
-    return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme);
-  }
-
-  public static int getDefaultPort(String protocol) {
-    if ("http".equals(protocol)) return 80;
-    if ("https".equals(protocol)) return 443;
-    return -1;
-  }
-
   public static void checkOffsetAndCount(long arrayLength, long offset, long count) {
     if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
       throw new ArrayIndexOutOfBoundsException();
@@ -103,6 +84,8 @@
     if (socket != null) {
       try {
         socket.close();
+      } catch (AssertionError e) {
+        if (!isAndroidGetsocknameError(e)) throw e;
       } catch (RuntimeException rethrown) {
         throw rethrown;
       } catch (Exception ignored) {
@@ -272,4 +255,37 @@
     }
     return result;
   }
+
+  public static String hostHeader(HttpUrl url) {
+    // TODO: square braces for IPv6 ?
+    return url.port() != HttpUrl.defaultPort(url.scheme())
+        ? url.host() + ":" + url.port()
+        : url.host();
+  }
+
+  /** Returns {@code s} with control characters and non-ASCII characters replaced with '?'. */
+  public static String toHumanReadableAscii(String s) {
+    for (int i = 0, length = s.length(), c; i < length; i += Character.charCount(c)) {
+      c = s.codePointAt(i);
+      if (c > '\u001f' && c < '\u007f') continue;
+
+      Buffer buffer = new Buffer();
+      buffer.writeUtf8(s, 0, i);
+      for (int j = i; j < length; j += Character.charCount(c)) {
+        c = s.codePointAt(j);
+        buffer.writeUtf8CodePoint(c > '\u001f' && c < '\u007f' ? c : '?');
+      }
+      return buffer.readUtf8();
+    }
+    return s;
+  }
+
+  /**
+   * Returns true if {@code e} is due to a firmware bug fixed after Android 4.2.2.
+   * https://code.google.com/p/android/issues/detail?id=54072
+   */
+  public static boolean isAndroidGetsocknameError(AssertionError e) {
+    return e.getCause() != null && e.getMessage() != null
+        && e.getMessage().contains("getsockname failed");
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/ErrorCode.java
similarity index 97%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/ErrorCode.java
index 701de92..0edd5ef 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/ErrorCode.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-7
 public enum ErrorCode {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameReader.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameReader.java
index e3736c5..9f7f086 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameReader.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -27,7 +27,7 @@
   void readConnectionPreface() throws IOException;
   boolean nextFrame(Handler handler) throws IOException;
 
-  public interface Handler {
+  interface Handler {
     void data(boolean inFinished, int streamId, BufferedSource source, int length)
         throws IOException;
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameWriter.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameWriter.java
index 0f4b799..dcaaf3a 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FrameWriter.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.Closeable;
 import java.io.IOException;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
similarity index 89%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
index 2966ce0..a86924b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.NamedRunnable;
@@ -33,12 +33,14 @@
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
 import okio.Buffer;
 import okio.BufferedSource;
 import okio.ByteString;
 import okio.Okio;
 
-import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.Internal.logger;
+import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
 
 /**
  * A socket connection to a remote peer. A connection hosts streams which can
@@ -49,7 +51,7 @@
  * for SPDY. This is motivated by exception transparency: an IOException that
  * was triggered by a certain caller can be caught and handled by that caller.
  */
-public final class SpdyConnection implements Closeable {
+public final class FramedConnection implements Closeable {
 
   // Internal state of this connection is guarded by 'this'. No blocking
   // operations may be performed while holding this lock!
@@ -65,9 +67,9 @@
 
   private static final ExecutorService executor = new ThreadPoolExecutor(0,
       Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
-      Util.threadFactory("OkHttp SpdyConnection", true));
+      Util.threadFactory("OkHttp FramedConnection", true));
 
-  /** The protocol variant, like {@link com.squareup.okhttp.internal.spdy.Spdy3}. */
+  /** The protocol variant, like {@link com.squareup.okhttp.internal.framed.Spdy3}. */
   final Protocol protocol;
 
   /** True if this peer initiated the connection. */
@@ -78,7 +80,7 @@
    * run on the callback executor.
    */
   private final IncomingStreamHandler handler;
-  private final Map<Integer, SpdyStream> streams = new HashMap<>();
+  private final Map<Integer, FramedStream> streams = new HashMap<>();
   private final String hostName;
   private int lastGoodStreamId;
   private int nextStreamId;
@@ -126,7 +128,7 @@
   // Visible for testing
   final Reader readerRunnable;
 
-  private SpdyConnection(Builder builder) throws IOException {
+  private FramedConnection(Builder builder) throws IOException {
     protocol = builder.protocol;
     pushObserver = builder.pushObserver;
     client = builder.client;
@@ -178,19 +180,19 @@
   }
 
   /**
-   * Returns the number of {@link SpdyStream#isOpen() open streams} on this
+   * Returns the number of {@link FramedStream#isOpen() open streams} on this
    * connection.
    */
   public synchronized int openStreamCount() {
     return streams.size();
   }
 
-  synchronized SpdyStream getStream(int id) {
+  synchronized FramedStream getStream(int id) {
     return streams.get(id);
   }
 
-  synchronized SpdyStream removeStream(int streamId) {
-    SpdyStream stream = streams.remove(streamId);
+  synchronized FramedStream removeStream(int streamId) {
+    FramedStream stream = streams.remove(streamId);
     if (stream != null && streams.isEmpty()) {
       setIdle(true);
     }
@@ -223,7 +225,7 @@
    * @param out true to create an output stream that we can use to send data
    *     to the remote peer. Corresponds to {@code FLAG_FIN}.
    */
-  public SpdyStream pushStream(int associatedStreamId, List<Header> requestHeaders, boolean out)
+  public FramedStream pushStream(int associatedStreamId, List<Header> requestHeaders, boolean out)
       throws IOException {
     if (client) throw new IllegalStateException("Client cannot push requests.");
     if (protocol != Protocol.HTTP_2) throw new IllegalStateException("protocol != HTTP_2");
@@ -238,16 +240,16 @@
    * @param in true to create an input stream that the remote peer can use to send data to us.
    *     Corresponds to {@code FLAG_UNIDIRECTIONAL}.
    */
-  public SpdyStream newStream(List<Header> requestHeaders, boolean out, boolean in)
+  public FramedStream newStream(List<Header> requestHeaders, boolean out, boolean in)
       throws IOException {
     return newStream(0, requestHeaders, out, in);
   }
 
-  private SpdyStream newStream(int associatedStreamId, List<Header> requestHeaders, boolean out,
+  private FramedStream newStream(int associatedStreamId, List<Header> requestHeaders, boolean out,
       boolean in) throws IOException {
     boolean outFinished = !out;
     boolean inFinished = !in;
-    SpdyStream stream;
+    FramedStream stream;
     int streamId;
 
     synchronized (frameWriter) {
@@ -257,7 +259,7 @@
         }
         streamId = nextStreamId;
         nextStreamId += 2;
-        stream = new SpdyStream(streamId, this, outFinished, inFinished, requestHeaders);
+        stream = new FramedStream(streamId, this, outFinished, inFinished, requestHeaders);
         if (stream.isOpen()) {
           streams.put(streamId, stream);
           setIdle(false);
@@ -306,7 +308,7 @@
 
     while (byteCount > 0) {
       int toWrite;
-      synchronized (SpdyConnection.this) {
+      synchronized (FramedConnection.this) {
         try {
           while (bytesLeftInWriteWindow <= 0) {
             // Before blocking, confirm that the stream we're writing is still open. It's possible
@@ -314,7 +316,7 @@
             if (!streams.containsKey(streamId)) {
               throw new IOException("stream closed");
             }
-            SpdyConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE.
+            FramedConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE.
           }
         } catch (InterruptedException e) {
           throw new InterruptedIOException();
@@ -336,7 +338,7 @@
    */
   void addBytesToWriteWindow(long delta) {
     bytesLeftInWriteWindow += delta;
-    if (delta > 0) SpdyConnection.this.notifyAll();
+    if (delta > 0) FramedConnection.this.notifyAll();
   }
 
   void writeSynResetLater(final int streamId, final ErrorCode errorCode) {
@@ -453,11 +455,11 @@
       thrown = e;
     }
 
-    SpdyStream[] streamsToClose = null;
+    FramedStream[] streamsToClose = null;
     Ping[] pingsToCancel = null;
     synchronized (this) {
       if (!streams.isEmpty()) {
-        streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
+        streamsToClose = streams.values().toArray(new FramedStream[streams.size()]);
         streams.clear();
         setIdle(false);
       }
@@ -468,7 +470,7 @@
     }
 
     if (streamsToClose != null) {
-      for (SpdyStream stream : streamsToClose) {
+      for (FramedStream stream : streamsToClose) {
         try {
           stream.close(streamCode);
         } catch (IOException e) {
@@ -550,8 +552,8 @@
       return this;
     }
 
-    public SpdyConnection build() throws IOException {
-      return new SpdyConnection(this);
+    public FramedConnection build() throws IOException {
+      return new FramedConnection(this);
     }
   }
 
@@ -578,7 +580,7 @@
         }
         connectionErrorCode = ErrorCode.NO_ERROR;
         streamErrorCode = ErrorCode.CANCEL;
-      } catch (RuntimeException | IOException e) {
+      } catch (IOException e) {
         connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
         streamErrorCode = ErrorCode.PROTOCOL_ERROR;
       } finally {
@@ -596,7 +598,7 @@
         pushDataLater(streamId, source, length, inFinished);
         return;
       }
-      SpdyStream dataStream = getStream(streamId);
+      FramedStream dataStream = getStream(streamId);
       if (dataStream == null) {
         writeSynResetLater(streamId, ErrorCode.INVALID_STREAM);
         source.skip(length);
@@ -614,8 +616,8 @@
         pushHeadersLater(streamId, headerBlock, inFinished);
         return;
       }
-      SpdyStream stream;
-      synchronized (SpdyConnection.this) {
+      FramedStream stream;
+      synchronized (FramedConnection.this) {
         // If we're shutdown, don't bother with this stream.
         if (shutdown) return;
 
@@ -635,7 +637,8 @@
           if (streamId % 2 == nextStreamId % 2) return;
 
           // Create a stream.
-          final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished,
+          final FramedStream
+              newStream = new FramedStream(streamId, FramedConnection.this, outFinished,
               inFinished, headerBlock);
           lastGoodStreamId = streamId;
           streams.put(streamId, newStream);
@@ -643,7 +646,8 @@
             @Override public void execute() {
               try {
                 handler.receive(newStream);
-              } catch (RuntimeException | IOException e) {
+              } catch (IOException e) {
+                logger.log(Level.INFO, "StreamHandler failure for " + hostName, e);
                 try {
                   newStream.close(ErrorCode.PROTOCOL_ERROR);
                 } catch (IOException ignored) {
@@ -672,7 +676,7 @@
         pushResetLater(streamId, errorCode);
         return;
       }
-      SpdyStream rstStream = removeStream(streamId);
+      FramedStream rstStream = removeStream(streamId);
       if (rstStream != null) {
         rstStream.receiveRstStream(errorCode);
       }
@@ -680,8 +684,8 @@
 
     @Override public void settings(boolean clearPrevious, Settings newSettings) {
       long delta = 0;
-      SpdyStream[] streamsToNotify = null;
-      synchronized (SpdyConnection.this) {
+      FramedStream[] streamsToNotify = null;
+      synchronized (FramedConnection.this) {
         int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
         if (clearPrevious) peerSettings.clear();
         peerSettings.merge(newSettings);
@@ -696,12 +700,12 @@
             receivedInitialPeerSettings = true;
           }
           if (!streams.isEmpty()) {
-            streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]);
+            streamsToNotify = streams.values().toArray(new FramedStream[streams.size()]);
           }
         }
       }
       if (streamsToNotify != null && delta != 0) {
-        for (SpdyStream stream : streamsToNotify) {
+        for (FramedStream stream : streamsToNotify) {
           synchronized (stream) {
             stream.addBytesToWriteWindow(delta);
           }
@@ -741,29 +745,29 @@
       }
 
       // Copy the streams first. We don't want to hold a lock when we call receiveRstStream().
-      SpdyStream[] streamsCopy;
-      synchronized (SpdyConnection.this) {
-        streamsCopy = streams.values().toArray(new SpdyStream[streams.size()]);
+      FramedStream[] streamsCopy;
+      synchronized (FramedConnection.this) {
+        streamsCopy = streams.values().toArray(new FramedStream[streams.size()]);
         shutdown = true;
       }
 
       // Fail all streams created after the last good stream ID.
-      for (SpdyStream spdyStream : streamsCopy) {
-        if (spdyStream.getId() > lastGoodStreamId && spdyStream.isLocallyInitiated()) {
-          spdyStream.receiveRstStream(ErrorCode.REFUSED_STREAM);
-          removeStream(spdyStream.getId());
+      for (FramedStream framedStream : streamsCopy) {
+        if (framedStream.getId() > lastGoodStreamId && framedStream.isLocallyInitiated()) {
+          framedStream.receiveRstStream(ErrorCode.REFUSED_STREAM);
+          removeStream(framedStream.getId());
         }
       }
     }
 
     @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
       if (streamId == 0) {
-        synchronized (SpdyConnection.this) {
+        synchronized (FramedConnection.this) {
           bytesLeftInWriteWindow += windowSizeIncrement;
-          SpdyConnection.this.notifyAll();
+          FramedConnection.this.notifyAll();
         }
       } else {
-        SpdyStream stream = getStream(streamId);
+        FramedStream stream = getStream(streamId);
         if (stream != null) {
           synchronized (stream) {
             stream.addBytesToWriteWindow(windowSizeIncrement);
@@ -810,7 +814,7 @@
         try {
           if (cancel) {
             frameWriter.rstStream(streamId, ErrorCode.CANCEL);
-            synchronized (SpdyConnection.this) {
+            synchronized (FramedConnection.this) {
               currentPushRequests.remove(streamId);
             }
           }
@@ -828,7 +832,7 @@
         try {
           if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL);
           if (cancel || inFinished) {
-            synchronized (SpdyConnection.this) {
+            synchronized (FramedConnection.this) {
               currentPushRequests.remove(streamId);
             }
           }
@@ -854,7 +858,7 @@
           boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished);
           if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL);
           if (cancel || inFinished) {
-            synchronized (SpdyConnection.this) {
+            synchronized (FramedConnection.this) {
               currentPushRequests.remove(streamId);
             }
           }
@@ -868,7 +872,7 @@
     pushExecutor.execute(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) {
       @Override public void execute() {
         pushObserver.onReset(streamId, errorCode);
-        synchronized (SpdyConnection.this) {
+        synchronized (FramedConnection.this) {
           currentPushRequests.remove(streamId);
         }
       }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedStream.java
similarity index 89%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedStream.java
index cdbd7aa..a47b12e 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedStream.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -29,16 +29,16 @@
 import okio.Source;
 import okio.Timeout;
 
-import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.framed.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
 
 /** A logical bidirectional stream. */
-public final class SpdyStream {
+public final class FramedStream {
   // Internal state is guarded by this. No long-running or potentially
   // blocking operations are performed while the lock is held.
 
   /**
    * The total number of bytes consumed by the application (with {@link
-   * SpdyDataSource#read}), but not yet acknowledged by sending a {@code
+   * FramedDataSource#read}), but not yet acknowledged by sending a {@code
    * WINDOW_UPDATE} frame on this stream.
    */
   // Visible for testing
@@ -53,7 +53,7 @@
   long bytesLeftInWriteWindow;
 
   private final int id;
-  private final SpdyConnection connection;
+  private final FramedConnection connection;
 
   /** Headers sent by the stream initiator. Immutable and non null. */
   private final List<Header> requestHeaders;
@@ -61,10 +61,10 @@
   /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
   private List<Header> responseHeaders;
 
-  private final SpdyDataSource source;
-  final SpdyDataSink sink;
-  private final SpdyTimeout readTimeout = new SpdyTimeout();
-  private final SpdyTimeout writeTimeout = new SpdyTimeout();
+  private final FramedDataSource source;
+  final FramedDataSink sink;
+  private final StreamTimeout readTimeout = new StreamTimeout();
+  private final StreamTimeout writeTimeout = new StreamTimeout();
 
   /**
    * The reason why this stream was abnormally closed. If there are multiple
@@ -73,7 +73,7 @@
    */
   private ErrorCode errorCode = null;
 
-  SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished,
+  FramedStream(int id, FramedConnection connection, boolean outFinished, boolean inFinished,
       List<Header> requestHeaders) {
     if (connection == null) throw new NullPointerException("connection == null");
     if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
@@ -81,9 +81,9 @@
     this.connection = connection;
     this.bytesLeftInWriteWindow =
         connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
-    this.source = new SpdyDataSource(
+    this.source = new FramedDataSource(
         connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
-    this.sink = new SpdyDataSink();
+    this.sink = new FramedDataSink();
     this.source.finished = inFinished;
     this.sink.finished = outFinished;
     this.requestHeaders = requestHeaders;
@@ -121,7 +121,7 @@
     return connection.client == streamIsClient;
   }
 
-  public SpdyConnection getConnection() {
+  public FramedConnection getConnection() {
     return connection;
   }
 
@@ -161,7 +161,7 @@
    * to the remote peer. Corresponds to {@code FLAG_FIN}.
    */
   public void reply(List<Header> responseHeaders, boolean out) throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
+    assert (!Thread.holdsLock(FramedStream.this));
     boolean outFinished = false;
     synchronized (this) {
       if (responseHeaders == null) {
@@ -251,7 +251,7 @@
   }
 
   void receiveHeaders(List<Header> headers, HeadersMode headersMode) {
-    assert (!Thread.holdsLock(SpdyStream.this));
+    assert (!Thread.holdsLock(FramedStream.this));
     ErrorCode errorCode = null;
     boolean open = true;
     synchronized (this) {
@@ -282,12 +282,12 @@
   }
 
   void receiveData(BufferedSource in, int length) throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
+    assert (!Thread.holdsLock(FramedStream.this));
     this.source.receive(in, length);
   }
 
   void receiveFin() {
-    assert (!Thread.holdsLock(SpdyStream.this));
+    assert (!Thread.holdsLock(FramedStream.this));
     boolean open;
     synchronized (this) {
       this.source.finished = true;
@@ -311,11 +311,11 @@
    * class uses synchronization to safely receive incoming data frames, it is
    * not intended for use by multiple readers.
    */
-  private final class SpdyDataSource implements Source {
+  private final class FramedDataSource implements Source {
     /** Buffer to receive data from the network into. Only accessed by the reader thread. */
     private final Buffer receiveBuffer = new Buffer();
 
-    /** Buffer with readable data. Guarded by SpdyStream.this. */
+    /** Buffer with readable data. Guarded by FramedStream.this. */
     private final Buffer readBuffer = new Buffer();
 
     /** Maximum number of bytes to buffer before reporting a flow control error. */
@@ -330,7 +330,7 @@
      */
     private boolean finished;
 
-    private SpdyDataSource(long maxByteCount) {
+    private FramedDataSource(long maxByteCount) {
       this.maxByteCount = maxByteCount;
     }
 
@@ -339,7 +339,7 @@
       if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
 
       long read;
-      synchronized (SpdyStream.this) {
+      synchronized (FramedStream.this) {
         waitUntilReadable();
         checkNotClosed();
         if (readBuffer.size() == 0) return -1; // This source is exhausted.
@@ -382,12 +382,12 @@
     }
 
     void receive(BufferedSource in, long byteCount) throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
+      assert (!Thread.holdsLock(FramedStream.this));
 
       while (byteCount > 0) {
         boolean finished;
         boolean flowControlError;
-        synchronized (SpdyStream.this) {
+        synchronized (FramedStream.this) {
           finished = this.finished;
           flowControlError = byteCount + readBuffer.size() > maxByteCount;
         }
@@ -411,11 +411,11 @@
         byteCount -= read;
 
         // Move the received data to the read buffer to the reader can read it.
-        synchronized (SpdyStream.this) {
+        synchronized (FramedStream.this) {
           boolean wasEmpty = readBuffer.size() == 0;
           readBuffer.writeAll(receiveBuffer);
           if (wasEmpty) {
-            SpdyStream.this.notifyAll();
+            FramedStream.this.notifyAll();
           }
         }
       }
@@ -426,10 +426,10 @@
     }
 
     @Override public void close() throws IOException {
-      synchronized (SpdyStream.this) {
+      synchronized (FramedStream.this) {
         closed = true;
         readBuffer.clear();
-        SpdyStream.this.notifyAll();
+        FramedStream.this.notifyAll();
       }
       cancelStreamIfNecessary();
     }
@@ -445,7 +445,7 @@
   }
 
   private void cancelStreamIfNecessary() throws IOException {
-    assert (!Thread.holdsLock(SpdyStream.this));
+    assert (!Thread.holdsLock(FramedStream.this));
     boolean open;
     boolean cancel;
     synchronized (this) {
@@ -457,7 +457,7 @@
       // is safe because the input stream is closed (we won't use any
       // further bytes) and the output stream is either finished or closed
       // (so RSTing both streams doesn't cause harm).
-      SpdyStream.this.close(ErrorCode.CANCEL);
+      FramedStream.this.close(ErrorCode.CANCEL);
     } else if (!open) {
       connection.removeStream(id);
     }
@@ -467,7 +467,7 @@
    * A sink that writes outgoing data frames of a stream. This class is not
    * thread safe.
    */
-  final class SpdyDataSink implements Sink {
+  final class FramedDataSink implements Sink {
     private static final long EMIT_BUFFER_SIZE = 16384;
 
     /**
@@ -485,7 +485,7 @@
     private boolean finished;
 
     @Override public void write(Buffer source, long byteCount) throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
+      assert (!Thread.holdsLock(FramedStream.this));
       sendBuffer.write(source, byteCount);
       while (sendBuffer.size() >= EMIT_BUFFER_SIZE) {
         emitDataFrame(false);
@@ -498,7 +498,7 @@
      */
     private void emitDataFrame(boolean outFinished) throws IOException {
       long toWrite;
-      synchronized (SpdyStream.this) {
+      synchronized (FramedStream.this) {
         writeTimeout.enter();
         try {
           while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) {
@@ -522,8 +522,8 @@
     }
 
     @Override public void flush() throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-      synchronized (SpdyStream.this) {
+      assert (!Thread.holdsLock(FramedStream.this));
+      synchronized (FramedStream.this) {
         checkOutNotClosed();
       }
       while (sendBuffer.size() > 0) {
@@ -537,8 +537,8 @@
     }
 
     @Override public void close() throws IOException {
-      assert (!Thread.holdsLock(SpdyStream.this));
-      synchronized (SpdyStream.this) {
+      assert (!Thread.holdsLock(FramedStream.this));
+      synchronized (FramedStream.this) {
         if (closed) return;
       }
       if (!sink.finished) {
@@ -552,7 +552,7 @@
           connection.writeData(id, true, null, 0);
         }
       }
-      synchronized (SpdyStream.this) {
+      synchronized (FramedStream.this) {
         closed = true;
       }
       connection.flush();
@@ -566,7 +566,7 @@
    */
   void addBytesToWriteWindow(long delta) {
     bytesLeftInWriteWindow += delta;
-    if (delta > 0) SpdyStream.this.notifyAll();
+    if (delta > 0) FramedStream.this.notifyAll();
   }
 
   private void checkOutNotClosed() throws IOException {
@@ -596,7 +596,7 @@
    * reached. In that case we close the stream (asynchronously) which will
    * notify the waiting thread.
    */
-  class SpdyTimeout extends AsyncTimeout {
+  class StreamTimeout extends AsyncTimeout {
     @Override protected void timedOut() {
       closeLater(ErrorCode.CANCEL);
     }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Header.java
similarity index 97%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Header.java
index d14d131..af5594f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Header.java
@@ -1,4 +1,4 @@
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import okio.ByteString;
 
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/HeadersMode.java
similarity index 96%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/HeadersMode.java
index c06327a..b42915b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/HeadersMode.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 public enum HeadersMode {
   SPDY_SYN_STREAM,
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Hpack.java
similarity index 99%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Hpack.java
index e8f9e51..171516f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Hpack.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Hpack.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 import java.util.ArrayList;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http2.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Http2.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http2.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Http2.java
index 34b0df4..0fde974 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http2.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Http2.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 import java.io.IOException;
@@ -26,7 +26,7 @@
 import okio.Source;
 import okio.Timeout;
 
-import static com.squareup.okhttp.internal.spdy.Http2.FrameLogger.formatHeader;
+import static com.squareup.okhttp.internal.framed.Http2.FrameLogger.formatHeader;
 import static java.lang.String.format;
 import static java.util.logging.Level.FINE;
 import static okio.ByteString.EMPTY;
@@ -425,7 +425,6 @@
     @Override public synchronized void pushPromise(int streamId, int promisedStreamId,
         List<Header> requestHeaders) throws IOException {
       if (closed) throw new IOException("closed");
-      if (hpackBuffer.size() != 0) throw new IllegalStateException();
       hpackWriter.writeHeaders(requestHeaders);
 
       long byteCount = hpackBuffer.size();
@@ -441,7 +440,6 @@
 
     void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {
       if (closed) throw new IOException("closed");
-      if (hpackBuffer.size() != 0) throw new IllegalStateException();
       hpackWriter.writeHeaders(headerBlock);
 
       long byteCount = hpackBuffer.size();
@@ -467,7 +465,7 @@
     @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
         throws IOException {
       if (closed) throw new IOException("closed");
-      if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
+      if (errorCode.httpCode == -1) throw new IllegalArgumentException();
 
       int length = 4;
       byte type = TYPE_RST_STREAM;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Huffman.java
similarity index 99%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Huffman.java
index 06d5243..f21a16d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Huffman.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
similarity index 75%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
index 44d4ea2..57863df 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
@@ -14,23 +14,23 @@
  * limitations under the License.
  */
 
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 
 /** Listener to be notified when a connected peer creates a new stream. */
 public interface IncomingStreamHandler {
   IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
-    @Override public void receive(SpdyStream stream) throws IOException {
+    @Override public void receive(FramedStream stream) throws IOException {
       stream.close(ErrorCode.REFUSED_STREAM);
     }
   };
 
   /**
    * Handle a new stream from this connection's peer. Implementations should
-   * respond by either {@link SpdyStream#reply replying to the stream} or
-   * {@link SpdyStream#close closing it}. This response does not need to be
+   * respond by either {@link FramedStream#reply replying to the stream} or
+   * {@link FramedStream#close closing it}. This response does not need to be
    * synchronous.
    */
-  void receive(SpdyStream stream) throws IOException;
+  void receive(FramedStream stream) throws IOException;
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/NameValueBlockReader.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/NameValueBlockReader.java
index 6413f36..d9554a3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/NameValueBlockReader.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 import java.util.ArrayList;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Ping.java
similarity index 97%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Ping.java
index 06b0aef..35f9cf5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Ping.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/PushObserver.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/PushObserver.java
index cdb51f6..33ebc61 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/PushObserver.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.io.IOException;
 import java.util.List;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Settings.java
similarity index 98%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Settings.java
index bb67b83..935d489 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Settings.java
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import java.util.Arrays;
 
 /**
  * Settings describe characteristics of the sending peer, which are used by the receiving peer.
- * Settings are {@link com.squareup.okhttp.internal.spdy.SpdyConnection connection} scoped.
+ * Settings are {@link FramedConnection connection} scoped.
  */
 public final class Settings {
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Spdy3.java
similarity index 99%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Spdy3.java
index c5cebe7..bea53f7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Spdy3.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.internal.Util;
@@ -406,7 +406,6 @@
     }
 
     private void writeNameValueBlockToBuffer(List<Header> headerBlock) throws IOException {
-      if (headerBlockBuffer.size() != 0) throw new IllegalStateException();
       headerBlockOut.writeInt(headerBlock.size());
       for (int i = 0, size = headerBlock.size(); i < size; i++) {
         ByteString name = headerBlock.get(i).name;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Variant.java
similarity index 96%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/framed/Variant.java
index c4b082d..0782ba1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/Variant.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.squareup.okhttp.internal.spdy;
+package com.squareup.okhttp.internal.framed;
 
 import com.squareup.okhttp.Protocol;
 import okio.BufferedSink;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
index a517ada..8d88410 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
@@ -18,6 +18,7 @@
 import com.squareup.okhttp.Authenticator;
 import com.squareup.okhttp.Challenge;
 import com.squareup.okhttp.Credentials;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import java.io.IOException;
@@ -26,7 +27,6 @@
 import java.net.InetSocketAddress;
 import java.net.PasswordAuthentication;
 import java.net.Proxy;
-import java.net.URL;
 import java.util.List;
 
 /** Adapts {@link java.net.Authenticator} to {@link com.squareup.okhttp.Authenticator}. */
@@ -37,14 +37,14 @@
   @Override public Request authenticate(Proxy proxy, Response response) throws IOException {
     List<Challenge> challenges = response.challenges();
     Request request = response.request();
-    URL url = request.url();
+    HttpUrl url = request.httpUrl();
     for (int i = 0, size = challenges.size(); i < size; i++) {
       Challenge challenge = challenges.get(i);
       if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
 
       PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
-          url.getHost(), getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
-          challenge.getRealm(), challenge.getScheme(), url, RequestorType.SERVER);
+          url.host(), getConnectToInetAddress(proxy, url), url.port(), url.scheme(),
+          challenge.getRealm(), challenge.getScheme(), url.url(), RequestorType.SERVER);
       if (auth == null) continue;
 
       String credential = Credentials.basic(auth.getUserName(), new String(auth.getPassword()));
@@ -59,7 +59,7 @@
   @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
     List<Challenge> challenges = response.challenges();
     Request request = response.request();
-    URL url = request.url();
+    HttpUrl url = request.httpUrl();
     for (int i = 0, size = challenges.size(); i < size; i++) {
       Challenge challenge = challenges.get(i);
       if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
@@ -67,7 +67,7 @@
       InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
       PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
           proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
-          url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
+          url.scheme(), challenge.getRealm(), challenge.getScheme(), url.url(),
           RequestorType.PROXY);
       if (auth == null) continue;
 
@@ -79,9 +79,9 @@
     return null;
   }
 
-  private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+  private InetAddress getConnectToInetAddress(Proxy proxy, HttpUrl url) throws IOException {
     return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
         ? ((InetSocketAddress) proxy.address()).getAddress()
-        : InetAddress.getByName(url.getHost());
+        : InetAddress.getByName(url.host());
   }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
index 3f07edd..aee0dae 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
@@ -4,7 +4,6 @@
 import com.squareup.okhttp.Headers;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
-
 import java.util.Date;
 
 import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
@@ -254,7 +253,7 @@
         long delta = expires.getTime() - servedMillis;
         return delta > 0 ? delta : 0;
       } else if (lastModified != null
-          && cacheResponse.request().url().getQuery() == null) {
+          && cacheResponse.request().httpUrl().query() == null) {
         // As recommended by the HTTP RFC and implemented in Firefox, the
         // max age of a document should be defaulted to 10% of the
         // document's age at the time it was served. Default expiration
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
similarity index 84%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
index 61b6610..abeaf86 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
@@ -22,10 +22,10 @@
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.ResponseBody;
 import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.spdy.ErrorCode;
-import com.squareup.okhttp.internal.spdy.Header;
-import com.squareup.okhttp.internal.spdy.SpdyConnection;
-import com.squareup.okhttp.internal.spdy.SpdyStream;
+import com.squareup.okhttp.internal.framed.ErrorCode;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.framed.FramedStream;
+import com.squareup.okhttp.internal.framed.Header;
 import java.io.IOException;
 import java.net.ProtocolException;
 import java.util.ArrayList;
@@ -38,15 +38,15 @@
 import okio.Okio;
 import okio.Sink;
 
-import static com.squareup.okhttp.internal.spdy.Header.RESPONSE_STATUS;
-import static com.squareup.okhttp.internal.spdy.Header.TARGET_AUTHORITY;
-import static com.squareup.okhttp.internal.spdy.Header.TARGET_HOST;
-import static com.squareup.okhttp.internal.spdy.Header.TARGET_METHOD;
-import static com.squareup.okhttp.internal.spdy.Header.TARGET_PATH;
-import static com.squareup.okhttp.internal.spdy.Header.TARGET_SCHEME;
-import static com.squareup.okhttp.internal.spdy.Header.VERSION;
+import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_HOST;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_METHOD;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
+import static com.squareup.okhttp.internal.framed.Header.VERSION;
 
-public final class SpdyTransport implements Transport {
+public final class FramedTransport implements Transport {
   /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
   private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
       ByteString.encodeUtf8("connection"),
@@ -67,12 +67,12 @@
       ByteString.encodeUtf8("upgrade"));
 
   private final HttpEngine httpEngine;
-  private final SpdyConnection spdyConnection;
-  private SpdyStream stream;
+  private final FramedConnection framedConnection;
+  private FramedStream stream;
 
-  public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
+  public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
     this.httpEngine = httpEngine;
-    this.spdyConnection = spdyConnection;
+    this.framedConnection = framedConnection;
   }
 
   @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
@@ -86,8 +86,8 @@
     boolean permitsRequestBody = httpEngine.permitsRequestBody();
     boolean hasResponseBody = true;
     String version = RequestLine.version(httpEngine.getConnection().getProtocol());
-    stream = spdyConnection.newStream(
-        writeNameValueBlock(request, spdyConnection.getProtocol(), version), permitsRequestBody,
+    stream = framedConnection.newStream(
+        writeNameValueBlock(request, framedConnection.getProtocol(), version), permitsRequestBody,
         hasResponseBody);
     stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
   }
@@ -101,7 +101,7 @@
   }
 
   @Override public Response.Builder readResponseHeaders() throws IOException {
-    return readNameValueBlock(stream.getResponseHeaders(), spdyConnection.getProtocol());
+    return readNameValueBlock(stream.getResponseHeaders(), framedConnection.getProtocol());
   }
 
   /**
@@ -114,8 +114,8 @@
     Headers headers = request.headers();
     List<Header> result = new ArrayList<>(headers.size() + 10);
     result.add(new Header(TARGET_METHOD, request.method()));
-    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
-    String host = HttpEngine.hostHeader(request.url());
+    result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+    String host = Util.hostHeader(request.httpUrl());
     if (Protocol.SPDY_3 == protocol) {
       result.add(new Header(VERSION, version));
       result.add(new Header(TARGET_HOST, host));
@@ -124,7 +124,7 @@
     } else {
       throw new AssertionError();
     }
-    result.add(new Header(TARGET_SCHEME, request.url().getProtocol()));
+    result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
 
     Set<ByteString> names = new LinkedHashSet<ByteString>();
     for (int i = 0, size = headers.size(); i < size; i++) {
@@ -216,7 +216,7 @@
   }
 
   @Override public boolean canReuseConnection() {
-    return true; // TODO: spdyConnection.isClosed() ?
+    return true; // TODO: framedConnection.isClosed() ?
   }
 
   /** When true, this header should not be emitted or consumed. */
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 0d7d4e5..1bbde80 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
@@ -464,7 +464,7 @@
       long read = source.read(sink, Math.min(byteCount, bytesRemainingInChunk));
       if (read == -1) {
         unexpectedEndOfInput(); // The server didn't supply the promised chunk length.
-        throw new IOException("unexpected end of stream");
+        throw new ProtocolException("unexpected end of stream");
       }
       bytesRemainingInChunk -= read;
       return 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 0b6af86..70eeaaa 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
@@ -22,6 +22,7 @@
 import com.squareup.okhttp.Connection;
 import com.squareup.okhttp.ConnectionPool;
 import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Interceptor;
 import com.squareup.okhttp.MediaType;
 import com.squareup.okhttp.OkHttpClient;
@@ -39,8 +40,7 @@
 import java.net.CookieHandler;
 import java.net.ProtocolException;
 import java.net.Proxy;
-import java.net.URL;
-import java.net.UnknownHostException;
+import java.net.SocketTimeoutException;
 import java.security.cert.CertificateException;
 import java.util.Date;
 import java.util.List;
@@ -59,8 +59,6 @@
 import okio.Timeout;
 
 import static com.squareup.okhttp.internal.Util.closeQuietly;
-import static com.squareup.okhttp.internal.Util.getDefaultPort;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
 import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
 import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
 import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
@@ -327,19 +325,9 @@
       }
     }
 
-    connection = nextConnection();
-    route = connection.getRoute();
-  }
-
-  /**
-   * Returns the next connection to attempt.
-   *
-   * @throws java.util.NoSuchElementException if there are no more routes to attempt.
-   */
-  private Connection nextConnection() throws RouteException {
-    Connection connection = createNextConnection();
+    connection = createNextConnection();
     Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
-    return connection;
+    route = connection.getRoute();
   }
 
   private Connection createNextConnection() throws RouteException {
@@ -443,12 +431,17 @@
 
     IOException ioe = e.getLastConnectException();
 
-    // TODO(nfuller): This is the same logic as in ConnectionSpecSelector
     // If there was a protocol problem, don't recover.
     if (ioe instanceof ProtocolException) {
       return false;
     }
 
+    // If there was an interruption don't recover, but if there was a timeout
+    // we should try the next route (if there is one).
+    if (ioe instanceof InterruptedIOException) {
+      return ioe instanceof SocketTimeoutException;
+    }
+
     // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
     // again with a different route.
     if (ioe instanceof SSLHandshakeException) {
@@ -462,7 +455,6 @@
       // e.g. a certificate pinning error.
       return false;
     }
-    // TODO(nfuller): End of common code.
 
     // An example of one we might want to retry with a different route is a problem connecting to a
     // proxy and would manifest as a standard IOException. Unless it is one we know we should not
@@ -567,17 +559,25 @@
   }
 
   /**
-   * Immediately closes the socket connection if it's currently held by this
-   * engine. Use this to interrupt an in-flight request from any thread. It's
-   * the caller's responsibility to close the request body and response body
-   * streams; otherwise resources may be leaked.
+   * Immediately closes the socket connection if it's currently held by this engine. Use this to
+   * interrupt an in-flight request from any thread. It's the caller's responsibility to close the
+   * request body and response body streams; otherwise resources may be leaked.
+   *
+   * <p>This method is safe to be called concurrently, but provides limited guarantees. If a
+   * transport layer connection has been established (such as a HTTP/2 stream) that is terminated.
+   * Otherwise if a socket connection is being established, that is terminated.
    */
   public void disconnect() {
-    if (transport != null) {
-      try {
+    try {
+      if (transport != null) {
         transport.disconnect(this);
-      } catch (IOException ignored) {
+      } else {
+        Connection connection = this.connection;
+        if (connection != null) {
+          Internal.instance.closeIfOwnedBy(connection, this);
+        }
       }
+    } catch (IOException ignored) {
     }
   }
 
@@ -691,7 +691,7 @@
     Request.Builder result = request.newBuilder();
 
     if (request.header("Host") == null) {
-      result.header("Host", hostHeader(request.url()));
+      result.header("Host", Util.hostHeader(request.httpUrl()));
     }
 
     if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0)
@@ -724,12 +724,6 @@
     return result.build();
   }
 
-  public static String hostHeader(URL url) {
-    return getEffectivePort(url) != getDefaultPort(url.getProtocol())
-        ? url.getHost() + ":" + url.getPort()
-        : url.getHost();
-  }
-
   /**
    * Flushes the remaining request header and body, parses the HTTP response
    * headers and starts reading the HTTP response body if it exists.
@@ -854,8 +848,8 @@
         Address address = connection().getRoute().getAddress();
 
         // Confirm that the interceptor uses the connection we've already prepared.
-        if (!request.url().getHost().equals(address.getUriHost())
-            || getEffectivePort(request.url()) != address.getUriPort()) {
+        if (!request.httpUrl().host().equals(address.getUriHost())
+            || request.httpUrl().port() != address.getUriPort()) {
           throw new IllegalStateException("network interceptor " + caller
               + " must retain the same host and port");
         }
@@ -894,7 +888,15 @@
         bufferedRequestBody.close();
       }
 
-      return readNetworkResponse();
+      Response response = readNetworkResponse();
+
+      int code = response.code();
+      if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
+        throw new ProtocolException(
+            "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
+      }
+
+      return response;
     }
   }
 
@@ -1080,14 +1082,14 @@
 
         String location = userResponse.header("Location");
         if (location == null) return null;
-        URL url = new URL(userRequest.url(), location);
+        HttpUrl url = userRequest.httpUrl().resolve(location);
 
         // Don't follow redirects to unsupported protocols.
-        if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) return null;
+        if (url == null) return null;
 
         // If configured, don't follow redirects between SSL and non-SSL.
-        boolean sameProtocol = url.getProtocol().equals(userRequest.url().getProtocol());
-        if (!sameProtocol && !client.getFollowSslRedirects()) return null;
+        boolean sameScheme = url.scheme().equals(userRequest.httpUrl().scheme());
+        if (!sameScheme && !client.getFollowSslRedirects()) return null;
 
         // Redirects don't include a request body.
         Request.Builder requestBuilder = userRequest.newBuilder();
@@ -1116,20 +1118,14 @@
    * Returns true if an HTTP request for {@code followUp} can reuse the
    * connection used by this engine.
    */
-  public boolean sameConnection(URL followUp) {
-    URL url = userRequest.url();
-    return url.getHost().equals(followUp.getHost())
-        && getEffectivePort(url) == getEffectivePort(followUp)
-        && url.getProtocol().equals(followUp.getProtocol());
+  public boolean sameConnection(HttpUrl followUp) {
+    HttpUrl url = userRequest.httpUrl();
+    return url.host().equals(followUp.host())
+        && url.port() == followUp.port()
+        && url.scheme().equals(followUp.scheme());
   }
 
-  private static Address createAddress(OkHttpClient client, Request request)
-      throws RequestException {
-    String uriHost = request.url().getHost();
-    if (uriHost == null || uriHost.length() == 0) {
-      throw new RequestException(new UnknownHostException(request.url().toString()));
-    }
-
+  private static Address createAddress(OkHttpClient client, Request request) {
     SSLSocketFactory sslSocketFactory = null;
     HostnameVerifier hostnameVerifier = null;
     CertificatePinner certificatePinner = null;
@@ -1139,7 +1135,7 @@
       certificatePinner = client.getCertificatePinner();
     }
 
-    return new Address(uriHost, getEffectivePort(request.url()),
+    return new Address(request.httpUrl().host(), request.httpUrl().port(),
         client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
         client.getAuthenticator(), client.getProxy(), client.getProtocols(),
         client.getConnectionSpecs(), client.getProxySelector());
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
index f764afd..d22be27 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
@@ -1,10 +1,10 @@
 package com.squareup.okhttp.internal.http;
 
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.Protocol;
 import com.squareup.okhttp.Request;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
-import java.net.URL;
 
 public final class RequestLine {
   private RequestLine() {
@@ -21,9 +21,9 @@
     result.append(' ');
 
     if (includeAuthorityInRequestLine(request, proxyType)) {
-      result.append(request.url());
+      result.append(request.httpUrl());
     } else {
-      result.append(requestPath(request.url()));
+      result.append(requestPath(request.httpUrl()));
     }
 
     result.append(' ');
@@ -44,11 +44,10 @@
    * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never empty,
    * even if the request URL is. Includes the query component if it exists.
    */
-  public static String requestPath(URL url) {
-    String pathAndQuery = url.getFile();
-    if (pathAndQuery == null) return "/";
-    if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery;
-    return pathAndQuery;
+  public static String requestPath(HttpUrl url) {
+    String path = url.encodedPath();
+    String query = url.encodedQuery();
+    return query != null ? (path + '?' + query) : path;
   }
 
   public static String version(Protocol protocol) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index 16448e4..b16bab3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -16,6 +16,7 @@
 package com.squareup.okhttp.internal.http;
 
 import com.squareup.okhttp.Address;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Route;
@@ -28,14 +29,11 @@
 import java.net.Proxy;
 import java.net.SocketAddress;
 import java.net.SocketException;
-import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.NoSuchElementException;
 
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-
 /**
  * Selects routes to connect to an origin server. Each connection requires a
  * choice of proxy server, IP address, and TLS mode. Connections may also be
@@ -43,7 +41,7 @@
  */
 public final class RouteSelector {
   private final Address address;
-  private final URI uri;
+  private final HttpUrl url;
   private final Network network;
   private final OkHttpClient client;
   private final RouteDatabase routeDatabase;
@@ -63,19 +61,19 @@
   /* State for negotiating failed routes */
   private final List<Route> postponedRoutes = new ArrayList<>();
 
-  private RouteSelector(Address address, URI uri, OkHttpClient client) {
+  private RouteSelector(Address address, HttpUrl url, OkHttpClient client) {
     this.address = address;
-    this.uri = uri;
+    this.url = url;
     this.client = client;
     this.routeDatabase = Internal.instance.routeDatabase(client);
     this.network = Internal.instance.network(client);
 
-    resetNextProxy(uri, address.getProxy());
+    resetNextProxy(url, address.getProxy());
   }
 
   public static RouteSelector get(Address address, Request request, OkHttpClient client)
       throws IOException {
-    return new RouteSelector(address, request.uri(), client);
+    return new RouteSelector(address, request.httpUrl(), client);
   }
 
   /**
@@ -118,14 +116,15 @@
   public void connectFailed(Route failedRoute, IOException failure) {
     if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && address.getProxySelector() != null) {
       // Tell the proxy selector when we fail to connect on a fresh connection.
-      address.getProxySelector().connectFailed(uri, failedRoute.getProxy().address(), failure);
+      address.getProxySelector().connectFailed(
+          url.uri(), failedRoute.getProxy().address(), failure);
     }
 
     routeDatabase.failed(failedRoute);
   }
 
   /** Prepares the proxy servers to try. */
-  private void resetNextProxy(URI uri, Proxy proxy) {
+  private void resetNextProxy(HttpUrl url, Proxy proxy) {
     if (proxy != null) {
       // If the user specifies a proxy, try that and only that.
       proxies = Collections.singletonList(proxy);
@@ -133,7 +132,7 @@
       // Try each of the ProxySelector choices until one connection succeeds. If none succeed
       // then we'll try a direct connection below.
       proxies = new ArrayList<>();
-      List<Proxy> selectedProxies = client.getProxySelector().select(uri);
+      List<Proxy> selectedProxies = client.getProxySelector().select(url.uri());
       if (selectedProxies != null) proxies.addAll(selectedProxies);
       // Finally try a direct connection. We only try it once!
       proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
@@ -167,7 +166,7 @@
     int socketPort;
     if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
       socketHost = address.getUriHost();
-      socketPort = getEffectivePort(uri);
+      socketPort = address.getUriPort();
     } else {
       SocketAddress proxyAddress = proxy.address();
       if (!(proxyAddress instanceof InetSocketAddress)) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java
deleted file mode 100644
index aba3af4..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2015 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.Address;
-import com.squareup.okhttp.CertificatePinner;
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.ConnectionSpec;
-import com.squareup.okhttp.Handshake;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.ConnectionSpecSelector;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-
-import java.io.IOException;
-import java.net.Proxy;
-import java.net.Socket;
-import java.net.URL;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-
-import okio.Source;
-
-import static com.squareup.okhttp.internal.Util.closeQuietly;
-import static com.squareup.okhttp.internal.Util.getDefaultPort;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
-
-/**
- * Helper that can establish a socket connection to a {@link com.squareup.okhttp.Route} using the
- * specified {@link ConnectionSpec} set. A {@link SocketConnector} can be used multiple times.
- */
-public class SocketConnector {
-  private final Connection connection;
-  private final ConnectionPool connectionPool;
-
-  public SocketConnector(Connection connection, ConnectionPool connectionPool) {
-    this.connection = connection;
-    this.connectionPool = connectionPool;
-  }
-
-  public ConnectedSocket connectCleartext(int connectTimeout, int readTimeout, Route route)
-      throws RouteException {
-    Socket socket = connectRawSocket(readTimeout, connectTimeout, route);
-    return new ConnectedSocket(route, socket);
-  }
-
-  public ConnectedSocket connectTls(int connectTimeout, int readTimeout,
-      int writeTimeout, Request request, Route route, List<ConnectionSpec> connectionSpecs,
-      boolean connectionRetryEnabled) throws RouteException {
-
-    Address address = route.getAddress();
-    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
-    RouteException routeException = null;
-    do {
-      Socket socket = connectRawSocket(readTimeout, connectTimeout, route);
-      if (route.requiresTunnel()) {
-        createTunnel(readTimeout, writeTimeout, request, route, socket);
-      }
-
-      SSLSocket sslSocket = null;
-      try {
-        SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
-
-        // Create the wrapper over the connected socket.
-        sslSocket = (SSLSocket) sslSocketFactory
-            .createSocket(socket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
-
-        // Configure the socket's ciphers, TLS versions, and extensions.
-        ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
-        Platform platform = Platform.get();
-        Handshake handshake = null;
-        Protocol alpnProtocol = null;
-        try {
-          if (connectionSpec.supportsTlsExtensions()) {
-            platform.configureTlsExtensions(
-                sslSocket, address.getUriHost(), address.getProtocols());
-          }
-          // Force handshake. This can throw!
-          sslSocket.startHandshake();
-
-          handshake = Handshake.get(sslSocket.getSession());
-
-          String maybeProtocol;
-          if (connectionSpec.supportsTlsExtensions()
-              && (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) {
-            alpnProtocol = Protocol.get(maybeProtocol); // Throws IOE on unknown.
-          }
-        } finally {
-          platform.afterHandshake(sslSocket);
-        }
-
-        // Verify that the socket's certificates are acceptable for the target host.
-        if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
-          X509Certificate cert = (X509Certificate) sslSocket.getSession()
-              .getPeerCertificates()[0];
-          throw new SSLPeerUnverifiedException(
-              "Hostname " + address.getUriHost() + " not verified:"
-              + "\n    certificate: " + CertificatePinner.pin(cert)
-              + "\n    DN: " + cert.getSubjectDN().getName()
-              + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
-        }
-
-        // Check that the certificate pinner is satisfied by the certificates presented.
-        address.getCertificatePinner().check(address.getUriHost(), handshake.peerCertificates());
-
-        return new ConnectedSocket(route, sslSocket, alpnProtocol, handshake);
-      } catch (IOException e) {
-        boolean canRetry = connectionRetryEnabled && connectionSpecSelector.connectionFailed(e);
-        closeQuietly(sslSocket);
-        closeQuietly(socket);
-        if (routeException == null) {
-          routeException = new RouteException(e);
-        } else {
-          routeException.addConnectException(e);
-        }
-        if (!canRetry) {
-          throw routeException;
-        }
-      }
-    } while (true);
-  }
-
-  private Socket connectRawSocket(int soTimeout, int connectTimeout, Route route)
-      throws RouteException {
-    Platform platform = Platform.get();
-    try {
-      Proxy proxy = route.getProxy();
-      Address address = route.getAddress();
-      Socket socket;
-      if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP) {
-        socket = address.getSocketFactory().createSocket();
-      } else {
-        socket = new Socket(proxy);
-      }
-      socket.setSoTimeout(soTimeout);
-      platform.connectSocket(socket, route.getSocketAddress(), connectTimeout);
-
-      return socket;
-    } catch (IOException e) {
-      throw new RouteException(e);
-    }
-  }
-
-  /**
-   * To make an HTTPS connection over an HTTP proxy, send an unencrypted
-   * CONNECT request to create the proxy connection. This may need to be
-   * retried if the proxy requires authorization.
-   */
-  private void createTunnel(int readTimeout, int writeTimeout, Request request, Route route,
-      Socket socket) throws RouteException {
-    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
-    try {
-      Request tunnelRequest = createTunnelRequest(request);
-      HttpConnection tunnelConnection = new HttpConnection(connectionPool, connection, socket);
-      tunnelConnection.setTimeouts(readTimeout, writeTimeout);
-      URL url = tunnelRequest.url();
-      String requestLine = "CONNECT " + url.getHost() + ":" + url.getPort() + " HTTP/1.1";
-      while (true) {
-        tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
-        tunnelConnection.flush();
-        Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
-        // The response body from a CONNECT should be empty, but if it is not then we should consume
-        // it before proceeding.
-        long contentLength = OkHeaders.contentLength(response);
-        if (contentLength == -1L) {
-          contentLength = 0L;
-        }
-        Source body = tunnelConnection.newFixedLengthSource(contentLength);
-        Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
-        body.close();
-
-        switch (response.code()) {
-          case HTTP_OK:
-            // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
-            // that happens, then we will have buffered bytes that are needed by the SSLSocket!
-            // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
-            // that it will almost certainly fail because the proxy has sent unexpected data.
-            if (tunnelConnection.bufferSize() > 0) {
-              throw new IOException("TLS tunnel buffered too many bytes!");
-            }
-            return;
-
-          case HTTP_PROXY_AUTH:
-            tunnelRequest = OkHeaders.processAuthHeader(
-                route.getAddress().getAuthenticator(), response, route.getProxy());
-            if (tunnelRequest != null) continue;
-            throw new IOException("Failed to authenticate with proxy");
-
-          default:
-            throw new IOException(
-                "Unexpected response code for CONNECT: " + response.code());
-        }
-      }
-    } catch (IOException e) {
-      throw new RouteException(e);
-    }
-  }
-
-  /**
-   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
-   * no tunnel is necessary. Everything in the tunnel request is sent
-   * unencrypted to the proxy server, so tunnels include only the minimum set of
-   * headers. This avoids sending potentially sensitive data like HTTP cookies
-   * to the proxy unencrypted.
-   */
-  private Request createTunnelRequest(Request request) throws IOException {
-    String host = request.url().getHost();
-    int port = getEffectivePort(request.url());
-    String authority = (port == getDefaultPort("https")) ? host : (host + ":" + port);
-    Request.Builder result = new Request.Builder()
-        .url(new URL("https", host, port, "/"))
-        .header("Host", authority)
-        .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
-
-    // Copy over the User-Agent header if it exists.
-    String userAgent = request.header("User-Agent");
-    if (userAgent != null) {
-      result.header("User-Agent", userAgent);
-    }
-
-    // Copy over the Proxy-Authorization header if it exists.
-    String proxyAuthorization = request.header("Proxy-Authorization");
-    if (proxyAuthorization != null) {
-      result.header("Proxy-Authorization", proxyAuthorization);
-    }
-
-    return result.build();
-  }
-
-  /**
-   * A connected socket with metadata.
-   */
-  public static class ConnectedSocket {
-    public final Route route;
-    public final Socket socket;
-    public final Protocol alpnProtocol;
-    public final Handshake handshake;
-
-    /** A connected plain / raw (i.e. unencrypted communication) socket. */
-    public ConnectedSocket(Route route, Socket socket) {
-      this.route = route;
-      this.socket = socket;
-      alpnProtocol = null;
-      handshake = null;
-    }
-
-    /** A connected {@link SSLSocket}. */
-    public ConnectedSocket(Route route, SSLSocket socket, Protocol alpnProtocol,
-        Handshake handshake) {
-      this.route = route;
-      this.socket = socket;
-      this.alpnProtocol = alpnProtocol;
-      this.handshake = handshake;
-    }
-  }
-}
diff --git a/okio/BUG-BOUNTY.md b/okio/BUG-BOUNTY.md
new file mode 100644
index 0000000..b2c35b2
--- /dev/null
+++ b/okio/BUG-BOUNTY.md
@@ -0,0 +1,10 @@
+Serious about security
+======================
+
+Square recognizes the important contributions the security research community
+can make. We therefore encourage reporting security issues with the code
+contained in this repository.
+
+If you believe you have discovered a security vulnerability, please follow the
+guidelines at https://hackerone.com/square-open-source
+
diff --git a/okio/CHANGELOG.md b/okio/CHANGELOG.md
index c5856fe..6fd5b29 100644
--- a/okio/CHANGELOG.md
+++ b/okio/CHANGELOG.md
@@ -1,6 +1,40 @@
 Change Log
 ==========
 
+## Version 1.6.0
+
+_2015-08-25_
+
+ * New: `BufferedSource.indexOf(ByteString)` searches a source for the next
+   occurrence of a byte string.
+ * Fix: Recover from unexpected `AssertionError` thrown on Android 4.2.2 and
+   earlier when asynchronously closing a socket.
+
+## Version 1.5.0
+
+_2015-06-19_
+
+ * Sockets streams now throw `SocketTimeoutException`. This builds on new
+   extension point in `AsyncTimeout` to customize the exception when a timeout
+   occurs.
+ * New: `ByteString` now implements `Comparable`. The comparison sorts bytes as
+   unsigned: {@code ff} sorts after {@code 00}.
+
+## Version 1.4.0
+
+_2015-05-16_
+
+ * **Timeout exception changed.** Previously `Timeout.throwIfReached()` would
+   throw `InterruptedIOException` on thread interruption, and `IOException` if
+   the deadline was reached. Now it throws `InterruptedIOException` in both
+   cases.
+ * Fix: throw `EOFException` when attempting to read digits from an empty
+   source. Previously this would crash with an unchecked exception.
+ * New: APIs to read and write UTF-8 code points without allocating strings.
+ * New: `BufferedSink` can now write substrings directly, potentially saving an
+   allocation for some callers.
+ * New: `ForwardingTimeout` class.
+
 ## Version 1.3.0
 
 _2015-03-16_
@@ -37,6 +71,7 @@
 ## Version 1.1.0
 
 _2014-12-11_
+
  * Do UTF-8 encoding natively for a performance increase, particularly on Android.
  * New APIs: `BufferedSink.emit()`, `BufferedSource.request()` and `BufferedSink.indexOfElement()`.
  * Fixed a performance bug in `Buffer.indexOf()`
diff --git a/okio/README.android b/okio/README.android
deleted file mode 100644
index a143b63..0000000
--- a/okio/README.android
+++ /dev/null
@@ -1,12 +0,0 @@
-URL: https://github.com/square/okio
-License: Apache 2
-Description: "A modern I/O API for Java"
-
-Local patches
--------------
-
-All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
-  - Removal of reference to a codehause annotation used in
-    okio/src/main/java/okio/DeflaterSink.java
-  - Commenting of code that references APIs not present on Android.
-  - Removal of test code that uses JUnit 4.11 features such as @Parameterized.Parameters
diff --git a/okio/README.md b/okio/README.md
index 77121c4..2dc4f60 100644
--- a/okio/README.md
+++ b/okio/README.md
@@ -120,12 +120,12 @@
 <dependency>
     <groupId>com.squareup.okio</groupId>
     <artifactId>okio</artifactId>
-    <version>1.3.0</version>
+    <version>1.6.0</version>
 </dependency>
 ```
 or Gradle:
 ```groovy
-compile 'com.squareup.okio:okio:1.3.0'
+compile 'com.squareup.okio:okio:1.6.0'
 ```
 
 Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
diff --git a/okio/benchmarks/pom.xml b/okio/benchmarks/pom.xml
index 6943f99..60e1a99 100644
--- a/okio/benchmarks/pom.xml
+++ b/okio/benchmarks/pom.xml
@@ -2,7 +2,7 @@
   <parent>
     <artifactId>okio-parent</artifactId>
     <groupId>com.squareup.okio</groupId>
-    <version>1.4.0-SNAPSHOT</version>
+    <version>1.7.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/okio/okio/pom.xml b/okio/okio/pom.xml
index 7f7f46a..afa9bb8 100644
--- a/okio/okio/pom.xml
+++ b/okio/okio/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okio</groupId>
     <artifactId>okio-parent</artifactId>
-    <version>1.4.0-SNAPSHOT</version>
+    <version>1.7.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>okio</artifactId>
diff --git a/okio/okio/src/main/java/okio/Buffer.java b/okio/okio/src/main/java/okio/Buffer.java
index 04d2793..a5cf04a 100644
--- a/okio/okio/src/main/java/okio/Buffer.java
+++ b/okio/okio/src/main/java/okio/Buffer.java
@@ -1242,8 +1242,8 @@
         fromIndex -= segmentByteCount;
       } else {
         byte[] data = s.data;
-        for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) {
-          if (data[(int) pos] == b) return offset + pos - s.pos;
+        for (int pos = (int) (s.pos + fromIndex), limit = s.limit; pos < limit; pos++) {
+          if (data[pos] == b) return offset + pos - s.pos;
         }
         fromIndex = 0;
       }
@@ -1253,6 +1253,24 @@
     return -1L;
   }
 
+  @Override public long indexOf(ByteString bytes) throws IOException {
+    return indexOf(bytes, 0);
+  }
+
+  @Override public long indexOf(ByteString bytes, long fromIndex) throws IOException {
+    if (bytes.size() == 0) throw new IllegalArgumentException("bytes is empty");
+    while (true) {
+      fromIndex = indexOf(bytes.getByte(0), fromIndex);
+      if (fromIndex == -1) {
+        return -1;
+      }
+      if (rangeEquals(fromIndex, bytes)) {
+        return fromIndex;
+      }
+      fromIndex++;
+    }
+  }
+
   @Override public long indexOfElement(ByteString targetBytes) {
     return indexOfElement(targetBytes, 0);
   }
@@ -1284,6 +1302,19 @@
     return -1L;
   }
 
+  boolean rangeEquals(long offset, ByteString bytes) {
+    int byteCount = bytes.size();
+    if (size - offset < byteCount) {
+      return false;
+    }
+    for (int i = 0; i < byteCount; i++) {
+      if (getByte(offset + i) != bytes.getByte(i)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   @Override public void flush() {
   }
 
diff --git a/okio/okio/src/main/java/okio/BufferedSource.java b/okio/okio/src/main/java/okio/BufferedSource.java
index ba4545f..cf62ba1 100644
--- a/okio/okio/src/main/java/okio/BufferedSource.java
+++ b/okio/okio/src/main/java/okio/BufferedSource.java
@@ -215,6 +215,21 @@
   long indexOf(byte b, long fromIndex) throws IOException;
 
   /**
+   * Returns the index of the first match for {@code bytes} in the buffer. This expands the buffer
+   * as necessary until {@code bytes} is found. This reads an unbounded number of bytes into the
+   * buffer. Returns -1 if the stream is exhausted before the requested bytes are found.
+   */
+  long indexOf(ByteString bytes) throws IOException;
+
+  /**
+   * Returns the index of the first match for {@code bytes} in the buffer at or after {@code
+   * fromIndex}. This expands the buffer as necessary until {@code bytes} is found. This reads an
+   * unbounded number of bytes into the buffer. Returns -1 if the stream is exhausted before the
+   * requested bytes are found.
+   */
+  long indexOf(ByteString bytes, long fromIndex) throws IOException;
+
+  /**
    * Returns the index of the first byte in {@code targetBytes} in the buffer.
    * This expands the buffer as necessary until a target byte is found. This
    * reads an unbounded number of bytes into the buffer. Returns -1 if the
diff --git a/okio/okio/src/main/java/okio/ByteString.java b/okio/okio/src/main/java/okio/ByteString.java
index 81be905..b2e17fa 100644
--- a/okio/okio/src/main/java/okio/ByteString.java
+++ b/okio/okio/src/main/java/okio/ByteString.java
@@ -33,15 +33,17 @@
 /**
  * An immutable sequence of bytes.
  *
- * <p><strong>Full disclosure:</strong> this class provides untrusted input and
- * output streams with raw access to the underlying byte array. A hostile
- * stream implementation could keep a reference to the mutable byte string,
- * violating the immutable guarantee of this class. For this reason a byte
- * string's immutability guarantee cannot be relied upon for security in applets
- * and other environments that run both trusted and untrusted code in the same
- * process.
+ * <p>Byte strings compare lexicographically as a sequence of <strong>unsigned</strong> bytes. That
+ * is, the byte string {@code ff} sorts after {@code 00}. This is counter to the sort order of the
+ * corresponding bytes, where {@code -1} sorts before {@code 0}.
+ *
+ * <p><strong>Full disclosure:</strong> this class provides untrusted input and output streams with
+ * raw access to the underlying byte array. A hostile stream implementation could keep a reference
+ * to the mutable byte string, violating the immutable guarantee of this class. For this reason a
+ * byte string's immutability guarantee cannot be relied upon for security in applets and other
+ * environments that run both trusted and untrusted code in the same process.
  */
-public class ByteString implements Serializable {
+public class ByteString implements Serializable, Comparable<ByteString> {
   static final char[] HEX_DIGITS =
       { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
   private static final long serialVersionUID = 1L;
@@ -331,6 +333,19 @@
     return result != 0 ? result : (hashCode = Arrays.hashCode(data));
   }
 
+  @Override public int compareTo(ByteString byteString) {
+    int sizeA = size();
+    int sizeB = byteString.size();
+    for (int i = 0, size = Math.min(sizeA, sizeB); i < size; i++) {
+      int byteA = getByte(i) & 0xff;
+      int byteB = byteString.getByte(i) & 0xff;
+      if (byteA == byteB) continue;
+      return byteA < byteB ? -1 : 1;
+    }
+    if (sizeA == sizeB) return 0;
+    return sizeA < sizeB ? -1 : 1;
+  }
+
   @Override public String toString() {
     if (data.length == 0) {
       return "ByteString[size=0]";
diff --git a/okio/okio/src/main/java/okio/DeflaterSink.java b/okio/okio/src/main/java/okio/DeflaterSink.java
index 3e325b2..a5bca15 100644
--- a/okio/okio/src/main/java/okio/DeflaterSink.java
+++ b/okio/okio/src/main/java/okio/DeflaterSink.java
@@ -17,6 +17,7 @@
 
 import java.io.IOException;
 import java.util.zip.Deflater;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
 
 import static okio.Util.checkOffsetAndCount;
 
@@ -79,9 +80,7 @@
     }
   }
 
-  // ANDROID-BEGIN
-  // @IgnoreJRERequirement
-  // ANDROID-END
+  @IgnoreJRERequirement
   private void deflate(boolean syncFlush) throws IOException {
     Buffer buffer = sink.buffer();
     while (true) {
diff --git a/okio/okio/src/main/java/okio/Okio.java b/okio/okio/src/main/java/okio/Okio.java
index 1eba4e0..7ba166e 100644
--- a/okio/okio/src/main/java/okio/Okio.java
+++ b/okio/okio/src/main/java/okio/Okio.java
@@ -25,8 +25,12 @@
 import java.io.OutputStream;
 import java.net.Socket;
 import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
 
 import static okio.Util.checkOffsetAndCount;
 
@@ -58,7 +62,7 @@
   }
 
   /** Returns a sink that writes to {@code out}. */
-  public static Sink sink(final OutputStream out) {
+  public static Sink sink(OutputStream out) {
     return sink(out, new Timeout());
   }
 
@@ -109,7 +113,7 @@
    * #sink(OutputStream)} because this method honors timeouts. When the socket
    * write times out, the socket is asynchronously closed by a watchdog thread.
    */
-  public static Sink sink(final Socket socket) throws IOException {
+  public static Sink sink(Socket socket) throws IOException {
     if (socket == null) throw new IllegalArgumentException("socket == null");
     AsyncTimeout timeout = timeout(socket);
     Sink sink = sink(socket.getOutputStream(), timeout);
@@ -117,7 +121,7 @@
   }
 
   /** Returns a source that reads from {@code in}. */
-  public static Source source(final InputStream in) {
+  public static Source source(InputStream in) {
     return source(in, new Timeout());
   }
 
@@ -129,14 +133,19 @@
       @Override public long read(Buffer sink, long byteCount) throws IOException {
         if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
         if (byteCount == 0) return 0;
-        timeout.throwIfReached();
-        Segment tail = sink.writableSegment(1);
-        int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
-        int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
-        if (bytesRead == -1) return -1;
-        tail.limit += bytesRead;
-        sink.size += bytesRead;
-        return bytesRead;
+        try {
+          timeout.throwIfReached();
+          Segment tail = sink.writableSegment(1);
+          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
+          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
+          if (bytesRead == -1) return -1;
+          tail.limit += bytesRead;
+          sink.size += bytesRead;
+          return bytesRead;
+        } catch (AssertionError e) {
+          if (isAndroidGetsocknameError(e)) throw new IOException(e);
+          throw e;
+        }
       }
 
       @Override public void close() throws IOException {
@@ -159,14 +168,12 @@
     return source(new FileInputStream(file));
   }
 
-  // ANDROID-BEGIN
-  //  /** Returns a source that reads from {@code path}. */
-  //  @IgnoreJRERequirement // Should only be invoked on Java 7+.
-  //  public static Source source(Path path, OpenOption... options) throws IOException {
-  //    if (path == null) throw new IllegalArgumentException("path == null");
-  //    return source(Files.newInputStream(path, options));
-  //  }
-  // ANDROID-END
+  /** Returns a source that reads from {@code path}. */
+  @IgnoreJRERequirement // Should only be invoked on Java 7+.
+  public static Source source(Path path, OpenOption... options) throws IOException {
+    if (path == null) throw new IllegalArgumentException("path == null");
+    return source(Files.newInputStream(path, options));
+  }
 
   /** Returns a sink that writes to {@code file}. */
   public static Sink sink(File file) throws FileNotFoundException {
@@ -180,21 +187,19 @@
     return sink(new FileOutputStream(file, true));
   }
 
-  // ANDROID-BEGIN
-  //  /** Returns a sink that writes to {@code path}. */
-  //  @IgnoreJRERequirement // Should only be invoked on Java 7+.
-  //  public static Sink sink(Path path, OpenOption... options) throws IOException {
-  //    if (path == null) throw new IllegalArgumentException("path == null");
-  //    return sink(Files.newOutputStream(path, options));
-  //  }
-  // ANDROID-END
+  /** Returns a sink that writes to {@code path}. */
+  @IgnoreJRERequirement // Should only be invoked on Java 7+.
+  public static Sink sink(Path path, OpenOption... options) throws IOException {
+    if (path == null) throw new IllegalArgumentException("path == null");
+    return sink(Files.newOutputStream(path, options));
+  }
 
   /**
    * Returns a source that reads from {@code socket}. Prefer this over {@link
    * #source(InputStream)} because this method honors timeouts. When the socket
    * read times out, the socket is asynchronously closed by a watchdog thread.
    */
-  public static Source source(final Socket socket) throws IOException {
+  public static Source source(Socket socket) throws IOException {
     if (socket == null) throw new IllegalArgumentException("socket == null");
     AsyncTimeout timeout = timeout(socket);
     Source source = source(socket.getInputStream(), timeout);
@@ -216,8 +221,25 @@
           socket.close();
         } catch (Exception e) {
           logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
+        } catch (AssertionError e) {
+          if (isAndroidGetsocknameError(e)) {
+            // Catch this exception due to a Firmware issue up to android 4.2.2
+            // https://code.google.com/p/android/issues/detail?id=54072
+            logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
+          } else {
+            throw e;
+          }
         }
       }
     };
   }
+
+  /**
+   * Returns true if {@code e} is due to a firmware bug fixed after Android 4.2.2.
+   * https://code.google.com/p/android/issues/detail?id=54072
+   */
+  private static boolean isAndroidGetsocknameError(AssertionError e) {
+    return e.getCause() != null && e.getMessage() != null
+        && e.getMessage().contains("getsockname failed");
+  }
 }
diff --git a/okio/okio/src/main/java/okio/RealBufferedSource.java b/okio/okio/src/main/java/okio/RealBufferedSource.java
index f1f0ad4..2582ced 100644
--- a/okio/okio/src/main/java/okio/RealBufferedSource.java
+++ b/okio/okio/src/main/java/okio/RealBufferedSource.java
@@ -313,6 +313,24 @@
     return index;
   }
 
+  @Override public long indexOf(ByteString bytes) throws IOException {
+    return indexOf(bytes, 0);
+  }
+
+  @Override public long indexOf(ByteString bytes, long fromIndex) throws IOException {
+    if (bytes.size() == 0) throw new IllegalArgumentException("bytes is empty");
+    while (true) {
+      fromIndex = indexOf(bytes.getByte(0), fromIndex);
+      if (fromIndex == -1) {
+        return -1;
+      }
+      if (rangeEquals(fromIndex, bytes)) {
+        return fromIndex;
+      }
+      fromIndex++;
+    }
+  }
+
   @Override public long indexOfElement(ByteString targetBytes) throws IOException {
     return indexOfElement(targetBytes, 0);
   }
@@ -330,6 +348,10 @@
     return index;
   }
 
+  private boolean rangeEquals(long offset, ByteString bytes) throws IOException {
+    return request(offset + bytes.size()) && buffer.rangeEquals(offset, bytes);
+  }
+
   @Override public InputStream inputStream() {
     return new InputStream() {
       @Override public int read() throws IOException {
diff --git a/okio/okio/src/test/java/okio/BufferedSinkTest.java b/okio/okio/src/test/java/okio/BufferedSinkTest.java
index f546214..05a7ccc 100644
--- a/okio/okio/src/test/java/okio/BufferedSinkTest.java
+++ b/okio/okio/src/test/java/okio/BufferedSinkTest.java
@@ -39,9 +39,7 @@
     BufferedSink create(Buffer data);
   }
 
-  // ANDROID-BEGIN
-  //  @Parameterized.Parameters(name = "{0}")
-  // ANDROID-END
+  @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> parameters() {
     return Arrays.asList(new Object[] {
         new Factory() {
@@ -66,10 +64,8 @@
     });
   }
 
-  // ANDROID-BEGIN
-  //  @Parameterized.Parameter
-  public Factory factory = (Factory) (parameters().get(0))[0];
-  // ANDROID-END
+  @Parameterized.Parameter
+  public Factory factory;
 
   private Buffer data;
   private BufferedSink sink;
diff --git a/okio/okio/src/test/java/okio/BufferedSourceTest.java b/okio/okio/src/test/java/okio/BufferedSourceTest.java
index 5a11a46..6e42e15 100644
--- a/okio/okio/src/test/java/okio/BufferedSourceTest.java
+++ b/okio/okio/src/test/java/okio/BufferedSourceTest.java
@@ -92,9 +92,7 @@
     BufferedSource source;
   }
 
-  // ANDROID-BEGIN
-  //  @Parameterized.Parameters(name = "{0}")
-  // ANDROID-END
+  @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> parameters() {
     return Arrays.asList(
         new Object[] { BUFFER_FACTORY },
@@ -102,10 +100,8 @@
         new Object[] { ONE_BYTE_AT_A_TIME_FACTORY });
   }
 
-  // ANDROID-BEGIN
-  // @Parameterized.Parameter
-  public Factory factory = (Factory) (parameters().get(0))[0];
-  // ANDROID-END
+  @Parameterized.Parameter
+  public Factory factory;
   private BufferedSink sink;
   private BufferedSource source;
 
@@ -467,6 +463,45 @@
     assertEquals(15, source.indexOf((byte) 'b', 15));
   }
 
+  @Test public void indexOfByteString() throws IOException {
+    assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop")));
+
+    sink.writeUtf8("flip flop");
+    assertEquals(5, source.indexOf(ByteString.encodeUtf8("flop")));
+    source.readUtf8(); // Clear stream.
+
+    // Make sure we backtrack and resume searching after partial match.
+    sink.writeUtf8("hi hi hi hey");
+    assertEquals(3, source.indexOf(ByteString.encodeUtf8("hi hi hey")));
+  }
+
+  @Test public void indexOfByteStringWithOffset() throws IOException {
+    assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop"), 1));
+
+    sink.writeUtf8("flop flip flop");
+    assertEquals(10, source.indexOf(ByteString.encodeUtf8("flop"), 1));
+    source.readUtf8(); // Clear stream
+
+    // Make sure we backtrack and resume searching after partial match.
+    sink.writeUtf8("hi hi hi hi hey");
+    assertEquals(6, source.indexOf(ByteString.encodeUtf8("hi hi hey"), 1));
+  }
+
+  @Test public void indexOfByteStringInvalidArgumentsThrows() throws IOException {
+    try {
+      source.indexOf(ByteString.of());
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("bytes is empty", e.getMessage());
+    }
+    try {
+      source.indexOf(ByteString.encodeUtf8("hi"), -1);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertEquals("fromIndex < 0", e.getMessage());
+    }
+  }
+
   @Test public void indexOfElement() throws IOException {
     sink.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
     assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK")));
diff --git a/okio/okio/src/test/java/okio/ByteStringTest.java b/okio/okio/src/test/java/okio/ByteStringTest.java
index 2835edb..68a2974 100644
--- a/okio/okio/src/test/java/okio/ByteStringTest.java
+++ b/okio/okio/src/test/java/okio/ByteStringTest.java
@@ -18,6 +18,11 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
 import org.junit.Test;
 
 import static okio.TestUtil.assertByteArraysEquals;
@@ -29,7 +34,6 @@
 import static org.junit.Assert.fail;
 
 public class ByteStringTest {
-
   @Test public void ofCopyRange() {
     byte[] bytes = "Hello, World!".getBytes(Util.UTF_8);
     ByteString byteString = ByteString.of(bytes, 2, 9);
@@ -269,4 +273,58 @@
     ByteString byteString = ByteString.of();
     assertEquivalent(byteString, TestUtil.reserialize(byteString));
   }
+
+  @Test public void compareToSingleBytes() throws Exception {
+    List<ByteString> originalByteStrings = Arrays.asList(
+        ByteString.decodeHex("00"),
+        ByteString.decodeHex("01"),
+        ByteString.decodeHex("7e"),
+        ByteString.decodeHex("7f"),
+        ByteString.decodeHex("80"),
+        ByteString.decodeHex("81"),
+        ByteString.decodeHex("fe"),
+        ByteString.decodeHex("ff"));
+
+    List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings);
+    Collections.shuffle(sortedByteStrings, new Random(0));
+    Collections.sort(sortedByteStrings);
+
+    assertEquals(originalByteStrings, sortedByteStrings);
+  }
+
+  @Test public void compareToMultipleBytes() throws Exception {
+    List<ByteString> originalByteStrings = Arrays.asList(
+        ByteString.decodeHex(""),
+        ByteString.decodeHex("00"),
+        ByteString.decodeHex("0000"),
+        ByteString.decodeHex("000000"),
+        ByteString.decodeHex("00000000"),
+        ByteString.decodeHex("0000000000"),
+        ByteString.decodeHex("0000000001"),
+        ByteString.decodeHex("000001"),
+        ByteString.decodeHex("00007f"),
+        ByteString.decodeHex("0000ff"),
+        ByteString.decodeHex("000100"),
+        ByteString.decodeHex("000101"),
+        ByteString.decodeHex("007f00"),
+        ByteString.decodeHex("00ff00"),
+        ByteString.decodeHex("010000"),
+        ByteString.decodeHex("010001"),
+        ByteString.decodeHex("01007f"),
+        ByteString.decodeHex("0100ff"),
+        ByteString.decodeHex("010100"),
+        ByteString.decodeHex("01010000"),
+        ByteString.decodeHex("0101000000"),
+        ByteString.decodeHex("0101000001"),
+        ByteString.decodeHex("010101"),
+        ByteString.decodeHex("7f0000"),
+        ByteString.decodeHex("7f0000ffff"),
+        ByteString.decodeHex("ffffff"));
+
+    List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings);
+    Collections.shuffle(sortedByteStrings, new Random(0));
+    Collections.sort(sortedByteStrings);
+
+    assertEquals(originalByteStrings, sortedByteStrings);
+  }
 }
diff --git a/okio/okio/src/test/java/okio/OkioTest.java b/okio/okio/src/test/java/okio/OkioTest.java
index 815a51f..ec92db6 100644
--- a/okio/okio/src/test/java/okio/OkioTest.java
+++ b/okio/okio/src/test/java/okio/OkioTest.java
@@ -19,6 +19,8 @@
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -65,21 +67,19 @@
     source.close();
   }
 
-  // ANDROID-BEGIN
-  //  @Test public void readWritePath() throws Exception {
-  //    Path path = temporaryFolder.newFile().toPath();
-  //
-  //    BufferedSink sink = Okio.buffer(Okio.sink(path));
-  //    sink.writeUtf8("Hello, java.nio file!");
-  //    sink.close();
-  //    assertTrue(Files.exists(path));
-  //    assertEquals(21, Files.size(path));
-  //
-  //    BufferedSource source = Okio.buffer(Okio.source(path));
-  //    assertEquals("Hello, java.nio file!", source.readUtf8());
-  //    source.close();
-  //  }
-  // ANDROID-END
+  @Test public void readWritePath() throws Exception {
+    Path path = temporaryFolder.newFile().toPath();
+
+    BufferedSink sink = Okio.buffer(Okio.sink(path));
+    sink.writeUtf8("Hello, java.nio file!");
+    sink.close();
+    assertTrue(Files.exists(path));
+    assertEquals(21, Files.size(path));
+
+    BufferedSource source = Okio.buffer(Okio.source(path));
+    assertEquals("Hello, java.nio file!", source.readUtf8());
+    source.close();
+  }
 
   @Test public void sinkFromOutputStream() throws Exception {
     Buffer data = new Buffer();
diff --git a/okio/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
index 9867ca0..5ea2dca 100644
--- a/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
+++ b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
@@ -34,9 +34,7 @@
     BufferedSource create(Buffer data);
   }
 
-  // ANDROID-BEGIN
-  //  @Parameterized.Parameters(name = "{0}")
-  // ANDROID-END
+  @Parameterized.Parameters(name = "{0}")
   public static List<Object[]> parameters() {
     return Arrays.asList(
         new Object[] { new Factory() {
@@ -73,10 +71,8 @@
     );
   }
 
-  // ANDROID-BEGIN
-  //  @Parameterized.Parameter
-  public Factory factory = (Factory) (parameters().get(0))[0];
-  // ANDROID-END
+  @Parameterized.Parameter
+  public Factory factory;
 
   private Buffer data;
   private BufferedSource source;
diff --git a/okio/pom.xml b/okio/pom.xml
index acd0fa7..501ac0d 100644
--- a/okio/pom.xml
+++ b/okio/pom.xml
@@ -11,7 +11,7 @@
 
   <groupId>com.squareup.okio</groupId>
   <artifactId>okio-parent</artifactId>
-  <version>1.4.0-SNAPSHOT</version>
+  <version>1.7.0-SNAPSHOT</version>
   <packaging>pom</packaging>
   <name>Okio (Parent)</name>
   <description>A modern I/O API for Java</description>
diff --git a/pom.xml b/pom.xml
index 3c1435a..7219018 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
 
   <groupId>com.squareup.okhttp</groupId>
   <artifactId>parent</artifactId>
-  <version>2.4.0-SNAPSHOT</version>
+  <version>2.6.0-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>OkHttp (Parent)</name>
@@ -42,7 +42,7 @@
 
     <!-- Compilation -->
     <java.version>1.7</java.version>
-    <okio.version>1.4.0-SNAPSHOT</okio.version>
+    <okio.version>1.6.0</okio.version>
     <!-- ALPN library targeted to Java 7 -->
     <alpn.jdk7.version>7.1.2.v20141202</alpn.jdk7.version>
     <!-- ALPN library targeted to Java 8 update 25. -->
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
index 59f51d3..aebd0eb 100644
--- a/samples/crawler/pom.xml
+++ b/samples/crawler/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>crawler</artifactId>
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
index 21d11c7..8c731be 100644
--- a/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
+++ b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
@@ -16,14 +16,13 @@
 package com.squareup.okhttp.sample;
 
 import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.HttpUrl;
 import com.squareup.okhttp.OkHttpClient;
 import com.squareup.okhttp.Request;
 import com.squareup.okhttp.Response;
 import com.squareup.okhttp.internal.NamedRunnable;
 import java.io.File;
 import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.Set;
@@ -41,8 +40,9 @@
  */
 public final class Crawler {
   private final OkHttpClient client;
-  private final Set<URL> fetchedUrls = Collections.synchronizedSet(new LinkedHashSet<URL>());
-  private final LinkedBlockingQueue<URL> queue = new LinkedBlockingQueue<>();
+  private final Set<HttpUrl> fetchedUrls = Collections.synchronizedSet(
+      new LinkedHashSet<HttpUrl>());
+  private final LinkedBlockingQueue<HttpUrl> queue = new LinkedBlockingQueue<>();
   private final ConcurrentHashMap<String, AtomicInteger> hostnames = new ConcurrentHashMap<>();
 
   public Crawler(OkHttpClient client) {
@@ -66,7 +66,7 @@
   }
 
   private void drainQueue() throws Exception {
-    for (URL url; (url = queue.take()) != null; ) {
+    for (HttpUrl url; (url = queue.take()) != null; ) {
       if (!fetchedUrls.add(url)) {
         continue;
       }
@@ -79,10 +79,10 @@
     }
   }
 
-  public void fetch(URL url) throws IOException {
+  public void fetch(HttpUrl url) throws IOException {
     // Skip hosts that we've visited many times.
     AtomicInteger hostnameCount = new AtomicInteger();
-    AtomicInteger previous = hostnames.putIfAbsent(url.getHost(), hostnameCount);
+    AtomicInteger previous = hostnames.putIfAbsent(url.host(), hostnameCount);
     if (previous != null) hostnameCount = previous;
     if (hostnameCount.incrementAndGet() > 100) return;
 
@@ -106,22 +106,11 @@
     Document document = Jsoup.parse(response.body().string(), url.toString());
     for (Element element : document.select("a[href]")) {
       String href = element.attr("href");
-      URL link = parseUrl(response.request().url(), href);
+      HttpUrl link = response.request().httpUrl().resolve(href);
       if (link != null) queue.add(link);
     }
   }
 
-  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>");
@@ -136,7 +125,7 @@
     client.setCache(cache);
 
     Crawler crawler = new Crawler(client);
-    crawler.queue.add(new URL(args[1]));
+    crawler.queue.add(HttpUrl.parse(args[1]));
     crawler.parallelDrainQueue(threadCount);
   }
 }
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index 299b065..55e2671 100644
--- a/samples/guide/pom.xml
+++ b/samples/guide/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>guide</artifactId>
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java
index d70f107..bcfa6e9 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java
@@ -32,12 +32,12 @@
         long t1 = System.nanoTime();
         Request request = chain.request();
         logger.info(String.format("Sending request %s on %s%n%s",
-            request.url(), chain.connection(), request.headers()));
+            request.httpUrl(), chain.connection(), request.headers()));
         Response response = chain.proceed(request);
 
         long t2 = System.nanoTime();
         logger.info(String.format("Received response for %s in %.1fms%n%s",
-            request.url(), (t2 - t1) / 1e6d, response.headers()));
+            request.httpUrl(), (t2 - t1) / 1e6d, response.headers()));
         return response;
       }
     });
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
index dfe3b90..d439e99 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
@@ -7,6 +7,8 @@
 import com.squareup.okhttp.ws.WebSocketCall;
 import com.squareup.okhttp.ws.WebSocketListener;
 import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import okio.Buffer;
 import okio.BufferedSource;
 
@@ -15,6 +17,8 @@
 import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
 
 public final class WebSocketEcho implements WebSocketListener {
+  private final Executor writeExecutor = Executors.newSingleThreadExecutor();
+
   private void run() throws IOException {
     OkHttpClient client = new OkHttpClient();
 
@@ -27,21 +31,28 @@
     client.getDispatcher().getExecutorService().shutdown();
   }
 
-  @Override public void onOpen(WebSocket webSocket, Request request, Response response)
-      throws IOException {
-    webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
-    webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
-    webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
-    webSocket.close(1000, "Goodbye, World!");
+  @Override public void onOpen(final WebSocket webSocket, Response response) {
+    writeExecutor.execute(new Runnable() {
+      @Override public void run() {
+        try {
+          webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
+          webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
+          webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
+          webSocket.close(1000, "Goodbye, World!");
+        } catch (IOException e) {
+          System.err.println("Unable to send messages: " + e.getMessage());
+        }
+      }
+    });
   }
 
   @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
     switch (type) {
       case TEXT:
-        System.out.println(payload.readUtf8());
+        System.out.println("MESSAGE: " + payload.readUtf8());
         break;
       case BINARY:
-        System.out.println(payload.readByteString().hex());
+        System.out.println("MESSAGE: " + payload.readByteString().hex());
         break;
       default:
         throw new IllegalStateException("Unknown payload type: " + type);
@@ -57,7 +68,7 @@
     System.out.println("CLOSE: " + code + " " + reason);
   }
 
-  @Override public void onFailure(IOException e) {
+  @Override public void onFailure(IOException e, Response response) {
     e.printStackTrace();
   }
 
diff --git a/samples/pom.xml b/samples/pom.xml
index 83b45a9..29f1e87 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp</groupId>
     <artifactId>parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <groupId>com.squareup.okhttp.sample</groupId>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index e3cb146..3a1aa7d 100644
--- a/samples/simple-client/pom.xml
+++ b/samples/simple-client/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>simple-client</artifactId>
diff --git a/samples/static-server/pom.xml b/samples/static-server/pom.xml
index 9771c9e..f223151 100644
--- a/samples/static-server/pom.xml
+++ b/samples/static-server/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>com.squareup.okhttp.sample</groupId>
     <artifactId>sample-parent</artifactId>
-    <version>2.4.0-SNAPSHOT</version>
+    <version>2.6.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>static-server</artifactId>