HttpWebRequest caches HttpClient in simple cases (dotnet/corefx#41462)
authorAlexander Nikolaev <55398552+alnikola@users.noreply.github.com>
Tue, 8 Oct 2019 09:22:15 +0000 (11:22 +0200)
committerGitHub <noreply@github.com>
Tue, 8 Oct 2019 09:22:15 +0000 (11:22 +0200)
HttpWebRequest caches and tries to reuse a single static HttpClient instance when it's safe to share the same instance among concurrent requests with the given parameters.
Fixes dotnet/corefx#15460

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

src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs
src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs

index c25feb5..6bfd49f 100644 (file)
@@ -73,6 +73,10 @@ namespace System.Net
         private bool _preAuthenticate;
         private DecompressionMethods _automaticDecompression = HttpHandlerDefaults.DefaultAutomaticDecompression;
 
+        private static readonly object s_syncRoot = new object();
+        private static volatile HttpClient s_cachedHttpClient;
+        private static HttpClientParameters s_cachedHttpClientParameters;
+
         //these should be safe.
         [Flags]
         private enum Booleans : uint
@@ -91,6 +95,69 @@ namespace System.Net
             IsWebSocketRequest = 0x00000800,
             Default = AllowAutoRedirect | AllowWriteStreamBuffering | ExpectContinue
         }
+
+        private class HttpClientParameters
+        {
+            public readonly DecompressionMethods AutomaticDecompression;
+            public readonly bool AllowAutoRedirect;
+            public readonly int MaximumAutomaticRedirections;
+            public readonly int MaximumResponseHeadersLength;
+            public readonly bool PreAuthenticate;
+            public readonly TimeSpan Timeout;
+            public readonly SecurityProtocolType SslProtocols;
+            public readonly bool CheckCertificateRevocationList;
+            public readonly ICredentials Credentials;
+            public readonly IWebProxy Proxy;
+            public readonly RemoteCertificateValidationCallback ServerCertificateValidationCallback;
+            public readonly X509CertificateCollection ClientCertificates;
+            public readonly CookieContainer CookieContainer;
+
+            public HttpClientParameters(HttpWebRequest webRequest)
+            {
+                AutomaticDecompression = webRequest.AutomaticDecompression;
+                AllowAutoRedirect = webRequest.AllowAutoRedirect;
+                MaximumAutomaticRedirections = webRequest.MaximumAutomaticRedirections;
+                MaximumResponseHeadersLength = webRequest.MaximumResponseHeadersLength;
+                PreAuthenticate = webRequest.PreAuthenticate;
+                Timeout = webRequest.Timeout == Threading.Timeout.Infinite
+                    ? Threading.Timeout.InfiniteTimeSpan
+                    : TimeSpan.FromMilliseconds(webRequest.Timeout);
+                SslProtocols = ServicePointManager.SecurityProtocol;
+                CheckCertificateRevocationList = ServicePointManager.CheckCertificateRevocationList;
+                Credentials = webRequest._credentials;
+                Proxy = webRequest._proxy;
+                ServerCertificateValidationCallback = webRequest.ServerCertificateValidationCallback ?? ServicePointManager.ServerCertificateValidationCallback;
+                ClientCertificates = webRequest._clientCertificates;
+                CookieContainer = webRequest._cookieContainer;
+            }
+
+            public bool Matches(HttpClientParameters requestParameters)
+            {
+                return AutomaticDecompression == requestParameters.AutomaticDecompression
+                    && AllowAutoRedirect == requestParameters.AllowAutoRedirect
+                    && MaximumAutomaticRedirections == requestParameters.MaximumAutomaticRedirections
+                    && MaximumResponseHeadersLength == requestParameters.MaximumResponseHeadersLength
+                    && PreAuthenticate == requestParameters.PreAuthenticate
+                    && Timeout == requestParameters.Timeout
+                    && SslProtocols == requestParameters.SslProtocols
+                    && CheckCertificateRevocationList == requestParameters.CheckCertificateRevocationList
+                    && ReferenceEquals(Credentials, requestParameters.Credentials)
+                    && ReferenceEquals(Proxy, requestParameters.Proxy)
+                    && ReferenceEquals(ServerCertificateValidationCallback, requestParameters.ServerCertificateValidationCallback)
+                    && ReferenceEquals(ClientCertificates, requestParameters.ClientCertificates)
+                    && ReferenceEquals(CookieContainer, requestParameters.CookieContainer);
+            }
+
+            public bool AreParametersAcceptableForCaching()
+            {
+                return Credentials == null
+                    && ReferenceEquals(Proxy, DefaultWebProxy)
+                    && ServerCertificateValidationCallback == null
+                    && ClientCertificates == null
+                    && CookieContainer == null;
+            }
+        }
+
         private const string ContinueHeader = "100-continue";
         private const string ChunkedHeader = "chunked";
 
