Minor follow-ups/cleanups in Http2Connection and Http2Stream (dotnet/corefx#38817)
authorStephen Toub <stoub@microsoft.com>
Mon, 24 Jun 2019 18:58:09 +0000 (14:58 -0400)
committerGitHub <noreply@github.com>
Mon, 24 Jun 2019 18:58:09 +0000 (14:58 -0400)
- Fix/adjust several uses of string interpolation in SocketsHttpHandler.
- Remove a Task.WhenAny allocation from SendAsync in the common case where there is no request content or where the request content sending completes synchronously.
- In GetWaiterTask use CT.UnsafeRegister instead of Register, as ExecutionContext, as the callback doesn't need context flowed.
- In GetWaiterTask, avoid allocating an unnecessary Task for the _waitSource if the token isn't cancelable.
- In GetWaiter task's cancellation callback, avoid allocating an OCE not associated with the wait.  Instead, just complete the waiter, and then rely on the subsequent cancellation check to propagate cancellation appropriately.
- Use CompareExchange to write to _abortException so that _abortException never changes its value once it's non-null.  We currently have several code paths that check the type of _abortException and then do something based on it, and that could cause issues if its type could change between the check and the action.
- Clean up HTTP2 SendAsync method, e.g. remove unnecessary tmps, remove duplicated await, use slightly more descriptive variable names, only wrap OCE with one for cancellationToken if cancellationToken has had cancellation requested.
- Replace a use of ExceptionDispatchInfo.Throw with just throw.  The former should be used when and only when throwing an exception object that may previously have been thrown; in this case we're throwing a new object that was never thrown before.
- Add detailed comment describing thread-safety around Http2Stream._waitSource
- Clean up a few comments
- Add some missing .ConfigureAwait(false) in loopback servers

Commit migrated from https://github.com/dotnet/corefx/commit/e771af6f93fe4bdc9e6172563e42422d5fdd30ff

src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs
src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
src/libraries/Common/tests/System/Net/RemoteServerQuery.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs

index 7ebf7f3..4d006cc 100644 (file)
@@ -115,7 +115,7 @@ namespace System.Net.Test.Common
         public async Task<Frame> ReadFrameAsync(TimeSpan timeout)
         {
             using CancellationTokenSource timeoutCts = new CancellationTokenSource(timeout);
-            return await ReadFrameAsync(timeoutCts.Token);
+            return await ReadFrameAsync(timeoutCts.Token).ConfigureAwait(false);
         }
 
         private async Task<Frame> ReadFrameAsync(CancellationToken cancellationToken)
@@ -241,8 +241,8 @@ namespace System.Net.Test.Common
         {
             try
             {
-                await SendGoAway(lastStreamId);
-                await WaitForConnectionShutdownAsync();
+                await SendGoAway(lastStreamId).ConfigureAwait(false);
+                await WaitForConnectionShutdownAsync().ConfigureAwait(false);
             }
             catch (IOException)
             {
@@ -545,7 +545,7 @@ namespace System.Net.Test.Common
             if (readBody && (frame.Flags & FrameFlags.EndStream) == 0)
             {
                 // Read body until end of stream if needed.
-                requestData.Body = await ReadBodyAsync();
+                requestData.Body = await ReadBodyAsync().ConfigureAwait(false);
             }
 
             return (streamId, requestData);
index b8523d8..b50376f 100644 (file)
@@ -94,7 +94,7 @@ namespace System.Net.Test.Common
 
         public async Task<Http2LoopbackConnection> EstablishConnectionAsync(params SettingsEntry[] settingsEntries)
         {
-            (Http2LoopbackConnection connection, _) = await EstablishConnectionGetSettingsAsync();
+            (Http2LoopbackConnection connection, _) = await EstablishConnectionGetSettingsAsync().ConfigureAwait(false);
             return connection;
         }
 
@@ -183,7 +183,7 @@ namespace System.Net.Test.Common
                 Task clientTask = clientFunc(server.Address);
                 Task serverTask = serverFunc(server);
 
-                await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed(timeout);
+                await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed(timeout).ConfigureAwait(false);
             }
         }
     }
