Fix SocketsHttpHandler canceling request when response stream dropped (dotnet/corefx...
authorStephen Toub <stoub@microsoft.com>
Mon, 15 Jul 2019 17:12:27 +0000 (13:12 -0400)
committerGitHub <noreply@github.com>
Mon, 15 Jul 2019 17:12:27 +0000 (13:12 -0400)
* Fix SocketsHttpHandler canceling request when response stream dropped

Good code using HttpClient should Dispose of the HttpResponseMessage and/or response Stream when its done with it.  However, if code fails to do so, we still want to ensure that we don't leak connections/requests to the server or leak resources on the client.  If the response isn't disposed but the code reads all the way to the end of it, then internally things are cleaned up.  But if the response isn't disposed and hasn't read to the end, for both HTTP/1.1 and HTTP/2, we have some issues.

For HTTP/1.1, we rely on the Socket's finalization to close the associated connection.  However, we use a cache of SocketAsyncEventArgs instances to handle creating connections, and it turns out that these SocketAsyncEventArgs instances are keeping around a reference to the last created Socket.  If the SAEA is reused for another connection, then there's no issue, but until it is, it maintains several references to the socket: in ConnectSocket, in _currentSocket, in _multipleConnect, and then further within another SAEA instance that the _multipleConnect itself is creating for some reason.  We should go through and clean all that up in SAEA, but for now, this just removes the SAEA cache.

For HTTP/2, nothing was cleaning up in this case.  To handle it, we:
1. Make the response stream finalizable, such that when finalized it behaves as if Dispose were called.
2. When SendAsync completes, we remove the strong reference from the Http2Stream to the response message, such that the response (and in turn the response stream) doesn't get rooted by the Http2Connection storing the Http2Stream in its dictionary).
3. The only reason that reference was still needed was to support trailing headers.  So we create a lazily-allocated list on the Http2Stream for storing trailers if any should arrive, and then when the response body completes, the response stream pulls those headers in from the temporary collection into the response that it then has a reference to.  This also helps to avoid a race condition where consuming code erroneously accesses TrailingHeaders before the request has completed, in which case we previously could have been trying to write to the collection while user code was reading from it; with this change, that mutation happens as part of a call the consumer makes.

* Address PR feedback

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

src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs
src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.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/tests/FunctionalTests/HttpClientHandlerTest.Finalization.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj

index 39aa0a2..9500c45 100644 (file)
@@ -48,17 +48,22 @@ namespace System.Net.Test.Common
     {
         public abstract void Dispose();
 
-        // Read request Headers and optionally request body as well.
+        /// <summary>Read request Headers and optionally request body as well.</summary>
         public abstract Task<HttpRequestData> ReadRequestDataAsync(bool readBody = true);
-        // Read complete request body if not done by ReadRequestData.
+        /// <summary>Read complete request body if not done by ReadRequestData.</summary>
         public abstract Task<Byte[]> ReadRequestBodyAsync();
 
-        // Sends Response back with provided statusCode, headers and content. Can be called multiple times on same response if isFinal was set to false before.
+        /// <summary>Sends Response back with provided statusCode, headers and content. Can be called multiple times on same response if isFinal was set to false before.</summary>
         public abstract Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string body = null, bool isFinal = true, int requestId = 0);
-        // Sends Response body after SendResponse was called with isFinal: false.
+        /// <summary>Sends response headers.</summary>
+        public abstract Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0);
+        /// <summary>Sends Response body after SendResponse was called with isFinal: false.</summary>
         public abstract Task SendResponseBodyAsync(byte[] data, bool isFinal = true, int requestId = 0);
 
-        // Helper function to make it easier to convert old test with strings.
+        /// <summary>Waits for the client to signal cancellation.</summary>
+        public abstract Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0);
+
+        /// <summary>Helper function to make it easier to convert old test with strings.</summary>
         public async Task SendResponseBodyAsync(string data, bool isFinal = true, int requestId = 0)
         {
             await SendResponseBodyAsync(String.IsNullOrEmpty(data) ? new byte[0] : Encoding.ASCII.GetBytes(data), isFinal, requestId);
index 3d20070..7809a49 100644 (file)
@@ -4,9 +4,9 @@
 
 using System.Collections.Generic;
 using System.IO;
+using System.Net.Http.Functional.Tests;
 using System.Net.Security;
 using System.Net.Sockets;
-using System.Security.Authentication;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
@@ -716,10 +716,39 @@ namespace System.Net.Test.Common
             return SendResponseHeadersAsync(streamId, endStream : isFinal, statusCode, isTrailingHeader : false, endHeaders : endHeaders, headers);
         }
 
