/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.http2;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.internal.Util;
import okhttp3.internal.http2.MockHttp2Peer.InFrame;
import okio.AsyncTimeout;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import okio.Sink;
import okio.Source;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import static okhttp3.TestUtil.headerEntries;
import static okhttp3.TestUtil.repeat;
import static okhttp3.internal.Util.EMPTY_BYTE_ARRAY;
import static okhttp3.internal.http2.Http2Connection.Listener.REFUSE_INCOMING_STREAMS;
import static okhttp3.internal.http2.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
import static okhttp3.internal.http2.Settings.ENABLE_PUSH;
import static okhttp3.internal.http2.Settings.HEADER_TABLE_SIZE;
import static okhttp3.internal.http2.Settings.INITIAL_WINDOW_SIZE;
import static okhttp3.internal.http2.Settings.MAX_CONCURRENT_STREAMS;
import static okhttp3.internal.http2.Settings.MAX_FRAME_SIZE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class Http2ConnectionTest {
private final MockHttp2Peer peer = new MockHttp2Peer();
@Rule public final TestRule timeout = new Timeout(5_000);
@After public void tearDown() throws Exception {
peer.close();
}
@Test public void serverPingsClientHttp2() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().ping(false, 2, 3);
peer.acceptFrame(); // PING
peer.play();
// play it back
connect(peer);
// verify the peer received what was expected
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
assertEquals(0, ping.streamId);
assertEquals(2, ping.payload1);
assertEquals(3, ping.payload2);
assertTrue(ping.ack);
}
@Test public void clientPingsServerHttp2() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 5);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Ping ping = connection.ping();
assertTrue(ping.roundTripTime() > 0);
assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
// verify the peer received what was expected
InFrame pingFrame = peer.takeFrame();
assertEquals(0, pingFrame.streamId);
assertEquals(1, pingFrame.payload1);
assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
assertFalse(pingFrame.ack);
}
@Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
Settings initial = new Settings();
initial.set(INITIAL_WINDOW_SIZE, 1684);
Settings shouldntImpactConnection = new Settings();
shouldntImpactConnection.set(INITIAL_WINDOW_SIZE, 3368);
peer.sendFrame().settings(initial);
peer.acceptFrame(); // ACK
peer.sendFrame().settings(shouldntImpactConnection);
peer.acceptFrame(); // ACK 2
peer.acceptFrame(); // HEADERS
peer.play();
Http2Connection connection = connect(peer);
// Verify the peer received the second ACK.
InFrame ackFrame = peer.takeFrame();
assertEquals(Http2.TYPE_SETTINGS, ackFrame.type);
assertEquals(0, ackFrame.streamId);
assertTrue(ackFrame.ack);
// This stream was created *after* the connection settings were adjusted.
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
assertEquals(3368, connection.peerSettings.getInitialWindowSize());
assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
// New Stream is has the most recent initial window size.
assertEquals(3368, stream.bytesLeftInWriteWindow);
}
@Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
boolean client = false; // Peer is server, so we are client.
Settings settings = new Settings();
settings.set(HEADER_TABLE_SIZE, 0);
Http2Connection connection = connectWithSettings(client, settings);
// Verify the peer's settings were read and applied.
assertEquals(0, connection.peerSettings.getHeaderTableSize());
Http2Writer writer = connection.writer;
assertEquals(0, writer.hpackWriter.dynamicTableByteCount);
assertEquals(0, writer.hpackWriter.headerTableSizeSetting);
}
@Test public void peerHttp2ClientDisablesPush() throws Exception {
boolean client = false; // Peer is client, so we are server.
Settings settings = new Settings();
settings.set(ENABLE_PUSH, 0); // The peer client disables push.
Http2Connection connection = connectWithSettings(client, settings);
// verify the peer's settings were read and applied.
assertFalse(connection.peerSettings.getEnablePush(true));
}
@Test public void peerIncreasesMaxFrameSize() throws Exception {
int newMaxFrameSize = 0x4001;
Settings settings = new Settings();
settings.set(MAX_FRAME_SIZE, newMaxFrameSize);
Http2Connection connection = connectWithSettings(true, settings);
// verify the peer's settings were read and applied.
assertEquals(newMaxFrameSize, connection.peerSettings.getMaxFrameSize(-1));
assertEquals(newMaxFrameSize, connection.writer.maxDataLength());
}
/**
* Webservers may set the initial window size to zero, which is a special case because it means
* that we have to flush headers immediately before any request body can be sent.
* https://github.com/square/okhttp/issues/2543
*/
@Test public void peerSetsZeroFlowControl() throws Exception {
peer.setClient(true);
// Write the mocking script.
peer.sendFrame().settings(new Settings().set(INITIAL_WINDOW_SIZE, 0));
peer.acceptFrame(); // ACK
peer.sendFrame().windowUpdate(0, 10); // Increase the connection window size.
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // HEADERS STREAM 3
peer.sendFrame().windowUpdate(3, 5);
peer.acceptFrame(); // DATA STREAM 3 "abcde"
peer.sendFrame().windowUpdate(3, 5);
peer.acceptFrame(); // DATA STREAM 3 "fghi"
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
connection.ping().roundTripTime(); // Ensure the SETTINGS have been received.
Http2Stream stream = connection.newStream(headerEntries("a", "android"), true);
BufferedSink sink = Okio.buffer(stream.getSink());
sink.writeUtf8("abcdefghi");
sink.flush();
// Verify the peer received what was expected.
peer.takeFrame(); // PING
InFrame headers = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, headers.type);
InFrame data1 = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data1.type);
assertEquals(3, data1.streamId);
assertTrue(Arrays.equals("abcde".getBytes("UTF-8"), data1.data));
InFrame data2 = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data2.type);
assertEquals(3, data2.streamId);
assertTrue(Arrays.equals("fghi".getBytes("UTF-8"), data2.data));
}
@Test public void receiveGoAwayHttp2() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM 3
peer.acceptFrame(); // SYN_STREAM 5
peer.sendFrame().goAway(3, ErrorCode.PROTOCOL_ERROR, EMPTY_BYTE_ARRAY);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // DATA STREAM 3
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream1 = connection.newStream(headerEntries("a", "android"), true);
Http2Stream stream2 = connection.newStream(headerEntries("b", "banana"), true);
connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
BufferedSink sink1 = Okio.buffer(stream1.getSink());
BufferedSink sink2 = Okio.buffer(stream2.getSink());
sink1.writeUtf8("abc");
try {
sink2.writeUtf8("abc");
sink2.flush();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
}
sink1.writeUtf8("def");
sink1.close();
try {
connection.newStream(headerEntries("c", "cola"), true);
fail();
} catch (ConnectionShutdownException expected) {
}
assertTrue(stream1.isOpen());
assertFalse(stream2.isOpen());
assertEquals(1, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream1 = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream1.type);
InFrame synStream2 = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream2.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
InFrame data1 = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data1.type);
assertEquals(3, data1.streamId);
assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
}
@Test public void readSendsWindowUpdateHttp2() throws Exception {
int windowSize = 100;
int windowUpdateThreshold = 50;
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
for (int i = 0; i < 3; i++) {
// Send frames of summing to size 50, which is windowUpdateThreshold.
peer.sendFrame().data(false, 3, data(24), 24);
peer.sendFrame().data(false, 3, data(25), 25);
peer.sendFrame().data(false, 3, data(1), 1);
peer.acceptFrame(); // connection WINDOW UPDATE
peer.acceptFrame(); // stream WINDOW UPDATE
}
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
connection.okHttpSettings.set(INITIAL_WINDOW_SIZE, windowSize);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(0, stream.unacknowledgedBytesRead);
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
Source in = stream.getSource();
Buffer buffer = new Buffer();
buffer.writeAll(in);
assertEquals(-1, in.read(buffer, 1));
assertEquals(150, buffer.size());
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
for (int i = 0; i < 3; i++) {
List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
for (int j = 0; j < 2; j++) {
InFrame windowUpdate = peer.takeFrame();
assertEquals(Http2.TYPE_WINDOW_UPDATE, windowUpdate.type);
windowUpdateStreamIds.add(windowUpdate.streamId);
assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
}
assertTrue(windowUpdateStreamIds.contains(0)); // connection
assertTrue(windowUpdateStreamIds.contains(3)); // stream
}
}
@Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
Http2Stream client = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(-1, client.getSource().read(new Buffer(), 1));
// Verify the peer received what was expected.
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertEquals(5, peer.frameCount());
}
@Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // DATA
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
Http2Stream client = connection.newStream(headerEntries("b", "banana"), true);
BufferedSink out = Okio.buffer(client.getSink());
out.write(EMPTY_BYTE_ARRAY);
out.flush();
out.close();
// Verify the peer received what was expected.
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_DATA, peer.takeFrame().type);
assertEquals(5, peer.frameCount());
}
@Test public void maxFrameSizeHonored() throws Exception {
byte[] buff = new byte[peer.maxOutboundDataLength() + 1];
Arrays.fill(buff, (byte) '*');
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // DATA
peer.acceptFrame(); // DATA
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
BufferedSink out = Okio.buffer(stream.getSink());
out.write(buff);
out.flush();
out.close();
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame data = peer.takeFrame();
assertEquals(peer.maxOutboundDataLength(), data.data.length);
data = peer.takeFrame();
assertEquals(1, data.data.length);
}
@Test public void pushPromiseStream() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
final List<Header> expectedRequestHeaders = Arrays.asList(
new Header(Header.TARGET_METHOD, "GET"),
new Header(Header.TARGET_SCHEME, "https"),
new Header(Header.TARGET_AUTHORITY, "squareup.com"),
new Header(Header.TARGET_PATH, "/cached")
);
peer.sendFrame().pushPromise(3, 2, expectedRequestHeaders);
final List<Header> expectedResponseHeaders = Arrays.asList(
new Header(Header.RESPONSE_STATUS, "200")
);
peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
RecordingPushObserver observer = new RecordingPushObserver();
// play it back
Http2Connection connection = connect(peer, observer, REFUSE_INCOMING_STREAMS);
Http2Stream client = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(-1, client.getSource().read(new Buffer(), 1));
// verify the peer received what was expected
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(expectedRequestHeaders, observer.takeEvent());
assertEquals(expectedResponseHeaders, observer.takeEvent());
}
@Test public void doublePushPromise() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().pushPromise(3, 2, headerEntries("a", "android"));
peer.acceptFrame(); // SYN_REPLY
peer.sendFrame().pushPromise(3, 2, headerEntries("b", "banana"));
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.newStream(headerEntries("b", "banana"), false);
// verify the peer received what was expected
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(ErrorCode.PROTOCOL_ERROR, peer.takeFrame().errorCode);
}
@Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().pushPromise(3, 2, Arrays.asList(
new Header(Header.TARGET_METHOD, "GET"),
new Header(Header.TARGET_SCHEME, "https"),
new Header(Header.TARGET_AUTHORITY, "squareup.com"),
new Header(Header.TARGET_PATH, "/cached")
));
peer.sendFrame().synReply(true, 2, Arrays.asList(
new Header(Header.RESPONSE_STATUS, "200")
));
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
connect(peer, PushObserver.CANCEL, REFUSE_INCOMING_STREAMS);
// verify the peer received what was expected
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(2, rstStream.streamId);
assertEquals(ErrorCode.CANCEL, rstStream.errorCode);
}
/**
* When writing a set of headers fails due to an {@code IOException}, make sure the writer is left
* in a consistent state so the next writer also gets an {@code IOException} also instead of
* something worse (like an {@link IllegalStateException}.
*
* <p>See https://github.com/square/okhttp/issues/1651
*/
@Test public void socketExceptionWhileWritingHeaders() throws Exception {
peer.acceptFrame(); // SYN_STREAM.
peer.play();
String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
Socket socket = peer.openSocket();
Http2Connection connection = new Http2Connection.Builder(true)
.socket(socket)
.pushObserver(IGNORE)
.build();
connection.start(false);
socket.shutdownOutput();
try {
connection.newStream(headerEntries("a", longString), false);
fail();
} catch (IOException expected) {
}
try {
connection.newStream(headerEntries("b", longString), false);
fail();
} catch (IOException expected) {
}
}
@Test public void clientCreatesStreamAndServerReplies() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // DATA
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("robot"), 5);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0); // PING
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
BufferedSink out = Okio.buffer(stream.getSink());
out.writeUtf8("c3po");
out.close();
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
assertStreamData("robot", stream.getSource());
connection.ping().roundTripTime();
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertFalse(synStream.outFinished);
assertEquals(3, synStream.streamId);
assertEquals(-1, synStream.associatedStreamId);
assertEquals(headerEntries("b", "banana"), synStream.headerBlock);
InFrame requestData = peer.takeFrame();
assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data));
}
@Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // PING
peer.sendFrame().synReply(true, 3, headerEntries("a", "android"));
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.newStream(headerEntries("b", "banana"), false);
assertEquals(1, connection.openStreamCount());
connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received.
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
}
@Test public void serverPingsClient() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
connect(peer);
// verify the peer received what was expected
InFrame ping = peer.takeFrame();
assertEquals(0, ping.streamId);
assertEquals(2, ping.payload1);
assertEquals(0, ping.payload2);
assertTrue(ping.ack);
}
@Test public void clientPingsServer() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 5);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Ping ping = connection.ping();
assertTrue(ping.roundTripTime() > 0);
assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
// verify the peer received what was expected
InFrame pingFrame = peer.takeFrame();
assertEquals(Http2.TYPE_PING, pingFrame.type);
assertEquals(1, pingFrame.payload1);
assertEquals(new Buffer().writeUtf8("OKok").readInt(), pingFrame.payload2);
assertFalse(pingFrame.ack);
}
@Test public void unexpectedPingIsNotReturned() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 3, 0); // This ping will not be returned.
peer.sendFrame().ping(false, 4, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
connect(peer);
// verify the peer received what was expected
InFrame ping2 = peer.takeFrame();
assertEquals(2, ping2.payload1);
InFrame ping4 = peer.takeFrame();
assertEquals(4, ping4.payload1);
}
@Test public void serverSendsSettingsToClient() throws Exception {
// write the mocking script
final Settings settings = new Settings();
settings.set(MAX_CONCURRENT_STREAMS, 10);
peer.sendFrame().settings(settings);
peer.acceptFrame(); // ACK
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
final CountDownLatch maxConcurrentStreamsUpdated = new CountDownLatch(1);
final AtomicInteger maxConcurrentStreams = new AtomicInteger();
Http2Connection.Listener listener = new Http2Connection.Listener() {
@Override public void onStream(Http2Stream stream) throws IOException {
throw new AssertionError();
}
@Override public void onSettings(Http2Connection connection) {
maxConcurrentStreams.set(connection.maxConcurrentStreams());
maxConcurrentStreamsUpdated.countDown();
}
};
Http2Connection connection = connect(peer, IGNORE, listener);
synchronized (connection) {
assertEquals(10, connection.peerSettings.getMaxConcurrentStreams(-1));
}
maxConcurrentStreamsUpdated.await();
assertEquals(10, maxConcurrentStreams.get());
}
@Test public void multipleSettingsFramesAreMerged() throws Exception {
// write the mocking script
Settings settings1 = new Settings();
settings1.set(HEADER_TABLE_SIZE, 10000);
settings1.set(INITIAL_WINDOW_SIZE, 20000);
settings1.set(MAX_FRAME_SIZE, 30000);
peer.sendFrame().settings(settings1);
peer.acceptFrame(); // ACK SETTINGS
Settings settings2 = new Settings();
settings2.set(INITIAL_WINDOW_SIZE, 40000);
settings2.set(MAX_FRAME_SIZE, 50000);
settings2.set(MAX_CONCURRENT_STREAMS, 60000);
peer.sendFrame().settings(settings2);
peer.acceptFrame(); // ACK SETTINGS
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
Http2Connection connection = connect(peer);
assertEquals(Http2.TYPE_SETTINGS, peer.takeFrame().type);
assertEquals(Http2.TYPE_PING, peer.takeFrame().type);
synchronized (connection) {
assertEquals(10000, connection.peerSettings.getHeaderTableSize());
assertEquals(40000, connection.peerSettings.getInitialWindowSize());
assertEquals(50000, connection.peerSettings.getMaxFrameSize(-1));
assertEquals(60000, connection.peerSettings.getMaxConcurrentStreams(-1));
}
}
@Test public void clearSettingsBeforeMerge() throws Exception {
// write the mocking script
Settings settings1 = new Settings();
settings1.set(HEADER_TABLE_SIZE, 10000);
settings1.set(INITIAL_WINDOW_SIZE, 20000);
settings1.set(MAX_FRAME_SIZE, 30000);
peer.sendFrame().settings(settings1);
peer.acceptFrame(); // ACK
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame();
peer.play();
// play it back
Http2Connection connection = connect(peer);
// fake a settings frame with clear flag set.
Settings settings2 = new Settings();
settings2.set(MAX_CONCURRENT_STREAMS, 60000);
connection.readerRunnable.settings(true, settings2);
synchronized (connection) {
assertEquals(-1, connection.peerSettings.getHeaderTableSize());
assertEquals(DEFAULT_INITIAL_WINDOW_SIZE, connection.peerSettings.getInitialWindowSize());
assertEquals(-1, connection.peerSettings.getMaxFrameSize(-1));
assertEquals(60000, connection.peerSettings.getMaxConcurrentStreams(-1));
}
}
@Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().data(true, 41, new Buffer().writeUtf8("bogus"), 5);
peer.acceptFrame(); // RST_STREAM
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
connect(peer);
// verify the peer received what was expected
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(41, rstStream.streamId);
assertEquals(ErrorCode.PROTOCOL_ERROR, rstStream.errorCode);
InFrame ping = peer.takeFrame();
assertEquals(2, ping.payload1);
}
@Test public void bogusReplySilentlyIgnored() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.sendFrame().synReply(false, 41, headerEntries("a", "android"));
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
connect(peer);
// verify the peer received what was expected
InFrame ping = peer.takeFrame();
assertEquals(2, ping.payload1);
}
@Test public void serverClosesClientOutputStream() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().rstStream(3, ErrorCode.CANCEL);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), true);
BufferedSink out = Okio.buffer(stream.getSink());
connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received.
try {
out.writeUtf8("square");
out.flush();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: CANCEL", expected.getMessage());
}
try {
out.close();
fail();
} catch (IOException expected) {
// Close throws because buffered data wasn't flushed.
}
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertFalse(synStream.inFinished);
assertFalse(synStream.outFinished);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
assertEquals(1, ping.payload1);
}
/**
* Test that the client sends a RST_STREAM if doing so won't disrupt the output stream.
*/
@Test public void clientClosesClientInputStream() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
Source in = stream.getSource();
BufferedSink out = Okio.buffer(stream.getSink());
in.close();
try {
in.read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream closed", expected.getMessage());
}
try {
out.writeUtf8("a");
out.flush();
fail();
} catch (IOException expected) {
assertEquals("stream finished", expected.getMessage());
}
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertTrue(synStream.inFinished);
assertFalse(synStream.outFinished);
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(ErrorCode.CANCEL, rstStream.errorCode);
}
/**
* Test that the client doesn't send a RST_STREAM if doing so will disrupt the output stream.
*/
@Test public void clientClosesClientInputStreamIfOutputStreamIsClosed() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // DATA
peer.acceptFrame(); // DATA with FLAG_FIN
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), true);
Source source = stream.getSource();
BufferedSink out = Okio.buffer(stream.getSink());
source.close();
try {
source.read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream closed", expected.getMessage());
}
out.writeUtf8("square");
out.flush();
out.close();
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertFalse(synStream.inFinished);
assertFalse(synStream.outFinished);
InFrame data = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data.type);
assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data));
InFrame fin = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, fin.type);
assertTrue(fin.inFinished);
assertFalse(fin.outFinished);
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(ErrorCode.CANCEL, rstStream.errorCode);
}
@Test public void serverClosesClientInputStream() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("b", "banana"));
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("square"), 6);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
Source source = stream.getSource();
assertStreamData("square", source);
connection.ping().roundTripTime(); // Ensure that inFinished has been received.
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertTrue(synStream.inFinished);
assertFalse(synStream.outFinished);
}
@Test public void remoteDoubleSynReply() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // PING
peer.sendFrame().synReply(false, 3, headerEntries("b", "banana"));
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("c", "cola"), false);
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received.
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
}
@Test public void remoteSendsDataAfterInFinished() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("robot"), 5);
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("c3po"), 4);
peer.acceptFrame(); // RST_STREAM
peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
peer.acceptFrame(); // PING
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
assertStreamData("robot", stream.getSource());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(3, rstStream.streamId);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
assertEquals(2, ping.payload1);
}
@Test public void clientDoesNotLimitFlowControl() throws Exception {
int dataLength = 16384;
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("b", "banana"));
peer.sendFrame().data(false, 3, new Buffer().write(new byte[dataLength]), dataLength);
peer.sendFrame().data(false, 3, new Buffer().write(new byte[dataLength]), dataLength);
peer.sendFrame().data(false, 3, new Buffer().write(new byte[dataLength]), dataLength);
peer.sendFrame().data(false, 3, new Buffer().write(new byte[dataLength]), dataLength);
peer.sendFrame().data(false, 3, new Buffer().write(new byte[1]), 1);
peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
peer.acceptFrame(); // PING
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
assertEquals(headerEntries("b", "banana"), stream.takeResponseHeaders());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
assertEquals(2, ping.payload1);
}
@Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().rstStream(3, ErrorCode.REFUSED_STREAM);
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
try {
stream.takeResponseHeaders();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
}
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
assertEquals(2, ping.payload1);
}
@Test public void receiveGoAway() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM 1
peer.acceptFrame(); // SYN_STREAM 3
peer.acceptFrame(); // PING.
peer.sendFrame().goAway(3, ErrorCode.PROTOCOL_ERROR, Util.EMPTY_BYTE_ARRAY);
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // DATA STREAM 1
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream1 = connection.newStream(headerEntries("a", "android"), true);
Http2Stream stream2 = connection.newStream(headerEntries("b", "banana"), true);
connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
BufferedSink sink1 = Okio.buffer(stream1.getSink());
BufferedSink sink2 = Okio.buffer(stream2.getSink());
sink1.writeUtf8("abc");
try {
sink2.writeUtf8("abc");
sink2.flush();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
}
sink1.writeUtf8("def");
sink1.close();
try {
connection.newStream(headerEntries("c", "cola"), false);
fail();
} catch (ConnectionShutdownException expected) {
}
assertTrue(stream1.isOpen());
assertFalse(stream2.isOpen());
assertEquals(1, connection.openStreamCount());
// verify the peer received what was expected
InFrame synStream1 = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream1.type);
InFrame synStream2 = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream2.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
InFrame data1 = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data1.type);
assertEquals(3, data1.streamId);
assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
}
@Test public void sendGoAway() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM 1
peer.acceptFrame(); // GOAWAY
peer.acceptFrame(); // PING
peer.sendFrame().synStream(false, 2, 0, headerEntries("b", "b")); // Should be ignored!
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.newStream(headerEntries("a", "android"), false);
Ping ping = connection.ping();
connection.shutdown(ErrorCode.PROTOCOL_ERROR);
assertEquals(1, connection.openStreamCount());
ping.roundTripTime(); // Prevent the peer from exiting prematurely.
// verify the peer received what was expected
InFrame synStream1 = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream1.type);
InFrame pingFrame = peer.takeFrame();
assertEquals(Http2.TYPE_PING, pingFrame.type);
InFrame goaway = peer.takeFrame();
assertEquals(Http2.TYPE_GOAWAY, goaway.type);
assertEquals(0, goaway.streamId);
assertEquals(ErrorCode.PROTOCOL_ERROR, goaway.errorCode);
}
@Test public void noPingsAfterShutdown() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // GOAWAY
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.shutdown(ErrorCode.INTERNAL_ERROR);
try {
connection.ping();
fail();
} catch (ConnectionShutdownException expected) {
}
// verify the peer received what was expected
InFrame goaway = peer.takeFrame();
assertEquals(Http2.TYPE_GOAWAY, goaway.type);
assertEquals(ErrorCode.INTERNAL_ERROR, goaway.errorCode);
}
@Test public void close() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // GOAWAY
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("a", "android"), false);
assertEquals(1, connection.openStreamCount());
connection.close();
assertEquals(0, connection.openStreamCount());
try {
connection.newStream(headerEntries("b", "banana"), false);
fail();
} catch (ConnectionShutdownException expected) {
}
BufferedSink sink = Okio.buffer(stream.getSink());
try {
sink.writeByte(0);
sink.flush();
fail();
} catch (IOException expected) {
assertEquals("stream finished", expected.getMessage());
}
try {
stream.getSource().read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream was reset: CANCEL", expected.getMessage());
}
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame goaway = peer.takeFrame();
assertEquals(Http2.TYPE_GOAWAY, goaway.type);
InFrame rstStream = peer.takeFrame();
assertEquals(Http2.TYPE_RST_STREAM, rstStream.type);
assertEquals(3, rstStream.streamId);
}
@Test public void closeCancelsPings() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // PING
peer.acceptFrame(); // GOAWAY
peer.play();
// play it back
Http2Connection connection = connect(peer);
Ping ping = connection.ping();
connection.close();
assertEquals(-1, ping.roundTripTime());
}
@Test public void getResponseHeadersTimesOut() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
long startNanos = System.nanoTime();
try {
stream.takeResponseHeaders();
fail();
} catch (InterruptedIOException expected) {
}
long elapsedNanos = System.nanoTime() - startNanos;
awaitWatchdogIdle();
assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_RST_STREAM, peer.takeFrame().type);
}
@Test public void readTimesOut() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
Source source = stream.getSource();
long startNanos = System.nanoTime();
try {
source.read(new Buffer(), 1);
fail();
} catch (InterruptedIOException expected) {
}
long elapsedNanos = System.nanoTime() - startNanos;
awaitWatchdogIdle();
assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_RST_STREAM, peer.takeFrame().type);
}
@Test public void writeTimesOutAwaitingStreamWindow() throws Exception {
// Set the peer's receive window to 5 bytes!
Settings peerSettings = new Settings().set(INITIAL_WINDOW_SIZE, 5);
// write the mocking script
peer.sendFrame().settings(peerSettings);
peer.acceptFrame(); // ACK SETTINGS
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // DATA
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.ping().roundTripTime(); // Make sure settings have been received.
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
Sink sink = stream.getSink();
sink.write(new Buffer().writeUtf8("abcde"), 5);
stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS);
long startNanos = System.nanoTime();
sink.write(new Buffer().writeUtf8("f"), 1);
try {
sink.flush(); // This will time out waiting on the write window.
fail();
} catch (InterruptedIOException expected) {
}
long elapsedNanos = System.nanoTime() - startNanos;
awaitWatchdogIdle();
assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
assertEquals(Http2.TYPE_PING, peer.takeFrame().type);
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_DATA, peer.takeFrame().type);
assertEquals(Http2.TYPE_RST_STREAM, peer.takeFrame().type);
}
@Test public void writeTimesOutAwaitingConnectionWindow() throws Exception {
// Set the peer's receive window to 5 bytes. Give the stream 5 bytes back, so only the
// connection-level window is applicable.
Settings peerSettings = new Settings().set(INITIAL_WINDOW_SIZE, 5);
// write the mocking script
peer.sendFrame().settings(peerSettings);
peer.acceptFrame(); // ACK SETTINGS
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0);
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().windowUpdate(3, 5);
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 3, 0);
peer.acceptFrame(); // DATA
peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
Http2Connection connection = connect(peer);
connection.ping().roundTripTime(); // Make sure settings have been acked.
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
connection.ping().roundTripTime(); // Make sure the window update has been received.
Sink sink = stream.getSink();
stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS);
sink.write(new Buffer().writeUtf8("abcdef"), 6);
long startNanos = System.nanoTime();
try {
sink.flush(); // This will time out waiting on the write window.
fail();
} catch (InterruptedIOException expected) {
}
long elapsedNanos = System.nanoTime() - startNanos;
awaitWatchdogIdle();
assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
assertEquals(Http2.TYPE_PING, peer.takeFrame().type);
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_PING, peer.takeFrame().type);
assertEquals(Http2.TYPE_DATA, peer.takeFrame().type);
assertEquals(Http2.TYPE_RST_STREAM, peer.takeFrame().type);
}
@Test public void outgoingWritesAreBatched() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // DATA
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
// two outgoing writes
Sink sink = stream.getSink();
sink.write(new Buffer().writeUtf8("abcde"), 5);
sink.write(new Buffer().writeUtf8("fghij"), 5);
sink.close();
// verify the peer received one incoming frame
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
InFrame data = peer.takeFrame();
assertEquals(Http2.TYPE_DATA, data.type);
assertTrue(Arrays.equals("abcdefghij".getBytes("UTF-8"), data.data));
assertTrue(data.inFinished);
}
@Test public void headers() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // PING
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().headers(3, headerEntries("c", "c3po"));
peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
connection.ping().roundTripTime(); // Ensure that the HEADERS has been received.
assertEquals(Arrays.asList(new Header("a", "android"), null, new Header("c", "c3po")),
stream.takeResponseHeaders());
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame ping = peer.takeFrame();
assertEquals(Http2.TYPE_PING, ping.type);
}
@Test public void readMultipleSetsOfResponseHeaders() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // PING
peer.sendFrame().ping(true, 1, 0); // PING
peer.sendFrame().synReply(true, 3, headerEntries("c", "cola"));
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), true);
stream.getConnection().flush();
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
connection.ping().roundTripTime();
assertEquals(headerEntries("c", "cola"), stream.takeResponseHeaders());
// verify the peer received what was expected
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_PING, peer.takeFrame().type);
}
@Test public void readSendsWindowUpdate() throws Exception {
int windowSize = 100;
int windowUpdateThreshold = 50;
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
for (int i = 0; i < 3; i++) {
// Send frames of summing to size 50, which is windowUpdateThreshold.
peer.sendFrame().data(false, 3, data(24), 24);
peer.sendFrame().data(false, 3, data(25), 25);
peer.sendFrame().data(false, 3, data(1), 1);
peer.acceptFrame(); // connection WINDOW UPDATE
peer.acceptFrame(); // stream WINDOW UPDATE
}
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
connection.okHttpSettings.set(INITIAL_WINDOW_SIZE, windowSize);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(0, stream.unacknowledgedBytesRead);
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
Source in = stream.getSource();
Buffer buffer = new Buffer();
buffer.writeAll(in);
assertEquals(-1, in.read(buffer, 1));
assertEquals(150, buffer.size());
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
for (int i = 0; i < 3; i++) {
List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
for (int j = 0; j < 2; j++) {
InFrame windowUpdate = peer.takeFrame();
assertEquals(Http2.TYPE_WINDOW_UPDATE, windowUpdate.type);
windowUpdateStreamIds.add(windowUpdate.streamId);
assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
}
assertTrue(windowUpdateStreamIds.contains(0)); // connection
assertTrue(windowUpdateStreamIds.contains(3)); // stream
}
}
@Test public void serverSendsEmptyDataClientDoesntSendWindowUpdate() throws Exception {
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(true, 3, data(0), 0);
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
Http2Stream client = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(-1, client.getSource().read(new Buffer(), 1));
// Verify the peer received what was expected.
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
assertEquals(5, peer.frameCount());
}
@Test public void clientSendsEmptyDataServerDoesntSendWindowUpdate() throws Exception {
// Write the mocking script.
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.acceptFrame(); // DATA
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
Http2Stream client = connection.newStream(headerEntries("b", "banana"), true);
BufferedSink out = Okio.buffer(client.getSink());
out.write(Util.EMPTY_BYTE_ARRAY);
out.flush();
out.close();
// Verify the peer received what was expected.
assertEquals(Http2.TYPE_HEADERS, peer.takeFrame().type);
assertEquals(Http2.TYPE_DATA, peer.takeFrame().type);
assertEquals(5, peer.frameCount());
}
@Test public void testTruncatedDataFrame() throws Exception {
// write the mocking script
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // ACK
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.sendFrame().data(false, 3, data(1024), 1024);
peer.truncateLastFrame(8 + 100);
peer.play();
// play it back
Http2Connection connection = connect(peer);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
assertEquals(headerEntries("a", "android"), stream.takeResponseHeaders());
Source in = stream.getSource();
try {
Okio.buffer(in).readByteString(101);
fail();
} catch (IOException expected) {
assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
}
}
@Test public void blockedStreamDoesntStarveNewStream() throws Exception {
int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, peer.maxOutboundDataLength());
// Write the mocking script. This accepts more data frames than necessary!
peer.sendFrame().settings(new Settings());
peer.acceptFrame(); // SETTINGS ACK
peer.acceptFrame(); // SYN_STREAM on stream 1
for (int i = 0; i < framesThatFillWindow; i++) {
peer.acceptFrame(); // DATA on stream 1
}
peer.acceptFrame(); // SYN_STREAM on stream 2
peer.acceptFrame(); // DATA on stream 2
peer.play();
// Play it back.
Http2Connection connection = connect(peer);
Http2Stream stream1 = connection.newStream(headerEntries("a", "apple"), true);
BufferedSink out1 = Okio.buffer(stream1.getSink());
out1.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
out1.flush();
// Check that we've filled the window for both the stream and also the connection.
assertEquals(0, connection.bytesLeftInWriteWindow);
assertEquals(0, connection.getStream(3).bytesLeftInWriteWindow);
// receiving a window update on the connection will unblock new streams.
connection.readerRunnable.windowUpdate(0, 3);
assertEquals(3, connection.bytesLeftInWriteWindow);
assertEquals(0, connection.getStream(3).bytesLeftInWriteWindow);
// Another stream should be able to send data even though 1 is blocked.
Http2Stream stream2 = connection.newStream(headerEntries("b", "banana"), true);
BufferedSink out2 = Okio.buffer(stream2.getSink());
out2.writeUtf8("foo");
out2.flush();
assertEquals(0, connection.bytesLeftInWriteWindow);
assertEquals(0, connection.getStream(3).bytesLeftInWriteWindow);
assertEquals(DEFAULT_INITIAL_WINDOW_SIZE - 3, connection.getStream(5).bytesLeftInWriteWindow);
}
@Test public void remoteOmitsInitialSettings() throws Exception {
// Write the mocking script. Note no SETTINGS frame is sent or acknowledged.
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
peer.acceptFrame(); // GOAWAY
peer.play();
Http2Connection connection = new Http2Connection.Builder(true)
.socket(peer.openSocket())
.build();
connection.start(false);
Http2Stream stream = connection.newStream(headerEntries("b", "banana"), false);
try {
stream.takeResponseHeaders();
fail();
} catch (IOException expected) {
assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage());
}
// verify the peer received what was expected
InFrame synStream = peer.takeFrame();
assertEquals(Http2.TYPE_HEADERS, synStream.type);
InFrame goaway = peer.takeFrame();
assertEquals(Http2.TYPE_GOAWAY, goaway.type);
assertEquals(ErrorCode.PROTOCOL_ERROR, goaway.errorCode);
}
private Buffer data(int byteCount) {
return new Buffer().write(new byte[byteCount]);
}
private void assertStreamData(String expected, Source source) throws IOException {
String actual = Okio.buffer(source).readUtf8();
assertEquals(expected, actual);
}
/**
* Returns true when all work currently in progress by the watchdog have completed. This method
* creates more work for the watchdog and waits for that work to be executed. When it is, we know
* work that preceded this call is complete.
*/
private void awaitWatchdogIdle() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
AsyncTimeout watchdogJob = new AsyncTimeout() {
@Override protected void timedOut() {
latch.countDown();
}
};
watchdogJob.deadlineNanoTime(System.nanoTime()); // Due immediately!
watchdogJob.enter();
latch.await();
}
static int roundUp(int num, int divisor) {
return (num + divisor - 1) / divisor;
}
private Http2Connection connectWithSettings(boolean client, Settings settings) throws Exception {
peer.setClient(client);
peer.sendFrame().settings(settings);
peer.acceptFrame(); // ACK
peer.play();
return connect(peer);
}
private Http2Connection connect(MockHttp2Peer peer) throws Exception {
return connect(peer, IGNORE, REFUSE_INCOMING_STREAMS);
}
/** Builds a new connection to {@code peer} with settings acked. */
private Http2Connection connect(MockHttp2Peer peer, PushObserver pushObserver,
Http2Connection.Listener listener) throws Exception {
Http2Connection connection = new Http2Connection.Builder(true)
.socket(peer.openSocket())
.pushObserver(pushObserver)
.listener(listener)
.build();
connection.start(false);
// verify the peer received the ACK
InFrame ackFrame = peer.takeFrame();
assertEquals(Http2.TYPE_SETTINGS, ackFrame.type);
assertEquals(0, ackFrame.streamId);
assertTrue(ackFrame.ack);
return connection;
}
static final PushObserver IGNORE = new PushObserver() {
@Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
return false;
}
@Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
return false;
}
@Override public boolean onData(int streamId, BufferedSource source, int byteCount,
boolean last) throws IOException {
source.skip(byteCount);
return false;
}
@Override public void onReset(int streamId, ErrorCode errorCode) {
}
};
private static class RecordingPushObserver implements PushObserver {
final List<Object> events = new ArrayList<>();
public synchronized Object takeEvent() throws InterruptedException {
while (events.isEmpty()) {
wait();
}
return events.remove(0);
}
@Override public synchronized boolean onRequest(int streamId, List<Header> requestHeaders) {
assertEquals(2, streamId);
events.add(requestHeaders);
notifyAll();
return false;
}
@Override public synchronized boolean onHeaders(
int streamId, List<Header> responseHeaders, boolean last) {
assertEquals(2, streamId);
assertTrue(last);
events.add(responseHeaders);
notifyAll();
return false;
}
@Override public synchronized boolean onData(
int streamId, BufferedSource source, int byteCount, boolean last) {
events.add(new AssertionError("onData"));
notifyAll();
return false;
}
@Override public synchronized void onReset(int streamId, ErrorCode errorCode) {
events.add(new AssertionError("onReset"));
notifyAll();
}
}
}