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",
-