From e63b895c1fa93848b2f696558d3a75f02fd7a67d Mon Sep 17 00:00:00 2001 From: Alexander Nikolaev <55398552+alnikola@users.noreply.github.com> Date: Tue, 8 Oct 2019 11:22:15 +0200 Subject: [PATCH] HttpWebRequest caches HttpClient in simple cases (dotnet/corefx#41462) 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/System/Net/HttpWebRequest.cs | 251 +++++++++++++++------ .../tests/HttpWebRequestTest.cs | 235 ++++++++++++++++++- 2 files changed, 420 insertions(+), 66 deletions(-) diff --git a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs index c25feb5..6bfd49f 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs @@ -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 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; + } + } } } diff --git a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs index 666bc97..d8f0fdb 100644 --- a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs @@ -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 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 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 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(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 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 secondAccept = listener.AcceptAsync(); + + Task 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(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 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 secondAccept = listener.AcceptAsync(); + + HttpWebRequest currentRequest = WebRequest.CreateHttp(uri); + currentParameters.Configure(currentRequest); + + Task 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 secondAccept, Task 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(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None); + } + + private static async Task VerifyResponse(string expectedResponse, Task 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() { -- 2.7.4