+        public override Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+        {
+            int streamId = requestId == 0 ? _lastStreamId : requestId;
+            return SendResponseHeadersAsync(streamId, endStream: false, statusCode, isTrailingHeader: false, endHeaders: true, headers);
+        }
+
         public override Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
         {
             int streamId = requestId == 0 ? _lastStreamId : requestId;
             return SendResponseBodyAsync(streamId, body, isFinal);
         }
+
+        public override async Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0)
+        {
+            int streamId = requestId == 0 ? _lastStreamId : requestId;
+
+            Frame frame;
+            do
+            {
+                frame = await ReadFrameAsync(TimeSpan.FromMilliseconds(TestHelper.PassingTestTimeoutMilliseconds));
+                Assert.NotNull(frame); // We should get Rst before closing connection.
+                Assert.Equal(0, (int)(frame.Flags & FrameFlags.EndStream));
+                if (ignoreIncomingData)
+                {
+                    Assert.True(frame.Type == FrameType.Data || frame.Type == FrameType.RstStream, $"Expected Data or RstStream, got {frame.Type}");
+                }
+                else
+                {
+                    Assert.Equal(FrameType.RstStream, frame.Type);
+                }
+            } while (frame.Type != FrameType.RstStream);
+
+            Assert.Equal(streamId, frame.StreamId);
+        }
     }
 }
index a739470..3d27560 100644 (file)
@@ -494,7 +494,7 @@ namespace System.Net.Test.Common
                 return totalLength;
             }
 
-            public async Task<int> ReadBlockAsync(char[]  result, int offset, int size)
+            public async Task<int> ReadBlockAsync(char[] result, int offset, int size)
             {
                 byte[] buffer = new byte[size];
                 int readLength = await ReadBlockAsync(buffer, 0, size).ConfigureAwait(false);
@@ -503,7 +503,7 @@ namespace System.Net.Test.Common
 
                 for (int i = 0; i < readLength; i++)
                 {
-                    result[offset + i ] = asString[i];
+                    result[offset + i] = asString[i];
                 }
 
                 return readLength;
@@ -520,7 +520,7 @@ namespace System.Net.Test.Common
                 {
                     bytesRead = await ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
                     totalLength += bytesRead;
-                    offset+=bytesRead;
+                    offset += bytesRead;
 
                     if (bytesRead == buffer.Length)
                     {
@@ -571,7 +571,7 @@ namespace System.Net.Test.Common
                         }
 
                         _readEnd += bytesRead;
-                   }
+                    }
 
                     index = Array.IndexOf(_readBuffer, (byte)'\n', startSearch, _readEnd - startSearch);
                     if (index == -1)
@@ -735,7 +735,7 @@ namespace System.Net.Test.Common
                         int chunkLength = int.Parse(chunkHeader, System.Globalization.NumberStyles.HexNumber);
                         if (chunkLength == 0)
                         {
-                                // Last chunk. Read CRLF and exit.
+                            // Last chunk. Read CRLF and exit.
                             await ReadLineAsync().ConfigureAwait(false);
                             break;
                         }
@@ -786,17 +786,53 @@ namespace System.Net.Test.Common
 
                 if (content != null || isFinal)
                 {
-                    headerString = GetHttpResponseHeaders(statusCode, headerString, contentLength, connectionClose : true);
+                    headerString = GetHttpResponseHeaders(statusCode, headerString, contentLength, connectionClose: true);
                 }
 
                 await SendResponseAsync(headerString).ConfigureAwait(false);
                 await SendResponseAsync(content).ConfigureAwait(false);
             }
 
+            public override async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+            {
+                string headerString = null;
+
+                if (headers != null)
+                {
+                    foreach (HttpHeaderData headerData in headers)
+                    {
+                        headerString = headerString + $"{headerData.Name}: {headerData.Value}\r\n";
+                    }
+                }
+
+                headerString = GetHttpResponseHeaders(statusCode, headerString, 0, connectionClose: true);
+
+                await SendResponseAsync(headerString).ConfigureAwait(false);
+            }
+
             public override async Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
             {
                 await SendResponseAsync(Encoding.UTF8.GetString(body)).ConfigureAwait(false);
             }
