Skip to content

Commit 97a8f6c

Browse files
oldergodswankjesse
andauthored
Confirm we can read a response that completed before RST_STREAM (#6293) (#6914)
The server can reject the request without breaking the response. (cherry picked from commit 480c20e) Co-authored-by: Jesse Wilson <[email protected]>
1 parent b1a39f4 commit 97a8f6c

File tree

2 files changed

+112
-15
lines changed

2 files changed

+112
-15
lines changed

okhttp/src/main/kotlin/okhttp3/internal/http2/Http2Stream.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
*/
1616
package okhttp3.internal.http2
1717

18-
import java.io.EOFException
19-
import java.io.IOException
20-
import java.io.InterruptedIOException
21-
import java.net.SocketTimeoutException
22-
import java.util.ArrayDeque
2318
import okhttp3.Headers
2419
import okhttp3.internal.EMPTY_HEADERS
2520
import okhttp3.internal.assertThreadDoesntHoldLock
@@ -32,6 +27,11 @@ import okio.BufferedSource
3227
import okio.Sink
3328
import okio.Source
3429
import okio.Timeout
30+
import java.io.EOFException
31+
import java.io.IOException
32+
import java.io.InterruptedIOException
33+
import java.net.SocketTimeoutException
34+
import java.util.ArrayDeque
3535

3636
/** A logical bidirectional stream. */
3737
@Suppress("NAME_SHADOWING")
@@ -61,7 +61,7 @@ class Http2Stream internal constructor(
6161
var writeBytesMaximum: Long = connection.peerSettings.initialWindowSize.toLong()
6262
internal set
6363

64-
/** Received headers yet to be [taken][takeHeaders], or [read][FramingSource.read]. */
64+
/** Received headers yet to be [taken][takeHeaders]. */
6565
private val headersQueue = ArrayDeque<Headers>()
6666

6767
/** True if response headers have been sent or received. */
@@ -154,13 +154,13 @@ class Http2Stream internal constructor(
154154
*/
155155
@Synchronized @Throws(IOException::class)
156156
fun trailers(): Headers {
157+
if (source.finished && source.receiveBuffer.exhausted() && source.readBuffer.exhausted()) {
158+
return source.trailers ?: EMPTY_HEADERS
159+
}
157160
if (errorCode != null) {
158161
throw errorException ?: StreamResetException(errorCode!!)
159162
}
160-
check(source.finished && source.receiveBuffer.exhausted() && source.readBuffer.exhausted()) {
161-
"too early; can't read the trailers yet"
162-
}
163-
return source.trailers ?: EMPTY_HEADERS
163+
throw IllegalStateException("too early; can't read the trailers yet")
164164
}
165165

166166
/**
@@ -276,10 +276,7 @@ class Http2Stream internal constructor(
276276
this.source.receive(source, length.toLong())
277277
}
278278

279-
/**
280-
* Accept headers from the network and store them until the client calls [takeHeaders], or
281-
* [FramingSource.read] them.
282-
*/
279+
/** Accept headers from the network and store them until the client calls [takeHeaders]. */
283280
fun receiveHeaders(headers: Headers, inFinished: Boolean) {
284281
this@Http2Stream.assertThreadDoesntHoldLock()
285282

@@ -560,7 +557,7 @@ class Http2Stream internal constructor(
560557
checkOutNotClosed() // Kick out if the stream was reset or closed while waiting.
561558
toWrite = minOf(writeBytesMaximum - writeBytesTotal, sendBuffer.size)
562559
writeBytesTotal += toWrite
563-
outFinished = outFinishedOnLastFrame && toWrite == sendBuffer.size && errorCode == null
560+
outFinished = outFinishedOnLastFrame && toWrite == sendBuffer.size
564561
}
565562

566563
writeTimeout.enter()
@@ -578,6 +575,7 @@ class Http2Stream internal constructor(
578575
synchronized(this@Http2Stream) {
579576
checkOutNotClosed()
580577
}
578+
// TODO(jwilson): flush the connection?!
581579
while (sendBuffer.size > 0L) {
582580
emitFrame(false)
583581
connection.flush()

okhttp/src/test/java/okhttp3/internal/http2/Http2ConnectionTest.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,105 @@ public final class Http2ConnectionTest {
610610
assertThat(synStream.headerBlock).isEqualTo(headerEntries("a", "artichaut"));
611611
}
612612

613+
/** A server RST_STREAM shouldn't prevent the client from consuming the response body. */
614+
@Test public void serverResponseBodyRstStream() throws Exception {
615+
// write the mocking script
616+
peer.sendFrame().settings(new Settings());
617+
peer.acceptFrame(); // ACK
618+
peer.acceptFrame(); // SYN_STREAM
619+
peer.acceptFrame(); // PING
620+
peer.sendFrame().headers(false, 3, headerEntries("a", "android"));
621+
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("robot"), 5);
622+
peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR);
623+
peer.sendFrame().ping(true, AWAIT_PING, 0); // PONG
624+
peer.play();
625+
626+
// play it back
627+
Http2Connection connection = connect(peer);
628+
Http2Stream stream = connection.newStream(headerEntries(), false);
629+
connection.writePingAndAwaitPong();
630+
assertThat(stream.takeHeaders()).isEqualTo(Headers.of("a", "android"));
631+
BufferedSource source = Okio.buffer(stream.getSource());
632+
assertThat(source.readUtf8(5)).isEqualTo("robot");
633+
stream.getSink().close();
634+
assertThat(connection.openStreamCount()).isEqualTo(0);
635+
636+
// verify the peer received what was expected
637+
InFrame synStream = peer.takeFrame();
638+
assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS);
639+
InFrame ping = peer.takeFrame();
640+
assertThat(ping.type).isEqualTo(Http2.TYPE_PING);
641+
}
642+
643+
/** A server RST_STREAM shouldn't prevent the client from consuming trailers. */
644+
@Test public void serverTrailersRstStream() throws Exception {
645+
// write the mocking script
646+
peer.sendFrame().settings(new Settings());
647+
peer.acceptFrame(); // ACK
648+
peer.acceptFrame(); // SYN_STREAM
649+
peer.acceptFrame(); // PING
650+
peer.sendFrame().headers(false, 3, headerEntries("a", "android"));
651+
peer.sendFrame().headers(true, 3, headerEntries("z", "zebra"));
652+
peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR);
653+
peer.sendFrame().ping(true, AWAIT_PING, 0); // PONG
654+
peer.play();
655+
656+
// play it back
657+
Http2Connection connection = connect(peer);
658+
Http2Stream stream = connection.newStream(headerEntries(), true);
659+
connection.writePingAndAwaitPong();
660+
assertThat(stream.takeHeaders()).isEqualTo(Headers.of("a", "android"));
661+
stream.getSink().close();
662+
assertThat(stream.trailers()).isEqualTo(Headers.of("z", "zebra"));
663+
assertThat(connection.openStreamCount()).isEqualTo(0);
664+
665+
// verify the peer received what was expected
666+
InFrame synStream = peer.takeFrame();
667+
assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS);
668+
InFrame ping = peer.takeFrame();
669+
assertThat(ping.type).isEqualTo(Http2.TYPE_PING);
670+
}
671+
672+
/**
673+
* A server RST_STREAM shouldn't prevent the client from consuming the response body, even if it
674+
* follows a truncated request body.
675+
*/
676+
@Test public void clientRequestBodyServerResponseBodyRstStream() throws Exception {
677+
// write the mocking script
678+
peer.sendFrame().settings(new Settings());
679+
peer.acceptFrame(); // ACK
680+
peer.acceptFrame(); // SYN_STREAM
681+
peer.acceptFrame(); // PING
682+
peer.sendFrame().headers(false, 3, headerEntries("a", "android"));
683+
peer.sendFrame().data(true, 3, new Buffer().writeUtf8("robot"), 5);
684+
peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR);
685+
peer.sendFrame().ping(true, AWAIT_PING, 0); // PONG
686+
peer.play();
687+
688+
// play it back
689+
Http2Connection connection = connect(peer);
690+
Http2Stream stream = connection.newStream(headerEntries(), true);
691+
connection.writePingAndAwaitPong();
692+
BufferedSink sink = Okio.buffer(stream.getSink());
693+
sink.writeUtf8("abc");
694+
try {
695+
sink.close();
696+
fail();
697+
} catch (StreamResetException expected) {
698+
assertThat(expected.errorCode).isEqualTo(ErrorCode.NO_ERROR);
699+
}
700+
assertThat(stream.takeHeaders()).isEqualTo(Headers.of("a", "android"));
701+
BufferedSource source = Okio.buffer(stream.getSource());
702+
assertThat(source.readUtf8(5)).isEqualTo("robot");
703+
assertThat(connection.openStreamCount()).isEqualTo(0);
704+
705+
// verify the peer received what was expected
706+
InFrame synStream = peer.takeFrame();
707+
assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS);
708+
InFrame ping = peer.takeFrame();
709+
assertThat(ping.type).isEqualTo(Http2.TYPE_PING);
710+
}
711+
613712
@Test public void serverWritesTrailersWithData() throws Exception {
614713
// We buffer some outbound data and headers and confirm that the END_STREAM flag comes with the
615714
// headers (and not with the data).

0 commit comments

Comments
 (0)