index 44b0dc9..a739470 100644 (file)
@@ -162,7 +162,7 @@ namespace System.Net.Test.Common
             await AcceptConnectionAsync(async connection =>
             {
                 lines = await connection.ReadRequestHeaderAndSendCustomResponseAsync(response).ConfigureAwait(false);
-            });
+            }).ConfigureAwait(false);
 
             return lines;
         }
@@ -176,7 +176,7 @@ namespace System.Net.Test.Common
             await AcceptConnectionAsync(async connection =>
             {
                 lines = await connection.ReadRequestHeaderAndSendCustomResponseAsync(response).ConfigureAwait(false);
-            });
+            }).ConfigureAwait(false);
 
             return lines;
         }
@@ -190,7 +190,7 @@ namespace System.Net.Test.Common
             await AcceptConnectionAsync(async connection =>
             {
                 lines = await connection.ReadRequestHeaderAndSendResponseAsync(statusCode, additionalHeaders + "Connection: close\r\n", content).ConfigureAwait(false);
-            });
+            }).ConfigureAwait(false);
 
             return lines;
         }
@@ -700,7 +700,7 @@ namespace System.Net.Test.Common
 
                 if (readBody)
                 {
-                    requestData.Body = await ReadRequestBodyAsync();
+                    requestData.Body = await ReadRequestBodyAsync().ConfigureAwait(false);
                     _bodyRead = true;
                 }
 
@@ -789,22 +789,22 @@ namespace System.Net.Test.Common
                     headerString = GetHttpResponseHeaders(statusCode, headerString, contentLength, connectionClose : true);
                 }
 