+
+            public override async Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0)
+            {
+                var buffer = new byte[1024];
+                while (true)
+                {
+                    int bytesRead = await ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+                    if (!ignoreIncomingData)
+                    {
+                        Assert.Equal(0, bytesRead);
+                    }
+
+                    if (bytesRead == 0)
+                    {
+                        break;
+                    }
+                }
+            }
         }
 
         public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = null)
index d6cb6bc..8f31281 100644 (file)
@@ -2,7 +2,6 @@
 // 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.Concurrent;
 using System.Diagnostics;
 using System.IO;
 using System.Net.Security;
@@ -16,11 +15,6 @@ namespace System.Net.Http
 {
     internal static class ConnectHelper
     {
-        /// <summary>Pool of event args to use to establish connections.</summary>
-        private static readonly ConcurrentQueueSegment<ConnectEventArgs> s_connectEventArgs =
-            new ConcurrentQueueSegment<ConnectEventArgs>(
-                ConcurrentQueueSegment<ConnectEventArgs>.RoundUpToPowerOf2(Math.Max(2, Environment.ProcessorCount)));
-
         /// <summary>
         /// Helper type used by HttpClientHandler when wrapping SocketsHttpHandler to map its
         /// certificate validation callback to the one used by SslStream.
@@ -42,13 +36,8 @@ namespace System.Net.Http
         {
             // Rather than creating a new Socket and calling ConnectAsync on it, we use the static
             // Socket.ConnectAsync with a SocketAsyncEventArgs, as we can then use Socket.CancelConnectAsync
-            // to cancel it if needed. Rent or allocate one.
-            ConnectEventArgs saea;
-            if (!s_connectEventArgs.TryDequeue(out saea))
-            {
-                saea = new ConnectEventArgs();
-            }
-
+            // to cancel it if needed.
+            var saea = new ConnectEventArgs();
             try
             {
                 saea.Initialize(cancellationToken);
@@ -87,12 +76,7 @@ namespace System.Net.Http
             }
             finally
             {
-                // Pool the event args, or if the pool is full, dispose of it.
-                saea.Clear();
-                if (!s_connectEventArgs.TryEnqueue(saea))
-                {
-                    saea.Dispose();
-                }
+                saea.Dispose();
             }
         }
 
@@ -110,8 +94,6 @@ namespace System.Net.Http
                 Builder = b;
             }
 
-            public void Clear() => CancellationToken = default;
-
             protected override void OnCompleted(SocketAsyncEventArgs _)
             {
                 switch (SocketError)
index e8a764b..6ea5cd0 100644 (file)
@@ -1613,6 +1613,8 @@ namespace System.Net.Http
 
                 // Wait for the response headers to complete if they haven't already, propagating any exceptions.
                 await responseHeadersTask.ConfigureAwait(false);
+
+                return http2Stream.GetAndClearResponse();
             }
             catch (Exception e)
             {
@@ -1651,8 +1653,6 @@ namespace System.Net.Http
                 }
                 throw;
             }
-
-            return http2Stream.Response;
         }
 
         private Http2Stream AddStream(HttpRequestMessage request)
index c24cd09..ddb9dfa 100644 (file)
@@ -2,6 +2,7 @@
 // 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.IO;
 using System.Net.Http.Headers;
@@ -32,6 +33,8 @@ namespace System.Net.Http
             private readonly CreditManager _streamWindow;
             private readonly HttpRequestMessage _request;
             private HttpResponseMessage _response;
+            /// <summary>Stores any trailers received after returning the response content to the caller.</summary>
+            private List<KeyValuePair<HeaderDescriptor, string>> _trailers;
 
             private ArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
             private int _pendingWindowUpdate;
@@ -100,8 +103,17 @@ namespace System.Net.Http
             private object SyncObject => _streamWindow;
 
             public int StreamId => _streamId;
