From e4d9392200de68fe4f516f2a97abe956f80a3e08 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 26 Apr 2019 11:12:09 -0400 Subject: [PATCH 1/2] Clean up HTTP/2 protocol exception handling - Propagate the GOAWAY and RST_STREAM error codes to the calling code via an exception. - Propagate to the Http2Stream exceptions incurred in the Http2Connection and that result in an Abort. - Don't Dispose of the Http2Stream before we try to Cancel it. - Make Http2ProtocolException serializable, as we generally do that for all of our exception types. - Add tests that verify sufficient HTTP/2 protocol details are in the exception messages. - Clean up the error message for the exception. - Separate the error code and exception into their own files. - Leave Http2ProtocolException internal for now; we can expose it publicly when we design the all-up public error model for HttpClient. --- .../src/Resources/Strings.resx | 3 + .../src/System.Net.Http.csproj | 7 + .../SocketsHttpHandler/Http2Connection.cs | 98 +++------ .../Http2ProtocolErrorCode.cs | 44 ++++ .../Http2ProtocolException.cs | 69 +++++++ .../Http/SocketsHttpHandler/Http2Stream.cs | 9 +- .../HttpClientHandlerTest.Http2.cs | 190 +++++++++++------- .../HttpClientHandlerTest.TrailingHeaders.cs | 28 +++ 8 files changed, 305 insertions(+), 143 deletions(-) create mode 100644 src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolErrorCode.cs create mode 100644 src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolException.cs diff --git a/src/System.Net.Http/src/Resources/Strings.resx b/src/System.Net.Http/src/Resources/Strings.resx index 466670154b18..93cdd56ed5d6 100644 --- a/src/System.Net.Http/src/Resources/Strings.resx +++ b/src/System.Net.Http/src/Resources/Strings.resx @@ -443,6 +443,9 @@ Error {0} calling {1}, '{2}'. + + The HTTP/2 request failed with protocol error '{0}' (0x{1}). + This method is not implemented by this class. diff --git a/src/System.Net.Http/src/System.Net.Http.csproj b/src/System.Net.Http/src/System.Net.Http.csproj index 555d6d9e6041..f758dc86ab71 100644 --- a/src/System.Net.Http/src/System.Net.Http.csproj +++ b/src/System.Net.Http/src/System.Net.Http.csproj @@ -140,6 +140,8 @@ + + @@ -707,4 +709,9 @@ + + + Designer + + 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 f1924f6d9d08..029185f952d9 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 @@ -162,9 +162,9 @@ private async Task FlushOutgoingBytesAsync() { await _stream.WriteAsync(_outgoingBuffer.ActiveMemory).ConfigureAwait(false); } - catch (Exception) + catch (Exception e) { - Abort(); + Abort(e); throw; } finally @@ -251,9 +251,9 @@ private async void ProcessIncomingFrames() } } } - catch (Exception) + catch (Exception e) { - Abort(); + Abort(e); } } @@ -604,12 +604,11 @@ private void ProcessRstStreamFrame(FrameHeader frameHeader) return; } - _incomingBuffer.Discard(frameHeader.Length); + var protocolError = (Http2ProtocolErrorCode)BinaryPrimitives.ReadInt32BigEndian(_incomingBuffer.ActiveSpan); - // CONSIDER: We ignore the error code in the RST_STREAM frame. - // We could read this and report it to the user as part of the request exception. + _incomingBuffer.Discard(frameHeader.Length); - http2Stream.OnResponseAbort(); + http2Stream.OnResponseAbort(new Http2ProtocolException(protocolError)); RemoveStream(http2Stream); } @@ -630,9 +629,10 @@ private void ProcessGoAwayFrame(FrameHeader frameHeader) throw new Http2ProtocolException(Http2ProtocolErrorCode.ProtocolError); } - int lastValidStream = (int)((uint)((_incomingBuffer.ActiveSpan[0] << 24) | (_incomingBuffer.ActiveSpan[1] << 16) | (_incomingBuffer.ActiveSpan[2] << 8) | _incomingBuffer.ActiveSpan[3]) & 0x7FFFFFFF); + int lastValidStream = (int)(BinaryPrimitives.ReadUInt32BigEndian(_incomingBuffer.ActiveSpan) & 0x7FFFFFFF); + var errorCode = (Http2ProtocolErrorCode)BinaryPrimitives.ReadInt32BigEndian(_incomingBuffer.ActiveSpan.Slice(sizeof(int))); - AbortStreams(lastValidStream); + AbortStreams(lastValidStream, new Http2ProtocolException(errorCode)); _incomingBuffer.Discard(frameHeader.Length); } @@ -1113,11 +1113,11 @@ private void WriteFrameHeader(FrameHeader frameHeader) _outgoingBuffer.Commit(FrameHeader.Size); } - private void Abort() + private void Abort(Exception abortException) { // The connection has failed, e.g. failed IO or a connection-level frame error. // Abort all streams and cause further processing to fail. - AbortStreams(0); + AbortStreams(0, abortException); } private bool IsAborted() @@ -1160,7 +1160,7 @@ public bool IsExpired(int nowTicks, return LifetimeExpired(nowTicks, connectionLifetime); } - private void AbortStreams(int lastValidStream) + private void AbortStreams(int lastValidStream, Exception abortException) { lock (SyncObject) { @@ -1178,7 +1178,7 @@ private void AbortStreams(int lastValidStream) if (streamId > lastValidStream) { - kvp.Value.OnResponseAbort(); + kvp.Value.OnResponseAbort(abortException); _httpStreams.Remove(kvp.Value.StreamId); } @@ -1338,8 +1338,6 @@ private enum SettingId : ushort public sealed override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - // TODO: ISSUE 31310: Cancellation support - Http2Stream http2Stream = null; try { @@ -1354,20 +1352,13 @@ public sealed override async Task SendAsync(HttpRequestMess } catch (Exception e) { - http2Stream?.Dispose(); + Exception replacementException = null; - if (e is IOException) - { - throw new HttpRequestException(SR.net_http_client_execution_error, e); - } - else if (e is ObjectDisposedException) - { - throw new HttpRequestException(SR.net_http_client_execution_error, e); - } - else if (e is Http2ProtocolException) + if (e is IOException || + e is ObjectDisposedException || + e is Http2ProtocolException) { - // ISSUE 31315: Determine if/how to expose HTTP2 error codes - throw new HttpRequestException(SR.net_http_client_execution_error, e); + replacementException = new HttpRequestException(SR.net_http_client_execution_error, e); } else if (e is OperationCanceledException oce) { @@ -1377,17 +1368,19 @@ public sealed override async Task SendAsync(HttpRequestMess http2Stream.Cancel(); } - if (oce.CancellationToken == cancellationToken) + if (oce.CancellationToken != cancellationToken) { - throw; + replacementException = new OperationCanceledException(oce.Message, oce, cancellationToken); } - - throw new OperationCanceledException(cancellationToken); } - else + + http2Stream?.Dispose(); + + if (replacementException != null) { - throw; + throw replacementException; } + throw; } return http2Stream.Response; @@ -1401,7 +1394,7 @@ private Http2Stream AddStream(HttpRequestMessage request) { // Throw a retryable request exception. This will cause retry logic to kick in // and perform another connection attempt. The user should never see this exception. - throw new HttpRequestException(null, null, true); + throw new HttpRequestException(null, null, allowRetry: true); } int streamId = _nextStream; @@ -1443,40 +1436,7 @@ private void RemoveStream(Http2Stream http2Stream) } } - // TODO: ISSUE 31315: Should this be public? - internal enum Http2ProtocolErrorCode - { - NoError = 0x0, - ProtocolError = 0x1, - InternalError = 0x2, - FlowControlError = 0x3, - SettingsTimeout = 0x4, - StreamClosed = 0x5, - FrameSizeError = 0x6, - RefusedStream = 0x7, - Cancel = 0x8, - CompressionError = 0x9, - ConnectError = 0xa, - EnhanceYourCalm = 0xb, - InadequateSecurity = 0xc, - Http11Required = 0xd - } - - // TODO: ISSUE 31315: Should this be public? - internal class Http2ProtocolException : Exception - { - private readonly Http2ProtocolErrorCode _errorCode; - - public Http2ProtocolException(Http2ProtocolErrorCode errorCode) - : base($"Http2 Protocol Error, errorCode = {errorCode}") - { - _errorCode = errorCode; - } - - public Http2ProtocolErrorCode ErrorCode => _errorCode; - } - - internal static async ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minReadBytes) + private static async ValueTask ReadAtLeastAsync(Stream stream, Memory buffer, int minReadBytes) { Debug.Assert(buffer.Length >= minReadBytes); diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolErrorCode.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolErrorCode.cs new file mode 100644 index 000000000000..ffc1e896f7e0 --- /dev/null +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolErrorCode.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Net.Http +{ + // NOTE: If any additional error codes are added here, they should also be added to Http2ProtocolException's mapping. + + /// + /// Error codes defined by the HTTP/2 protocol, used in RST_STREAM and GOAWAY frames to convey the reasons for the stream or connection error. + /// https://http2.github.io/http2-spec/#PROTOCOL_ERROR + /// + internal enum Http2ProtocolErrorCode + { + /// The associated condition is not a result of an error. + NoError = 0x0, + /// The endpoint detected an unspecific protocol error. This error is for use when a more specific error code is not available. + ProtocolError = 0x1, + /// The endpoint encountered an unexpected internal error. + InternalError = 0x2, + /// The endpoint detected that its peer violated the flow-control protocol. + FlowControlError = 0x3, + /// The endpoint sent a SETTINGS frame but did not receive a response in a timely manner. + SettingsTimeout = 0x4, + /// The endpoint received a frame after a stream was half-closed. + StreamClosed = 0x5, + /// The endpoint received a frame with an invalid size. + FrameSizeError = 0x6, + /// The endpoint refused the stream prior to performing any application processing. + RefusedStream = 0x7, + /// Used by the endpoint to indicate that the stream is no longer needed. + Cancel = 0x8, + /// The endpoint is unable to maintain the header compression context for the connection. + CompressionError = 0x9, + /// The connection established in response to a CONNECT request was reset or abnormally closed. + ConnectError = 0xa, + /// The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load. + EnhanceYourCalm = 0xb, + /// The underlying transport has properties that do not meet minimum security requirements. + InadequateSecurity = 0xc, + /// The endpoint requires that HTTP/1.1 be used instead of HTTP/2. + Http11Required = 0xd + } +} diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolException.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolException.cs new file mode 100644 index 000000000000..f2b135ca8b7e --- /dev/null +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolException.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace System.Net.Http +{ + [Serializable] + internal sealed class Http2ProtocolException : Exception + { + public Http2ProtocolException(Http2ProtocolErrorCode protocolError) + : base(SR.Format(SR.net_http_http2_protocol_error, GetName(protocolError), ((int)protocolError).ToString("x"))) + { + ProtocolError = protocolError; + } + + private Http2ProtocolException(SerializationInfo info, StreamingContext context) : base(info, context) + { + ProtocolError = (Http2ProtocolErrorCode)info.GetInt32(nameof(ProtocolError)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(ProtocolError), (int)ProtocolError); + base.GetObjectData(info, context); + } + + internal Http2ProtocolErrorCode ProtocolError { get; } + + private static string GetName(Http2ProtocolErrorCode code) + { + // These strings are the names used in the HTTP2 spec and should not be localized. + switch (code) + { + case Http2ProtocolErrorCode.NoError: + return "NO_ERROR"; + default: // any unrecognized error code is treated as a protocol error + case Http2ProtocolErrorCode.ProtocolError: + return "PROTOCOL_ERROR"; + case Http2ProtocolErrorCode.InternalError: + return "INTERNAL_ERROR"; + case Http2ProtocolErrorCode.FlowControlError: + return "FLOW_CONTROL_ERROR"; + case Http2ProtocolErrorCode.SettingsTimeout: + return "SETTINGS_TIMEOUT"; + case Http2ProtocolErrorCode.StreamClosed: + return "STREAM_CLOSED"; + case Http2ProtocolErrorCode.FrameSizeError: + return "FRAME_SIZE_ERROR"; + case Http2ProtocolErrorCode.RefusedStream: + return "REFUSED_STREAM"; + case Http2ProtocolErrorCode.Cancel: + return "CANCEL"; + case Http2ProtocolErrorCode.CompressionError: + return "COMPRESSION_ERROR"; + case Http2ProtocolErrorCode.ConnectError: + return "CONNECT_ERROR"; + case Http2ProtocolErrorCode.EnhanceYourCalm: + return "ENHANCE_YOUR_CALM"; + case Http2ProtocolErrorCode.InadequateSecurity: + return "INADEQUATE_SECURITY"; + case Http2ProtocolErrorCode.Http11Required: + return "HTTP_1_1_REQUIRED"; + } + } + } +} 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 b322ad7287ae..53f793c515e0 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 @@ -43,6 +43,7 @@ private enum StreamState : byte private StreamState _state; private bool _disposed; + private Exception _abortException; /// The core logic for the IValueTaskSource implementation. private ManualResetValueTaskSourceCore _waitSource = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; // mutable struct, do not make this readonly @@ -262,7 +263,7 @@ public void OnResponseData(ReadOnlySpan buffer, bool endStream) } } - public void OnResponseAbort() + public void OnResponseAbort(Exception abortException) { bool signalWaiter; lock (SyncObject) @@ -277,6 +278,7 @@ public void OnResponseAbort() return; } + _abortException = abortException; _state = StreamState.Aborted; signalWaiter = _hasWaiter; @@ -300,7 +302,7 @@ public void OnResponseAbort() if (_state == StreamState.Aborted) { - throw new IOException(SR.net_http_request_aborted); + throw new IOException(SR.net_http_request_aborted, _abortException); } else if (_state == StreamState.ExpectingHeaders) { @@ -393,7 +395,7 @@ private void ExtendWindow(int amount) } else if (_state == StreamState.Aborted) { - throw new IOException(SR.net_http_request_aborted); + throw new IOException(SR.net_http_request_aborted, _abortException); } Debug.Assert(_state == StreamState.ExpectingData || _state == StreamState.ExpectingTrailingHeaders); @@ -493,6 +495,7 @@ public void Cancel() lock (SyncObject) { Task ignored = _connection.SendRstStreamAsync(_streamId, Http2ProtocolErrorCode.Cancel); + _abortException = new OperationCanceledException(); _state = StreamState.Aborted; signalWaiter = _hasWaiter; diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index d25d7cc7c99a..c55839b33821 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -21,6 +23,37 @@ public abstract class HttpClientHandlerTest_Http2 : HttpClientHandlerTestBase public HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output) { } + private async Task AssertProtocolError(Task task, ProtocolErrors errorCode) + { + Exception e = await Assert.ThrowsAsync(() => task); + if (UseSocketsHttpHandler) + { + string text = e.ToString(); + Assert.Contains(((int)errorCode).ToString("x"), text); + Assert.Contains( + Enum.IsDefined(typeof(ProtocolErrors), errorCode) ? errorCode.ToString() : ProtocolErrors.PROTOCOL_ERROR.ToString(), + text); + } + } + + public enum ProtocolErrors + { + NO_ERROR = 0x0, + PROTOCOL_ERROR = 0x1, + INTERNAL_ERROR = 0x2, + FLOW_CONTROL_ERROR = 0x3, + SETTINGS_TIMEOUT = 0x4, + STREAM_CLOSED = 0x5, + FRAME_SIZE_ERROR = 0x6, + REFUSED_STREAM = 0x7, + CANCEL = 0x8, + COMPRESSION_ERROR = 0x9, + CONNECT_ERROR = 0xa, + ENHANCE_YOUR_CALM = 0xb, + INADEQUATE_SECURITY = 0xc, + HTTP_1_1_REQUIRED = 0xd + } + [Fact] public async Task Http2_ClientPreface_Sent() { @@ -81,7 +114,7 @@ public async Task Http2_DataSentBeforeServerPreface_ProtocolError() DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.Padded, 10, 1); await server.WriteFrameAsync(invalidFrame); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); } } @@ -162,10 +195,10 @@ await server.EstablishConnectionAsync( } [ConditionalTheory(nameof(SupportsAlpn))] - [InlineData(SettingId.MaxFrameSize, 16383, true)] - [InlineData(SettingId.MaxFrameSize, 162777216, true)] - [InlineData(SettingId.InitialWindowSize, 0x80000000, false)] - public async Task Http2_ServerSendsInvalidSettingsValue_ProtocolError(SettingId settingId, uint value, bool skipForWinHttp) + [InlineData(SettingId.MaxFrameSize, 16383, ProtocolErrors.PROTOCOL_ERROR, true)] + [InlineData(SettingId.MaxFrameSize, 162777216, ProtocolErrors.PROTOCOL_ERROR, true)] + [InlineData(SettingId.InitialWindowSize, 0x80000000, ProtocolErrors.FLOW_CONTROL_ERROR, false)] + public async Task Http2_ServerSendsInvalidSettingsValue_Error(SettingId settingId, uint value, ProtocolErrors expectedError, bool skipForWinHttp) { if (IsWinHttpHandler && skipForWinHttp) { @@ -181,7 +214,7 @@ public async Task Http2_ServerSendsInvalidSettingsValue_ProtocolError(SettingId // Send invalid initial SETTINGS value await server.EstablishConnectionAsync(new SettingsEntry { SettingId = settingId, Value = value }); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, expectedError); } } @@ -204,10 +237,10 @@ public async Task Http2_StreamResetByServerBeforeHeadersSent_RequestFails() int streamId = await server.ReadRequestHeaderAsync(); // Send a reset stream frame so that the stream moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, 0x2, streamId); + RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -226,10 +259,10 @@ public async Task Http2_StreamResetByServerAfterHeadersSent_RequestFails() await server.SendDefaultResponseHeadersAsync(streamId); // Send a reset stream frame so that the stream moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, 0x2, streamId); + RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -250,10 +283,10 @@ public async Task Http2_StreamResetByServerAfterPartialBodySent_RequestFails() await server.WriteFrameAsync(dataFrame); // Send a reset stream frame so that the stream moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, 0x2, streamId); + RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -276,7 +309,7 @@ public async Task DataFrame_NoStream_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -308,7 +341,7 @@ public async Task DataFrame_IdleStream_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -339,7 +372,7 @@ public async Task HeadersFrame_IdleStream_ConnectionError() await server.SendDefaultResponseHeadersAsync(5); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -374,7 +407,7 @@ public async Task ResponseStreamFrames_ContinuationBeforeHeaders_ConnectionError await server.WriteFrameAsync(MakeSimpleContinuationFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -394,7 +427,7 @@ public async Task ResponseStreamFrames_DataBeforeHeaders_ConnectionError() await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -415,7 +448,7 @@ public async Task ResponseStreamFrames_HeadersAfterHeadersWithoutEndHeaders_Conn await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -437,50 +470,7 @@ public async Task ResponseStreamFrames_HeadersAfterHeadersAndContinuationWithout await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); - - // The client should close the connection as this is a fatal connection level error. - Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); - } - } - - [ConditionalFact(nameof(SupportsAlpn))] - public async Task ResponseStreamFrames_HeadersAfterHeadersWithEndHeaders_ConnectionError() - { - using (var server = Http2LoopbackServer.CreateServer()) - using (var client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - await server.EstablishConnectionAsync(); - int streamId = await server.ReadRequestHeaderAsync(); - - await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: true)); - await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); - - // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); - - // The client should close the connection as this is a fatal connection level error. - Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); - } - } - - [ConditionalFact(nameof(SupportsAlpn))] - public async Task ResponseStreamFrames_HeadersAfterHeadersAndContinuationWithEndHeaders_ConnectionError() - { - using (var server = Http2LoopbackServer.CreateServer()) - using (var client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - await server.EstablishConnectionAsync(); - int streamId = await server.ReadRequestHeaderAsync(); - - await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); - await server.WriteFrameAsync(MakeSimpleContinuationFrame(streamId, endHeaders: true)); - await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); - - // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -501,7 +491,7 @@ public async Task ResponseStreamFrames_DataAfterHeadersWithoutEndHeaders_Connect await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -523,7 +513,7 @@ public async Task ResponseStreamFrames_DataAfterHeadersAndContinuationWithoutEnd await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -545,11 +535,11 @@ public async Task GoAwayFrame_NonzeroStream_ConnectionError() await server.ReadRequestHeaderAsync(); // Send a GoAway frame on stream 1. - GoAwayFrame invalidFrame = new GoAwayFrame(0, 0, new byte[0], 1); + GoAwayFrame invalidFrame = new GoAwayFrame(0, (int)ProtocolErrors.ENHANCE_YOUR_CALM, new byte[0], 1); await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -572,7 +562,7 @@ public async Task DataFrame_TooLong_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, ProtocolErrors.FRAME_SIZE_ERROR); } } @@ -663,8 +653,15 @@ public async Task CompletedResponse_WindowUpdateFrameReceived_Success() } } - [ConditionalFact(nameof(SupportsAlpn))] - public async Task ResetResponseStream_FrameReceived_ConnectionError() + public static IEnumerable ValidAndInvalidProtocolErrors() => + Enum.GetValues(typeof(ProtocolErrors)) + .Cast() + .Concat(new[] { (ProtocolErrors)12345 }) + .Select(p => new object[] { p }); + + [ConditionalTheory(nameof(SupportsAlpn))] + [MemberData(nameof(ValidAndInvalidProtocolErrors))] + public async Task ResetResponseStream_FrameReceived_ConnectionError(ProtocolErrors error) { using (var server = Http2LoopbackServer.CreateServer()) using (var client = CreateHttpClient()) @@ -676,10 +673,10 @@ public async Task ResetResponseStream_FrameReceived_ConnectionError() await server.SendDefaultResponseHeadersAsync(streamId); // Send a reset stream frame so that stream 1 moves to a terminal state. - RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, 0x1, streamId); + RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)error, streamId); await server.WriteFrameAsync(resetStream); - await Assert.ThrowsAsync(async () => await sendTask); + await AssertProtocolError(sendTask, error); // Send a frame on the now-closed stream. DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.None, 0, streamId); @@ -794,6 +791,57 @@ public async Task GoAwayFrame_AllPendingStreamsValid_RequestsSucceedAndConnectio } } + [ConditionalFact(nameof(SupportsAlpn))] + public async Task GoAwayFrame_AbortAllPendingStreams_StreamFailWithExpectedException() + { + using (var server = Http2LoopbackServer.CreateServer()) + using (var client = CreateHttpClient()) + { + await EstablishConnectionAndProcessOneRequestAsync(client, server); + + // Issue three requests + Task sendTask1 = client.GetAsync(server.Address); + Task sendTask2 = client.GetAsync(server.Address); + Task sendTask3 = client.GetAsync(server.Address); + + // Receive three requests + int streamId1 = await server.ReadRequestHeaderAsync(); + int streamId2 = await server.ReadRequestHeaderAsync(); + int streamId3 = await server.ReadRequestHeaderAsync(); + + Assert.InRange(streamId1, int.MinValue, streamId2 - 1); + Assert.InRange(streamId2, int.MinValue, streamId3 - 1); + + // Send various partial responses + + // First response: Don't send anything yet + + // Second response: Send headers, no body yet + await server.SendDefaultResponseHeadersAsync(streamId2); + + // Third response: Send headers, partial body + await server.SendDefaultResponseHeadersAsync(streamId3); + await server.SendResponseDataAsync(streamId3, new byte[5], endStream: false); + + // Send a GOAWAY frame that indicates that we will abort all the requests. + var goAwayFrame = new GoAwayFrame(0, (int)ProtocolErrors.ENHANCE_YOUR_CALM, new byte[0], 0); + await server.WriteFrameAsync(goAwayFrame); + + // We will not send any more frames, so send EOF now, and ensure the client handles this properly. + server.ShutdownSend(); + + await AssertProtocolError(sendTask1, ProtocolErrors.ENHANCE_YOUR_CALM); + await AssertProtocolError(sendTask2, ProtocolErrors.ENHANCE_YOUR_CALM); + await AssertProtocolError(sendTask3, ProtocolErrors.ENHANCE_YOUR_CALM); + + // Now that all pending responses have been sent, the client should close the connection. + await server.WaitForConnectionShutdownAsync(); + + // New request should cause a new connection + await EstablishConnectionAndProcessOneRequestAsync(client, server); + } + } + private static async Task ReadToEndOfStream(Http2LoopbackServer server, int streamId) { int bytesReceived = 0; diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs index 557dad56b68f..afcfddcc37b8 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs @@ -257,6 +257,34 @@ public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_TrailingHeaders_NoData_EmptyResponseObserved() + { + using (var server = Http2LoopbackServer.CreateServer()) + using (var client = new HttpClient(CreateHttpClientHandler(useSocketsHttpHandler: true, useHttp2LoopbackServer: true))) + { + Task sendTask = client.GetAsync(server.Address); + + await server.EstablishConnectionAsync(); + + int streamId = await server.ReadRequestHeaderAsync(); + + // Response header. + await server.SendDefaultResponseHeadersAsync(streamId); + + // No data. + + // Response trailing headers + await server.SendResponseHeadersAsync(streamId, isTrailingHeader: true, headers: s_trailingHeaders); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Array.Empty(), await response.Content.ReadAsByteArrayAsync()); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted() { From 6f0d9d35aed22d2dc8462a8a98e47203e56f5574 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 27 Apr 2019 14:38:28 -0400 Subject: [PATCH 2/2] Address PR feedback --- .../src/System.Net.Http.csproj | 5 -- .../HttpClientHandlerTest.Http2.cs | 46 +++++++++---------- .../HttpClientHandlerTest.TrailingHeaders.cs | 4 +- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/System.Net.Http/src/System.Net.Http.csproj b/src/System.Net.Http/src/System.Net.Http.csproj index f758dc86ab71..fde227d811b8 100644 --- a/src/System.Net.Http/src/System.Net.Http.csproj +++ b/src/System.Net.Http/src/System.Net.Http.csproj @@ -709,9 +709,4 @@ - - - Designer - - diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index c55839b33821..176b4093e8b6 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -23,7 +23,7 @@ public abstract class HttpClientHandlerTest_Http2 : HttpClientHandlerTestBase public HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output) { } - private async Task AssertProtocolError(Task task, ProtocolErrors errorCode) + private async Task AssertProtocolErrorAsync(Task task, ProtocolErrors errorCode) { Exception e = await Assert.ThrowsAsync(() => task); if (UseSocketsHttpHandler) @@ -114,7 +114,7 @@ public async Task Http2_DataSentBeforeServerPreface_ProtocolError() DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.Padded, 10, 1); await server.WriteFrameAsync(invalidFrame); - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); } } @@ -214,7 +214,7 @@ public async Task Http2_ServerSendsInvalidSettingsValue_Error(SettingId settingI // Send invalid initial SETTINGS value await server.EstablishConnectionAsync(new SettingsEntry { SettingId = settingId, Value = value }); - await AssertProtocolError(sendTask, expectedError); + await AssertProtocolErrorAsync(sendTask, expectedError); } } @@ -240,7 +240,7 @@ public async Task Http2_StreamResetByServerBeforeHeadersSent_RequestFails() RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -262,7 +262,7 @@ public async Task Http2_StreamResetByServerAfterHeadersSent_RequestFails() RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -286,7 +286,7 @@ public async Task Http2_StreamResetByServerAfterPartialBodySent_RequestFails() RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.INTERNAL_ERROR, streamId); await server.WriteFrameAsync(resetStream); - await AssertProtocolError(sendTask, ProtocolErrors.INTERNAL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.INTERNAL_ERROR); } } @@ -309,7 +309,7 @@ public async Task DataFrame_NoStream_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -341,7 +341,7 @@ public async Task DataFrame_IdleStream_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -372,7 +372,7 @@ public async Task HeadersFrame_IdleStream_ConnectionError() await server.SendDefaultResponseHeadersAsync(5); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -407,7 +407,7 @@ public async Task ResponseStreamFrames_ContinuationBeforeHeaders_ConnectionError await server.WriteFrameAsync(MakeSimpleContinuationFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -427,7 +427,7 @@ public async Task ResponseStreamFrames_DataBeforeHeaders_ConnectionError() await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -448,7 +448,7 @@ public async Task ResponseStreamFrames_HeadersAfterHeadersWithoutEndHeaders_Conn await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -470,7 +470,7 @@ public async Task ResponseStreamFrames_HeadersAfterHeadersAndContinuationWithout await server.WriteFrameAsync(MakeSimpleHeadersFrame(streamId, endHeaders: false)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -491,7 +491,7 @@ public async Task ResponseStreamFrames_DataAfterHeadersWithoutEndHeaders_Connect await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -513,7 +513,7 @@ public async Task ResponseStreamFrames_DataAfterHeadersAndContinuationWithoutEnd await server.WriteFrameAsync(MakeSimpleDataFrame(streamId)); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -539,7 +539,7 @@ public async Task GoAwayFrame_NonzeroStream_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.PROTOCOL_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.PROTOCOL_ERROR); // The client should close the connection as this is a fatal connection level error. Assert.Null(await server.ReadFrameAsync(TimeSpan.FromSeconds(30))); @@ -562,7 +562,7 @@ public async Task DataFrame_TooLong_ConnectionError() await server.WriteFrameAsync(invalidFrame); // As this is a connection level error, the client should see the request fail. - await AssertProtocolError(sendTask, ProtocolErrors.FRAME_SIZE_ERROR); + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.FRAME_SIZE_ERROR); } } @@ -676,7 +676,7 @@ public async Task ResetResponseStream_FrameReceived_ConnectionError(ProtocolErro RstStreamFrame resetStream = new RstStreamFrame(FrameFlags.None, (int)error, streamId); await server.WriteFrameAsync(resetStream); - await AssertProtocolError(sendTask, error); + await AssertProtocolErrorAsync(sendTask, error); // Send a frame on the now-closed stream. DataFrame invalidFrame = new DataFrame(new byte[10], FrameFlags.None, 0, streamId); @@ -794,8 +794,8 @@ public async Task GoAwayFrame_AllPendingStreamsValid_RequestsSucceedAndConnectio [ConditionalFact(nameof(SupportsAlpn))] public async Task GoAwayFrame_AbortAllPendingStreams_StreamFailWithExpectedException() { - using (var server = Http2LoopbackServer.CreateServer()) - using (var client = CreateHttpClient()) + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) { await EstablishConnectionAndProcessOneRequestAsync(client, server); @@ -830,9 +830,9 @@ public async Task GoAwayFrame_AbortAllPendingStreams_StreamFailWithExpectedExcep // We will not send any more frames, so send EOF now, and ensure the client handles this properly. server.ShutdownSend(); - await AssertProtocolError(sendTask1, ProtocolErrors.ENHANCE_YOUR_CALM); - await AssertProtocolError(sendTask2, ProtocolErrors.ENHANCE_YOUR_CALM); - await AssertProtocolError(sendTask3, ProtocolErrors.ENHANCE_YOUR_CALM); + await AssertProtocolErrorAsync(sendTask1, ProtocolErrors.ENHANCE_YOUR_CALM); + await AssertProtocolErrorAsync(sendTask2, ProtocolErrors.ENHANCE_YOUR_CALM); + await AssertProtocolErrorAsync(sendTask3, ProtocolErrors.ENHANCE_YOUR_CALM); // Now that all pending responses have been sent, the client should close the connection. await server.WaitForConnectionShutdownAsync(); diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs index afcfddcc37b8..cf85747a76df 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs @@ -260,8 +260,8 @@ public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] public async Task Http2GetAsync_TrailingHeaders_NoData_EmptyResponseObserved() { - using (var server = Http2LoopbackServer.CreateServer()) - using (var client = new HttpClient(CreateHttpClientHandler(useSocketsHttpHandler: true, useHttp2LoopbackServer: true))) + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = new HttpClient(CreateHttpClientHandler(useSocketsHttpHandler: true, useHttp2LoopbackServer: true))) { Task sendTask = client.GetAsync(server.Address);