-                await SendResponseAsync(headerString);
-                await SendResponseAsync(content);
+                await SendResponseAsync(headerString).ConfigureAwait(false);
+                await SendResponseAsync(content).ConfigureAwait(false);
             }
 
             public override async Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
             {
-                await SendResponseAsync(Encoding.UTF8.GetString(body));
+                await SendResponseAsync(Encoding.UTF8.GetString(body)).ConfigureAwait(false);
             }
         }
 
         public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = null)
         {
-            using (Connection connection = await EstablishConnectionAsync())
+            using (Connection connection = await EstablishConnectionAsync().ConfigureAwait(false))
             {
-                HttpRequestData requestData = await connection.ReadRequestDataAsync();
-                await connection.SendResponseAsync(statusCode, headers, content : content);
+                HttpRequestData requestData = await connection.ReadRequestDataAsync().ConfigureAwait(false);
+                await connection.SendResponseAsync(statusCode, headers, content: content).ConfigureAwait(false);
 
                 return requestData;
             }
index 4b40af4..6adb635 100644 (file)
@@ -32,7 +32,7 @@ namespace System.Net.Test.Common
         {
             try
             {
-                return await testCode();
+                return await testCode().ConfigureAwait(false);
             }
             catch (Exception actualException)
             {
@@ -49,7 +49,7 @@ namespace System.Net.Test.Common
         {
             try
             {
-                await testCode();
+                await testCode().ConfigureAwait(false);
             }
             catch (Exception actualException)
             {
index 619ca57..c7060e0 100644 (file)
@@ -553,7 +553,7 @@ namespace System.Net.Http
             // cancellation (e.g. WebException when reading from canceled response stream).
             if (cts.IsCancellationRequested && e is HttpRequestException)
             {
-                if (NetEventSource.IsEnabled) NetEventSource.Error(this, $"Canceled");
+                if (NetEventSource.IsEnabled) NetEventSource.Error(this, "Canceled");
                 throw new OperationCanceledException(cts.Token);
             }
         }
index eef4466..3fc6ab1 100644 (file)
@@ -185,7 +185,7 @@ namespace System.Net.Http
             {
                 if (initialFrame && NetEventSource.IsEnabled)
                 {
-                    string response = System.Text.Encoding.ASCII.GetString(_incomingBuffer.ActiveSpan.Slice(0, Math.Min(20, _incomingBuffer.ActiveSpan.Length)));
+                    string response = Encoding.ASCII.GetString(_incomingBuffer.ActiveSpan.Slice(0, Math.Min(20, _incomingBuffer.ActiveSpan.Length)));
                     Trace($"HTTP/2 handshake failed. Server returned {response}");
                 }
 
@@ -261,7 +261,7 @@ namespace System.Net.Http
             }
             catch (Exception e)
             {
-                if (NetEventSource.IsEnabled) Trace($"ProcessIncomingFramesAsync: {e.Message}");
+                if (NetEventSource.IsEnabled) Trace($"{nameof(ProcessIncomingFramesAsync)}: {e.Message}");
 
                 if (!_disposed)
                 {
@@ -1384,41 +1384,45 @@ namespace System.Net.Http
                 }
                 else
                 {
-                    // Send request body, if any
-                    Task bodyTask = http2Stream.SendRequestBodyAsync(cancellationToken);
-                    // read response headers.
+                    // Send request body, if any, and read response headers.
+                    Task requestBodyTask = http2Stream.SendRequestBodyAsync(cancellationToken);
                     Task responseHeadersTask = http2Stream.ReadResponseHeadersAsync(cancellationToken);
 
-                    if (bodyTask == await Task.WhenAny(bodyTask, responseHeadersTask).ConfigureAwait(false) ||
-                        bodyTask.IsCompleted)
+                    // Wait for either task to complete.  The best and most common case is when the request body completes
+                    // before the response headers, in which case we can fully process the sending of the request and then
+                    // fully process the sending of the response.  WhenAny is not free, so we do a fast-path check to see
+                    // if the request body completed synchronously, only progressing to do the WhenAny if it didn't. Then
+                    // if the WhenAny completes and either the WhenAny indicated that the request body completed or
+                    // both tasks completed, we can proceed to handle the request body as if it completed first.
+                    if (requestBodyTask.IsCompleted ||
+                        requestBodyTask == await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) ||
+                        requestBodyTask.IsCompleted)
                     {
                         // The sending of the request body completed before receiving all of the request headers.
-                        Task t = bodyTask;
-                        bodyTask = null;
+                        // This is the common and desirable case.
                         try
                         {
-                            await t.ConfigureAwait(false);
+                            await requestBodyTask.ConfigureAwait(false);
                         }
                         catch (Exception e)
                         {
-                            if (NetEventSource.IsEnabled) Trace($"SendRequestBody Task failed. {e}");
-                            // Observe exception (if any) on responseHeadersTask.
-                            LogExceptions(responseHeadersTask);
+                            if (NetEventSource.IsEnabled) Trace($"{nameof(http2Stream.SendRequestBodyAsync)} failed. {e}");
+                            LogExceptions(responseHeadersTask); // Observe exception (if any) on responseHeadersTask.
                             throw;
                         }
-
-                        await responseHeadersTask.ConfigureAwait(false);
                     }
                     else
                     {
-                        // We received the response headers but the request body hasn't yet finished.
-                        // If the connection is aborted or if we get RST or GOAWAY from server, exception will be
-                        // stored in stream._abortException and propagated to up to caller if possible while processing response.
-                        LogExceptions(bodyTask);
-                        bodyTask = null;
-                        // Pick up any exceptions from the header Task.
-                        await responseHeadersTask.ConfigureAwait(false);
+                        // We received the response headers but the request body hasn't yet finished; this most commonly happens
+                        // when the protocol is being used to enable duplex communication. If the connection is aborted or if we
+                        // get RST or GOAWAY from server, exception will be stored in stream._abortException and propagated up
+                        // to caller if possible while processing response, but make sure that we log any exceptions from this task
+                        // completing asynchronously).
+                        LogExceptions(requestBodyTask);
                     }
+
+                    // Wait for the response headers to complete if they haven't already, propagating any exceptions.
+                    await responseHeadersTask.ConfigureAwait(false);
                 }
             }
             catch (Exception e)
@@ -1439,7 +1443,7 @@ namespace System.Net.Http
                         http2Stream.Cancel();
                     }
 
-                    if (oce.CancellationToken != cancellationToken)
+                    if (cancellationToken.IsCancellationRequested && oce.CancellationToken != cancellationToken)
                     {
                         replacementException = new OperationCanceledException(oce.Message, oce, cancellationToken);
                     }
index 6925cee..a71b7e3 100644 (file)
@@ -17,17 +17,6 @@ namespace System.Net.Http
     {
         private sealed class Http2Stream : IValueTaskSource, IDisposable
         {
-            private enum StreamState : byte
-            {
-                ExpectingStatus,
-                ExpectingIgnoredHeaders,
-                ExpectingHeaders,
-                ExpectingData,
-                ExpectingTrailingHeaders,
-                Complete,
-                Aborted
-            }
-
             private const int InitialStreamBufferSize =
 #if DEBUG
                 10;
@@ -48,11 +37,26 @@ namespace System.Net.Http
             private bool _disposed;
             private Exception _abortException;
 
-            /// <summary>The core logic for the IValueTaskSource implementation.</summary>
+            /// <summary>
+            /// The core logic for the IValueTaskSource implementation.
+            /// 
+            /// Thread-safety:
+            /// _waitSource is used to coordinate between a producer indicating that something is available to process (either the connection's event loop
+            /// or a cancellation request) and a consumer doing that processing.  There must only ever be a single consumer, namely this stream reading
+            /// data associated with the response.  Because there is only ever at most one consumer, producers can trust that if _hasWaiter is true,
+            /// until the _waitSource is then set, no consumer will attempt to reset the _waitSource.  A producer must still take SyncObj in order to
+            /// coordinate with other producers (e.g. a race between data arriving from the event loop and cancellation being requested), but while holding
+            /// the lock it can check whether _hasWaiter is true, and if it is, set _hasWaiter to false, exit the lock, and then set the _waitSource. Another
+            /// producer coming along will then see _hasWaiter as false and will not attempt to concurrently set _waitSource (which would violate _waitSource's
+            /// thread-safety), and no other consumer could come along in the interim, because _hasWaiter being true means that a consumer is already waiting
+            /// for _waitSource to be set, and legally there can only be one consumer.  Once this producer sets _waitSource, the consumer could quickly loop
+            /// around to wait again, but invariants have all been maintained in the interim, and the consumer would need to take the SyncObj lock in order to
+            /// Reset _waitSource.
+            /// </summary>
             private ManualResetValueTaskSourceCore<bool> _waitSource = new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true }; // mutable struct, do not make this readonly
             /// <summary>
             /// Whether code has requested or is about to request a wait be performed and thus requires a call to SetResult to complete it.
-            /// This is read and written while holding the lock so that most operations on _waitSourceCore don't need to be.
+            /// This is read and written while holding the lock so that most operations on _waitSource don't need to be.
             /// </summary>
             private bool _hasWaiter;
 
@@ -98,9 +102,19 @@ namespace System.Net.Http
                     {
                         using (Http2WriteStream writeStream = new Http2WriteStream(this))
                         {
-                            // TODO: until #9071 is fixed, cancellation on content.CopyToAsync does not work.
-                            // To work around it, register delegate and set _abortException as needed.
-                            using (cancellationToken.UnsafeRegister(stream => { if (((Http2Stream)stream)._abortException == null) ((Http2Stream)stream)._abortException = new OperationCanceledException(); }, this))
+                            // TODO: until #9071 is fixed, cancellation on content.CopyToAsync does not apply for most content types,
+                            // because most content types aren't passed the token given to this internal overload of CopyToAsync.
+                            // To work around it, we register to set _abortException as needed; this won't preempt reads issued to
+                            // the source content, but it will at least enable the writes then performed on our write stream to see
+                            // that cancellation was requested and abort, rather than waiting for the whole copy to complete.
+                            using (cancellationToken.UnsafeRegister(stream =>
+                            {
+                                var thisRef = (Http2Stream)stream;
+                                if (thisRef._abortException == null)
+                                {
+                                    Interlocked.CompareExchange(ref thisRef._abortException, new OperationCanceledException(), null);
+                                }
+                            }, this))
                             {
                                 await _request.Content.CopyToAsync(writeStream, null, cancellationToken).ConfigureAwait(false);
                             }
@@ -122,9 +136,9 @@ namespace System.Net.Http
 
                         if (_abortException == null)
                         {
-                            // If we are still the response after receiving response headers, this will give us a chance to propagate exception up.
-                            // Since we failed while Copying stream, wrap it as IOException if needed.
-                            _abortException = e;
+                            // If we are still processing the response after receiving response headers,
+                            // this will give us a chance to propagate exception up.
+                            Interlocked.CompareExchange(ref _abortException, e, null);
                         }
 
                         throw;
@@ -145,8 +159,8 @@ namespace System.Net.Http
                 Task response = ReadResponseHeadersAsync(cancellationToken);
 
                 using (var expect100Timer = new Timer(
-                            s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
-                            allowExpect100ToContinue, _connection._pool.Settings._expect100ContinueTimeout, Timeout.InfiniteTimeSpan))
+                    s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
+                    allowExpect100ToContinue, _connection._pool.Settings._expect100ContinueTimeout, Timeout.InfiniteTimeSpan))
                 {
                     // By now, either we got response from server or timer expired.
                     sendRequestContent = await allowExpect100ToContinue.Task.ConfigureAwait(false);
@@ -244,7 +258,7 @@ namespace System.Net.Http
                         }
                         else
                         {
-                            if (NetEventSource.IsEnabled) _connection.Trace("Invalid response pseudo-header '{System.Text.Encoding.ASCII.GetString(name)}'.");
+                            if (NetEventSource.IsEnabled) _connection.Trace($"Invalid response pseudo-header '{Encoding.ASCII.GetString(name)}'.");
                             throw new Http2ProtocolException(SR.net_http_invalid_response);
                         }
                     }
@@ -258,7 +272,7 @@ namespace System.Net.Http
 
                         if (_state != StreamState.ExpectingHeaders && _state != StreamState.ExpectingTrailingHeaders)
                         {
-                            if (NetEventSource.IsEnabled) _connection.Trace($"Received header before status.");
+                            if (NetEventSource.IsEnabled) _connection.Trace("Received header before status.");
                             throw new Http2ProtocolException(SR.net_http_invalid_response);
                         }
 
@@ -410,7 +424,7 @@ namespace System.Net.Http
                         return;
                     }
 
-                    _abortException = abortException;
+                    Interlocked.CompareExchange(ref _abortException, abortException, null);
                     _state = StreamState.Aborted;
 
                     signalWaiter = _hasWaiter;
@@ -466,6 +480,7 @@ namespace System.Net.Http
                 (wait, emptyResponse) = TryEnsureHeaders();
                 if (wait)
                 {
+                    Debug.Assert(_hasWaiter, $"{nameof(TryEnsureHeaders)} should have set _hasWaiter to true.");
                     await GetWaiterTask(cancellationToken).ConfigureAwait(false);
 
                     (wait, emptyResponse) = TryEnsureHeaders();
@@ -557,7 +572,8 @@ namespace System.Net.Http
                 {
                     // Synchronously block waiting for data to be produced.
                     Debug.Assert(bytesRead == 0);
-                    GetWaiterTask(cancellationToken).GetAwaiter().GetResult();
+                    Debug.Assert(_hasWaiter, $"{nameof(TryReadFromBuffer)} should have set _hasWaiter to true.");
+                    GetWaiterTask(cancellationToken).AsTask().GetAwaiter().GetResult();
                     CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
                     (wait, bytesRead) = TryReadFromBuffer(buffer);
                     Debug.Assert(!wait);
@@ -583,6 +599,7 @@ namespace System.Net.Http
                 if (wait)
                 {
                     Debug.Assert(bytesRead == 0);
+                    Debug.Assert(_hasWaiter, $"{nameof(TryReadFromBuffer)} should have set _hasWaiter to true.");
                     await GetWaiterTask(cancellationToken).ConfigureAwait(false);
                     (wait, bytesRead) = TryReadFromBuffer(buffer.Span);
                     Debug.Assert(!wait);
@@ -634,7 +651,7 @@ namespace System.Net.Http
                 lock (SyncObject)
                 {
                     IgnoreExceptions(_connection.SendRstStreamAsync(_streamId, Http2ProtocolErrorCode.Cancel));
-                    _abortException = new OperationCanceledException();
+                    Interlocked.CompareExchange(ref _abortException, new OperationCanceledException(), null);
                     _state = StreamState.Aborted;
 
                     signalWaiter = _hasWaiter;
@@ -654,26 +671,61 @@ namespace System.Net.Http
             ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _waitSource.GetStatus(token);
             void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _waitSource.OnCompleted(continuation, state, token, flags);
             void IValueTaskSource.GetResult(short token) => _waitSource.GetResult(token);
-            private async Task GetWaiterTask(CancellationToken cancellationToken)
+            private ValueTask GetWaiterTask(CancellationToken cancellationToken)
             {
-                var vt = new ValueTask(this, _waitSource.Version).AsTask();
-                using (cancellationToken.Register(s =>
-                {
-                    Http2Stream stream = (Http2Stream)s;
-                    bool signalWaiter;
-                    lock (stream.SyncObject)
+                // No locking is required here to access _waitSource.  To be here, we've already updated _hasWaiter (while holding the lock)
+                // to indicate that we would be creating this waiter, and at that point the only code that could be await'ing _waitSource or
+                // Reset'ing it is this code here.  It's possible for this to race with the _waitSource being completed, but that's ok and is
+                // handled by _waitSource as one of its primary purposes.
+                Debug.Assert(_hasWaiter, $"This should only be called after we've transitioned _hasWaiter to true to enable this {nameof(GetWaiterTask)} call.");
+
+                // With HttpClient, the supplied cancellation token will always be cancelable, as HttpClient supplies a token that
+                // will have cancellation requested if CancelPendingRequests is called (or when a non-infinite Timeout expires).
+                // However, this could still be non-cancelable if HttpMessageInvoker was used, at which point this will only be
+                // cancelable if the caller's token was cancelable.  To avoid the extra allocation here in such a case, we make
+                // this pay-for-play: if the token isn't cancelable, return a ValueTask wrapping this object directly, and only
+                // if it is cancelable, then register for the cancellation callback, allocate a task for the asynchronously
+                // completing case, etc.
+                return cancellationToken.CanBeCanceled ?
+                    new ValueTask(GetWaiterTaskCore()) :
+                    new ValueTask(this, _waitSource.Version);
+
+                async Task GetWaiterTaskCore()
+                {
+                    using (cancellationToken.UnsafeRegister(s =>
+                    {
+                        var thisRef = (Http2Stream)s;
+
+                        bool signalWaiter;
+                        lock (thisRef.SyncObject)
+                        {
+                            signalWaiter = thisRef._hasWaiter;
+                            thisRef._hasWaiter = false;
+                        }
+
+                        if (signalWaiter)
+                        {
+                            // Wake up the wait.  It will then immediately check whether cancellation was requested and throw if it was.
+                            thisRef._waitSource.SetResult(true);
+                        }
+                    }, this))
                     {
-                        signalWaiter = stream._hasWaiter;
-                        stream._hasWaiter = false;
+                        await new ValueTask(this, _waitSource.Version).ConfigureAwait(false);
                     }
-                    if (signalWaiter) stream._waitSource.SetException(new OperationCanceledException());
-                }, this))
-                {
 
-                    await vt.ConfigureAwait(false);
+                    CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
                 }
+            }
 
-                CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
+            private enum StreamState : byte
+            {
+                ExpectingStatus,
+                ExpectingIgnoredHeaders,
+                ExpectingHeaders,
+                ExpectingData,
+                ExpectingTrailingHeaders,
+                Complete,
+                Aborted
             }
 
             private sealed class Http2ReadStream : HttpBaseStream
@@ -716,7 +768,7 @@ namespace System.Net.Http
                     Http2Stream http2Stream = _http2Stream ?? throw new ObjectDisposedException(nameof(Http2ReadStream));
                     if (http2Stream._abortException != null)
                     {
-                        ExceptionDispatchInfo.Throw(new IOException(SR.net_http_client_execution_error, http2Stream._abortException));
+                        throw new IOException(SR.net_http_client_execution_error, http2Stream._abortException);
                     }
 
                     return http2Stream.ReadData(destination, CancellationToken.None);
index efaf51f..d284e10 100644 (file)
@@ -63,7 +63,7 @@ namespace System.Net.Http
                     if (NetEventSource.IsEnabled) Trace($"Exception from asynchronous processing: {e}");
                 }
             }
-    }
+        }
 
     }
 }