-            public HttpRequestMessage Request => _request;
-            public HttpResponseMessage Response => _response;
+
+            public HttpResponseMessage GetAndClearResponse()
+            {
+                // Once SendAsync completes, the Http2Stream should no longer hold onto the response message.
+                // Since the Http2Stream is rooted by the Http2Connection dictionary, doing so would prevent
+                // the response stream from being collected and finalized if it were to be dropped without
+                // being disposed first.
+                HttpResponseMessage r = _response;
+                _response = null;
+                return r;
+            }
 
             public async Task SendRequestBodyAsync(CancellationToken cancellationToken)
             {
@@ -272,7 +284,7 @@ namespace System.Net.Http
                             {
                                 _state = StreamState.ExpectingHeaders;
                                 // If we tried 100-Continue and got rejected signal that we should not send request body.
-                                _shouldSendRequestBody = (int)Response.StatusCode < 300;
+                                _shouldSendRequestBody = (int)_response.StatusCode < 300;
                                 shouldSendRequestBodyWaiter?.TrySetResult(_shouldSendRequestBody);
                             }
                         }
@@ -308,14 +320,17 @@ namespace System.Net.Http
                         // if the header can't be added, we silently drop it.
                         if (_state == StreamState.ExpectingTrailingHeaders)
                         {
-                            _response.TrailingHeaders.TryAddWithoutValidation(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
+                            Debug.Assert(_trailers != null);
+                            _trailers.Add(KeyValuePair.Create(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue));
                         }
                         else if (descriptor.HeaderType == HttpHeaderType.Content)
                         {
+                            Debug.Assert(_response != null);
                             _response.Content.Headers.TryAddWithoutValidation(descriptor, headerValue);
                         }
                         else
                         {
+                            Debug.Assert(_response != null);
                             _response.Headers.TryAddWithoutValidation(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
                         }
                     }
@@ -339,6 +354,7 @@ namespace System.Net.Http
                     if (_state == StreamState.ExpectingData)
                     {
                         _state = StreamState.ExpectingTrailingHeaders;
+                        _trailers ??= new List<KeyValuePair<HeaderDescriptor, string>>();
                     }
                 }
             }
@@ -538,9 +554,18 @@ namespace System.Net.Http
                 }
 
                 // Start to process the response body.
-                ((HttpConnectionResponseContent)_response.Content).SetStream(emptyResponse ?
-                    EmptyReadStream.Instance :
-                    (Stream)new Http2ReadStream(this));
+                var responseContent = (HttpConnectionResponseContent)_response.Content;
+                if (emptyResponse)
+                {
+                    // If there are any trailers, copy them over to the response.  Normally this would be handled by
+                    // the response stream hitting EOF, but if there is no response body, we do it here.
+                    CopyTrailersToResponseMessage(_response);
+                    responseContent.SetStream(EmptyReadStream.Instance);
+                }
+                else
+                {
+                    responseContent.SetStream(new Http2ReadStream(this));
+                }
 
                 // Process Set-Cookie headers.
                 if (_connection._pool.Settings._useCookies)
@@ -603,7 +628,7 @@ namespace System.Net.Http
                 }
             }
 
