From c260486e788ef057909ff4dde813522a5816d2b1 Mon Sep 17 00:00:00 2001 From: Geoff Kizer Date: Thu, 13 Dec 2018 13:28:21 -0800 Subject: [PATCH] improve SETTINGS handling and other small changes --- .../tests/System/Net/Http/Http2Frames.cs | 64 +++++++++ .../System/Net/Http/Http2LoopbackServer.cs | 36 ++++- .../SocketsHttpHandler/Http2Connection.cs | 114 ++++++++++++--- .../Http/SocketsHttpHandler/Http2Stream.cs | 105 ++++++++------ .../SocketsHttpHandlerTest.Http2.cs | 131 +++++++++--------- 5 files changed, 318 insertions(+), 132 deletions(-) diff --git a/src/Common/tests/System/Net/Http/Http2Frames.cs b/src/Common/tests/System/Net/Http/Http2Frames.cs index c6fefaff7336..0dfb4f745560 100644 --- a/src/Common/tests/System/Net/Http/Http2Frames.cs +++ b/src/Common/tests/System/Net/Http/Http2Frames.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers.Binary; +using System.Collections.Generic; namespace System.Net.Test.Common { @@ -37,6 +38,16 @@ public enum FrameFlags : byte ValidBits = 0b00101101 } + public enum SettingId : ushort + { + HeaderTableSize = 0x1, + EnablePush = 0x2, + MaxConcurrentStreams = 0x3, + InitialWindowSize = 0x4, + MaxFrameSize = 0x5, + MaxHeaderListSize = 0x6 + } + public class Frame { public int Length; @@ -332,4 +343,57 @@ public override string ToString() return base.ToString() + $"\nUpdateSize: {UpdateSize}"; } } + + public struct SettingsEntry + { + public SettingId SettingId; + public uint Value; + } + + public class SettingsFrame : Frame + { + public List Entries; + + public SettingsFrame(params SettingsEntry[] entries) : + base(entries.Length * 6, FrameType.Settings, FrameFlags.None, 0) + { + Entries = new List(entries); + } + + public static SettingsFrame ReadFrom(Frame header, ReadOnlySpan buffer) + { + var entries = new List(); + + while (buffer.Length > 0) + { + SettingId id = (SettingId)BinaryPrimitives.ReadUInt16BigEndian(buffer); + buffer = buffer.Slice(2); + uint value = BinaryPrimitives.ReadUInt32BigEndian(buffer); + buffer = buffer.Slice(4); + + entries.Add(new SettingsEntry { SettingId = id, Value = value }); + } + + return new SettingsFrame(entries.ToArray()); + } + + public override void WriteTo(Span buffer) + { + base.WriteTo(buffer); + buffer = buffer.Slice(Frame.FrameHeaderLength); + + foreach (SettingsEntry entry in Entries) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)entry.SettingId); + buffer = buffer.Slice(2); + BinaryPrimitives.WriteUInt32BigEndian(buffer, entry.Value); + buffer = buffer.Slice(4); + } + } + + public override string ToString() + { + return base.ToString() + $"\nEntry Count: {Entries.Count}"; + } + } } diff --git a/src/Common/tests/System/Net/Http/Http2LoopbackServer.cs b/src/Common/tests/System/Net/Http/Http2LoopbackServer.cs index 395446fb654b..ce6e7bac0bc5 100644 --- a/src/Common/tests/System/Net/Http/Http2LoopbackServer.cs +++ b/src/Common/tests/System/Net/Http/Http2LoopbackServer.cs @@ -20,6 +20,7 @@ public class Http2LoopbackServer : IDisposable private Stream _connectionStream; private Http2Options _options; private Uri _uri; + private bool _ignoreSettingsAck; public Uri Address { @@ -113,6 +114,12 @@ public async Task ReadFrameAsync(TimeSpan timeout) } } + if (_ignoreSettingsAck && header.Type == FrameType.Settings && header.Flags == FrameFlags.Ack) + { + _ignoreSettingsAck = false; + return await ReadFrameAsync(timeout); + } + // Construct the correct frame type and return it. switch (header.Type) { @@ -176,7 +183,7 @@ public async Task AcceptConnectionAsync() } // Accept connection and handle connection setup - public async Task EstablishConnectionAsync() + public async Task EstablishConnectionAsync(params SettingsEntry[] settingsEntries) { await AcceptConnectionAsync(); @@ -184,19 +191,34 @@ public async Task EstablishConnectionAsync() Frame receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30)); Assert.Equal(FrameType.Settings, receivedFrame.Type); Assert.Equal(FrameFlags.None, receivedFrame.Flags); + Assert.Equal(0, receivedFrame.StreamId); + + // Receive the initial client window update frame. + receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30)); + Assert.Equal(FrameType.WindowUpdate, receivedFrame.Type); + Assert.Equal(FrameFlags.None, receivedFrame.Flags); + Assert.Equal(0, receivedFrame.StreamId); // Send the initial server settings frame. - Frame emptySettings = new Frame(0, FrameType.Settings, FrameFlags.None, 0); - await WriteFrameAsync(emptySettings).ConfigureAwait(false); + SettingsFrame settingsFrame = new SettingsFrame(settingsEntries); + await WriteFrameAsync(settingsFrame).ConfigureAwait(false); // Send the client settings frame ACK. Frame settingsAck = new Frame(0, FrameType.Settings, FrameFlags.Ack, 0); await WriteFrameAsync(settingsAck).ConfigureAwait(false); - // Receive the server settings frame ACK. - receivedFrame = await ReadFrameAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(FrameType.Settings, receivedFrame.Type); - Assert.True(receivedFrame.AckFlag); + // The client will send us a SETTINGS ACK eventually, but not necessarily right away. + // To simplify frame processing, set this flag to true so we will ignore the next SETTINGS ACK in ReadNextFrame. + _ignoreSettingsAck = true; + } + + public async Task ReadRequestHeaderAsync() + { + // Receive HEADERS frame for request. + Frame frame = await ReadFrameAsync(TimeSpan.FromSeconds(30)); + Assert.Equal(FrameType.Headers, frame.Type); + Assert.Equal(FrameFlags.EndHeaders | FrameFlags.EndStream, frame.Flags); + return frame.StreamId; } public async Task SendDefaultResponseAsync(int streamId) diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 720bbcb55d9c..fb10d3071782 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -47,6 +47,11 @@ internal sealed partial class Http2Connection : HttpConnectionBase, IDisposable private const int InitialWindowSize = 65535; + // We don't really care about limiting control flow at the connection level. + // We limit it per stream, and the user controls how many streams are created. + // So set the connection window size to a large value. + private const int ConnectionWindowSize = 64 * 1024 * 1024; + public Http2Connection(HttpConnectionPool pool, SslStream stream) { _pool = pool; @@ -68,36 +73,39 @@ public Http2Connection(HttpConnectionPool pool, SslStream stream) public async Task SetupAsync() { + _outgoingBuffer.EnsureAvailableSpace(s_http2ConnectionPreface.Length + + FrameHeader.Size + (FrameHeader.SettingLength * 2) + + FrameHeader.Size + FrameHeader.WindowUpdateLength); + // Send connection preface - _outgoingBuffer.EnsureAvailableSpace(s_http2ConnectionPreface.Length); s_http2ConnectionPreface.AsSpan().CopyTo(_outgoingBuffer.AvailableSpan); _outgoingBuffer.Commit(s_http2ConnectionPreface.Length); - // Send empty settings frame - _outgoingBuffer.EnsureAvailableSpace(FrameHeader.Size); - WriteFrameHeader(new FrameHeader(0, FrameType.Settings, FrameFlags.None, 0)); + // Send SETTINGS frame + WriteFrameHeader(new FrameHeader(FrameHeader.SettingLength * 2, FrameType.Settings, FrameFlags.None, 0)); - // TODO: ISSUE 31295: We should disable PUSH_PROMISE here. + // First setting: Disable push promise + BinaryPrimitives.WriteUInt16BigEndian(_outgoingBuffer.AvailableSpan, (ushort)SettingId.EnablePush); + _outgoingBuffer.Commit(2); + BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, 0); + _outgoingBuffer.Commit(4); - // TODO: ISSUE 31298: We should send a connection-level WINDOW_UPDATE to allow - // a large amount of data to be received on the connection. - // We don't care that much about connection-level flow control, we'll manage it per-stream. + // Second setting: Set header table size to 0 to disable dynamic header compression + BinaryPrimitives.WriteUInt16BigEndian(_outgoingBuffer.AvailableSpan, (ushort)SettingId.HeaderTableSize); + _outgoingBuffer.Commit(2); + BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, 0); + _outgoingBuffer.Commit(4); + + // Send initial connection-level WINDOW_UPDATE + WriteFrameHeader(new FrameHeader(FrameHeader.WindowUpdateLength, FrameType.WindowUpdate, FrameFlags.None, 0)); + BinaryPrimitives.WriteUInt32BigEndian(_outgoingBuffer.AvailableSpan, (ConnectionWindowSize - InitialWindowSize)); + _outgoingBuffer.Commit(4); await _stream.WriteAsync(_outgoingBuffer.ActiveMemory).ConfigureAwait(false); _outgoingBuffer.Discard(_outgoingBuffer.ActiveMemory.Length); _expectingSettingsAck = true; - // Receive the initial SETTINGS frame from the peer. - FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false); - if (frameHeader.Type != FrameType.Settings || frameHeader.AckFlag) - { - throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError); - } - - // Process the SETTINGS frame. This will send an ACK. - ProcessSettingsFrame(frameHeader); - ProcessIncomingFrames(); } @@ -155,9 +163,20 @@ private async void ProcessIncomingFrames() { try { + // Receive the initial SETTINGS frame from the peer. + FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false); + if (frameHeader.Type != FrameType.Settings || frameHeader.AckFlag) + { + throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError); + } + + // Process the SETTINGS frame. This will send an ACK. + ProcessSettingsFrame(frameHeader); + + // Keep processing frames as they arrive. while (true) { - FrameHeader frameHeader = await ReadFrameAsync().ConfigureAwait(false); + frameHeader = await ReadFrameAsync().ConfigureAwait(false); switch (frameHeader.Type) { @@ -367,10 +386,48 @@ private void ProcessSettingsFrame(FrameHeader frameHeader) throw new Http2ProtocolException(Http2ProtocolErrorCode.FrameSizeError); } - // Just eat settings for now + // Parse settings and process the ones we care about. + ReadOnlySpan settings = _incomingBuffer.ActiveSpan.Slice(0, frameHeader.Length); + while (settings.Length > 0) + { + Debug.Assert((settings.Length % 6) == 0); + + ushort settingId = BinaryPrimitives.ReadUInt16BigEndian(settings); + settings = settings.Slice(2); + uint settingValue = BinaryPrimitives.ReadUInt32BigEndian(settings); + settings = settings.Slice(4); + + switch ((SettingId)settingId) + { + case SettingId.MaxConcurrentStreams: + // ISSUE 31296: Handle SETTINGS_MAX_CONCURRENT_STREAMS. + break; + + case SettingId.InitialWindowSize: + if (settingValue > 0x7FFFFFFF) + { + throw new Http2ProtocolException(Http2ProtocolErrorCode.FlowControlError); + } + + // ISSUE 34059: Handle SETTINGS_MAX_CONCURRENT_STREAMS. + break; + + case SettingId.MaxFrameSize: + if (settingValue < 16384 || settingValue > 16777215) + { + throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError); + } + + // We don't actually store this value; we always send frames of the minimum size (16K). + break; + + default: + // All others are ignored because we don't care about them. + // Note, per RFC, unknown settings IDs should be ignored. + break; + } + } - // TODO: ISSUE 31296: We should handle SETTINGS_MAX_CONCURRENT_STREAMS. - // Others we don't care about, or are advisory. _incomingBuffer.Discard(frameHeader.Length); // Send acknowledgement @@ -911,6 +968,7 @@ private struct FrameHeader public const int Size = 9; public const int MaxLength = 16384; + public const int SettingLength = 6; // per setting (total SETTINGS length must be a multiple of this) public const int PriorityInfoLength = 5; // for both PRIORITY frame and priority info within HEADERS public const int PingLength = 8; public const int WindowUpdateLength = 4; @@ -919,7 +977,6 @@ private struct FrameHeader public FrameHeader(int length, FrameType type, FrameFlags flags, int streamId) { - Debug.Assert(length <= MaxLength); Debug.Assert(streamId >= 0); Length = length; @@ -950,6 +1007,7 @@ public void WriteTo(Span buffer) Debug.Assert(buffer.Length >= Size); Debug.Assert(Type <= FrameType.Last); Debug.Assert((Flags & FrameFlags.ValidBits) == Flags); + Debug.Assert(Length <= MaxLength); buffer[0] = (byte)((Length & 0x00FF0000) >> 16); buffer[1] = (byte)((Length & 0x0000FF00) >> 8); @@ -981,6 +1039,16 @@ private enum FrameFlags : byte ValidBits = 0b00101101 } + private enum SettingId : ushort + { + HeaderTableSize = 0x1, + EnablePush = 0x2, + MaxConcurrentStreams = 0x3, + InitialWindowSize = 0x4, + MaxFrameSize = 0x5, + MaxHeaderListSize = 0x6 + } + // Note that this is safe to be called concurrently by multiple threads. public sealed override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index f247c39cc8a1..262d96f0edd0 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -52,63 +52,88 @@ public async Task SendAsync(HttpRequestMessage request, Can { // TODO: ISSUE 31310: Cancellation support - HttpConnectionResponseContent responseContent = new HttpConnectionResponseContent(); - _response = new HttpResponseMessage() { Version = HttpVersion.Version20, RequestMessage = request, Content = responseContent }; + try + { + HttpConnectionResponseContent responseContent = new HttpConnectionResponseContent(); + _response = new HttpResponseMessage() { Version = HttpVersion.Version20, RequestMessage = request, Content = responseContent }; - // TODO: ISSUE 31312: Expect: 100-continue and early response handling - // Note that in an "early response" scenario, where we get a response before we've finished sending the request body - // (either with a 100-continue that timed out, or without 100-continue), - // we can stop send a RST_STREAM on the request stream and stop sending the request without tearing down the entire connection. + // TODO: ISSUE 31312: Expect: 100-continue and early response handling + // Note that in an "early response" scenario, where we get a response before we've finished sending the request body + // (either with a 100-continue that timed out, or without 100-continue), + // we can stop send a RST_STREAM on the request stream and stop sending the request without tearing down the entire connection. - // TODO: ISSUE 31313: Avoid allocating a TaskCompletionSource repeatedly by using a resettable ValueTaskSource. - // See: https://github.com/dotnet/corefx/blob/master/src/Common/tests/System/Threading/Tasks/Sources/ManualResetValueTaskSource.cs - Debug.Assert(_responseDataAvailable == null); - _responseDataAvailable = new TaskCompletionSource(); - Task readDataAvailableTask = _responseDataAvailable.Task; + // TODO: ISSUE 31313: Avoid allocating a TaskCompletionSource repeatedly by using a resettable ValueTaskSource. + // See: https://github.com/dotnet/corefx/blob/master/src/Common/tests/System/Threading/Tasks/Sources/ManualResetValueTaskSource.cs + Debug.Assert(_responseDataAvailable == null); + _responseDataAvailable = new TaskCompletionSource(); + Task readDataAvailableTask = _responseDataAvailable.Task; - // Send headers - await _connection.SendHeadersAsync(_streamId, request).ConfigureAwait(false); + // Send headers + await _connection.SendHeadersAsync(_streamId, request).ConfigureAwait(false); - // Send request body, if any - if (request.Content != null) - { - using (Http2WriteStream writeStream = new Http2WriteStream(this)) + // Send request body, if any + if (request.Content != null) { - await request.Content.CopyToAsync(writeStream).ConfigureAwait(false); + using (Http2WriteStream writeStream = new Http2WriteStream(this)) + { + await request.Content.CopyToAsync(writeStream).ConfigureAwait(false); + } } - } - // Wait for response headers to be read. - await readDataAvailableTask.ConfigureAwait(false); + // Wait for response headers to be read. + await readDataAvailableTask.ConfigureAwait(false); - // Start to process the response body. - bool emptyResponse = false; - lock (_syncObject) - { - if (_responseComplete && _responseBuffer.ActiveSpan.Length == 0) + // Start to process the response body. + bool emptyResponse = false; + lock (_syncObject) { - if (_responseAborted) + if (_responseComplete && _responseBuffer.ActiveSpan.Length == 0) { - throw new IOException(SR.net_http_invalid_response); + if (_responseAborted) + { + throw new IOException(SR.net_http_invalid_response); + } + + emptyResponse = true; } + } - emptyResponse = true; + if (emptyResponse) + { + responseContent.SetStream(EmptyReadStream.Instance); + } + else + { + responseContent.SetStream(new Http2ReadStream(this)); } - } - if (emptyResponse) - { - responseContent.SetStream(EmptyReadStream.Instance); + // Process Set-Cookie headers. + if (_connection._pool.Settings._useCookies) + { + CookieHelper.ProcessReceivedCookies(_response, _connection._pool.Settings._cookieContainer); + } } - else + catch (Exception e) { - responseContent.SetStream(new Http2ReadStream(this)); - } + Dispose(); - // Process Set-Cookie headers. - if (_connection._pool.Settings._useCookies) - { - CookieHelper.ProcessReceivedCookies(_response, _connection._pool.Settings._cookieContainer); + if (e is IOException ioe) + { + throw new HttpRequestException(SR.net_http_client_execution_error, ioe); + } + else if (e is ObjectDisposedException) + { + throw new HttpRequestException(SR.net_http_client_execution_error); + } + else if (e is Http2ProtocolException) + { + // ISSUE 31315: Determine if/how to expose HTTP2 error codes + throw new HttpRequestException(SR.net_http_client_execution_error); + } + else + { + throw e; + } } return _response; diff --git a/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2.cs b/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2.cs index bb05090a792f..e528a87203b3 100644 --- a/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2.cs +++ b/src/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2.cs @@ -55,14 +55,20 @@ public async Task Http2_InitialSettings_SentAndAcked() await server.WriteFrameAsync(emptySettings).ConfigureAwait(false); // Receive the server settings frame ACK. - receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(FrameType.Settings, receivedFrame.Type); - Assert.True(receivedFrame.AckFlag); + // This doesn't have to be the next frame, as the client is allowed to send before receiving our SETTINGS frame. + // So, loop until we see it (or the timeout expires) + while (true) + { + receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + if (receivedFrame.Type == FrameType.Settings && receivedFrame.AckFlag) + { + break; + } + } } } [ConditionalFact(nameof(SupportsAlpn))] - [ActiveIssue(31315)] public async Task Http2_DataSentBeforeServerPreface_ProtocolError() { HttpClientHandler handler = CreateHttpClientHandler(); @@ -79,14 +85,12 @@ public async Task Http2_DataSentBeforeServerPreface_ProtocolError() DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.Padded, 10, 1); await server.WriteFrameAsync(invalidFrame); - // This currently throws an Http2ProtocolException, but that type is not public. await Assert.ThrowsAsync(async () => await sendTask); } } [ConditionalFact(nameof(SupportsAlpn))] - [ActiveIssue(31315)] - public async Task Http2_StreamResetByServer_RequestFails() + public async Task Http2_ServerSendsValidSettingsValues_Success() { HttpClientHandler handler = CreateHttpClientHandler(); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -94,26 +98,55 @@ public async Task Http2_StreamResetByServer_RequestFails() using (var server = Http2LoopbackServer.CreateServer()) using (var client = new HttpClient(handler)) { - Task sendTask = client.GetAsync(server.Address); + Task sendTask = client.GetAsync(server.Address); + + // Send a bunch of valid SETTINGS values (that won't interfere with processing requests) + await server.EstablishConnectionAsync( + new SettingsEntry { SettingId = SettingId.HeaderTableSize, Value = 0 }, + new SettingsEntry { SettingId = SettingId.HeaderTableSize, Value = 1 }, + new SettingsEntry { SettingId = SettingId.HeaderTableSize, Value = 345678 }, + new SettingsEntry { SettingId = SettingId.InitialWindowSize, Value = 0 }, + new SettingsEntry { SettingId = SettingId.InitialWindowSize, Value = 1 }, + new SettingsEntry { SettingId = SettingId.InitialWindowSize, Value = 4567890 }, + new SettingsEntry { SettingId = SettingId.MaxConcurrentStreams, Value = 1 }, + new SettingsEntry { SettingId = SettingId.MaxFrameSize, Value = 16384 }, + new SettingsEntry { SettingId = SettingId.MaxFrameSize, Value = 16777215 }, + new SettingsEntry { SettingId = SettingId.MaxHeaderListSize, Value = 0 }, + new SettingsEntry { SettingId = SettingId.MaxHeaderListSize, Value = 10000000 }, + new SettingsEntry { SettingId = (SettingId)5678, Value = 1234 }); + + int streamId = await server.ReadRequestHeaderAsync(); - await server.AcceptConnectionAsync(); - await server.SendConnectionPrefaceAsync(); + await server.SendDefaultResponseAsync(streamId); - // Receive the request header frame. - Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } - // Send a reset stream frame so that stream 1 moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.Padded, 0x1, 1); - await server.WriteFrameAsync(resetStream); + [ConditionalTheory(nameof(SupportsAlpn))] + [InlineData(SettingId.MaxFrameSize, 16383)] + [InlineData(SettingId.MaxFrameSize, 162777216)] + [InlineData(SettingId.InitialWindowSize, 0x80000000)] + public async Task Http2_ServerSendsInvalidSettingsValue_ProtocolError(SettingId settingId, uint value) + { + HttpClientHandler handler = CreateHttpClientHandler(); + handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; + + using (var server = Http2LoopbackServer.CreateServer()) + using (var client = new HttpClient(handler)) + { + Task sendTask = client.GetAsync(server.Address); + + // Send invalid initial SETTINGS value + await server.EstablishConnectionAsync(new SettingsEntry { SettingId = settingId, Value = value }); - // This currently throws an IOException. await Assert.ThrowsAsync(async () => await sendTask); } } [ConditionalFact(nameof(SupportsAlpn))] - [ActiveIssue(31394)] - public async Task DataFrame_NoStream_ConnectionError() + public async Task Http2_StreamResetByServer_RequestFails() { HttpClientHandler handler = CreateHttpClientHandler(); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -123,28 +156,19 @@ public async Task DataFrame_NoStream_ConnectionError() { Task sendTask = client.GetAsync(server.Address); - await server.AcceptConnectionAsync(); - await server.SendConnectionPrefaceAsync(); - - // Receive the request header frame. - Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + await server.EstablishConnectionAsync(); + await server.ReadRequestHeaderAsync(); - // Send a malformed frame. - DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.None, 0, 0); - await server.WriteFrameAsync(invalidFrame); + // Send a reset stream frame so that stream 1 moves to a terminal state. + RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.Padded, 0x1, 1); + await server.WriteFrameAsync(resetStream); - // As this is a connection level error, the client should see the request fail. await Assert.ThrowsAsync(async () => await sendTask); - - // The server should receive a GOAWAY frame. - receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(FrameType.GoAway, receivedFrame.Type); } } [ConditionalFact(nameof(SupportsAlpn))] - [ActiveIssue(31520)] - public async Task DataFrame_TooLong_ConnectionError() + public async Task DataFrame_NoStream_ConnectionError() { HttpClientHandler handler = CreateHttpClientHandler(); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -154,28 +178,20 @@ public async Task DataFrame_TooLong_ConnectionError() { Task sendTask = client.GetAsync(server.Address); - await server.AcceptConnectionAsync(); - await server.SendConnectionPrefaceAsync(); - - // Receive the request header frame. - Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + await server.EstablishConnectionAsync(); + await server.ReadRequestHeaderAsync(); - // Send a malformed frame. - DataFrame invalidFrame = new DataFrame(new byte[Frame.MaxFrameLength + 1], FrameFlags.None, 0, 0); + // Send a malformed frame (streamId is 0) + DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.None, 0, 0); await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. await Assert.ThrowsAsync(async () => await sendTask); - - // The server should receive a GOAWAY frame. - receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(FrameType.GoAway, receivedFrame.Type); } } [ConditionalFact(nameof(SupportsAlpn))] - [ActiveIssue(31315)] - public async Task DataFrame_PaddingOnly_ResetsStream() + public async Task DataFrame_TooLong_ConnectionError() { HttpClientHandler handler = CreateHttpClientHandler(); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -185,21 +201,15 @@ public async Task DataFrame_PaddingOnly_ResetsStream() { Task sendTask = client.GetAsync(server.Address); - await server.AcceptConnectionAsync(); - await server.SendConnectionPrefaceAsync(); - - // Receive the request header frame. - Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + await server.EstablishConnectionAsync(); + await server.ReadRequestHeaderAsync(); // Send a malformed frame. - DataFrame invalidFrame = new DataFrame(new byte[0], FrameFlags.Padded, 10, 1); + DataFrame invalidFrame = new DataFrame(new byte[Frame.MaxFrameLength + 1], FrameFlags.None, 0, 0); await server.WriteFrameAsync(invalidFrame); + // As this is a connection level error, the client should see the request fail. await Assert.ThrowsAsync(async () => await sendTask); - - // Receive a RST_STREAM frame. - receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); - Assert.Equal(FrameType.RstStream, receivedFrame.Type); } } @@ -215,11 +225,8 @@ public async Task ClosedStream_FrameReceived_ResetsStream() { Task sendTask = client.GetAsync(server.Address); - await server.AcceptConnectionAsync(); - await server.SendConnectionPrefaceAsync(); - - // Receive the request header frame. - Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + await server.EstablishConnectionAsync(); + await server.ReadRequestHeaderAsync(); // Send a reset stream frame so that stream 1 moves to a terminal state. RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.Padded, 0x1, 1); @@ -230,7 +237,7 @@ public async Task ClosedStream_FrameReceived_ResetsStream() await server.WriteFrameAsync(invalidFrame); // Receive a RST_STREAM frame. - receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); + Frame receivedFrame = await server.ReadFrameAsync(TimeSpan.FromSeconds(30)); Assert.Equal(FrameType.RstStream, receivedFrame.Type); await Assert.ThrowsAsync(async () => await sendTask);