/* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3; 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.InetAddress; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.Proxy; import java.net.ServerSocket; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.net.UnknownServiceException; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLProtocolException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import okhttp3.internal.DoubleInetAddressDns; import okhttp3.internal.RecordingOkAuthenticator; import okhttp3.internal.SingleInetAddressDns; import okhttp3.internal.Util; import okhttp3.internal.Version; import okhttp3.internal.http.RecordingProxySelector; import okhttp3.internal.io.InMemoryFileSystem; import okhttp3.internal.tls.HeldCertificate; import okhttp3.internal.tls.SslClient; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import okhttp3.mockwebserver.SocketPolicy; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; import okio.GzipSink; 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.TestRule; import org.junit.rules.Timeout; import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER; import static okhttp3.TestUtil.awaitGarbageCollection; import static okhttp3.TestUtil.defaultClient; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; 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 { @Rule public final TestRule timeout = new Timeout(30_000, TimeUnit.MILLISECONDS); @Rule public final MockWebServer server = new MockWebServer(); @Rule public final MockWebServer server2 = new MockWebServer(); @Rule public final InMemoryFileSystem fileSystem = new InMemoryFileSystem(); private SslClient sslClient = SslClient.localhost(); private OkHttpClient client = defaultClient(); private RecordingCallback callback = new RecordingCallback(); private TestLogHandler logHandler = new TestLogHandler(); private Cache cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem); private ServerSocket nullServer; private Logger logger = Logger.getLogger(OkHttpClient.class.getName()); @Before public void setUp() throws Exception { logger.addHandler(logHandler); } @After public void tearDown() throws Exception { cache.delete(); Util.closeQuietly(nullServer); logger.removeHandler(logHandler); } @Test public void get() throws Exception { server.enqueue(new MockResponse() .setBody("abc") .clearHeaders() .addHeader("content-type: text/plain") .addHeader("content-length", "3")); long sentAt = System.currentTimeMillis(); RecordedResponse recordedResponse = executeSynchronously("/", "User-Agent", "SyncApiTest"); long receivedAt = System.currentTimeMillis(); recordedResponse.assertCode(200) .assertSuccessful() .assertHeaders(new Headers.Builder() .add("content-type", "text/plain") .add("content-length", "3") .build()) .assertBody("abc") .assertSentRequestAtMillis(sentAt, receivedAt) .assertReceivedResponseAtMillis(sentAt, receivedAt); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("GET", recordedRequest.getMethod()); assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent")); assertEquals(0, recordedRequest.getBody().size()); assertNull(recordedRequest.getHeader("Content-Length")); } @Test public void buildRequestUsingHttpUrl() throws Exception { server.enqueue(new MockResponse()); executeSynchronously("/").assertSuccessful(); } @Test public void invalidScheme() throws Exception { Request.Builder requestBuilder = new Request.Builder(); try { requestBuilder.url("ftp://hostname/path"); fail(); } catch (IllegalArgumentException expected) { assertEquals(expected.getMessage(), "unexpected url: ftp://hostname/path"); } } @Test public void invalidPort() throws Exception { 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)); executeSynchronously("/") .assertCode(500) .assertNotSuccessful(); } @Test public void get_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); get(); } @Test public void get_HTTPS() throws Exception { enableTls(); get(); } @Test public void repeatedHeaderNames() throws Exception { server.enqueue(new MockResponse() .addHeader("B", "123") .addHeader("B", "234")); executeSynchronously("/", "A", "345", "A", "456") .assertCode(200) .assertHeader("B", "123", "234"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(Arrays.asList("345", "456"), recordedRequest.getHeaders().values("A")); } @Test public void repeatedHeaderNames_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); repeatedHeaderNames(); } @Test public void getWithRequestBody() throws Exception { server.enqueue(new MockResponse()); try { new Request.Builder().method("GET", RequestBody.create(MediaType.parse("text/plain"), "abc")); fail(); } catch (IllegalArgumentException expected) { } } @Test public void head() throws Exception { server.enqueue(new MockResponse().addHeader("Content-Type: text/plain")); Request request = new Request.Builder() .url(server.url("/")) .head() .header("User-Agent", "SyncApiTest") .build(); executeSynchronously(request) .assertCode(200) .assertHeader("Content-Type", "text/plain"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("HEAD", recordedRequest.getMethod()); assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent")); assertEquals(0, recordedRequest.getBody().size()); assertNull(recordedRequest.getHeader("Content-Length")); } @Test public void headResponseContentLengthIsIgnored() throws Exception { server.enqueue(new MockResponse() .clearHeaders() .addHeader("Content-Length", "100")); server.enqueue(new MockResponse() .setBody("abc")); Request headRequest = new Request.Builder() .url(server.url("/")) .head() .build(); executeSynchronously(headRequest) .assertCode(200) .assertHeader("Content-Length", "100") .assertBody(""); Request getRequest = new Request.Builder() .url(server.url("/")) .build(); executeSynchronously(getRequest) .assertCode(200) .assertBody("abc"); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void headResponseContentEncodingIsIgnored() throws Exception { server.enqueue(new MockResponse() .clearHeaders() .addHeader("Content-Encoding", "chunked")); server.enqueue(new MockResponse() .setBody("abc")); Request headRequest = new Request.Builder() .url(server.url("/")) .head() .build(); executeSynchronously(headRequest) .assertCode(200) .assertHeader("Content-Encoding", "chunked") .assertBody(""); Request getRequest = new Request.Builder() .url(server.url("/")) .build(); executeSynchronously(getRequest) .assertCode(200) .assertBody("abc"); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void head_HTTPS() throws Exception { enableTls(); head(); } @Test public void head_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); head(); } @Test public void post() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "def")) .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("POST", recordedRequest.getMethod()); assertEquals("def", recordedRequest.getBody().readUtf8()); assertEquals("3", recordedRequest.getHeader("Content-Length")); assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type")); } @Test public void post_HTTPS() throws Exception { enableTls(); post(); } @Test public void post_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); post(); } @Test public void postZeroLength() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .method("POST", RequestBody.create(null, new byte[0])) .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("POST", recordedRequest.getMethod()); assertEquals(0, recordedRequest.getBody().size()); assertEquals("0", recordedRequest.getHeader("Content-Length")); assertEquals(null, recordedRequest.getHeader("Content-Type")); } @Test public void postZerolength_HTTPS() throws Exception { enableTls(); postZeroLength(); } @Test public void postZerolength_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); postZeroLength(); } @Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception { postBodyRetransmittedAfterAuthorizationFail("abc"); } @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception { enableTls(); postBodyRetransmittedAfterAuthorizationFail("abc"); } @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); postBodyRetransmittedAfterAuthorizationFail("abc"); } /** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail() throws Exception { postBodyRetransmittedAfterAuthorizationFail(""); } @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception { enableTls(); postBodyRetransmittedAfterAuthorizationFail(""); } @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); postBodyRetransmittedAfterAuthorizationFail(""); } private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception { server.enqueue(new MockResponse().setResponseCode(401)); server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/")) .method("POST", RequestBody.create(null, body)) .build(); String credential = Credentials.basic("jesse", "secret"); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator(credential)) .build(); Response response = client.newCall(request).execute(); assertEquals(200, response.code()); response.body().close(); RecordedRequest recordedRequest1 = server.takeRequest(); assertEquals("POST", recordedRequest1.getMethod()); assertEquals(body, recordedRequest1.getBody().readUtf8()); assertNull(recordedRequest1.getHeader("Authorization")); RecordedRequest recordedRequest2 = server.takeRequest(); assertEquals("POST", recordedRequest2.getMethod()); assertEquals(body, recordedRequest2.getBody().readUtf8()); assertEquals(credential, recordedRequest2.getHeader("Authorization")); } @Test public void attemptAuthorization20Times() throws Exception { for (int i = 0; i < 20; i++) { server.enqueue(new MockResponse().setResponseCode(401)); } server.enqueue(new MockResponse().setBody("Success!")); String credential = Credentials.basic("jesse", "secret"); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator(credential)) .build(); executeSynchronously("/") .assertCode(200) .assertBody("Success!"); } @Test public void doesNotAttemptAuthorization21Times() throws Exception { for (int i = 0; i < 21; i++) { server.enqueue(new MockResponse().setResponseCode(401)); } String credential = Credentials.basic("jesse", "secret"); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator(credential)) .build(); try { client.newCall(new Request.Builder().url(server.url("/0")).build()).execute(); fail(); } catch (IOException expected) { assertEquals("Too many follow-up requests: 21", expected.getMessage()); } } @Test public void delete() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .delete() .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("DELETE", recordedRequest.getMethod()); assertEquals(0, recordedRequest.getBody().size()); assertEquals("0", recordedRequest.getHeader("Content-Length")); assertEquals(null, recordedRequest.getHeader("Content-Type")); } @Test public void delete_HTTPS() throws Exception { enableTls(); delete(); } @Test public void delete_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); delete(); } @Test public void deleteWithRequestBody() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .method("DELETE", RequestBody.create(MediaType.parse("text/plain"), "def")) .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("DELETE", recordedRequest.getMethod()); assertEquals("def", recordedRequest.getBody().readUtf8()); } @Test public void put() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .put(RequestBody.create(MediaType.parse("text/plain"), "def")) .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("PUT", recordedRequest.getMethod()); assertEquals("def", recordedRequest.getBody().readUtf8()); assertEquals("3", recordedRequest.getHeader("Content-Length")); assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type")); } @Test public void put_HTTPS() throws Exception { enableTls(); put(); } @Test public void put_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); put(); } @Test public void patch() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .patch(RequestBody.create(MediaType.parse("text/plain"), "def")) .build(); executeSynchronously(request) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("PATCH", recordedRequest.getMethod()); assertEquals("def", recordedRequest.getBody().readUtf8()); assertEquals("3", recordedRequest.getHeader("Content-Length")); assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type")); } @Test public void patch_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); patch(); } @Test public void patch_HTTPS() throws Exception { enableTls(); patch(); } @Test public void unspecifiedRequestBodyContentTypeDoesNotGetDefault() throws Exception { server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/")) .method("POST", RequestBody.create(null, "abc")) .build(); executeSynchronously(request).assertCode(200); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(null, recordedRequest.getHeader("Content-Type")); assertEquals("3", recordedRequest.getHeader("Content-Length")); assertEquals("abc", recordedRequest.getBody().readUtf8()); } @Test public void illegalToExecuteTwice() throws Exception { server.enqueue(new MockResponse() .setBody("abc") .addHeader("Content-Type: text/plain")); Request request = new Request.Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build(); Call call = client.newCall(request); Response response = call.execute(); response.body().close(); try { call.execute(); fail(); } catch (IllegalStateException e) { assertEquals("Already Executed", e.getMessage()); } try { call.enqueue(callback); fail(); } catch (IllegalStateException e) { assertEquals("Already Executed", e.getMessage()); } assertEquals("SyncApiTest", server.takeRequest().getHeader("User-Agent")); } @Test public void illegalToExecuteTwice_Async() throws Exception { server.enqueue(new MockResponse() .setBody("abc") .addHeader("Content-Type: text/plain")); Request request = new Request.Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build(); Call call = client.newCall(request); call.enqueue(callback); try { call.execute(); fail(); } catch (IllegalStateException e) { assertEquals("Already Executed", e.getMessage()); } try { call.enqueue(callback); fail(); } catch (IllegalStateException e) { assertEquals("Already Executed", e.getMessage()); } assertEquals("SyncApiTest", server.takeRequest().getHeader("User-Agent")); } @Test public void legalToExecuteTwiceCloning() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); Request request = new Request.Builder() .url(server.url("/")) .build(); Call call = client.newCall(request); Response response1 = call.execute(); Call cloned = call.clone(); Response response2 = cloned.execute(); assertEquals(response1.body().string(), "abc"); assertEquals(response2.body().string(), "def"); } @Test public void legalToExecuteTwiceCloning_Async() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); Request request = new Request.Builder() .url(server.url("/")) .build(); Call call = client.newCall(request); call.enqueue(callback); Call cloned = call.clone(); cloned.enqueue(callback); RecordedResponse firstResponse = callback.await(request.url()).assertSuccessful(); RecordedResponse secondResponse = callback.await(request.url()).assertSuccessful(); Set<String> bodies = new LinkedHashSet<>(); bodies.add(firstResponse.getBody()); bodies.add(secondResponse.getBody()); assertTrue(bodies.contains("abc")); assertTrue(bodies.contains("def")); } @Test public void get_Async() throws Exception { server.enqueue(new MockResponse() .setBody("abc") .addHeader("Content-Type: text/plain")); Request request = new Request.Builder() .url(server.url("/")) .header("User-Agent", "AsyncApiTest") .build(); client.newCall(request).enqueue(callback); callback.await(request.url()) .assertCode(200) .assertHeader("Content-Type", "text/plain") .assertBody("abc"); assertEquals("AsyncApiTest", server.takeRequest().getHeader("User-Agent")); } @Test public void exceptionThrownByOnResponseIsRedactedAndLogged() throws Exception { server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/secret")) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { fail(); } @Override public void onResponse(Call call, Response response) throws IOException { throw new IOException("a"); } }); assertEquals("INFO: Callback failure for call to " + server.url("/") + "...", logHandler.take()); } @Test public void connectionPooling() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); server.enqueue(new MockResponse().setBody("ghi")); executeSynchronously("/a").assertBody("abc"); executeSynchronously("/b").assertBody("def"); executeSynchronously("/c").assertBody("ghi"); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); assertEquals(2, server.takeRequest().getSequenceNumber()); } @Test public void connectionPooling_Async() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); server.enqueue(new MockResponse().setBody("ghi")); 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.url("/b")).build()).enqueue(callback); callback.await(server.url("/b")).assertBody("def"); 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()); assertEquals(2, server.takeRequest().getSequenceNumber()); } @Test public void connectionReuseWhenResponseBodyConsumed_Async() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); Request request = new Request.Builder().url(server.url("/a")).build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { throw new AssertionError(); } @Override public void onResponse(Call call, Response response) throws IOException { InputStream bytes = response.body().byteStream(); assertEquals('a', bytes.read()); assertEquals('b', bytes.read()); assertEquals('c', bytes.read()); // This request will share a connection with 'A' cause it's all done. client.newCall(new Request.Builder().url(server.url("/b")).build()).enqueue(callback); } }); callback.await(server.url("/b")).assertCode(200).assertBody("def"); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reuse! } @Test public void timeoutsUpdatedOnReusedConnections() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def").throttleBody(1, 750, TimeUnit.MILLISECONDS)); // First request: time out after 1000ms. client = client.newBuilder() .readTimeout(1000, TimeUnit.MILLISECONDS) .build(); executeSynchronously("/a").assertBody("abc"); // Second request: time out after 250ms. client = client.newBuilder() .readTimeout(250, TimeUnit.MILLISECONDS) .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()); // The second byte of this request will be delayed by 750ms so we should time out after 250ms. long startNanos = System.nanoTime(); try { bodySource.readByte(); fail(); } catch (IOException expected) { // Timed out as expected. long elapsedNanos = System.nanoTime() - startNanos; long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos); assertTrue(Util.format("Timed out: %sms", elapsedMillis), elapsedMillis < 500); } finally { bodySource.close(); } } /** https://github.com/square/okhttp/issues/442 */ @Test public void tlsTimeoutsNotRetried() throws Exception { enableTls(); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.NO_RESPONSE)); server.enqueue(new MockResponse() .setBody("unreachable!")); client = client.newBuilder() .readTimeout(100, TimeUnit.MILLISECONDS) .build(); Request request = new Request.Builder().url(server.url("/")).build(); try { // If this succeeds, too many requests were made. client.newCall(request).execute(); fail(); } catch (InterruptedIOException expected) { } } /** * Make a request with two routes. The first route will time out because it's connecting to a * special address that never connects. The automatic retry will succeed. */ @Test public void connectTimeoutsAttemptsAlternateRoute() throws Exception { InetSocketAddress unreachableAddress = new InetSocketAddress("10.255.255.1", 8080); RecordingProxySelector proxySelector = new RecordingProxySelector(); proxySelector.proxies.add(new Proxy(Proxy.Type.HTTP, unreachableAddress)); proxySelector.proxies.add(server.toProxyAddress()); server.enqueue(new MockResponse() .setBody("success!")); client = client.newBuilder() .proxySelector(proxySelector) .readTimeout(100, TimeUnit.MILLISECONDS) .connectTimeout(100, TimeUnit.MILLISECONDS) .build(); Request request = new Request.Builder().url("http://android.com/").build(); executeSynchronously(request) .assertCode(200) .assertBody("success!"); } /** * Make a request with two routes. The first route will fail because the null server connects but * never responds. The manual retry will succeed. */ @Test public void readTimeoutFails() throws Exception { InetSocketAddress nullServerAddress = startNullServer(); RecordingProxySelector proxySelector = new RecordingProxySelector(); proxySelector.proxies.add(new Proxy(Proxy.Type.HTTP, nullServerAddress)); proxySelector.proxies.add(server.toProxyAddress()); server.enqueue(new MockResponse() .setBody("success!")); client = client.newBuilder() .proxySelector(proxySelector) .readTimeout(100, TimeUnit.MILLISECONDS) .build(); Request request = new Request.Builder().url("http://android.com/").build(); executeSynchronously(request) .assertFailure(SocketTimeoutException.class); executeSynchronously(request) .assertCode(200) .assertBody("success!"); } /** https://github.com/square/okhttp/issues/1801 */ @Test public void asyncCallEngineInitialized() throws Exception { OkHttpClient c = defaultClient().newBuilder() .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { throw new IOException(); } }) .build(); Request request = new Request.Builder().url(server.url("/")).build(); c.newCall(request).enqueue(callback); RecordedResponse response = callback.await(request.url()); assertEquals(request, response.request); } @Test public void reusedSinksGetIndependentTimeoutInstances() throws Exception { server.enqueue(new MockResponse()); server.enqueue(new MockResponse()); // Call 1: set a deadline on the request body. RequestBody requestBody1 = new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain"); } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("abc"); sink.timeout().deadline(5, TimeUnit.SECONDS); } }; Request request1 = new Request.Builder() .url(server.url("/")) .method("POST", requestBody1) .build(); Response response1 = client.newCall(request1).execute(); assertEquals(200, response1.code()); // Call 2: check for the absence of a deadline on the request body. RequestBody requestBody2 = new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain"); } @Override public void writeTo(BufferedSink sink) throws IOException { assertFalse(sink.timeout().hasDeadline()); sink.writeUtf8("def"); } }; Request request2 = new Request.Builder() .url(server.url("/")) .method("POST", requestBody2) .build(); Response response2 = client.newCall(request2).execute(); assertEquals(200, response2.code()); // Use sequence numbers to confirm the connection was pooled. assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void reusedSourcesGetIndependentTimeoutInstances() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); // Call 1: set a deadline on the response body. 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.url("/")).build(); Response response2 = client.newCall(request2).execute(); BufferedSource body2 = response2.body().source(); assertEquals("def", body2.readUtf8()); assertFalse(body2.timeout().hasDeadline()); // Use sequence numbers to confirm the connection was pooled. assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void tls() throws Exception { enableTls(); server.enqueue(new MockResponse() .setBody("abc") .addHeader("Content-Type: text/plain")); executeSynchronously("/").assertHandshake(); } @Test public void tls_Async() throws Exception { enableTls(); server.enqueue(new MockResponse() .setBody("abc") .addHeader("Content-Type: text/plain")); Request request = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request).enqueue(callback); callback.await(request.url()).assertHandshake(); } @Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception { server.enqueue(new MockResponse().setBody("seed connection pool")); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); server.enqueue(new MockResponse().setBody("retry success")); client = client.newBuilder() .dns(new DoubleInetAddressDns()) .build(); assertTrue(client.retryOnConnectionFailure()); executeSynchronously("/").assertBody("seed connection pool"); executeSynchronously("/").assertBody("retry success"); } @Test public void recoverWhenRetryOnConnectionFailureIsTrue_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); recoverWhenRetryOnConnectionFailureIsTrue(); } @Test public void noRecoverWhenRetryOnConnectionFailureIsFalse() throws Exception { server.enqueue(new MockResponse().setBody("seed connection pool")); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); server.enqueue(new MockResponse().setBody("unreachable!")); client = client.newBuilder() .dns(new DoubleInetAddressDns()) .retryOnConnectionFailure(false) .build(); executeSynchronously("/").assertBody("seed connection pool"); // If this succeeds, too many requests were made. executeSynchronously("/") .assertFailure(IOException.class) .assertFailureMatches("stream was reset: CANCEL", "unexpected end of stream on Connection.*" + server.getHostName() + ":" + server.getPort() + ".*"); } @Test public void recoverWhenRetryOnConnectionFailureIsFalse_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); noRecoverWhenRetryOnConnectionFailureIsFalse(); } @Test public void tlsHandshakeFailure_noFallbackByDefault() throws Exception { server.useHttps(sslClient.socketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); server.enqueue(new MockResponse().setBody("response that will never be received")); RecordedResponse response = executeSynchronously("/"); response.assertFailure( SSLProtocolException.class, // RI response to the FAIL_HANDSHAKE SSLHandshakeException.class // Android's response to the FAIL_HANDSHAKE ); assertFalse(client.connectionSpecs().contains(ConnectionSpec.COMPATIBLE_TLS)); } @Test public void recoverFromTlsHandshakeFailure() throws Exception { server.useHttps(sslClient.socketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); server.enqueue(new MockResponse().setBody("abc")); client = client.newBuilder() .hostnameVerifier(new RecordingHostnameVerifier()) .dns(new SingleInetAddressDns()) // opt-in to fallback to COMPATIBLE_TLS .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .sslSocketFactory(suppressTlsFallbackClientSocketFactory(), sslClient.trustManager) .build(); executeSynchronously("/").assertBody("abc"); } @Test public void recoverFromTlsHandshakeFailure_tlsFallbackScsvEnabled() throws Exception { final String tlsFallbackScsv = "TLS_FALLBACK_SCSV"; List<String> supportedCiphers = Arrays.asList(sslClient.socketFactory.getSupportedCipherSuites()); if (!supportedCiphers.contains(tlsFallbackScsv)) { // This only works if the client socket supports TLS_FALLBACK_SCSV. return; } server.useHttps(sslClient.socketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); RecordingSSLSocketFactory clientSocketFactory = new RecordingSSLSocketFactory(sslClient.socketFactory); client = client.newBuilder() .sslSocketFactory(clientSocketFactory, sslClient.trustManager) // opt-in to fallback to COMPATIBLE_TLS .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .hostnameVerifier(new RecordingHostnameVerifier()) .dns(new SingleInetAddressDns()) .build(); Request request = new Request.Builder().url(server.url("/")).build(); try { client.newCall(request).execute(); fail(); } catch (SSLHandshakeException expected) { } List<SSLSocket> clientSockets = clientSocketFactory.getSocketsCreated(); SSLSocket firstSocket = clientSockets.get(0); assertFalse(Arrays.asList(firstSocket.getEnabledCipherSuites()).contains(tlsFallbackScsv)); SSLSocket secondSocket = clientSockets.get(1); assertTrue(Arrays.asList(secondSocket.getEnabledCipherSuites()).contains(tlsFallbackScsv)); } @Test public void recoverFromTlsHandshakeFailure_Async() throws Exception { server.useHttps(sslClient.socketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); server.enqueue(new MockResponse().setBody("abc")); client = client.newBuilder() .hostnameVerifier(new RecordingHostnameVerifier()) .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .sslSocketFactory(suppressTlsFallbackClientSocketFactory(), sslClient.trustManager) .build(); Request request = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request).enqueue(callback); callback.await(request.url()).assertBody("abc"); } @Test public void noRecoveryFromTlsHandshakeFailureWhenTlsFallbackIsDisabled() throws Exception { client = client.newBuilder() .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT)) .hostnameVerifier(new RecordingHostnameVerifier()) .dns(new SingleInetAddressDns()) .sslSocketFactory(suppressTlsFallbackClientSocketFactory(), sslClient.trustManager) .build(); server.useHttps(sslClient.socketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); Request request = new Request.Builder().url(server.url("/")).build(); try { client.newCall(request).execute(); fail(); } catch (SSLProtocolException expected) { // RI response to the FAIL_HANDSHAKE } catch (SSLHandshakeException expected) { // Android's response to the FAIL_HANDSHAKE } } @Test public void cleartextCallsFailWhenCleartextIsDisabled() throws Exception { // Configure the client with only TLS configurations. No cleartext! client = client.newBuilder() .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .build(); server.enqueue(new MockResponse()); Request request = new Request.Builder().url(server.url("/")).build(); try { client.newCall(request).execute(); fail(); } catch (UnknownServiceException expected) { assertEquals("CLEARTEXT communication not enabled for client", expected.getMessage()); } } @Test public void setFollowSslRedirectsFalse() throws Exception { enableTls(); server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: http://square.com")); client = client.newBuilder() .followSslRedirects(false) .build(); Request request = new Request.Builder().url(server.url("/")).build(); Response response = client.newCall(request).execute(); assertEquals(301, response.code()); response.body().close(); } @Test public void matchingPinnedCertificate() throws Exception { enableTls(); server.enqueue(new MockResponse()); server.enqueue(new MockResponse()); // Make a first request without certificate pinning. Use it to collect certificates to pin. 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.getHostName(), CertificatePinner.pin(certificate)); } response1.body().close(); // Make another request with certificate pinning. It should complete normally. client = client.newBuilder() .certificatePinner(certificatePinnerBuilder.build()) .build(); Request request2 = new Request.Builder().url(server.url("/")).build(); Response response2 = client.newCall(request2).execute(); assertNotSame(response2.handshake(), response1.handshake()); response2.body().close(); } @Test public void unmatchingPinnedCertificate() throws Exception { enableTls(); server.enqueue(new MockResponse()); // Pin publicobject.com's cert. client = client.newBuilder() .certificatePinner(new CertificatePinner.Builder() .add(server.getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=") .build()) .build(); // When we pin the wrong certificate, connectivity fails. Request request = new Request.Builder().url(server.url("/")).build(); try { client.newCall(request).execute(); fail(); } catch (SSLPeerUnverifiedException expected) { assertTrue(expected.getMessage().startsWith("Certificate pinning failure!")); } } @Test public void post_Async() throws Exception { server.enqueue(new MockResponse().setBody("abc")); Request request = new Request.Builder() .url(server.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "def")) .build(); client.newCall(request).enqueue(callback); callback.await(request.url()) .assertCode(200) .assertBody("abc"); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("def", recordedRequest.getBody().readUtf8()); assertEquals("3", recordedRequest.getHeader("Content-Length")); assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type")); } @Test public void postBodyRetransmittedOnFailureRecovery() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)); server.enqueue(new MockResponse().setBody("def")); // Seed the connection pool so we have something that can fail. 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.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "body!")) .build(); Response response2 = client.newCall(request2).execute(); assertEquals("def", response2.body().string()); RecordedRequest get = server.takeRequest(); assertEquals(0, get.getSequenceNumber()); RecordedRequest post1 = server.takeRequest(); assertEquals("body!", post1.getBody().readUtf8()); assertEquals(1, post1.getSequenceNumber()); RecordedRequest post2 = server.takeRequest(); assertEquals("body!", post2.getBody().readUtf8()); assertEquals(0, post2.getSequenceNumber()); } @Test public void postBodyRetransmittedOnFailureRecovery_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); postBodyRetransmittedOnFailureRecovery(); } @Test public void cacheHit() throws Exception { server.enqueue(new MockResponse() .addHeader("ETag: v1") .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Charset") .setBody("A")); client = client.newBuilder() .cache(cache) .build(); // Store a response in the cache. HttpUrl url = server.url("/"); long request1SentAt = System.currentTimeMillis(); executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertBody("A"); long request1ReceivedAt = System.currentTimeMillis(); assertNull(server.takeRequest().getHeader("If-None-Match")); // Hit that stored response. It's different, but Vary says it doesn't matter. Thread.sleep(10); // Make sure the timestamps are unique. RecordedResponse cacheHit = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8"); // Check the merged response. The request is the application's original request. cacheHit.assertCode(200) .assertBody("A") .assertHeaders(new Headers.Builder() .add("ETag", "v1") .add("Cache-Control", "max-age=60") .add("Vary", "Accept-Charset") .add("Content-Length", "1") .build()) .assertRequestUrl(url) .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt); // Check the cached response. Its request contains only the saved Vary headers. cacheHit.cacheResponse() .assertCode(200) .assertHeaders(new Headers.Builder() .add("ETag", "v1") .add("Cache-Control", "max-age=60") .add("Vary", "Accept-Charset") .add("Content-Length", "1") .build()) .assertRequestMethod("GET") .assertRequestUrl(url) .assertRequestHeader("Accept-Language") .assertRequestHeader("Accept-Charset", "UTF-8") .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt); cacheHit.assertNoNetworkResponse(); } @Test public void conditionalCacheHit() throws Exception { server.enqueue(new MockResponse() .addHeader("ETag: v1") .addHeader("Vary: Accept-Charset") .addHeader("Donut: a") .setBody("A")); server.enqueue(new MockResponse().clearHeaders() .addHeader("Donut: b") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); client = client.newBuilder() .cache(cache) .build(); // Store a response in the cache. long request1SentAt = System.currentTimeMillis(); executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertHeader("Donut", "a") .assertBody("A"); long request1ReceivedAt = System.currentTimeMillis(); assertNull(server.takeRequest().getHeader("If-None-Match")); // Hit that stored response. It's different, but Vary says it doesn't matter. Thread.sleep(10); // Make sure the timestamps are unique. long request2SentAt = System.currentTimeMillis(); RecordedResponse cacheHit = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8"); long request2ReceivedAt = System.currentTimeMillis(); assertEquals("v1", server.takeRequest().getHeader("If-None-Match")); // Check the merged response. The request is the application's original request. cacheHit.assertCode(200) .assertBody("A") .assertHeader("Donut", "b") .assertRequestUrl(server.url("/")) .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertRequestHeader("If-None-Match") // No If-None-Match on the user's request. .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt); // Check the cached response. Its request contains only the saved Vary headers. cacheHit.cacheResponse() .assertCode(200) .assertHeader("Donut", "a") .assertHeader("ETag", "v1") .assertRequestUrl(server.url("/")) .assertRequestHeader("Accept-Language") // No Vary on Accept-Language. .assertRequestHeader("Accept-Charset", "UTF-8") // Because of Vary on Accept-Charset. .assertRequestHeader("If-None-Match") // This wasn't present in the original request. .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt); // Check the network response. It has the caller's request, plus some caching headers. cacheHit.networkResponse() .assertCode(304) .assertHeader("Donut", "b") .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertRequestHeader("If-None-Match", "v1") // If-None-Match in the validation request. .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt); } @Test public void conditionalCacheHit_Async() throws Exception { server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1")); server.enqueue(new MockResponse() .clearHeaders() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); client = client.newBuilder() .cache(cache) .build(); Request request1 = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request1).enqueue(callback); callback.await(request1.url()).assertCode(200).assertBody("A"); assertNull(server.takeRequest().getHeader("If-None-Match")); Request request2 = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request2).enqueue(callback); callback.await(request2.url()).assertCode(200).assertBody("A"); assertEquals("v1", server.takeRequest().getHeader("If-None-Match")); } @Test public void conditionalCacheMiss() throws Exception { server.enqueue(new MockResponse() .addHeader("ETag: v1") .addHeader("Vary: Accept-Charset") .addHeader("Donut: a") .setBody("A")); server.enqueue(new MockResponse() .addHeader("Donut: b") .setBody("B")); client = client.newBuilder() .cache(cache) .build(); long request1SentAt = System.currentTimeMillis(); executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertBody("A"); long request1ReceivedAt = System.currentTimeMillis(); assertNull(server.takeRequest().getHeader("If-None-Match")); // Different request, but Vary says it doesn't matter. Thread.sleep(10); // Make sure the timestamps are unique. long request2SentAt = System.currentTimeMillis(); RecordedResponse cacheMiss = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8"); long request2ReceivedAt = System.currentTimeMillis(); assertEquals("v1", server.takeRequest().getHeader("If-None-Match")); // Check the user response. It has the application's original request. cacheMiss.assertCode(200) .assertBody("B") .assertHeader("Donut", "b") .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt); // Check the cache response. Even though it's a miss, we used the cache. cacheMiss.cacheResponse() .assertCode(200) .assertHeader("Donut", "a") .assertHeader("ETag", "v1") .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt); // Check the network response. It has the network request, plus caching headers. cacheMiss.networkResponse() .assertCode(200) .assertHeader("Donut", "b") .assertRequestHeader("If-None-Match", "v1") // If-None-Match in the validation request. .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt); } @Test public void conditionalCacheMiss_Async() throws Exception { server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1")); server.enqueue(new MockResponse().setBody("B")); client = client.newBuilder() .cache(cache) .build(); Request request1 = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request1).enqueue(callback); callback.await(request1.url()).assertCode(200).assertBody("A"); assertNull(server.takeRequest().getHeader("If-None-Match")); Request request2 = new Request.Builder() .url(server.url("/")) .build(); client.newCall(request2).enqueue(callback); callback.await(request2.url()).assertCode(200).assertBody("B"); assertEquals("v1", server.takeRequest().getHeader("If-None-Match")); } @Test public void onlyIfCachedReturns504WhenNotCached() throws Exception { executeSynchronously("/", "Cache-Control", "only-if-cached") .assertCode(504) .assertBody("") .assertNoNetworkResponse() .assertNoCacheResponse(); } @Test public void networkDropsOnConditionalGet() throws IOException { client = client.newBuilder() .cache(cache) .build(); // Seed the cache. server.enqueue(new MockResponse() .addHeader("ETag: v1") .setBody("A")); executeSynchronously("/") .assertCode(200) .assertBody("A"); // Attempt conditional cache validation and a DNS miss. client.connectionPool().evictAll(); client = client.newBuilder() .dns(new FakeDns()) .build(); executeSynchronously("/").assertFailure(UnknownHostException.class); } @Test public void redirect() throws Exception { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /b") .addHeader("Test", "Redirect from /a to /b") .setBody("/a has moved!")); server.enqueue(new MockResponse() .setResponseCode(302) .addHeader("Location: /c") .addHeader("Test", "Redirect from /b to /c") .setBody("/b has moved!")); server.enqueue(new MockResponse().setBody("C")); executeSynchronously("/a") .assertCode(200) .assertBody("C") .priorResponse() .assertCode(302) .assertHeader("Test", "Redirect from /b to /c") .priorResponse() .assertCode(301) .assertHeader("Test", "Redirect from /a to /b"); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused. assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again! } @Test public void postRedirectsToGet() throws Exception { server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /page2") .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("Page 2")); Response response = client.newCall(new Request.Builder() .url(server.url("/page1")) .post(RequestBody.create(MediaType.parse("text/plain"), "Request Body")) .build()).execute(); assertEquals("Page 2", response.body().string()); RecordedRequest page1 = server.takeRequest(); assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); assertEquals("Request Body", page1.getBody().readUtf8()); RecordedRequest page2 = server.takeRequest(); assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); } @Test public void getClientRequestTimeout() throws Exception { enqueueRequestTimeoutResponses(); Response response = client.newCall(new Request.Builder() .url(server.url("/")).build()).execute(); assertEquals("Body", response.body().string()); } private void enqueueRequestTimeoutResponses() { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END) .setResponseCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT) .setHeader("Connection", "Close") .setBody("You took too long!")); server.enqueue(new MockResponse().setBody("Body")); } @Test public void requestBodyRetransmittedOnClientRequestTimeout() throws Exception { enqueueRequestTimeoutResponses(); Response response = client.newCall(new Request.Builder() .url(server.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "Hello")) .build()).execute(); assertEquals("Body", response.body().string()); RecordedRequest request1 = server.takeRequest(); assertEquals("Hello", request1.getBody().readUtf8()); RecordedRequest request2 = server.takeRequest(); assertEquals("Hello", request2.getBody().readUtf8()); } @Test public void propfindRedirectsToPropfindAndMaintainsRequestBody() throws Exception { // given server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /page2") .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("Page 2")); // when Response response = client.newCall(new Request.Builder() .url(server.url("/page1")) .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), "Request Body")) .build()).execute(); // then assertEquals("Page 2", response.body().string()); RecordedRequest page1 = server.takeRequest(); assertEquals("PROPFIND /page1 HTTP/1.1", page1.getRequestLine()); assertEquals("Request Body", page1.getBody().readUtf8()); RecordedRequest page2 = server.takeRequest(); assertEquals("PROPFIND /page2 HTTP/1.1", page2.getRequestLine()); assertEquals("Request Body", page2.getBody().readUtf8()); } @Test public void responseCookies() throws Exception { server.enqueue(new MockResponse() .addHeader("Set-Cookie", "a=b; Expires=Thu, 01 Jan 1970 00:00:00 GMT") .addHeader("Set-Cookie", "c=d; Expires=Fri, 02 Jan 1970 23:59:59 GMT; path=/bar; secure")); RecordingCookieJar cookieJar = new RecordingCookieJar(); client = client.newBuilder() .cookieJar(cookieJar) .build(); executeSynchronously("/").assertCode(200); List<Cookie> responseCookies = cookieJar.takeResponseCookies(); assertEquals(2, responseCookies.size()); assertEquals("a=b; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", responseCookies.get(0).toString()); assertEquals("c=d; expires=Fri, 02 Jan 1970 23:59:59 GMT; path=/bar; secure", responseCookies.get(1).toString()); } @Test public void requestCookies() throws Exception { server.enqueue(new MockResponse()); RecordingCookieJar cookieJar = new RecordingCookieJar(); cookieJar.enqueueRequestCookies( new Cookie.Builder().name("a").value("b").domain(server.getHostName()).build(), new Cookie.Builder().name("c").value("d").domain(server.getHostName()).build()); client = client.newBuilder() .cookieJar(cookieJar) .build(); executeSynchronously("/").assertCode(200); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("a=b; c=d", recordedRequest.getHeader("Cookie")); } @Test public void redirectsDoNotIncludeTooManyCookies() throws Exception { server2.enqueue(new MockResponse().setBody("Page 2")); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: " + server2.url("/"))); CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER); HttpCookie cookie = new HttpCookie("c", "cookie"); cookie.setDomain(server.getHostName()); cookie.setPath("/"); String portList = Integer.toString(server.getPort()); cookie.setPortlist(portList); cookieManager.getCookieStore().add(server.url("/").uri(), cookie); client = client.newBuilder() .cookieJar(new JavaNetCookieJar(cookieManager)) .build(); Response response = client.newCall(new Request.Builder() .url(server.url("/page1")) .build()).execute(); assertEquals("Page 2", response.body().string()); RecordedRequest request1 = server.takeRequest(); assertEquals("c=cookie", request1.getHeader("Cookie")); RecordedRequest request2 = server2.takeRequest(); assertNull(request2.getHeader("Cookie")); } @Test public void redirectsDoNotIncludeTooManyAuthHeaders() throws Exception { server2.enqueue(new MockResponse().setBody("Page 2")); server.enqueue(new MockResponse() .setResponseCode(401)); server.enqueue(new MockResponse() .setResponseCode(302) .addHeader("Location: " + server2.url("/b"))); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator(Credentials.basic("jesse", "secret"))) .build(); Request request = new Request.Builder().url(server.url("/a")).build(); Response response = client.newCall(request).execute(); assertEquals("Page 2", response.body().string()); RecordedRequest redirectRequest = server2.takeRequest(); assertNull(redirectRequest.getHeader("Authorization")); assertEquals("/b", redirectRequest.getPath()); } @Test public void redirect_Async() throws Exception { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /b") .addHeader("Test", "Redirect from /a to /b") .setBody("/a has moved!")); server.enqueue(new MockResponse() .setResponseCode(302) .addHeader("Location: /c") .addHeader("Test", "Redirect from /b to /c") .setBody("/b has moved!")); server.enqueue(new MockResponse().setBody("C")); Request request = new Request.Builder().url(server.url("/a")).build(); client.newCall(request).enqueue(callback); callback.await(server.url("/a")) .assertCode(200) .assertBody("C") .priorResponse() .assertCode(302) .assertHeader("Test", "Redirect from /b to /c") .priorResponse() .assertCode(301) .assertHeader("Test", "Redirect from /a to /b"); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused. assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again! } @Test public void follow20Redirects() throws Exception { for (int i = 0; i < 20; i++) { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /" + (i + 1)) .setBody("Redirecting to /" + (i + 1))); } server.enqueue(new MockResponse().setBody("Success!")); executeSynchronously("/0") .assertCode(200) .assertBody("Success!"); } @Test public void follow20Redirects_Async() throws Exception { for (int i = 0; i < 20; i++) { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /" + (i + 1)) .setBody("Redirecting to /" + (i + 1))); } server.enqueue(new MockResponse().setBody("Success!")); Request request = new Request.Builder().url(server.url("/0")).build(); client.newCall(request).enqueue(callback); callback.await(server.url("/0")) .assertCode(200) .assertBody("Success!"); } @Test public void doesNotFollow21Redirects() throws Exception { for (int i = 0; i < 21; i++) { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /" + (i + 1)) .setBody("Redirecting to /" + (i + 1))); } try { client.newCall(new Request.Builder().url(server.url("/0")).build()).execute(); fail(); } catch (IOException expected) { assertEquals("Too many follow-up requests: 21", expected.getMessage()); } } @Test public void doesNotFollow21Redirects_Async() throws Exception { for (int i = 0; i < 21; i++) { server.enqueue(new MockResponse() .setResponseCode(301) .addHeader("Location: /" + (i + 1)) .setBody("Redirecting to /" + (i + 1))); } Request request = new Request.Builder().url(server.url("/0")).build(); client.newCall(request).enqueue(callback); callback.await(server.url("/0")).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.")); executeSynchronously("/") .assertFailure("HTTP 204 had non-zero Content-Length: 39"); } @Test public void http205WithBodyDisallowed() throws IOException { server.enqueue(new MockResponse() .setResponseCode(205) .setBody("I'm not even supposed to be here today.")); executeSynchronously("/") .assertFailure("HTTP 205 had non-zero Content-Length: 39"); } @Test public void canceledBeforeExecute() throws Exception { Call call = client.newCall(new Request.Builder().url(server.url("/a")).build()); call.cancel(); try { call.execute(); fail(); } catch (IOException expected) { } 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 cancelImmediatelyAfterEnqueue() throws Exception { server.enqueue(new MockResponse()); Call call = client.newCall(new Request.Builder() .url(server.url("/a")) .build()); call.enqueue(callback); call.cancel(); callback.await(server.url("/a")).assertFailure("Canceled", "Socket closed"); } @Test public void cancelAll() throws Exception { Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); call.enqueue(callback); client.dispatcher().cancelAll(); callback.await(server.url("/")).assertFailure("Canceled", "Socket closed"); } @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.url("/a")).build()); ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Response> result = executor.submit(new Callable<Response>() { @Override public Response call() throws Exception { return call.execute(); } }); Thread.sleep(100); // wait for it to go in flight. call.cancel(); try { result.get().body().bytes(); fail(); } catch (IOException expected) { } assertEquals(1, server.getRequestCount()); } @Test public void cancelInFlightBeforeResponseReadThrowsIOE() throws Exception { Request request = new Request.Builder().url(server.url("/a")).build(); final Call call = client.newCall(request); server.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { call.cancel(); return new MockResponse().setBody("A"); } }); try { call.execute(); fail(); } catch (IOException expected) { } } @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTPS() throws Exception { enableTls(); cancelInFlightBeforeResponseReadThrowsIOE(); } @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); cancelInFlightBeforeResponseReadThrowsIOE(); } /** * This test puts a request in front of one that is to be canceled, so that it is canceled before * I/O takes place. */ @Test public void canceledBeforeIOSignalsOnFailure() throws Exception { // Force requests to be executed serially. okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher(client.dispatcher().executorService()); dispatcher.setMaxRequests(1); client = client.newBuilder() .dispatcher(dispatcher) .build(); Request requestA = new Request.Builder().url(server.url("/a")).build(); Request requestB = new Request.Builder().url(server.url("/b")).build(); final Call callA = client.newCall(requestA); final Call callB = client.newCall(requestB); server.setDispatcher(new Dispatcher() { char nextResponse = 'A'; @Override public MockResponse dispatch(RecordedRequest request) { callB.cancel(); return new MockResponse().setBody(Character.toString(nextResponse++)); } }); callA.enqueue(callback); callB.enqueue(callback); assertEquals("/a", server.takeRequest().getPath()); callback.await(requestA.url()).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", "Socket closed"); } @Test public void canceledBeforeIOSignalsOnFailure_HTTPS() throws Exception { enableTls(); canceledBeforeIOSignalsOnFailure(); } @Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); canceledBeforeIOSignalsOnFailure(); } @Test public void canceledBeforeResponseReadSignalsOnFailure() throws Exception { Request requestA = new Request.Builder().url(server.url("/a")).build(); final Call call = client.newCall(requestA); server.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { call.cancel(); return new MockResponse().setBody("A"); } }); call.enqueue(callback); assertEquals("/a", server.takeRequest().getPath()); callback.await(requestA.url()).assertFailure("Canceled", "stream was reset: CANCEL", "Socket closed"); } @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTPS() throws Exception { enableTls(); canceledBeforeResponseReadSignalsOnFailure(); } @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); canceledBeforeResponseReadSignalsOnFailure(); } /** * There's a race condition where the cancel may apply after the stream has already been * processed. */ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce() throws Exception { server.enqueue(new MockResponse().setBody("A")); final CountDownLatch latch = new CountDownLatch(1); final AtomicReference<String> bodyRef = new AtomicReference<>(); final AtomicBoolean failureRef = new AtomicBoolean(); Request request = new Request.Builder().url(server.url("/a")).build(); final Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { failureRef.set(true); latch.countDown(); } @Override public void onResponse(Call call, Response response) throws IOException { call.cancel(); try { bodyRef.set(response.body().string()); } catch (IOException e) { // It is ok if this broke the stream. bodyRef.set("A"); throw e; // We expect to not loop into onFailure in this case. } finally { latch.countDown(); } } }); latch.await(); assertEquals("A", bodyRef.get()); assertFalse(failureRef.get()); } @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTPS() throws Exception { enableTls(); canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce(); } @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2() throws Exception { enableProtocol(Protocol.HTTP_2); canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce(); } @Test public void cancelWithInterceptor() throws Exception { client = client.newBuilder() .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { chain.proceed(chain.request()); throw new AssertionError(); // We expect an exception. } }).build(); Call call = client.newCall(new Request.Builder().url(server.url("/a")).build()); call.cancel(); try { call.execute(); fail(); } catch (IOException expected) { } assertEquals(0, server.getRequestCount()); } @Test public void gzip() throws Exception { Buffer gzippedBody = gzip("abcabcabc"); String bodySize = Long.toString(gzippedBody.size()); server.enqueue(new MockResponse() .setBody(gzippedBody) .addHeader("Content-Encoding: gzip")); // Confirm that the user request doesn't have Accept-Encoding, and the user // response doesn't have a Content-Encoding or Content-Length. RecordedResponse userResponse = executeSynchronously("/"); userResponse.assertCode(200) .assertRequestHeader("Accept-Encoding") .assertHeader("Content-Encoding") .assertHeader("Content-Length") .assertBody("abcabcabc"); // But the network request doesn't lie. OkHttp used gzip for this call. userResponse.networkResponse() .assertHeader("Content-Encoding", "gzip") .assertHeader("Content-Length", bodySize) .assertRequestHeader("Accept-Encoding", "gzip"); } /** https://github.com/square/okhttp/issues/1927 */ @Test public void gzipResponseAfterAuthenticationChallenge() throws Exception { server.enqueue(new MockResponse() .setResponseCode(401)); server.enqueue(new MockResponse() .setBody(gzip("abcabcabc")) .addHeader("Content-Encoding: gzip")); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator("password")) .build(); executeSynchronously("/").assertBody("abcabcabc"); } @Test public void rangeHeaderPreventsAutomaticGzip() throws Exception { Buffer gzippedBody = gzip("abcabcabc"); // Enqueue a gzipped response. Our request isn't expecting it, but that's okay. server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_PARTIAL) .setBody(gzippedBody) .addHeader("Content-Encoding: gzip") .addHeader("Content-Range: bytes 0-" + (gzippedBody.size() - 1))); // Make a range request. Request request = new Request.Builder() .url(server.url("/")) .header("Range", "bytes=0-") .build(); Call call = client.newCall(request); // The response is not decompressed. Response response = call.execute(); assertEquals("gzip", response.header("Content-Encoding")); assertEquals(gzippedBody.snapshot(), response.body().source().readByteString()); // The request did not offer gzip support. RecordedRequest recordedRequest = server.takeRequest(); assertNull(recordedRequest.getHeader("Accept-Encoding")); } @Test public void asyncResponseCanBeConsumedLater() throws Exception { server.enqueue(new MockResponse().setBody("abc")); server.enqueue(new MockResponse().setBody("def")); Request request = new Request.Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build(); final BlockingQueue<Response> responseRef = new SynchronousQueue<>(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { throw new AssertionError(); } @Override public void onResponse(Call call, Response response) throws IOException { try { responseRef.put(response); } catch (InterruptedException e) { throw new AssertionError(); } } }); Response response = responseRef.take(); assertEquals(200, response.code()); assertEquals("abc", response.body().string()); // Make another request just to confirm that that connection can be reused... executeSynchronously("/").assertBody("def"); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused. // ... even before we close the response body! response.body().close(); } @Test public void userAgentIsIncludedByDefault() throws Exception { server.enqueue(new MockResponse()); executeSynchronously("/"); RecordedRequest recordedRequest = server.takeRequest(); assertTrue(recordedRequest.getHeader("User-Agent") .matches(Version.userAgent())); } @Test public void setFollowRedirectsFalse() throws Exception { server.enqueue(new MockResponse() .setResponseCode(302) .addHeader("Location: /b") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); client = client.newBuilder() .followRedirects(false) .build(); executeSynchronously("/a") .assertBody("A") .assertCode(302); } @Test public void expect100ContinueNonEmptyRequestBody() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.EXPECT_CONTINUE)); Request request = new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build(); executeSynchronously(request) .assertCode(200) .assertSuccessful(); assertEquals("abc", server.takeRequest().getBody().readUtf8()); } @Test public void expect100ContinueEmptyRequestBody() throws Exception { server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "")) .build(); executeSynchronously(request) .assertCode(200) .assertSuccessful(); } @Test public void expect100ContinueEmptyRequestBody_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); expect100ContinueEmptyRequestBody(); } @Test public void expect100ContinueTimesOutWithoutContinue() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.NO_RESPONSE)); client = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build(); Request request = new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build(); Call call = client.newCall(request); try { call.execute(); fail(); } catch (SocketTimeoutException expected) { } RecordedRequest recordedRequest = server.takeRequest(); assertEquals("", recordedRequest.getBody().readUtf8()); } @Test public void expect100ContinueTimesOutWithoutContinue_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); expect100ContinueTimesOutWithoutContinue(); } @Test public void serverRespondsWithUnsolicited100Continue() throws Exception { server.enqueue(new MockResponse() .setStatus("HTTP/1.1 100 Continue")); Request request = new Request.Builder() .url(server.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build(); Call call = client.newCall(request); Response response = call.execute(); assertEquals(100, response.code()); assertEquals("Continue", response.message()); assertEquals("", response.body().string()); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("abc", recordedRequest.getBody().readUtf8()); } @Test public void serverRespondsWithUnsolicited100Continue_HTTP2() throws Exception { enableProtocol(Protocol.HTTP_2); serverRespondsWithUnsolicited100Continue(); } @Test public void successfulExpectContinuePermitsConnectionReuse() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.EXPECT_CONTINUE)); server.enqueue(new MockResponse()); executeSynchronously(new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build()); executeSynchronously(new Request.Builder() .url(server.url("/")) .build()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void successfulExpectContinuePermitsConnectionReuseWithHttp2() throws Exception { enableProtocol(Protocol.HTTP_2); successfulExpectContinuePermitsConnectionReuse(); } @Test public void unsuccessfulExpectContinuePreventsConnectionReuse() throws Exception { server.enqueue(new MockResponse()); server.enqueue(new MockResponse()); executeSynchronously(new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build()); executeSynchronously(new Request.Builder() .url(server.url("/")) .build()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(0, server.takeRequest().getSequenceNumber()); } @Test public void unsuccessfulExpectContinuePermitsConnectionReuseWithHttp2() throws Exception { enableProtocol(Protocol.HTTP_2); server.enqueue(new MockResponse()); server.enqueue(new MockResponse()); executeSynchronously(new Request.Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build()); executeSynchronously(new Request.Builder() .url(server.url("/")) .build()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } /** 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)); executeSynchronously("/") .assertHeader("a\tb", "c\u007fd") .assertHeader("\ud83c\udf69", "\u2615\ufe0f") .assertHeader("", "ef"); } @Test public void customDns() throws Exception { // Configure a DNS that returns our local MockWebServer for android.com. FakeDns dns = new FakeDns(); dns.set("android.com", Dns.SYSTEM.lookup(server.url("/").host())); client = client.newBuilder() .dns(dns) .build(); server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/").newBuilder().host("android.com").build()) .build(); executeSynchronously(request).assertCode(200); dns.assertRequests("android.com"); } @Test public void dnsReturnsZeroIpAddresses() throws Exception { // Configure a DNS that returns our local MockWebServer for android.com. FakeDns dns = new FakeDns(); List<InetAddress> ipAddresses = new ArrayList<>(); dns.set("android.com", ipAddresses); client = client.newBuilder() .dns(dns) .build(); server.enqueue(new MockResponse()); Request request = new Request.Builder() .url(server.url("/").newBuilder().host("android.com").build()) .build(); executeSynchronously(request).assertFailure(dns + " returned no addresses for android.com"); dns.assertRequests("android.com"); } /** We had a bug where failed HTTP/2 calls could break the entire connection. */ @Test public void failingCallsDoNotInterfereWithConnection() throws Exception { enableProtocol(Protocol.HTTP_2); server.enqueue(new MockResponse().setBody("Response 1")); server.enqueue(new MockResponse().setBody("Response 2")); RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return null; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("abc"); sink.flush(); makeFailingCall(); sink.writeUtf8("def"); sink.flush(); } }; Call call = client.newCall(new Request.Builder() .url(server.url("/")) .post(requestBody) .build()); assertEquals("Response 1", call.execute().body().string()); } /** Test which headers are sent unencrypted to the HTTP proxy. */ @Test public void proxyConnectOmitsApplicationHeaders() throws Exception { server.useHttps(sslClient.socketFactory, true); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse() .setBody("encrypted response from the origin server")); RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .proxy(server.toProxyAddress()) .hostnameVerifier(hostnameVerifier) .build(); Request request = new Request.Builder() .url("https://android.com/foo") .header("Private", "Secret") .header("User-Agent", "App 1.0") .build(); Response response = client.newCall(request).execute(); assertEquals("encrypted response from the origin server", response.body().string()); RecordedRequest connect = server.takeRequest(); assertNull(connect.getHeader("Private")); assertEquals(Version.userAgent(), connect.getHeader("User-Agent")); assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection")); assertEquals("android.com:443", connect.getHeader("Host")); RecordedRequest get = server.takeRequest(); assertEquals("Secret", get.getHeader("Private")); assertEquals("App 1.0", get.getHeader("User-Agent")); assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); } /** Respond to a proxy authorization challenge. */ @Test public void proxyAuthenticateOnConnect() throws Exception { server.useHttps(sslClient.socketFactory, 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("response body")); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .proxy(server.toProxyAddress()) .proxyAuthenticator(new RecordingOkAuthenticator("password")) .hostnameVerifier(new RecordingHostnameVerifier()) .build(); Request request = new Request.Builder() .url("https://android.com/foo") .build(); Response response = client.newCall(request).execute(); assertEquals("response body", response.body().string()); RecordedRequest connect1 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); assertNull(connect1.getHeader("Proxy-Authorization")); RecordedRequest connect2 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); assertEquals("password", connect2.getHeader("Proxy-Authorization")); RecordedRequest get = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); assertNull(get.getHeader("Proxy-Authorization")); } /** Confirm that the proxy authenticator works for unencrypted HTTP proxies. */ @Test public void httpProxyAuthenticate() throws Exception { server.enqueue(new MockResponse() .setResponseCode(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); server.enqueue(new MockResponse() .setBody("response body")); client = client.newBuilder() .proxy(server.toProxyAddress()) .proxyAuthenticator(new RecordingOkAuthenticator("password")) .build(); Request request = new Request.Builder() .url("http://android.com/foo") .build(); Response response = client.newCall(request).execute(); assertEquals("response body", response.body().string()); RecordedRequest get1 = server.takeRequest(); assertEquals("GET http://android.com/foo HTTP/1.1", get1.getRequestLine()); assertNull(get1.getHeader("Proxy-Authorization")); RecordedRequest get2 = server.takeRequest(); assertEquals("GET http://android.com/foo HTTP/1.1", get2.getRequestLine()); assertEquals("password", get2.getHeader("Proxy-Authorization")); } /** * OkHttp has a bug where a `Connection: close` response header is not honored when establishing a * TLS tunnel. https://github.com/square/okhttp/issues/2426 */ @Test public void proxyAuthenticateOnConnectWithConnectionClose() throws Exception { server.useHttps(sslClient.socketFactory, true); server.setProtocols(Collections.singletonList(Protocol.HTTP_1_1)); server.enqueue(new MockResponse() .setResponseCode(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"") .addHeader("Connection: close")); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse() .setBody("response body")); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .proxy(server.toProxyAddress()) .proxyAuthenticator(new RecordingOkAuthenticator("password")) .hostnameVerifier(new RecordingHostnameVerifier()) .build(); Request request = new Request.Builder() .url("https://android.com/foo") .build(); Response response = client.newCall(request).execute(); assertEquals("response body", response.body().string()); // First CONNECT call needs a new connection. assertEquals(0, server.takeRequest().getSequenceNumber()); // Second CONNECT call needs a new connection. assertEquals(0, server.takeRequest().getSequenceNumber()); // GET reuses the connection from the second connect. assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void tooManyProxyAuthFailuresWithConnectionClose() throws IOException { server.useHttps(sslClient.socketFactory, true); server.setProtocols(Collections.singletonList(Protocol.HTTP_1_1)); for (int i = 0; i < 21; i++) { server.enqueue(new MockResponse() .setResponseCode(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"") .addHeader("Connection: close")); } client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .proxy(server.toProxyAddress()) .proxyAuthenticator(new RecordingOkAuthenticator("password")) .hostnameVerifier(new RecordingHostnameVerifier()) .build(); Request request = new Request.Builder() .url("https://android.com/foo") .build(); try { client.newCall(request).execute(); fail(); } catch (ProtocolException expected) { } } /** * Confirm that we don't send the Proxy-Authorization header from the request to the proxy server. * We used to have that behavior but it is problematic because unrelated requests end up sharing * credentials. Worse, that approach leaks proxy credentials to the origin server. */ @Test public void noProactiveProxyAuthorization() throws Exception { server.useHttps(sslClient.socketFactory, true); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse() .setBody("response body")); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .proxy(server.toProxyAddress()) .hostnameVerifier(new RecordingHostnameVerifier()) .build(); Request request = new Request.Builder() .url("https://android.com/foo") .header("Proxy-Authorization", "password") .build(); Response response = client.newCall(request).execute(); assertEquals("response body", response.body().string()); RecordedRequest connect = server.takeRequest(); assertNull(connect.getHeader("Proxy-Authorization")); RecordedRequest get = server.takeRequest(); assertEquals("password", get.getHeader("Proxy-Authorization")); } @Test public void interceptorGetsHttp2() throws Exception { enableProtocol(Protocol.HTTP_2); // Capture the protocol as it is observed by the interceptor. final AtomicReference<Protocol> protocolRef = new AtomicReference<>(); Interceptor interceptor = new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { protocolRef.set(chain.connection().protocol()); return chain.proceed(chain.request()); } }; client = client.newBuilder() .addNetworkInterceptor(interceptor) .build(); // Make an HTTP/2 request and confirm that the protocol matches. server.enqueue(new MockResponse()); executeSynchronously("/"); assertEquals(Protocol.HTTP_2, protocolRef.get()); } @Test public void serverSendsInvalidResponseHeaders() throws Exception { server.enqueue(new MockResponse() .setStatus("HTP/1.1 200 OK")); executeSynchronously("/") .assertFailure("Unexpected status line: HTP/1.1 200 OK"); } @Test public void serverSendsInvalidCodeTooLarge() throws Exception { server.enqueue(new MockResponse() .setStatus("HTTP/1.1 2147483648 OK")); executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 2147483648 OK"); } @Test public void serverSendsInvalidCodeNotANumber() throws Exception { server.enqueue(new MockResponse() .setStatus("HTTP/1.1 00a OK")); executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 00a OK"); } @Test public void serverSendsUnnecessaryWhitespace() throws Exception { server.enqueue(new MockResponse() .setStatus(" HTTP/1.1 200 OK")); executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 200 OK"); } @Test public void requestHeaderNameWithSpaceForbidden() throws Exception { try { new Request.Builder().addHeader("a b", "c"); fail(); } catch (IllegalArgumentException expected) { assertEquals("Unexpected char 0x20 at 1 in header name: a b", expected.getMessage()); } } @Test public void requestHeaderNameWithTabForbidden() throws Exception { try { new Request.Builder().addHeader("a\tb", "c"); fail(); } catch (IllegalArgumentException expected) { assertEquals("Unexpected char 0x09 at 1 in header name: a\tb", expected.getMessage()); } } @Test public void responseHeaderNameWithSpacePermitted() throws Exception { server.enqueue(new MockResponse() .clearHeaders() .addHeader("content-length: 0") .addHeaderLenient("a b", "c")); Call call = client.newCall(new Request.Builder().url(server.url("/")).build()); Response response = call.execute(); assertEquals("c", response.header("a b")); } @Test public void responseHeaderNameWithTabPermitted() throws Exception { server.enqueue(new MockResponse() .clearHeaders() .addHeader("content-length: 0") .addHeaderLenient("a\tb", "c")); Call call = client.newCall(new Request.Builder().url(server.url("/")).build()); Response response = call.execute(); assertEquals("c", response.header("a\tb")); } @Test public void connectFails() throws Exception { server.shutdown(); executeSynchronously("/") .assertFailure(IOException.class); } @Test public void requestBodySurvivesRetries() throws Exception { server.enqueue(new MockResponse()); // Enable a misconfigured proxy selector to guarantee that the request is retried. client = client.newBuilder() .proxySelector(new FakeProxySelector() .addProxy(server2.toProxyAddress()) .addProxy(Proxy.NO_PROXY)) .build(); server2.shutdown(); Request request = new Request.Builder() .url(server.url("/")) .post(RequestBody.create(MediaType.parse("text/plain"), "abc")) .build(); executeSynchronously(request); assertEquals("abc", server.takeRequest().getBody().readUtf8()); } @Ignore // This may fail in DNS lookup, which we don't have timeouts for. @Test public void invalidHost() throws Exception { Request request = new Request.Builder() .url(HttpUrl.parse("http://1234.1.1.1/")) .build(); executeSynchronously(request) .assertFailure(UnknownHostException.class); } @Test public void uploadBodySmallChunkedEncoding() throws Exception { upload(true, 1048576, 256); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(1048576, recordedRequest.getBodySize()); assertFalse(recordedRequest.getChunkSizes().isEmpty()); } @Test public void uploadBodyLargeChunkedEncoding() throws Exception { upload(true, 1048576, 65536); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(1048576, recordedRequest.getBodySize()); assertFalse(recordedRequest.getChunkSizes().isEmpty()); } @Test public void uploadBodySmallFixedLength() throws Exception { upload(false, 1048576, 256); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(1048576, recordedRequest.getBodySize()); assertTrue(recordedRequest.getChunkSizes().isEmpty()); } @Test public void uploadBodyLargeFixedLength() throws Exception { upload(false, 1048576, 65536); RecordedRequest recordedRequest = server.takeRequest(); assertEquals(1048576, recordedRequest.getBodySize()); assertTrue(recordedRequest.getChunkSizes().isEmpty()); } private void upload( final boolean chunked, final int size, final int writeSize) throws Exception { server.enqueue(new MockResponse()); executeSynchronously(new Request.Builder() .url(server.url("/")) .post(requestBody(chunked, size, writeSize)) .build()); } /** https://github.com/square/okhttp/issues/2344 */ @Test public void ipv6HostHasSquareBraces() throws Exception { // Use a proxy to fake IPv6 connectivity, even if localhost doesn't have IPv6. server.useHttps(sslClient.socketFactory, true); server.setProtocols(Collections.singletonList(Protocol.HTTP_1_1)); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse() .setBody("response body")); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .hostnameVerifier(new RecordingHostnameVerifier()) .proxy(server.toProxyAddress()) .build(); Request request = new Request.Builder() .url("https://[::1]/") .build(); Response response = client.newCall(request).execute(); assertEquals("response body", response.body().string()); RecordedRequest connect = server.takeRequest(); assertEquals("CONNECT [::1]:443 HTTP/1.1", connect.getRequestLine()); assertEquals("[::1]:443", connect.getHeader("Host")); RecordedRequest get = server.takeRequest(); assertEquals("GET / HTTP/1.1", get.getRequestLine()); assertEquals("[::1]", get.getHeader("Host")); } private RequestBody requestBody(final boolean chunked, final long size, final int writeSize) { final byte[] buffer = new byte[writeSize]; Arrays.fill(buffer, (byte) 'x'); return new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain; charset=utf-8"); } @Override public long contentLength() throws IOException { return chunked ? -1L : size; } @Override public void writeTo(BufferedSink sink) throws IOException { for (int count = 0; count < size; count += writeSize) { sink.write(buffer, 0, (int) Math.min(size - count, writeSize)); } } }; } @Test public void emptyResponseBody() throws Exception { server.enqueue(new MockResponse() .addHeader("abc", "def")); executeSynchronously("/") .assertCode(200) .assertHeader("abc", "def") .assertBody(""); } @Test public void leakedResponseBodyLogsStackTrace() throws Exception { server.enqueue(new MockResponse() .setBody("This gets leaked.")); client = defaultClient().newBuilder() .connectionPool(new ConnectionPool(0, 10, TimeUnit.MILLISECONDS)) .build(); Request request = new Request.Builder() .url(server.url("/")) .build(); Level original = logger.getLevel(); logger.setLevel(Level.FINE); logHandler.setFormatter(new SimpleFormatter()); try { client.newCall(request).execute(); // Ignore the response so it gets leaked then GC'd. awaitGarbageCollection(); String message = logHandler.take(); assertTrue(message.contains("A connection to " + server.url("/") + " was leaked." + " Did you forget to close a response body?")); assertTrue(message.contains("okhttp3.RealCall.execute(")); assertTrue(message.contains("okhttp3.CallTest.leakedResponseBodyLogsStackTrace(")); } finally { logger.setLevel(original); } } @Test public void asyncLeakedResponseBodyLogsStackTrace() throws Exception { server.enqueue(new MockResponse() .setBody("This gets leaked.")); client = defaultClient().newBuilder() .connectionPool(new ConnectionPool(0, 10, TimeUnit.MILLISECONDS)) .build(); Request request = new Request.Builder() .url(server.url("/")) .build(); Level original = logger.getLevel(); logger.setLevel(Level.FINE); logHandler.setFormatter(new SimpleFormatter()); try { final CountDownLatch latch = new CountDownLatch(1); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { fail(); } @Override public void onResponse(Call call, Response response) throws IOException { // Ignore the response so it gets leaked then GC'd. latch.countDown(); } }); latch.await(); // There's some flakiness when triggering a GC for objects in a separate thread. Adding a // small delay appears to ensure the objects will get GC'd. Thread.sleep(200); awaitGarbageCollection(); String message = logHandler.take(); assertTrue(message.contains("A connection to " + server.url("/") + " was leaked." + " Did you forget to close a response body?")); assertTrue(message.contains("okhttp3.RealCall.enqueue(")); assertTrue(message.contains("okhttp3.CallTest.asyncLeakedResponseBodyLogsStackTrace(")); } finally { logger.setLevel(original); } } @Test public void httpsWithIpAddress() throws Exception { String localIpAddress = InetAddress.getLoopbackAddress().getHostAddress(); // Create a certificate with an IP address in the subject alt name. HeldCertificate heldCertificate = new HeldCertificate.Builder() .commonName("example.com") .subjectAlternativeName(localIpAddress) .build(); SslClient sslClient = new SslClient.Builder() .certificateChain(heldCertificate.keyPair, heldCertificate.certificate) .addTrustedCertificate(heldCertificate.certificate) .build(); // Use that certificate on the server and trust it on the client. server.useHttps(sslClient.socketFactory, false); client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .hostnameVerifier(new RecordingHostnameVerifier()) .protocols(Collections.singletonList(Protocol.HTTP_1_1)) .build(); // Make a request. server.enqueue(new MockResponse()); HttpUrl url = server.url("/").newBuilder() .host(localIpAddress) .build(); Request request = new Request.Builder() .url(url) .build(); executeSynchronously(request) .assertCode(200); // Confirm that the IP address was used in the host header. RecordedRequest recordedRequest = server.takeRequest(); assertEquals(localIpAddress + ":" + server.getPort(), recordedRequest.getHeader("Host")); } private void makeFailingCall() { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return null; } @Override public long contentLength() throws IOException { return 1; } @Override public void writeTo(BufferedSink sink) throws IOException { throw new IOException("write body fail!"); } }; OkHttpClient nonRetryingClient = client.newBuilder() .retryOnConnectionFailure(false) .build(); Call call = nonRetryingClient.newCall(new Request.Builder() .url(server.url("/")) .post(requestBody) .build()); try { call.execute(); fail(); } catch (IOException expected) { assertEquals("write body fail!", expected.getMessage()); } } private RecordedResponse executeSynchronously(String path, String... headers) throws IOException { Request.Builder builder = new Request.Builder(); builder.url(server.url(path)); for (int i = 0, size = headers.length; i < size; i += 2) { builder.addHeader(headers[i], headers[i + 1]); } return executeSynchronously(builder.build()); } private RecordedResponse executeSynchronously(Request request) throws IOException { Call call = client.newCall(request); try { Response response = call.execute(); String bodyString = response.body().string(); return new RecordedResponse(request, response, null, bodyString, null); } catch (IOException e) { return new RecordedResponse(request, null, null, null, e); } } /** * Tests that use this will fail unless boot classpath is set. Ex. {@code * -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317} */ private void enableProtocol(Protocol protocol) { enableTls(); client = client.newBuilder() .protocols(Arrays.asList(protocol, Protocol.HTTP_1_1)) .build(); server.setProtocols(client.protocols()); } private void enableTls() { client = client.newBuilder() .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .hostnameVerifier(new RecordingHostnameVerifier()) .build(); server.useHttps(sslClient.socketFactory, false); } private Buffer gzip(String data) throws IOException { Buffer result = new Buffer(); BufferedSink sink = Okio.buffer(new GzipSink(result)); sink.writeUtf8(data); sink.close(); 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<>(); public RecordingSSLSocketFactory(SSLSocketFactory delegate) { super(delegate); } @Override protected SSLSocket configureSocket(SSLSocket sslSocket) throws IOException { socketsCreated.add(sslSocket); return sslSocket; } public List<SSLSocket> getSocketsCreated() { return socketsCreated; } } /** * Used during tests that involve TLS connection fallback attempts. OkHttp includes the * TLS_FALLBACK_SCSV cipher on fallback connections. See {@link FallbackTestClientSocketFactory} * for details. */ private FallbackTestClientSocketFactory suppressTlsFallbackClientSocketFactory() { return new FallbackTestClientSocketFactory(sslClient.socketFactory); } }