-            public int ReadData(Span<byte> buffer, CancellationToken cancellationToken)
+            public int ReadData(Span<byte> buffer, HttpResponseMessage responseMessage)
             {
                 if (buffer.Length == 0)
                 {
@@ -615,8 +640,7 @@ namespace System.Net.Http
                 {
                     // Synchronously block waiting for data to be produced.
                     Debug.Assert(bytesRead == 0);
-                    GetWaiterTask(cancellationToken).AsTask().GetAwaiter().GetResult();
-                    CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
+                    GetWaiterTask(default).AsTask().GetAwaiter().GetResult();
                     (wait, bytesRead) = TryReadFromBuffer(buffer);
                     Debug.Assert(!wait);
                 }
@@ -624,13 +648,17 @@ namespace System.Net.Http
                 if (bytesRead != 0)
                 {
                     ExtendWindow(bytesRead);
-                    _connection.ExtendWindow(bytesRead);
+                }
+                else
+                {
+                    // We've hit EOF.  Pull in from the Http2Stream any trailers that were temporarily stored there.
+                    CopyTrailersToResponseMessage(responseMessage);
                 }
 
                 return bytesRead;
             }
 
-            public async ValueTask<int> ReadDataAsync(Memory<byte> buffer, CancellationToken cancellationToken)
+            public async ValueTask<int> ReadDataAsync(Memory<byte> buffer, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
             {
                 if (buffer.Length == 0)
                 {
@@ -650,10 +678,27 @@ namespace System.Net.Http
                 {
                     ExtendWindow(bytesRead);
                 }
+                else
+                {
+                    // We've hit EOF.  Pull in from the Http2Stream any trailers that were temporarily stored there.
+                    CopyTrailersToResponseMessage(responseMessage);
+                }
 
                 return bytesRead;
             }
 
+            private void CopyTrailersToResponseMessage(HttpResponseMessage responseMessage)
+            {
+                if (_trailers != null && _trailers.Count > 0)
+                {
+                    foreach (KeyValuePair<HeaderDescriptor, string> trailer in _trailers)
+                    {
+                        responseMessage.TrailingHeaders.TryAddWithoutValidation(trailer.Key, trailer.Value);
+                    }
+                    _trailers.Clear();
+                }
+            }
+
             private async Task SendDataAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
             {
                 ReadOnlyMemory<byte> remaining = buffer;
@@ -774,11 +819,27 @@ namespace System.Net.Http
             private sealed class Http2ReadStream : HttpBaseStream
             {
                 private Http2Stream _http2Stream;
+                private readonly HttpResponseMessage _responseMessage;
 
                 public Http2ReadStream(Http2Stream http2Stream)
                 {
                     Debug.Assert(http2Stream != null);
+                    Debug.Assert(http2Stream._response != null);
                     _http2Stream = http2Stream;
+                    _responseMessage = _http2Stream._response;
+                }
+
+                ~Http2ReadStream()
+                {
+                    if (NetEventSource.IsEnabled) _http2Stream?.Trace("");
+                    try
+                    {
+                        Dispose(disposing: false);
+                    }
+                    catch (Exception e)
+                    {
+                        if (NetEventSource.IsEnabled) _http2Stream?.Trace($"Error: {e}");
+                    }
                 }
 
                 protected override void Dispose(bool disposing)
@@ -789,17 +850,20 @@ namespace System.Net.Http
                         return;
                     }
 
-                    if (disposing)
-                    {
-                        if (http2Stream._state != StreamState.Aborted && http2Stream._state != StreamState.Complete)
-                        {
-                            // If we abort response stream before endOfStream, let server know.
-                            IgnoreExceptions(http2Stream._connection.SendRstStreamAsync(http2Stream._streamId, Http2ProtocolErrorCode.Cancel));
-                        }
+                    // Technically we shouldn't be doing the following work when disposing == false,
+                    // as the following work relies on other finalizable objects.  But given the HTTP/2
+                    // protocol, we have little choice: if someone drops the Http2ReadStream without
+                    // disposing of it, we need to a) signal to the server that the stream is being
+                    // canceled, and b) clean up the associated state in the Http2Connection.
 
-                        http2Stream.Dispose();
+                    if (http2Stream._state != StreamState.Aborted && http2Stream._state != StreamState.Complete)
+                    {
+                        // If we abort response stream before endOfStream, let server know.
+                        IgnoreExceptions(http2Stream._connection.SendRstStreamAsync(http2Stream._streamId, Http2ProtocolErrorCode.Cancel));
                     }
 
+                    http2Stream.Dispose();
+
                     base.Dispose(disposing);
                 }
 
@@ -814,7 +878,7 @@ namespace System.Net.Http
                         throw new IOException(SR.net_http_client_execution_error, http2Stream._abortException);
                     }
 
-                    return http2Stream.ReadData(destination, CancellationToken.None);
+                    return http2Stream.ReadData(destination, _responseMessage);
                 }
 
                 public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken)
@@ -835,7 +899,7 @@ namespace System.Net.Http
                         return new ValueTask<int>(Task.FromCanceled<int>(cancellationToken));
                     }
 