@@ -1093,80 +1160,19 @@ namespace System.Net
                 throw new InvalidOperationException(SR.net_reqsubmitted);
             }
 
-            var handler = new HttpClientHandler();
             var request = new HttpRequestMessage(new HttpMethod(_originVerb), _requestUri);
 
-            using (var client = new HttpClient(handler))
+            bool disposeRequired = false;
+            HttpClient client = null;
+            try
             {
+                client = GetCachedOrCreateHttpClient(out disposeRequired);
                 if (_requestStream != null)
                 {
                     ArraySegment<byte> bytes = _requestStream.GetBuffer();
                     request.Content = new ByteArrayContent(bytes.Array, bytes.Offset, bytes.Count);
                 }
 
-                handler.AutomaticDecompression = AutomaticDecompression;
-                handler.Credentials = _credentials;
-                handler.AllowAutoRedirect = AllowAutoRedirect;
-                handler.MaxAutomaticRedirections = MaximumAutomaticRedirections;
-                handler.MaxResponseHeadersLength = MaximumResponseHeadersLength;
-                handler.PreAuthenticate = PreAuthenticate;
-                client.Timeout = Timeout == Threading.Timeout.Infinite ?
-                    Threading.Timeout.InfiniteTimeSpan :
-                    TimeSpan.FromMilliseconds(Timeout);
-
-                if (_cookieContainer != null)
-                {
-                    handler.CookieContainer = _cookieContainer;
-                    Debug.Assert(handler.UseCookies); // Default of handler.UseCookies is true.
-                }
-                else
-                {
-                    handler.UseCookies = false;
-                }
-
-                Debug.Assert(handler.UseProxy); // Default of handler.UseProxy is true.
-                Debug.Assert(handler.Proxy == null); // Default of handler.Proxy is null.
-
-                // HttpClientHandler default is to use a proxy which is the system proxy.
-                // This is indicated by the properties 'UseProxy == true' and 'Proxy == null'.
-                //
-                // However, HttpWebRequest doesn't have a separate 'UseProxy' property. Instead,
-                // the default of the 'Proxy' property is a non-null IWebProxy object which is the
-                // system default proxy object. If the 'Proxy' property were actually null, then
-                // that means don't use any proxy.
-                //
-                // So, we need to map the desired HttpWebRequest proxy settings to equivalent
-                // HttpClientHandler settings.
-                if (_proxy == null)
-                {
-                    handler.UseProxy = false;
-                }
-                else if (!object.ReferenceEquals(_proxy, WebRequest.GetSystemWebProxy()))
-                {
-                    handler.Proxy = _proxy;
-                }
-                else
-                {
-                    // Since this HttpWebRequest is using the default system proxy, we need to
-                    // pass any proxy credentials that the developer might have set via the
-                    // WebRequest.DefaultWebProxy.Credentials property.
-                    handler.DefaultProxyCredentials = _proxy.Credentials;
-                }
-
-                handler.ClientCertificates.AddRange(ClientCertificates);
-
-                // Set relevant properties from ServicePointManager
-                handler.SslProtocols = (SslProtocols)ServicePointManager.SecurityProtocol;
-                handler.CheckCertificateRevocationList = ServicePointManager.CheckCertificateRevocationList;
-                RemoteCertificateValidationCallback rcvc = ServerCertificateValidationCallback != null ?
-                                                ServerCertificateValidationCallback :
-                                                ServicePointManager.ServerCertificateValidationCallback;
-                if (rcvc != null)
-                {
-                    RemoteCertificateValidationCallback localRcvc = rcvc;
-                    handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => localRcvc(this, cert, chain, errors);
-                }
-
                 if (_hostUri != null)
                 {
                     request.Headers.Host = Host;
@@ -1227,6 +1233,13 @@ namespace System.Net
 
                 return response;
             }
+            finally
+            {
+                if (disposeRequired)
+                {
+                    client?.Dispose();
+                }
+            }
         }
 
         public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state)
