Merge "Be consistent about host names in RouteSelector."
diff --git a/Android.mk b/Android.mk
index 4baf397..7444ae8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -68,7 +68,7 @@
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(okhttp_test_src_files)
 LOCAL_JAVACFLAGS := -encoding UTF-8
-LOCAL_JAVA_LIBRARIES := core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar
+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)
diff --git a/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
new file mode 100644
index 0000000..81aef8e
--- /dev/null
+++ b/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
@@ -0,0 +1,169 @@
+/*
+ *  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 java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Duck-typing for methods: Represents a method that may or may not be present on an object.
+ *
+ * @param <T> the type of the object the method might be on, typically an interface or base class
+ */
+class OptionalMethod<T> {
+
+  /** The return type of the method. null means "don't care". */
+  private final Class<?> returnType;
+
+  private final String methodName;
+
+  private final Class[] methodParams;
+
+  /**
+   * Creates an optional method.
+   *
+   * @param returnType the return type to required, null if it does not matter
+   * @param methodName the name of the method
+   * @param methodParams the method parameter types
+   */
+  public OptionalMethod(Class<?> returnType, String methodName, Class... methodParams) {
+    this.returnType = returnType;
+    this.methodName = methodName;
+    this.methodParams = methodParams;
+  }
+
+  /**
+   * Returns true if the method exists on the supplied {@code target}.
+   */
+  public boolean isSupported(T target) {
+    return getMethod(target.getClass()) != null;
+  }
+
+  /**
+   * Invokes the method on {@code target} with {@code args}. If the method does not exist or is not
+   * public then {@code null} is returned. See also
+   * {@link #invokeOptionalWithoutCheckedException(Object, Object...)}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   * @throws InvocationTargetException if the invocation throws an exception
+   */
+  public Object invokeOptional(T target, Object... args) throws InvocationTargetException {
+    Method m = getMethod(target.getClass());
+    if (m == null) {
+      return null;
+    }
+    try {
+      return m.invoke(target, args);
+    } catch (IllegalAccessException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target}.  If the method does not exist or is not
+   * public then {@code null} is returned. Any RuntimeException thrown by the method is thrown,
+   * checked exceptions are wrapped in an {@link AssertionError}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   */
+  public Object invokeOptionalWithoutCheckedException(T target, Object... args) {
+    try {
+      return invokeOptional(target, args);
+    } catch (InvocationTargetException e) {
+      Throwable targetException = e.getTargetException();
+      if (targetException instanceof RuntimeException) {
+        throw (RuntimeException) targetException;
+      }
+      throw new AssertionError("Unexpected exception", targetException);
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target} with {@code args}. Throws an error if the method is not
+   * supported. See also {@link #invokeWithoutCheckedException(Object, Object...)}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   * @throws InvocationTargetException if the invocation throws an exception
+   */
+  public Object invoke(T target, Object... args) throws InvocationTargetException {
+    Method m = getMethod(target.getClass());
+    if (m == null) {
+      throw new AssertionError("Method " + methodName + " not supported for object " + target);
+    }
+    try {
+      return m.invoke(target, args);
+    } catch (IllegalAccessException e) {
+      // Method should be public: we checked.
+      throw new AssertionError("Unexpectedly could not call: " + m, e);
+    }
+  }
+
+  /**
+   * Invokes the method on {@code target}. Throws an error if the method is not supported. Any
+   * RuntimeException thrown by the method is thrown, checked exceptions are wrapped in
+   * an {@link AssertionError}.
+   *
+   * @throws IllegalArgumentException if the arguments are invalid
+   */
+  public Object invokeWithoutCheckedException(T target, Object... args) {
+    try {
+      return invoke(target, args);
+    } catch (InvocationTargetException e) {
+      Throwable targetException = e.getTargetException();
+      if (targetException instanceof RuntimeException) {
+        throw (RuntimeException) targetException;
+      }
+      throw new AssertionError("Unexpected exception", targetException);
+    }
+  }
+
+  /**
+   * Perform a lookup for the method. No caching.
+   * In order to return a method the method name and arguments must match those specified when
+   * the {@link OptionalMethod} was created. If the return type is specified (i.e. non-null) it
+   * must also be compatible. The method must also be public.
+   */
+  private Method getMethod(Class<?> clazz) {
+    Method method = null;
+    if (methodName != null) {
+      method = getPublicMethod(clazz, methodName, methodParams);
+      if (method != null
+          && returnType != null
+          && !returnType.isAssignableFrom(method.getReturnType())) {
+
+        // If the return type is non-null it must be compatible.
+        method = null;
+      }
+    }
+    return method;
+  }
+
+  private static Method getPublicMethod(Class<?> clazz, String methodName, Class[] parameterTypes) {
+    Method method = null;
+    try {
+      method = clazz.getMethod(methodName, parameterTypes);
+      if ((method.getModifiers() & Modifier.PUBLIC) == 0) {
+        method = null;
+      }
+    } catch (NoSuchMethodException e) {
+      // None.
+    }
+    return method;
+  }
+}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 7d0e847..121b156 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -30,8 +30,8 @@
 import java.util.zip.DeflaterOutputStream;
 import javax.net.ssl.SSLSocket;
 
-import com.android.org.conscrypt.OpenSSLSocketImpl;
 import com.squareup.okhttp.Protocol;
+
 import okio.ByteString;
 
 /**
@@ -44,6 +44,25 @@
         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 );
+    /** byte[] getNpnSelectedProtocol() */
+    private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL =
+            new OptionalMethod<Socket>(byte[].class, "getNpnSelectedProtocol");
+    /** setNpnSelectedProtocol(byte[]) */
+    private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
+            new OptionalMethod<Socket>(null, "setNpnProtocols", byte[].class);
+
     public void logW(String warning) {
         System.logW(warning);
     }
@@ -61,11 +80,8 @@
     }
 
     public void enableTlsExtensions(SSLSocket socket, String uriHost) {
-        if (socket instanceof OpenSSLSocketImpl) {
-            OpenSSLSocketImpl openSSLSocket = (OpenSSLSocketImpl) socket;
-            openSSLSocket.setUseSessionTickets(true);
-            openSSLSocket.setHostname(uriHost);
-        }
+        SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(socket, true);
+        SET_HOSTNAME.invokeOptionalWithoutCheckedException(socket, uriHost);
     }
 
     public void supportTlsIntolerantServer(SSLSocket socket) {
@@ -97,18 +113,28 @@
      * Returns the negotiated protocol, or null if no protocol was negotiated.
      */
     public ByteString getNpnSelectedProtocol(SSLSocket socket) {
-        if (!(socket instanceof OpenSSLSocketImpl)) {
+        boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
+        boolean npnSupported = GET_NPN_SELECTED_PROTOCOL.isSupported(socket);
+        if (!(alpnSupported || npnSupported)) {
             return null;
         }
 
-        OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
         // Prefer ALPN's result if it is present.
-        byte[] alpnResult = socketImpl.getAlpnSelectedProtocol();
-        if (alpnResult != null) {
-            return ByteString.of(alpnResult);
+        if (alpnSupported) {
+            byte[] alpnResult =
+                (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+            if (alpnResult != null) {
+                return ByteString.of(alpnResult);
+            }
         }
-        byte[] npnResult = socketImpl.getNpnSelectedProtocol();
-        return npnResult == null ? null : ByteString.of(npnResult);
+        if (npnSupported) {
+            byte[] npnResult =
+                (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+            if (npnResult != null) {
+                return ByteString.of(npnResult);
+            }
+        }
+        return null;
     }
 
     /**
@@ -116,11 +142,20 @@
      * protocols are only sent if the socket implementation supports NPN.
      */
     public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
-        if (socket instanceof OpenSSLSocketImpl) {
-            OpenSSLSocketImpl socketImpl = (OpenSSLSocketImpl) socket;
-            byte[] protocols = concatLengthPrefixed(npnProtocols);
-            socketImpl.setAlpnProtocols(protocols);
-            socketImpl.setNpnProtocols(protocols);
+        boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(socket);
+        boolean npnSupported = SET_NPN_PROTOCOLS.isSupported(socket);
+        if (!(alpnSupported || npnSupported)) {
+            return;
+        }
+
+        byte[] protocols = concatLengthPrefixed(npnProtocols);
+        if (alpnSupported) {
+            SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(
+                socket, new Object[] { protocols });
+        }
+        if (npnSupported) {
+            SET_NPN_PROTOCOLS.invokeWithoutCheckedException(
+                socket, new Object[] { protocols });
         }
     }
 
diff --git a/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
new file mode 100644
index 0000000..c53fb21
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
@@ -0,0 +1,337 @@
+/*
+ *  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 org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+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;
+
+/**
+ * Tests for {@link OptionalMethod}.
+ */
+public class OptionalMethodTest {
+  @SuppressWarnings("unused")
+  private static class BaseClass {
+    public String stringMethod() {
+      return "string";
+    }
+
+    public void voidMethod() {}
+  }
+
+  @SuppressWarnings("unused")
+  private static class SubClass1 extends BaseClass {
+    public String subclassMethod() {
+      return "subclassMethod1";
+    }
+
+    public String methodWithArgs(String arg) {
+      return arg;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class SubClass2 extends BaseClass {
+    public int subclassMethod() {
+      return 1234;
+    }
+
+    public String methodWithArgs(String arg) {
+      return arg;
+    }
+
+    public void throwsException() throws IOException {
+      throw new IOException();
+    }
+
+    public void throwsRuntimeException() throws Exception {
+      throw new NumberFormatException();
+    }
+
+    protected void nonPublic() {}
+  }
+
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "stringMethod");
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_STRING =
+      new OptionalMethod<BaseClass>(String.class, "stringMethod");
+  private final static OptionalMethod<BaseClass> STRING_METHOD_RETURNS_INT =
+      new OptionalMethod<BaseClass>(Integer.TYPE, "stringMethod");
+  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "voidMethod");
+  private final static OptionalMethod<BaseClass> VOID_METHOD_RETURNS_VOID =
+      new OptionalMethod<BaseClass>(Void.TYPE, "voidMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_ANY =
+      new OptionalMethod<BaseClass>(null, "subclassMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_STRING =
+      new OptionalMethod<BaseClass>(String.class, "subclassMethod");
+  private final static OptionalMethod<BaseClass> SUBCLASS_METHOD_RETURNS_INT =
+      new OptionalMethod<BaseClass>(Integer.TYPE, "subclassMethod");
+  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_WRONG_PARAMS =
+      new OptionalMethod<BaseClass>(null, "methodWithArgs", Integer.class);
+  private final static OptionalMethod<BaseClass> METHOD_WITH_ARGS_CORRECT_PARAMS =
+      new OptionalMethod<BaseClass>(null, "methodWithArgs", String.class);
+
+  private final static OptionalMethod<BaseClass> THROWS_EXCEPTION =
+      new OptionalMethod<BaseClass>(null, "throwsException");
+  private final static OptionalMethod<BaseClass> THROWS_RUNTIME_EXCEPTION =
+      new OptionalMethod<BaseClass>(null, "throwsRuntimeException");
+  private final static OptionalMethod<BaseClass> NON_PUBLIC =
+      new OptionalMethod<BaseClass>(null, "nonPublic");
+
+  @Test
+  public void isSupported() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(base));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(base));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(base));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(base));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_ANY.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(base));
+      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(base));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(base));
+      assertFalse(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(base));
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass1));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass1));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass1));
+      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass1));
+      assertTrue(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass1));
+      assertFalse(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass1));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass1));
+      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass1));
+    }
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertTrue(STRING_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertTrue(STRING_METHOD_RETURNS_STRING.isSupported(subClass2));
+      assertFalse(STRING_METHOD_RETURNS_INT.isSupported(subClass2));
+      assertTrue(VOID_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertTrue(VOID_METHOD_RETURNS_VOID.isSupported(subClass2));
+      assertTrue(SUBCLASS_METHOD_RETURNS_ANY.isSupported(subClass2));
+      assertFalse(SUBCLASS_METHOD_RETURNS_STRING.isSupported(subClass2));
+      assertTrue(SUBCLASS_METHOD_RETURNS_INT.isSupported(subClass2));
+      assertFalse(METHOD_WITH_ARGS_WRONG_PARAMS.isSupported(subClass2));
+      assertTrue(METHOD_WITH_ARGS_CORRECT_PARAMS.isSupported(subClass2));
+    }
+  }
+
+  @Test
+  public void invoke() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(base));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(base));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, base);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(base));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(base));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_ANY, base);
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, base);
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, base);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, base);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, base);
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass1));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass1);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invoke(subClass1));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_INT, subClass1);
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass1);
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass1, "arg"));
+    }
+
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invoke(subClass2));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertErrorOnInvoke(STRING_METHOD_RETURNS_INT, subClass2);
+      assertNull(VOID_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertNull(VOID_METHOD_RETURNS_VOID.invoke(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invoke(subClass2));
+      assertErrorOnInvoke(SUBCLASS_METHOD_RETURNS_STRING, subClass2);
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invoke(subClass2));
+      assertErrorOnInvoke(METHOD_WITH_ARGS_WRONG_PARAMS, subClass2);
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invoke(subClass2, "arg"));
+    }
+  }
+
+  @Test
+  public void invokeBadArgs() throws Exception {
+    SubClass1 subClass1 = new SubClass1();
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+    assertIllegalArgumentExceptionOnInvoke(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+  }
+
+  @Test
+  public void invokeWithException() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    try {
+      THROWS_EXCEPTION.invoke(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof IOException);
+    }
+
+    try {
+      THROWS_RUNTIME_EXCEPTION.invoke(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof NumberFormatException);
+    }
+  }
+
+  @Test
+  public void invokeNonPublic() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    assertFalse(NON_PUBLIC.isSupported(subClass2));
+    assertErrorOnInvoke(NON_PUBLIC, subClass2);
+  }
+
+  @Test
+  public void invokeOptional() throws Exception {
+    {
+      BaseClass base = new BaseClass();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(base));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(base));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(base));
+      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(base));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(base));
+      assertNull(METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(base));
+    }
+    {
+      SubClass1 subClass1 = new SubClass1();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass1));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass1));
+      assertEquals("subclassMethod1", SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass1));
+      assertNull(SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass1));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass1));
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass1, "arg"));
+    }
+
+    {
+      SubClass2 subClass2 = new SubClass2();
+      assertEquals("string", STRING_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+      assertEquals("string", STRING_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(STRING_METHOD_RETURNS_INT.invokeOptional(subClass2));
+      assertNull(VOID_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(VOID_METHOD_RETURNS_VOID.invokeOptional(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_ANY.invokeOptional(subClass2));
+      assertNull(SUBCLASS_METHOD_RETURNS_STRING.invokeOptional(subClass2));
+      assertEquals(1234, SUBCLASS_METHOD_RETURNS_INT.invokeOptional(subClass2));
+      assertNull(METHOD_WITH_ARGS_WRONG_PARAMS.invokeOptional(subClass2));
+      assertEquals("arg", METHOD_WITH_ARGS_CORRECT_PARAMS.invokeOptional(subClass2, "arg"));
+    }
+  }
+
+  @Test
+  public void invokeOptionalBadArgs() throws Exception {
+    SubClass1 subClass1 = new SubClass1();
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1); // no args
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, 123);
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, true);
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, new Object());
+    assertIllegalArgumentExceptionOnInvokeOptional(METHOD_WITH_ARGS_CORRECT_PARAMS, subClass1, "one", "two");
+  }
+
+  @Test
+  public void invokeOptionalWithException() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    try {
+      THROWS_EXCEPTION.invokeOptional(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof IOException);
+    }
+
+    try {
+      THROWS_RUNTIME_EXCEPTION.invokeOptional(subClass2);
+    } catch (InvocationTargetException expected) {
+      assertTrue(expected.getTargetException() instanceof NumberFormatException);
+    }
+  }
+
+  @Test
+  public void invokeOptionalNonPublic() throws Exception {
+    SubClass2 subClass2 = new SubClass2();
+    assertFalse(NON_PUBLIC.isSupported(subClass2));
+    assertErrorOnInvokeOptional(NON_PUBLIC, subClass2);
+  }
+
+  private static <T> void assertErrorOnInvoke(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invoke(base, args);
+      fail();
+    } catch (Error expected) {
+    }
+  }
+
+  private static <T> void assertIllegalArgumentExceptionOnInvoke(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invoke(base, args);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  private static <T> void assertErrorOnInvokeOptional(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invokeOptional(base, args);
+      fail();
+    } catch (Error expected) {
+    }
+  }
+
+  private static <T> void assertIllegalArgumentExceptionOnInvokeOptional(
+      OptionalMethod<T> optionalMethod, T base, Object... args) throws Exception {
+    try {
+      optionalMethod.invokeOptional(base, args);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+}
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
new file mode 100644
index 0000000..9e293f6
--- /dev/null
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -0,0 +1,234 @@
+/*
+ *  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.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.assertEquals;
+import static org.junit.Assert.assertNotNull;
+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();
+    platform.enableTlsExtensions(arbitrarySocketImpl, "host");
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    platform.enableTlsExtensions(openSslSocket, "host");
+    assertTrue(openSslSocket.useSessionTickets);
+    assertEquals("host", openSslSocket.hostname);
+  }
+
+  @Test
+  public void getNpnSelectedProtocol() throws Exception {
+    Platform platform = new Platform();
+    byte[] npnBytes = "npn".getBytes();
+    byte[] alpnBytes = "alpn".getBytes();
+
+    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+    assertNull(platform.getNpnSelectedProtocol(arbitrarySocketImpl));
+
+    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+    npnOnlySSLSocketImpl.npnProtocols = npnBytes;
+    assertEquals(ByteString.of(npnBytes), platform.getNpnSelectedProtocol(npnOnlySSLSocketImpl));
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    openSslSocket.npnProtocols = npnBytes;
+    openSslSocket.alpnProtocols = alpnBytes;
+    assertEquals(ByteString.of(alpnBytes), platform.getNpnSelectedProtocol(openSslSocket));
+  }
+
+  @Test
+  public void setNpnProtocols() throws Exception {
+    Platform platform = new Platform();
+    List<Protocol> protocols = Arrays.asList(Protocol.SPDY_3);
+
+    // No error
+    TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
+    platform.setNpnProtocols(arbitrarySocketImpl, protocols);
+
+    NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+    platform.setNpnProtocols(npnOnlySSLSocketImpl, protocols);
+    assertNotNull(npnOnlySSLSocketImpl.npnProtocols);
+
+    FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
+    platform.setNpnProtocols(openSslSocket, protocols);
+    assertNotNull(openSslSocket.npnProtocols);
+    assertNotNull(openSslSocket.alpnProtocols);
+  }
+
+  private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
+    private boolean useSessionTickets;
+    private String hostname;
+    private byte[] npnProtocols;
+    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 setNpnProtocols(byte[] npnProtocols) {
+      this.npnProtocols = npnProtocols;
+    }
+
+    @Override
+    public byte[] getNpnSelectedProtocol() {
+      return npnProtocols;
+    }
+
+    @Override
+    public void setAlpnProtocols(byte[] alpnProtocols) {
+      this.alpnProtocols = alpnProtocols;
+    }
+
+    @Override
+    public byte[] getAlpnSelectedProtocol() {
+      return alpnProtocols;
+    }
+  }
+
+  // Legacy case
+  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/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
new file mode 100644
index 0000000..db84214
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public final class DisconnectTest {
+  private final MockWebServer server = new MockWebServer();
+  private final OkHttpClient client = new OkHttpClient();
+
+  @Test public void interruptWritingRequestBody() throws Exception {
+    int requestBodySize = 2 * 1024 * 1024; // 2 MiB
+
+    server.enqueue(new MockResponse()
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    disconnectLater(connection, 500);
+
+    connection.setDoOutput(true);
+    connection.setFixedLengthStreamingMode(requestBodySize);
+    OutputStream requestBody = connection.getOutputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      for (int i = 0; i < requestBodySize; i += buffer.length) {
+        requestBody.write(buffer);
+        requestBody.flush();
+      }
+      fail("Expected connection to be closed");
+    } catch (IOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  @Test public void interruptReadingResponseBody() throws Exception {
+    int responseBodySize = 2 * 1024 * 1024; // 2 MiB
+
+    server.enqueue(new MockResponse()
+        .setBody(new byte[responseBodySize])
+        .throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
+    server.play();
+
+    HttpURLConnection connection = client.open(server.getUrl("/"));
+    disconnectLater(connection, 500);
+
+    InputStream responseBody = connection.getInputStream();
+    byte[] buffer = new byte[1024];
+    try {
+      while (responseBody.read(buffer) != -1) {
+      }
+      fail("Expected connection to be closed");
+    } catch (IOException expected) {
+    }
+
+    connection.disconnect();
+  }
+
+  private void disconnectLater(final HttpURLConnection connection, final int delayMillis) {
+    Thread interruptingCow = new Thread() {
+      @Override public void run() {
+        try {
+          sleep(delayMillis);
+          connection.disconnect();
+        } catch (InterruptedException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    };
+    interruptingCow.start();
+  }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index a9f902a..127807f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -65,6 +65,7 @@
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
@@ -1029,7 +1030,9 @@
   }
 
   @Test public void disconnectedConnection() throws IOException {
-    server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+    server.enqueue(new MockResponse()
+        .throttleBody(2, 100, TimeUnit.MILLISECONDS)
+        .setBody("ABCD"));
     server.play();
 
     connection = client.open(server.getUrl("/"));
@@ -1037,6 +1040,10 @@
     assertEquals('A', (char) in.read());
     connection.disconnect();
     try {
+      // Reading 'B' may succeed if it's buffered.
+      in.read();
+
+      // But 'C' shouldn't be buffered (the response is throttled) and this should fail.
       in.read();
       fail("Expected a connection closed exception");
     } catch (IOException expected) {
@@ -1317,11 +1324,13 @@
     HttpURLConnection connection1 = client.open(server.getUrl("/"));
     InputStream in1 = connection1.getInputStream();
     assertEquals("ABCDE", readAscii(in1, 5));
+    in1.close();
     connection1.disconnect();
 
     HttpURLConnection connection2 = client.open(server.getUrl("/"));
     InputStream in2 = connection2.getInputStream();
     assertEquals("LMNOP", readAscii(in2, 5));
+    in2.close();
     connection2.disconnect();
 
     assertEquals(0, server.takeRequest().getSequenceNumber());
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
index b12b12d..718d471 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -122,6 +122,10 @@
     return state == STATE_CLOSED;
   }
 
+  public void closeIfOwnedBy(Object owner) throws IOException {
+    connection.closeIfOwnedBy(owner);
+  }
+
   public void flush() throws IOException {
     sink.flush();
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index f00fbe7..d796a6c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -411,6 +411,18 @@
   }
 
   /**
+   * Immediately closes the socket connection if it's currently held by this
+   * engine. Use this to interrupt an in-flight request from any thread. It's
+   * the caller's responsibility to close the request body and response body
+   * streams; otherwise resources may be leaked.
+   */
+  public final void disconnect() throws IOException {
+    if (transport != null) {
+      transport.disconnect(this);
+    }
+  }
+
+  /**
    * Release any resources held by this engine. If a connection is still held by
    * this engine, it is returned.
    */
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
index a1b367f..2ffe039 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -150,4 +150,8 @@
     // reference escapes.
     return httpConnection.newUnknownLengthSource(cacheRequest);
   }