-                    return http2Stream.ReadDataAsync(destination, cancellationToken);
+                    return http2Stream.ReadDataAsync(destination, _responseMessage, cancellationToken);
                 }
 
                 public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.net_http_content_readonly_stream);
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Finalization.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Finalization.cs
new file mode 100644 (file)
index 0000000..70a6962
--- /dev/null
@@ -0,0 +1,61 @@
+// 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.Net.Test.Common;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Http.Functional.Tests
+{
+    public abstract class HttpClientHandler_Finalization_Test : HttpClientHandlerTestBase
+    {
+        public HttpClientHandler_Finalization_Test(ITestOutputHelper output) : base(output) { }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static Task GetAndDropResponse(HttpClient client, Uri url)
+        {
+            return Task.Run(async () =>
+            {
+                // Get the response stream, but don't dispose it or return it. Just drop it.
+                await client.GetStreamAsync(url);
+            });
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
+        public async Task IncompleteResponseStream_ResponseDropped_CancelsRequestToServer()
+        {
+            using (HttpClient client = CreateHttpClient())
+            {
+                bool stopGCs = false;
+                await LoopbackServerFactory.CreateClientAndServerAsync(async url =>
+                {
+                    await GetAndDropResponse(client, url);
+
+                    while (!Volatile.Read(ref stopGCs))
+                    {
+                        await Task.Delay(10);
+                        GC.Collect();
+                        GC.WaitForPendingFinalizers();
+                    }
+                },
+                server => server.AcceptConnectionAsync(async connection =>
+                {
+                    try
+                    {
+                        HttpRequestData data = await connection.ReadRequestDataAsync(readBody: false);
+                        await connection.SendResponseHeadersAsync(headers: new HttpHeaderData[] { new HttpHeaderData("SomeHeaderName", "AndValue") });
+                        await connection.WaitForCancellationAsync();
+                    }
+                    finally
+                    {
+                        Volatile.Write(ref stopGCs, true);
+                    }
+                }));
+            }
+        }
+    }
+}
index 20e2563..b1b774d 100644 (file)
@@ -152,6 +152,19 @@ namespace System.Net.Http.Functional.Tests
         protected override bool UseSocketsHttpHandler => true;
     }
 
+    public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test : HttpClientHandler_Finalization_Test
+    {
+        public SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test(ITestOutputHelper output) : base(output) { }
+        protected override bool UseSocketsHttpHandler => true;
+    }
+
+    public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test : HttpClientHandler_Finalization_Test
+    {
+        public SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test(ITestOutputHelper output) : base(output) { }
+        protected override bool UseSocketsHttpHandler => true;
+        protected override bool UseHttp2 => true;
+    }
+
     public sealed class SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test : HttpClientHandler_MaxConnectionsPerServer_Test
     {
         protected override bool UseSocketsHttpHandler => true;
index bc19dce..8b94518 100644 (file)
     <Compile Include="FakeDiagnosticSourceListenerObserver.cs" />
     <Compile Include="FormUrlEncodedContentTest.cs" />
     <Compile Include="ByteAtATimeContent.cs" />
+    <Compile Include="HttpClientHandlerTest.cs" />
+    <Compile Include="HttpClientHandlerTest.Asynchrony.cs" />
     <Compile Include="HttpClientHandlerTest.Authentication.cs" />
     <Compile Include="HttpClientHandlerTest.AutoRedirect.cs" />
-    <Compile Include="HttpClientHandlerTest.cs" />
     <Compile Include="HttpClientHandlerTest.Cancellation.cs" />
     <Compile Include="HttpClientHandlerTest.ClientCertificates.cs" />
-    <Compile Include="HttpClientHandlerTest.Asynchrony.cs" />
+    <Compile Include="HttpClientHandlerTest.Cookies.cs" />
     <Compile Include="HttpClientHandlerTest.DefaultProxyCredentials.cs" />
-    <Compile Include="HttpClientHandlerTest.Proxy.cs" />
-    <Compile Include="HttpClientHandlerTest.ResponseDrain.cs" />
+    <Compile Include="HttpClientHandlerTest.Finalization.cs" />
+    <Compile Include="HttpClientHandlerTest.Headers.cs" />
     <Compile Include="HttpClientHandlerTest.MaxConnectionsPerServer.cs" />
     <Compile Include="HttpClientHandlerTest.MaxResponseHeadersLength.cs" />
+    <Compile Include="HttpClientHandlerTest.Proxy.cs" />
+    <Compile Include="HttpClientHandlerTest.ResponseDrain.cs" />
     <Compile Include="HttpClientHandlerTest.ServerCertificates.cs" />
     <Compile Include="HttpClientHandlerTest.ServerCertificates.Unix.cs" Condition="'$(TargetsUnix)' == 'true'" />
     <Compile Include="HttpClientHandlerTest.ServerCertificates.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
     <Compile Include="HttpContentTest.cs" />
     <Compile Include="HttpMessageInvokerTest.cs" />
     <Compile Include="HttpMethodTest.cs" />
-    <Compile Include="HttpClientHandlerTest.Cookies.cs" />
-    <Compile Include="HttpClientHandlerTest.Headers.cs" />
     <Compile Include="HttpRetryProtocolTests.cs" />
     <Compile Include="IdnaProtocolTests.cs" />
     <Compile Include="HttpProtocolTests.cs" />