@@ -1486,5 +1499,113 @@ namespace System.Net
             string s = Address.Scheme + "://" + hostName + Address.PathAndQuery;
             return Uri.TryCreate(s, UriKind.Absolute, out hostUri);
         }
+
+        private HttpClient GetCachedOrCreateHttpClient(out bool disposeRequired)
+        {
+            var parameters = new HttpClientParameters(this);
+            if (parameters.AreParametersAcceptableForCaching())
+            {
+                disposeRequired = false;
+                if (s_cachedHttpClient == null)
+                {
+                    lock (s_syncRoot)
+                    {
+                        if (s_cachedHttpClient == null)
+                        {
+                            s_cachedHttpClientParameters = parameters;
+                            s_cachedHttpClient = CreateHttpClient(parameters, null);
+                            return s_cachedHttpClient;
+                        }
+                    }
+                }
+
+                if (s_cachedHttpClientParameters.Matches(parameters))
+                {
+                    return s_cachedHttpClient;
+                }
+            }
+
+            disposeRequired = true;
+            return CreateHttpClient(parameters, this);
+        }
+
+        private static HttpClient CreateHttpClient(HttpClientParameters parameters, HttpWebRequest request)
+        {
+            HttpClient client = null;
+            try
+            {
+                var handler = new HttpClientHandler();
+                client = new HttpClient(handler);
+                handler.AutomaticDecompression = parameters.AutomaticDecompression;
+                handler.Credentials = parameters.Credentials;
+                handler.AllowAutoRedirect = parameters.AllowAutoRedirect;
+                handler.MaxAutomaticRedirections = parameters.MaximumAutomaticRedirections;
+                handler.MaxResponseHeadersLength = parameters.MaximumResponseHeadersLength;
+                handler.PreAuthenticate = parameters.PreAuthenticate;
+                client.Timeout = parameters.Timeout;
+
+                if (parameters.CookieContainer != null)
+                {
+                    handler.CookieContainer = parameters.CookieContainer;
+                    Debug.Assert(handler.UseCookies); // Default of handler.UseCookies is true.
+                }
+                else
+                {
+                    handler.UseCookies = false;
+                }
+
+                Debug.Assert(handler.UseProxy); // Default of handler.UseProxy is true.
+                Debug.Assert(handler.Proxy == null); // Default of handler.Proxy is null.
+
+                // HttpClientHandler default is to use a proxy which is the system proxy.
+                // This is indicated by the properties 'UseProxy == true' and 'Proxy == null'.
+                //
+                // However, HttpWebRequest doesn't have a separate 'UseProxy' property. Instead,
+                // the default of the 'Proxy' property is a non-null IWebProxy object which is the
+                // system default proxy object. If the 'Proxy' property were actually null, then
+                // that means don't use any proxy.
+                //
+                // So, we need to map the desired HttpWebRequest proxy settings to equivalent
+                // HttpClientHandler settings.
+                if (parameters.Proxy == null)
+                {
+                    handler.UseProxy = false;
+                }
+                else if (!object.ReferenceEquals(parameters.Proxy, WebRequest.GetSystemWebProxy()))
+                {
+                    handler.Proxy = parameters.Proxy;
+                }
+                else
+                {
+                    // Since this HttpWebRequest is using the default system proxy, we need to
+                    // pass any proxy credentials that the developer might have set via the
+                    // WebRequest.DefaultWebProxy.Credentials property.
+                    handler.DefaultProxyCredentials = parameters.Proxy.Credentials;
+                }
+
+                if (parameters.ClientCertificates != null)
+                {
+                    handler.ClientCertificates.AddRange(parameters.ClientCertificates);
+                }
+
+                // Set relevant properties from ServicePointManager
+                handler.SslProtocols = (SslProtocols)parameters.SslProtocols;
+                handler.CheckCertificateRevocationList = parameters.CheckCertificateRevocationList;
+                RemoteCertificateValidationCallback rcvc = parameters.ServerCertificateValidationCallback;
+                if (rcvc != null)
+                {
+                    RemoteCertificateValidationCallback localRcvc = rcvc;
+                    HttpWebRequest localRequest = request;
+                    handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => localRcvc(localRequest, cert, chain, errors);
+                }
+
+                return client;
+            }
+            catch
+            {
+                client?.Dispose();
+                throw;
+            }
+        }
     }
 }
