/* * 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.internal.http2; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.net.ssl.HostnameVerifier; import okhttp3.Cache; import okhttp3.Call; import okhttp3.Cookie; import okhttp3.Credentials; import okhttp3.Headers; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.RecordingCookieJar; import okhttp3.RecordingHostnameVerifier; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.TestUtil; import okhttp3.internal.DoubleInetAddressDns; import okhttp3.internal.RecordingOkAuthenticator; import okhttp3.internal.SingleInetAddressDns; import okhttp3.internal.SocketRecorder; import okhttp3.internal.Util; import okhttp3.internal.connection.RealConnection; import okhttp3.internal.tls.SslClient; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.PushPromise; import okhttp3.mockwebserver.QueueDispatcher; 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.TemporaryFolder; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static okhttp3.TestUtil.defaultClient; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** Test how SPDY interacts with HTTP/2 features. */ public final class HttpOverHttp2Test { @Rule public final TemporaryFolder tempDir = new TemporaryFolder(); @Rule public final MockWebServer server = new MockWebServer(); private SslClient sslClient = SslClient.localhost(); private HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); private OkHttpClient client; private Cache cache; @Before public void setUp() throws Exception { server.useHttps(sslClient.socketFactory, false); cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE); client = defaultClient().newBuilder() .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) .dns(new SingleInetAddressDns()) .sslSocketFactory(sslClient.socketFactory, sslClient.trustManager) .hostnameVerifier(hostnameVerifier) .build(); } @After public void tearDown() throws Exception { Authenticator.setDefault(null); } @Test public void get() throws Exception { server.enqueue(new MockResponse() .setBody("ABCDE") .setStatus("HTTP/1.1 200 Sweet")); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); assertEquals(200, response.code()); assertEquals("Sweet", response.message()); RecordedRequest request = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); assertEquals("https", request.getHeader(":scheme")); assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(":authority")); } @Test public void emptyResponse() throws IOException { server.enqueue(new MockResponse()); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .build()); Response response = call.execute(); assertEquals(-1, response.body().byteStream().read()); response.body().close(); } @Test public void noDefaultContentLengthOnStreamingPost() throws Exception { final byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8); server.enqueue(new MockResponse().setBody("ABCDE")); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .post(new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain; charset=utf-8"); } @Override public void writeTo(BufferedSink sink) throws IOException { sink.write(postBytes); } }) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); RecordedRequest request = server.takeRequest(); assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); assertArrayEquals(postBytes, request.getBody().readByteArray()); assertNull(request.getHeader("Content-Length")); } @Test public void userSuppliedContentLengthHeader() throws Exception { final byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8); server.enqueue(new MockResponse().setBody("ABCDE")); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .post(new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain; charset=utf-8"); } @Override public long contentLength() throws IOException { return postBytes.length; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.write(postBytes); } }) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); RecordedRequest request = server.takeRequest(); assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); assertArrayEquals(postBytes, request.getBody().readByteArray()); assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length"))); } @Test public void closeAfterFlush() throws Exception { final byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8); server.enqueue(new MockResponse().setBody("ABCDE")); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .post(new RequestBody() { @Override public MediaType contentType() { return MediaType.parse("text/plain; charset=utf-8"); } @Override public long contentLength() throws IOException { return postBytes.length; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.write(postBytes); // push bytes into the stream's buffer sink.flush(); // Http2Connection.writeData subject to write window sink.close(); // Http2Connection.writeData empty frame } }) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); RecordedRequest request = server.takeRequest(); assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); assertArrayEquals(postBytes, request.getBody().readByteArray()); assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length"))); } @Test public void connectionReuse() throws Exception { server.enqueue(new MockResponse().setBody("ABCDEF")); server.enqueue(new MockResponse().setBody("GHIJKL")); Call call1 = client.newCall(new Request.Builder() .url(server.url("/r1")) .build()); Call call2 = client.newCall(new Request.Builder() .url(server.url("/r1")) .build()); Response response1 = call1.execute(); Response response2 = call2.execute(); assertEquals("ABC", response1.body().source().readUtf8(3)); assertEquals("GHI", response2.body().source().readUtf8(3)); assertEquals("DEF", response1.body().source().readUtf8(3)); assertEquals("JKL", response2.body().source().readUtf8(3)); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); response1.close(); response2.close(); } /** https://github.com/square/okhttp/issues/373 */ @Test @Ignore public void synchronousRequest() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.enqueue(new MockResponse().setBody("A")); ExecutorService executor = Executors.newCachedThreadPool(); CountDownLatch countDownLatch = new CountDownLatch(2); executor.execute(new AsyncRequest("/r1", countDownLatch)); executor.execute(new AsyncRequest("/r2", countDownLatch)); countDownLatch.await(); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void gzippedResponseBody() throws Exception { server.enqueue(new MockResponse() .addHeader("Content-Encoding: gzip") .setBody(gzip("ABCABCABC"))); Call call = client.newCall(new Request.Builder() .url(server.url("/r1")) .build()); Response response = call.execute(); assertEquals("ABCABCABC", response.body().string()); } @Test public void authenticate() throws Exception { server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) .addHeader("www-authenticate: Basic realm=\"protected area\"") .setBody("Please authenticate.")); server.enqueue(new MockResponse() .setBody("Successful auth!")); String credential = Credentials.basic("username", "password"); client = client.newBuilder() .authenticator(new RecordingOkAuthenticator(credential)) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("Successful auth!", response.body().string()); RecordedRequest denied = server.takeRequest(); assertNull(denied.getHeader("Authorization")); RecordedRequest accepted = server.takeRequest(); assertEquals("GET / HTTP/1.1", accepted.getRequestLine()); assertEquals(credential, accepted.getHeader("Authorization")); } @Test public void redirect() throws Exception { server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo") .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("This is the new location!")); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("This is the new location!", response.body().string()); RecordedRequest request1 = server.takeRequest(); assertEquals("/", request1.getPath()); RecordedRequest request2 = server.takeRequest(); assertEquals("/foo", request2.getPath()); } @Test public void readAfterLastByte() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); InputStream in = response.body().byteStream(); assertEquals('A', in.read()); assertEquals('B', in.read()); assertEquals('C', in.read()); assertEquals(-1, in.read()); assertEquals(-1, in.read()); in.close(); } @Test public void readResponseHeaderTimeout() throws Exception { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); server.enqueue(new MockResponse().setBody("A")); client = client.newBuilder() .readTimeout(1000, MILLISECONDS) .build(); // Make a call expecting a timeout reading the response headers. Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); try { call1.execute(); fail("Should have timed out!"); } catch (SocketTimeoutException expected) { assertEquals("timeout", expected.getMessage()); } // Confirm that a subsequent request on the same connection is not impacted. Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("A", response2.body().string()); // Confirm that the connection was reused. assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } /** * Test to ensure we don't throw a read timeout on responses that are progressing. For this * case, we take a 4KiB body and throttle it to 1KiB/second. We set the read timeout to two * seconds. If our implementation is acting correctly, it will not throw, as it is progressing. */ @Test public void readTimeoutMoreGranularThanBodySize() throws Exception { char[] body = new char[4096]; // 4KiB to read. Arrays.fill(body, 'y'); server.enqueue(new MockResponse().setBody(new String(body)) .throttleBody(1024, 1, SECONDS)); // Slow connection 1KiB/second. client = client.newBuilder() .readTimeout(2, SECONDS) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals(new String(body), response.body().string()); } /** * Test to ensure we throw a read timeout on responses that are progressing too slowly. For this * case, we take a 2KiB body and throttle it to 1KiB/second. We set the read timeout to half a * second. If our implementation is acting correctly, it will throw, as a byte doesn't arrive in * time. */ @Test public void readTimeoutOnSlowConnection() throws Exception { String body = TestUtil.repeat('y', 2048); server.enqueue(new MockResponse() .setBody(body) .throttleBody(1024, 1, SECONDS)); // Slow connection 1KiB/second. server.enqueue(new MockResponse() .setBody(body)); client = client.newBuilder() .readTimeout(500, MILLISECONDS) // Half a second to read something. .build(); // Make a call expecting a timeout reading the response body. Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); try { response1.body().string(); fail("Should have timed out!"); } catch (SocketTimeoutException expected) { assertEquals("timeout", expected.getMessage()); } // Confirm that a subsequent request on the same connection is not impacted. Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals(body, response2.body().string()); // Confirm that the connection was reused. assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void connectionTimeout() throws Exception { server.enqueue(new MockResponse() .setBody("A") .setBodyDelay(1, SECONDS)); OkHttpClient client1 = client.newBuilder() .readTimeout(2000, MILLISECONDS) .build(); Call call1 = client1 .newCall(new Request.Builder() .url(server.url("/")) .build()); OkHttpClient client2 = client.newBuilder() .readTimeout(200, MILLISECONDS) .build(); Call call2 = client2 .newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); assertEquals("A", response1.body().string()); try { call2.execute(); fail(); } catch (IOException expected) { } // Confirm that the connection was reused. assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } @Test public void responsesAreCached() throws IOException { client = client.newBuilder() .cache(cache) .build(); server.enqueue(new MockResponse() .addHeader("cache-control: max-age=60") .setBody("A")); Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); assertEquals("A", response1.body().string()); assertEquals(1, cache.requestCount()); assertEquals(1, cache.networkCount()); assertEquals(0, cache.hitCount()); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("A", response2.body().string()); Call call3 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response3 = call3.execute(); assertEquals("A", response3.body().string()); assertEquals(3, cache.requestCount()); assertEquals(1, cache.networkCount()); assertEquals(2, cache.hitCount()); } @Test public void conditionalCache() throws IOException { client = client.newBuilder() .cache(cache) .build(); server.enqueue(new MockResponse() .addHeader("ETag: v1") .setBody("A")); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); assertEquals("A", response1.body().string()); assertEquals(1, cache.requestCount()); assertEquals(1, cache.networkCount()); assertEquals(0, cache.hitCount()); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("A", response2.body().string()); assertEquals(2, cache.requestCount()); assertEquals(2, cache.networkCount()); assertEquals(1, cache.hitCount()); } @Test public void responseCachedWithoutConsumingFullBody() throws IOException { client = client.newBuilder() .cache(cache) .build(); server.enqueue(new MockResponse() .addHeader("cache-control: max-age=60") .setBody("ABCD")); server.enqueue(new MockResponse() .addHeader("cache-control: max-age=60") .setBody("EFGH")); Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); assertEquals("AB", response1.body().source().readUtf8(2)); response1.body().close(); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("ABCD", response2.body().source().readUtf8()); response2.body().close(); } @Test public void sendRequestCookies() throws Exception { RecordingCookieJar cookieJar = new RecordingCookieJar(); Cookie requestCookie = new Cookie.Builder() .name("a") .value("b") .domain(server.getHostName()) .build(); cookieJar.enqueueRequestCookies(requestCookie); client = client.newBuilder() .cookieJar(cookieJar) .build(); server.enqueue(new MockResponse()); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("", response.body().string()); RecordedRequest request = server.takeRequest(); assertEquals("a=b", request.getHeader("Cookie")); } @Test public void receiveResponseCookies() throws Exception { RecordingCookieJar cookieJar = new RecordingCookieJar(); client = client.newBuilder() .cookieJar(cookieJar) .build(); server.enqueue(new MockResponse() .addHeader("set-cookie: a=b")); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("", response.body().string()); cookieJar.assertResponseCookies("a=b; path=/"); } /** https://github.com/square/okhttp/issues/1191 */ @Ignore // TODO: recover gracefully when a connection is shutdown. @Test public void cancelWithStreamNotCompleted() throws Exception { // Ensure that the (shared) connection pool is in a consistent state. client.connectionPool().evictAll(); assertEquals(0, client.connectionPool().connectionCount()); server.enqueue(new MockResponse() .setBody("abc")); server.enqueue(new MockResponse() .setBody("def")); // Disconnect before the stream is created. A connection is still established! Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call1.execute(); call1.cancel(); // That connection is pooled, and it works. assertEquals(1, client.connectionPool().connectionCount()); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("def", response2.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); // Clean up the connection. response.close(); } @Test public void recoverFromOneRefusedStreamReusesConnection() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); server.enqueue(new MockResponse() .setBody("abc")); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("abc", response.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Reused connection. } @Test public void recoverFromOneInternalErrorRequiresNewConnection() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) .setHttp2ErrorCode(ErrorCode.INTERNAL_ERROR.httpCode)); server.enqueue(new MockResponse() .setBody("abc")); client = client.newBuilder() .dns(new DoubleInetAddressDns()) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("abc", response.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. } @Test public void recoverFromMultipleRefusedStreamsRequiresNewConnection() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) .setHttp2ErrorCode(ErrorCode.REFUSED_STREAM.httpCode)); server.enqueue(new MockResponse() .setBody("abc")); client = client.newBuilder() .dns(new DoubleInetAddressDns()) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("abc", response.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Reused connection. assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection. } @Test public void noRecoveryFromRefusedStreamWithRetryDisabled() throws Exception { noRecoveryFromErrorWithRetryDisabled(ErrorCode.REFUSED_STREAM); } @Test public void noRecoveryFromInternalErrorWithRetryDisabled() throws Exception { noRecoveryFromErrorWithRetryDisabled(ErrorCode.INTERNAL_ERROR); } private void noRecoveryFromErrorWithRetryDisabled(ErrorCode errorCode) throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.RESET_STREAM_AT_START) .setHttp2ErrorCode(errorCode.httpCode)); server.enqueue(new MockResponse() .setBody("abc")); client = client.newBuilder() .retryOnConnectionFailure(false) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); try { call.execute(); fail(); } catch (StreamResetException expected) { assertEquals(errorCode, expected.errorCode); } } @Test public void nonAsciiResponseHeader() throws Exception { server.enqueue(new MockResponse() .addHeaderLenient("Alpha", "α") .addHeaderLenient("β", "Beta")); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); response.close(); assertEquals("α", response.header("Alpha")); assertEquals("Beta", response.header("β")); } @Test public void serverSendsPushPromise_GET() throws Exception { PushPromise pushPromise = new PushPromise("GET", "/foo/bar", Headers.of("foo", "bar"), new MockResponse().setBody("bar").setStatus("HTTP/1.1 200 Sweet")); server.enqueue(new MockResponse() .setBody("ABCDE") .setStatus("HTTP/1.1 200 Sweet") .withPush(pushPromise)); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); assertEquals(200, response.code()); assertEquals("Sweet", response.message()); RecordedRequest request = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); assertEquals("https", request.getHeader(":scheme")); assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(":authority")); RecordedRequest pushedRequest = server.takeRequest(); assertEquals("GET /foo/bar HTTP/1.1", pushedRequest.getRequestLine()); assertEquals("bar", pushedRequest.getHeader("foo")); } @Test public void serverSendsPushPromise_HEAD() throws Exception { PushPromise pushPromise = new PushPromise("HEAD", "/foo/bar", Headers.of("foo", "bar"), new MockResponse().setStatus("HTTP/1.1 204 Sweet")); server.enqueue(new MockResponse() .setBody("ABCDE") .setStatus("HTTP/1.1 200 Sweet") .withPush(pushPromise)); Call call = client.newCall(new Request.Builder() .url(server.url("/foo")) .build()); Response response = call.execute(); assertEquals("ABCDE", response.body().string()); assertEquals(200, response.code()); assertEquals("Sweet", response.message()); RecordedRequest request = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); assertEquals("https", request.getHeader(":scheme")); assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(":authority")); RecordedRequest pushedRequest = server.takeRequest(); assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine()); assertEquals("bar", pushedRequest.getHeader("foo")); } @Test public void noDataFramesSentWithNullRequestBody() throws Exception { server.enqueue(new MockResponse() .setBody("ABC")); SocketRecorder socketRecorder = new SocketRecorder(); client = client.newBuilder() .sslSocketFactory(socketRecorder.sslSocketFactory(sslClient.socketFactory), sslClient.trustManager) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .method("DELETE", null) .build()); Response response = call.execute(); assertEquals("ABC", response.body().string()); // Replay the bytes written by the client to confirm no data frames were sent. SocketRecorder.RecordedSocket recordedSocket = socketRecorder.takeSocket(); Buffer buffer = new Buffer(); buffer.write(recordedSocket.bytesWritten()); RecordingHandler handler = new RecordingHandler(); Http2Reader reader = new Http2Reader(buffer, false); reader.readConnectionPreface(null); while (reader.nextFrame(false, handler)) { } assertEquals(1, handler.headerFrameCount); assertTrue(handler.dataFrames.isEmpty()); } @Test public void emptyDataFrameSentWithEmptyBody() throws Exception { server.enqueue(new MockResponse() .setBody("ABC")); SocketRecorder socketRecorder = new SocketRecorder(); client = client.newBuilder() .sslSocketFactory(socketRecorder.sslSocketFactory(sslClient.socketFactory), sslClient.trustManager) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .method("DELETE", Util.EMPTY_REQUEST) .build()); Response response = call.execute(); assertEquals("ABC", response.body().string()); // Replay the bytes written by the client to confirm an empty data frame was sent. SocketRecorder.RecordedSocket recordedSocket = socketRecorder.takeSocket(); Buffer buffer = new Buffer(); buffer.write(recordedSocket.bytesWritten()); RecordingHandler handler = new RecordingHandler(); Http2Reader reader = new Http2Reader(buffer, false); reader.readConnectionPreface(null); while (reader.nextFrame(false, handler)) { } assertEquals(1, handler.headerFrameCount); assertEquals(Collections.singletonList(0), handler.dataFrames); } /** * Push a setting that permits up to 2 concurrent streams, then make 3 concurrent requests and * confirm that the third concurrent request prepared a new connection. */ @Test public void settingsLimitsMaxConcurrentStreams() throws Exception { Settings settings = new Settings(); settings.set(Settings.MAX_CONCURRENT_STREAMS, 2); // Read & write a full request to confirm settings are accepted. server.enqueue(new MockResponse().withSettings(settings)); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("", response.body().string()); server.enqueue(new MockResponse() .setBody("ABC")); server.enqueue(new MockResponse() .setBody("DEF")); server.enqueue(new MockResponse() .setBody("GHI")); Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); Call call3 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response3 = call3.execute(); assertEquals("ABC", response1.body().string()); assertEquals("DEF", response2.body().string()); assertEquals("GHI", response3.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); // Settings connection. assertEquals(1, server.takeRequest().getSequenceNumber()); // Reuse settings connection. assertEquals(2, server.takeRequest().getSequenceNumber()); // Reuse settings connection. assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection! } @Test public void connectionNotReusedAfterShutdown() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END) .setBody("ABC")); server.enqueue(new MockResponse() .setBody("DEF")); Call call1 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response1 = call1.execute(); assertEquals("ABC", response1.body().string()); Call call2 = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response2 = call2.execute(); assertEquals("DEF", response2.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(0, server.takeRequest().getSequenceNumber()); } /** * This simulates a race condition where we receive a healthy HTTP/2 connection and just prior to * writing our request, we get a GOAWAY frame from the server. */ @Test public void connectionShutdownAfterHealthCheck() throws Exception { server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END) .setBody("ABC")); server.enqueue(new MockResponse() .setBody("DEF")); OkHttpClient client2 = client.newBuilder() .addNetworkInterceptor(new Interceptor() { boolean executedCall; @Override public Response intercept(Chain chain) throws IOException { if (!executedCall) { // At this point, we have a healthy HTTP/2 connection. This call will trigger the // server to send a GOAWAY frame, leaving the connection in a shutdown state. executedCall = true; Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("ABC", response.body().string()); // Wait until the GOAWAY has been processed. RealConnection connection = (RealConnection) chain.connection(); while (connection.isHealthy(false)) ; } return chain.proceed(chain.request()); } }) .build(); Call call = client2.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("DEF", response.body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(0, server.takeRequest().getSequenceNumber()); } /** * We don't know if the connection will support HTTP/2 until after we've connected. When multiple * connections are requested concurrently OkHttp will pessimistically connect multiple times, then * close any unnecessary connections. This test confirms that behavior works as intended. * * <p>This test uses proxy tunnels to get a hook while a connection is being established. */ @Test public void concurrentHttp2ConnectionsDeduplicated() throws Exception { server.useHttps(sslClient.socketFactory, true); // Force a fresh connection pool for the test. client.connectionPool().evictAll(); final QueueDispatcher queueDispatcher = new QueueDispatcher(); queueDispatcher.enqueueResponse(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); queueDispatcher.enqueueResponse(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); queueDispatcher.enqueueResponse(new MockResponse() .setBody("call2 response")); queueDispatcher.enqueueResponse(new MockResponse() .setBody("call1 response")); // We use a re-entrant dispatcher to initiate one HTTPS connection while the other is in flight. server.setDispatcher(new Dispatcher() { int requestCount; @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { MockResponse result = queueDispatcher.dispatch(request); requestCount++; if (requestCount == 1) { // Before handling call1's CONNECT we do all of call2. This part re-entrant! try { Call call2 = client.newCall(new Request.Builder() .url("https://android.com/call2") .build()); Response response2 = call2.execute(); assertEquals("call2 response", response2.body().string()); } catch (IOException e) { throw new RuntimeException(e); } } return result; } @Override public MockResponse peek() { return queueDispatcher.peek(); } @Override public void shutdown() { queueDispatcher.shutdown(); } }); client = client.newBuilder() .proxy(server.toProxyAddress()) .build(); Call call1 = client.newCall(new Request.Builder() .url("https://android.com/call1") .build()); Response response2 = call1.execute(); assertEquals("call1 response", response2.body().string()); RecordedRequest call1Connect = server.takeRequest(); assertEquals("CONNECT", call1Connect.getMethod()); assertEquals(0, call1Connect.getSequenceNumber()); RecordedRequest call2Connect = server.takeRequest(); assertEquals("CONNECT", call2Connect.getMethod()); assertEquals(0, call2Connect.getSequenceNumber()); RecordedRequest call2Get = server.takeRequest(); assertEquals("GET", call2Get.getMethod()); assertEquals("/call2", call2Get.getPath()); assertEquals(0, call2Get.getSequenceNumber()); RecordedRequest call1Get = server.takeRequest(); assertEquals("GET", call1Get.getMethod()); assertEquals("/call1", call1Get.getPath()); assertEquals(1, call1Get.getSequenceNumber()); assertEquals(1, client.connectionPool().connectionCount()); } /** https://github.com/square/okhttp/issues/3103 */ @Test public void domainFronting() throws Exception { client = client.newBuilder() .addNetworkInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request().newBuilder() .header("Host", "privateobject.com") .build(); return chain.proceed(request); } }) .build(); server.enqueue(new MockResponse()); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); assertEquals("", response.body().string()); RecordedRequest recordedRequest = server.takeRequest(); assertEquals("privateobject.com", recordedRequest.getHeader(":authority")); } public Buffer gzip(String bytes) throws IOException { Buffer bytesOut = new Buffer(); BufferedSink sink = Okio.buffer(new GzipSink(bytesOut)); sink.writeUtf8(bytes); sink.close(); return bytesOut; } class AsyncRequest implements Runnable { String path; CountDownLatch countDownLatch; public AsyncRequest(String path, CountDownLatch countDownLatch) { this.path = path; this.countDownLatch = countDownLatch; } @Override public void run() { try { Call call = client.newCall(new Request.Builder() .url(server.url(path)) .build()); Response response = call.execute(); assertEquals("A", response.body().string()); countDownLatch.countDown(); } catch (Exception e) { throw new RuntimeException(e); } } } static final class RecordingHandler extends BaseTestHandler { int headerFrameCount; final List<Integer> dataFrames = new ArrayList<>(); @Override public void settings(boolean clearPrevious, Settings settings) { } @Override public void ackSettings() { } @Override public void windowUpdate(int streamId, long windowSizeIncrement) { } @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { dataFrames.add(length); } @Override public void headers(boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock) { headerFrameCount++; } } }