+
+  @Override public void disconnect(HttpEngine engine) throws IOException {
+    httpConnection.closeIfOwnedBy(engine);
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index 899d914..32be0be 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -109,9 +109,18 @@
 
   @Override public final void disconnect() {
     // Calling disconnect() before a connection exists should have no effect.
-    if (httpEngine != null) {
-      httpEngine.close();
+    if (httpEngine == null) return;
+
+    try {
+      httpEngine.disconnect();
+    } catch (IOException ignored) {
     }
+
+    // This doesn't close the stream because doing so would require all stream
+    // access to be synchronized. It's expected that the thread using the
+    // connection will close its streams directly. If it doesn't, the worst
+    // case is that the GzipSource's Inflater won't be released until it's
+    // finalized. (This logs a warning on Android.)
   }
 
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index e775d34..9db9643 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -219,6 +219,10 @@
   @Override public void releaseConnectionOnIdle() {
   }
 
+  @Override public void disconnect(HttpEngine engine) throws IOException {
+    stream.close(ErrorCode.CANCEL);
+  }
+
   @Override public boolean canReuseConnection() {
     return true; // TODO: spdyConnection.isClosed() ?
   }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index 94c90d4..852a15b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -76,6 +76,8 @@
    */
   void releaseConnectionOnIdle() throws IOException;
 
+  void disconnect(HttpEngine engine) throws IOException;
+
   /**
    * Returns true if the socket connection held by this transport can be reused
    * for a follow-up exchange.