Skip to content

Commit d6dd5ec

Browse files
authored
[http2] Gracefully receive headers on canceled streams (#1800)
* [http2] Gracefully receive headers on canceled streams If a client opens a stream and then cancels it, the server may have already sent response frames back. The HTTP/2 spec says that the client MUST be prepared to receive any such frames. We already handle this correctly for data frames, but prior to this patch, header frames on a canceled stream would cause the whole connection to be torn down. This patch fixes that. Fixes #1799. Test Plan: Unit tests included. Before this change, the new test would fail with "Connection is being forcefully terminated. (errorCode: 1)". wchargin-branch: http2-headers-after-cancel wchargin-source: 428af2c7cf2fc9f18884558e2b471d2d29a05998 * [http2-headers-after-cancel: update changelog and pubspec] wchargin-branch: http2-headers-after-cancel wchargin-source: 8aed54b8d5bcc0450db8f46ab08a8012f4de1bd8
1 parent d8e6929 commit d6dd5ec

File tree

4 files changed

+144
-7
lines changed

4 files changed

+144
-7
lines changed

pkgs/http2/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.0.1-wip
2+
3+
- Gracefully handle receiving headers on a stream that the client has canceled. (#1799)
4+
15
## 3.0.0
26

37
- Require Dart SDK `3.7.0`.

pkgs/http2/lib/src/streams/stream_handler.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -613,12 +613,12 @@ class StreamHandler extends Object with TerminatableMixin, ClosableMixin {
613613
_handleHeadersFrame(newStream, frame);
614614
_newStreamsC.add(newStream);
615615
} else {
616-
// A server cannot open new streams to the client. The only way
617-
// for a server to start a new stream is via a PUSH_PROMISE_FRAME.
618-
throw ProtocolException(
619-
'HTTP/2 clients cannot receive HEADER_FRAMEs as a connection'
620-
'attempt.',
621-
);
616+
// We must be able to receive header frames for streams that have
617+
// already been closed. This can occur if we send RST_STREAM while
618+
// the server already had header frames in flight.
619+
//
620+
// Still respond with an error, as the stream is closed.
621+
throw _throwStreamClosedException(frame.header.streamId);
622622
}
623623
} else if (frame is WindowUpdateFrame) {
624624
if (frameBelongsToIdleStream()) {

pkgs/http2/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: http2
2-
version: 3.0.0
2+
version: 3.0.1-wip
33
description: A HTTP/2 implementation in Dart.
44
repository: https://github.com/dart-lang/http/tree/master/pkgs/http2
55

pkgs/http2/test/client_test.dart

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,139 @@ void main() {
426426
await Future.wait([serverFun(), clientFun()]);
427427
});
428428

429+
clientTest('header-frame-received-after-stream-cancel', (
430+
ClientTransportConnection client,
431+
FrameWriter serverWriter,
432+
StreamIterator<Frame> serverReader,
433+
Future<Frame> Function() nextFrame,
434+
) async {
435+
var handshakeCompleter = Completer<void>();
436+
var serverReceivedHeaders = Completer<void>();
437+
var cancelDone = Completer<void>();
438+
439+
Future serverFun() async {
440+
serverWriter.writeSettingsFrame([]);
441+
expect(await nextFrame(), isA<SettingsFrame>());
442+
serverWriter.writeSettingsAckFrame();
443+
expect(await nextFrame(), isA<SettingsFrame>());
444+
445+
handshakeCompleter.complete();
446+
447+
var headers1 = await nextFrame() as HeadersFrame;
448+
var streamId1 = headers1.header.streamId;
449+
expect(
450+
await nextFrame(),
451+
isA<DataFrame>().having(
452+
(p0) => p0.hasEndStreamFlag,
453+
'Last data frame',
454+
true,
455+
),
456+
);
457+
458+
var headers2 = await nextFrame() as HeadersFrame;
459+
var streamId2 = headers2.header.streamId;
460+
expect(
461+
await nextFrame(),
462+
isA<DataFrame>().having(
463+
(p0) => p0.hasEndStreamFlag,
464+
'Last data frame',
465+
true,
466+
),
467+
);
468+
469+
serverReceivedHeaders.complete();
470+
await cancelDone.future;
471+
expect(
472+
await nextFrame(),
473+
isA<RstStreamFrame>().having(
474+
(f) => f.header.streamId,
475+
'header.streamId',
476+
streamId1,
477+
),
478+
);
479+
480+
// Client has canceled, but we already had a response going out over
481+
// the wire...
482+
serverWriter.writeHeadersFrame(streamId1, [Header.ascii('e', 'f')]);
483+
// Client will send an extra [RstStreamFrame] in response to this
484+
// unexpected header. That's not required, but it is current
485+
// behavior, so advance past it.
486+
expect(
487+
await nextFrame(),
488+
isA<RstStreamFrame>().having(
489+
(f) => f.header.streamId,
490+
'header.streamId',
491+
streamId1,
492+
),
493+
);
494+
495+
// Respond on the second stream.
496+
var data2 = [43];
497+
serverWriter.writeDataFrame(streamId2, data2, endStream: true);
498+
serverWriter.writeRstStreamFrame(streamId2, ErrorCode.STREAM_CLOSED);
499+
500+
// The two WindowUpdateFrames for the data2 DataFrame.
501+
expect(
502+
await nextFrame(),
503+
isA<WindowUpdateFrame>().having(
504+
(p0) => p0.header.streamId,
505+
'Stream update',
506+
streamId2,
507+
),
508+
);
509+
expect(
510+
await nextFrame(),
511+
isA<WindowUpdateFrame>().having(
512+
(p0) => p0.header.streamId,
513+
'Connection update',
514+
0,
515+
),
516+
);
517+
518+
expect(await nextFrame(), isA<GoawayFrame>());
519+
expect(await serverReader.moveNext(), false);
520+
521+
await serverWriter.close();
522+
}
523+
524+
Future clientFun() async {
525+
await handshakeCompleter.future;
526+
527+
// First stream: we'll send data and then cancel quickly, but the
528+
// server will already have a response in flight.
529+
var stream1 = client.makeRequest([Header.ascii('a', 'b')]);
530+
await stream1.outgoingMessages.close();
531+
532+
// Second stream: server will respond only after we've canceled the
533+
// first stream.
534+
var stream2 = client.makeRequest([Header.ascii('c', 'd')]);
535+
await stream2.outgoingMessages.close();
536+
537+
await serverReceivedHeaders.future;
538+
stream1.terminate();
539+
cancelDone.complete();
540+
541+
var messages2 = await stream2.incomingMessages.toList();
542+
expect(messages2, hasLength(1));
543+
var message2 = messages2[0];
544+
expect(
545+
message2,
546+
isA<DataStreamMessage>().having(
547+
(p0) => p0.bytes,
548+
'Same as `data2` above',
549+
[43],
550+
),
551+
);
552+
553+
expect(client.isOpen, true);
554+
var future = client.finish();
555+
expect(client.isOpen, false);
556+
await future;
557+
}
558+
559+
await Future.wait([serverFun(), clientFun()]);
560+
});
561+
429562
clientTest('data-frame-received-after-stream-cancel', (
430563
ClientTransportConnection client,
431564
FrameWriter serverWriter,

0 commit comments

Comments
 (0)