index 666bc97..d8f0fdb 100644 (file)
@@ -9,10 +9,14 @@ using System.IO;
 using System.Linq;
 using System.Net.Cache;
 using System.Net.Http;
+using System.Net.Security;
 using System.Net.Sockets;
 using System.Net.Test.Common;
 using System.Runtime.Serialization.Formatters.Binary;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
 using System.Text;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.DotNet.RemoteExecutor;
@@ -25,6 +29,45 @@ namespace System.Net.Tests
 
     public partial class HttpWebRequestTest
     {
+        public class HttpWebRequestParameters
+        {
+            public DecompressionMethods AutomaticDecompression { get; set; }
+            public bool AllowAutoRedirect { get; set; }
+            public int MaximumAutomaticRedirections { get; set; }
+            public int MaximumResponseHeadersLength { get; set; }
+            public bool PreAuthenticate { get; set; }
+            public int Timeout { get; set; }
+            public SecurityProtocolType SslProtocols { get; set; }
+            public bool CheckCertificateRevocationList { get; set; }
+            public bool NewCredentials { get; set; }
+            public bool NewProxy { get; set; }
+            public bool NewServerCertificateValidationCallback { get; set; }
+            public bool NewClientCertificates { get; set; }
+            public bool NewCookieContainer { get; set; }
+
+            public void Configure(HttpWebRequest webRequest)
+            {
+                webRequest.AutomaticDecompression = AutomaticDecompression;
+                webRequest.AllowAutoRedirect = AllowAutoRedirect;
+                webRequest.MaximumAutomaticRedirections = MaximumAutomaticRedirections;
+                webRequest.MaximumResponseHeadersLength = MaximumResponseHeadersLength;
+                webRequest.PreAuthenticate = PreAuthenticate;
+                webRequest.Timeout = Timeout;
+                ServicePointManager.SecurityProtocol = SslProtocols;
+                ServicePointManager.CheckCertificateRevocationList = CheckCertificateRevocationList;
+                if (NewCredentials)
+                    webRequest.Credentials = CredentialCache.DefaultCredentials;
+                if (NewProxy)
+                    webRequest.Proxy = new WebProxy();
+                if (NewServerCertificateValidationCallback)
+                    ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
+                if (NewClientCertificates)
+                    webRequest.ClientCertificates = new X509CertificateCollection();
+                if (NewCookieContainer)
+                    webRequest.CookieContainer = new CookieContainer();
+            }
+        }
+
         private const string RequestBody = "This is data to POST.";
         private readonly byte[] _requestBodyBytes = Encoding.UTF8.GetBytes(RequestBody);
         private readonly NetworkCredential _explicitCredential = new NetworkCredential("user", "password", "domain");
@@ -32,6 +75,47 @@ namespace System.Net.Tests
 
         public static readonly object[][] EchoServers = Configuration.Http.EchoServers;
 
+        public static IEnumerable<object[]> CachableWebRequestParameters()
+        {
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.Deflate,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 3, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 110, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = false, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls11, Timeout = 10000}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10250}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000}, true};
+        }
+
+        public static IEnumerable<object[]> MixedWebRequestParameters()
+        {
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000}, true};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 10000,
+                NewServerCertificateValidationCallback = true }, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000,
+                NewCredentials = true}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000,
+                NewProxy = true}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000,
+                NewClientCertificates = true}, false};
+            yield return new object[] {new HttpWebRequestParameters { AllowAutoRedirect = true, AutomaticDecompression = DecompressionMethods.GZip,
+                MaximumAutomaticRedirections = 2, MaximumResponseHeadersLength = 100, PreAuthenticate = true, SslProtocols = SecurityProtocolType.Tls12, Timeout = 100000,
+                NewCookieContainer = true}, false};
+        }
+
         public static IEnumerable<object[]> Dates_ReadValue_Data()
         {
             var zero_formats = new[]
@@ -1415,7 +1499,7 @@ namespace System.Net.Tests
                     WebRequest.DefaultWebProxy.Credentials = new NetworkCredential(user, pw);
                     HttpWebRequest request = HttpWebRequest.CreateHttp(Configuration.Http.RemoteEchoServer);
 
-                    using (var response = (HttpWebResponse) await request.GetResponseAsync())
+                    using (var response = (HttpWebResponse)await request.GetResponseAsync())
                     {
                         Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                     }
@@ -1531,6 +1615,121 @@ namespace System.Net.Tests
             Assert.Equal(MediaType, request.MediaType);
         }
 
+        [Theory, MemberData(nameof(MixedWebRequestParameters))]
+        [SkipOnTargetFramework(TargetFrameworkMonikers.Uap)]
+        public void GetResponseAsync_ParametersAreNotCachable_CreateNewClient(HttpWebRequestParameters requestParameters, bool connectionReusedParameter)
+        {
+            RemoteExecutor.Invoke(async (serializedParameters, connectionReusedString) =>
+            {
+                var parameters = JsonSerializer.Deserialize<HttpWebRequestParameters>(serializedParameters);
+
+                using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
+                {
+                    listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+                    listener.Listen(1);
+                    var ep = (IPEndPoint)listener.LocalEndPoint;
+                    var uri = new Uri($"http://{ep.Address}:{ep.Port}/");
+
+                    HttpWebRequest request0 = WebRequest.CreateHttp(uri);
+                    HttpWebRequest request1 = WebRequest.CreateHttp(uri);
+                    parameters.Configure(request0);
+                    parameters.Configure(request1);
+                    request0.Method = HttpMethod.Get.Method;
+                    request1.Method = HttpMethod.Get.Method;
+
+                    string responseContent = "Test response.";
+
+                    Task<WebResponse> firstResponseTask = request0.GetResponseAsync();
+                    using (Socket server = await listener.AcceptAsync())
+                    using (var serverStream = new NetworkStream(server, ownsSocket: false))
+                    using (var serverReader = new StreamReader(serverStream))
+                    {
+                        await ReplyToClient(responseContent, server, serverReader);
+                        await VerifyResponse(responseContent, firstResponseTask);
+
+                        Task<Socket> secondAccept = listener.AcceptAsync();
+
+                        Task<WebResponse> secondResponseTask = request1.GetResponseAsync();
+                        await ReplyToClient(responseContent, server, serverReader);
+                        if (bool.Parse(connectionReusedString))
+                        {
+                            Assert.False(secondAccept.IsCompleted);
+                            await VerifyResponse(responseContent, secondResponseTask);
+                        }
+                        else
+                        {
+                            await VerifyNewConnection(responseContent, secondAccept, secondResponseTask);
+                        }
+                    }
+                }
+                return RemoteExecutor.SuccessExitCode;
+            }, JsonSerializer.Serialize<HttpWebRequestParameters>(requestParameters), connectionReusedParameter.ToString()).Dispose();
+        }
+
+        [Fact]
+        [SkipOnTargetFramework(TargetFrameworkMonikers.Uap)]
+        public void GetResponseAsync_ParametersAreCachableButDifferent_CreateNewClient()
+        {
+            RemoteExecutor.Invoke(async () =>
+             {
+                 using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
+                 {
+                     listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+                     listener.Listen(1);
+                     var ep = (IPEndPoint)listener.LocalEndPoint;
+                     var uri = new Uri($"http://{ep.Address}:{ep.Port}/");
+
+                     var referenceParameters = new HttpWebRequestParameters
+                     {
+                         AllowAutoRedirect = true,
+                         AutomaticDecompression = DecompressionMethods.GZip,
+                         MaximumAutomaticRedirections = 2,
+                         MaximumResponseHeadersLength = 100,
+                         PreAuthenticate = true,
+                         SslProtocols = SecurityProtocolType.Tls12,
+                         Timeout = 100000
+                     };
+                     HttpWebRequest firstRequest = WebRequest.CreateHttp(uri);
+                     referenceParameters.Configure(firstRequest);
+                     firstRequest.Method = HttpMethod.Get.Method;
+
+                     string responseContent = "Test response.";
+
+                     Task<WebResponse> firstResponseTask = firstRequest.GetResponseAsync();
+                     using (Socket server = await listener.AcceptAsync())
+                     using (var serverStream = new NetworkStream(server, ownsSocket: false))
+                     using (var serverReader = new StreamReader(serverStream))
+                     {
+                         await ReplyToClient(responseContent, server, serverReader);
+                         await VerifyResponse(responseContent, firstResponseTask);
+
+                         foreach (object[] caseRow in CachableWebRequestParameters())
+                         {
+                             var currentParameters = (HttpWebRequestParameters)caseRow[0];
+                             bool connectionReused = (bool)caseRow[1];
+                             Task<Socket> secondAccept = listener.AcceptAsync();
+
+                             HttpWebRequest currentRequest = WebRequest.CreateHttp(uri);
+                             currentParameters.Configure(currentRequest);
+
+                             Task<WebResponse> currentResponseTask = currentRequest.GetResponseAsync();
+                             if (connectionReused)
+                             {
+                                 await ReplyToClient(responseContent, server, serverReader);
+                                 Assert.False(secondAccept.IsCompleted);
+                                 await VerifyResponse(responseContent, currentResponseTask);
+                             }
+                             else
+                             {
+                                 await VerifyNewConnection(responseContent, secondAccept, currentResponseTask);
+                             }
+                         }
+                     }
+                 }
+                 return RemoteExecutor.SuccessExitCode;
+             }).Dispose();
+        }
+
         [Fact]
         public async Task HttpWebRequest_EndGetRequestStreamContext_ExpectedValue()
         {
@@ -1670,6 +1869,40 @@ namespace System.Net.Tests
             }
         }
 
+        private static async Task VerifyNewConnection(string responseContent, Task<Socket> secondAccept, Task<WebResponse> currentResponseTask)
+        {
+            Socket secondServer = await secondAccept;
+            Assert.True(secondAccept.IsCompleted);
+            using (var secondStream = new NetworkStream(secondServer, ownsSocket: false))
+            using (var secondReader = new StreamReader(secondStream))
+            {
+                await ReplyToClient(responseContent, secondServer, secondReader);
+                await VerifyResponse(responseContent, currentResponseTask);
+            }
+        }
+
+        private static async Task ReplyToClient(string responseContent, Socket server, StreamReader serverReader)
+        {
+            string responseBody =
+                    "HTTP/1.1 200 OK\r\n" +
+                    $"Date: {DateTimeOffset.UtcNow:R}\r\n" +
+                    $"Content-Length: {responseContent.Length}\r\n" +
+                    "\r\n" + responseContent;
+            while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())) ;
+            await server.SendAsync(new ArraySegment<byte>(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None);
+        }
+
+        private static async Task VerifyResponse(string expectedResponse, Task<WebResponse> responseTask)
+        {
+            WebResponse firstRequest = await responseTask;
+            using (Stream firstResponseStream = firstRequest.GetResponseStream())
+            using (var reader = new StreamReader(firstResponseStream))
+            {
+                string response = reader.ReadToEnd();
+                Assert.Equal(expectedResponse, response);
+            }
+        }
+
         [Fact]
         public void HttpWebRequest_Serialize_Fails()
         {