diff --git a/src/libraries/Common/tests/System/IO/DelegateStream.cs b/src/libraries/Common/tests/System/IO/DelegateStream.cs index 4325a00ecc876..65fa44ce683eb 100644 --- a/src/libraries/Common/tests/System/IO/DelegateStream.cs +++ b/src/libraries/Common/tests/System/IO/DelegateStream.cs @@ -54,14 +54,14 @@ public DelegateStream( _positionSetFunc = positionSetFunc ?? (_ => { throw new NotSupportedException(); }); _positionGetFunc = positionGetFunc ?? (() => { throw new NotSupportedException(); }); - _readFunc = readFunc; _readAsyncFunc = readAsyncFunc ?? ((buffer, offset, count, token) => base.ReadAsync(buffer, offset, count, token)); + _readFunc = readFunc ?? ((buffer, offset, count) => readAsyncFunc(buffer, offset, count, default).GetAwaiter().GetResult()); _seekFunc = seekFunc ?? ((_, __) => { throw new NotSupportedException(); }); _setLengthFunc = setLengthFunc ?? (_ => { throw new NotSupportedException(); }); - _writeFunc = writeFunc; _writeAsyncFunc = writeAsyncFunc ?? ((buffer, offset, count, token) => base.WriteAsync(buffer, offset, count, token)); + _writeFunc = writeFunc ?? ((buffer, offset, count) => writeAsyncFunc(buffer, offset, count, default).GetAwaiter().GetResult()); _disposeFunc = disposeFunc; } diff --git a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs index 67a95370cbde1..4fad8e55c30e4 100644 --- a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs +++ b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.IO; +using System.Threading; using System.Threading.Tasks; namespace System.Net.Http.Functional.Tests @@ -24,6 +25,11 @@ public ByteAtATimeContent(int length, Task waitToSend, TaskCompletionSource + SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); +#endif + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { await _waitToSend; diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs index 9c2c1e2c74e53..ce4d1bc9e38ff 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs @@ -44,6 +44,59 @@ private async Task CreateAndValidateRequest(HttpClientHandler handler, Uri url, public HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { } + [Theory] + [MemberData(nameof(Authentication_SocketsHttpHandler_TestData))] + public async Task SocketsHttpHandler_Authentication_Succeeds(string authenticateHeader, bool result) + { + await HttpClientHandler_Authentication_Succeeds(authenticateHeader, result); + } + + public static IEnumerable Authentication_SocketsHttpHandler_TestData() + { + // These test cases successfully authenticate on SocketsHttpHandler but fail on the other handlers. + // These are legal as per the RFC, so authenticating is the expected behavior. + // See https://github.com/dotnet/runtime/issues/25643 for details. + if (!IsWinHttpHandler) + { + // Unauthorized on WinHttpHandler + yield return new object[] {"Basic realm=\"testrealm1\" basic realm=\"testrealm1\"", true}; + yield return new object[] {"Basic something digest something", true}; + } + yield return new object[] { "Digest realm=\"api@example.org\", qop=\"auth\", algorithm=MD5-sess, nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", " + + "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true", true }; + yield return new object[] { "dIgEsT realm=\"api@example.org\", qop=\"auth\", algorithm=MD5-sess, nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", " + + "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true", true }; + + // These cases fail on WinHttpHandler because of a behavior in WinHttp that causes requests to be duplicated + // when the digest header has certain parameters. See https://github.com/dotnet/runtime/issues/25644 for details. + if (!IsWinHttpHandler) + { + // Timeouts on WinHttpHandler + yield return new object[] { "Digest ", false }; + yield return new object[] { "Digest realm=\"testrealm\", nonce=\"testnonce\", algorithm=\"myown\"", false }; + } + + // These cases fail to authenticate on SocketsHttpHandler, but succeed on the other handlers. + // they are all invalid as per the RFC, so failing is the expected behavior. See https://github.com/dotnet/runtime/issues/25645 for details. + if (!IsWinHttpHandler) + { + // Timeouts on WinHttpHandler + yield return new object[] {"Digest realm=withoutquotes, nonce=withoutquotes", false}; + } + yield return new object[] { "Digest realm=\"testrealm\" nonce=\"testnonce\"", false }; + yield return new object[] { "Digest realm=\"testrealm1\", nonce=\"testnonce1\" Digest realm=\"testrealm2\", nonce=\"testnonce2\"", false }; + + // These tests check that the algorithm parameter is treated in case insensitive way. + // WinHTTP only supports plain MD5, so other algorithms are included here. + yield return new object[] { $"Digest realm=\"testrealm\", algorithm=md5-Sess, nonce=\"testnonce\", qop=\"auth\"", true }; + if (!IsWinHttpHandler) + { + // Unauthorized on WinHttpHandler + yield return new object[] { $"Digest realm=\"testrealm\", algorithm=sha-256, nonce=\"testnonce\"", true }; + yield return new object[] { $"Digest realm=\"testrealm\", algorithm=sha-256-SESS, nonce=\"testnonce\", qop=\"auth\"", true }; + } + } + [Theory] [MemberData(nameof(Authentication_TestData))] public async Task HttpClientHandler_Authentication_Succeeds(string authenticateHeader, bool result) @@ -568,7 +621,7 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHost _output.WriteLine(request.RequestUri.AbsoluteUri.ToString()); _output.WriteLine($"Host: {request.Headers.Host}"); - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string body = await response.Content.ReadAsStringAsync(); @@ -602,12 +655,11 @@ public async Task Credentials_ServerUsesWindowsAuthentication_Success(string ser [InlineData("Negotiate")] public async Task Credentials_ServerChallengesWithWindowsAuth_ClientSendsWindowsAuthHeader(string authScheme) { -#if WINHTTPHANDLER_TEST - if (UseVersion > HttpVersion.Version11) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateClientAndServerAsync( async uri => { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs index 54c620cf0bb5b..8d1c598129764 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs @@ -40,32 +40,15 @@ public static IEnumerable RemoteServersAndRedirectStatusCodes() } } - public static readonly object[][] RedirectStatusCodesOldMethodsNewMethods = { - new object[] { 300, "GET", "GET" }, - new object[] { 300, "POST", "GET" }, - new object[] { 300, "HEAD", "HEAD" }, - - new object[] { 301, "GET", "GET" }, - new object[] { 301, "POST", "GET" }, - new object[] { 301, "HEAD", "HEAD" }, - - new object[] { 302, "GET", "GET" }, - new object[] { 302, "POST", "GET" }, - new object[] { 302, "HEAD", "HEAD" }, - - new object[] { 303, "GET", "GET" }, - new object[] { 303, "POST", "GET" }, - new object[] { 303, "HEAD", "HEAD" }, - - new object[] { 307, "GET", "GET" }, - new object[] { 307, "POST", "POST" }, - new object[] { 307, "HEAD", "HEAD" }, - - new object[] { 308, "GET", "GET" }, - new object[] { 308, "POST", "POST" }, - new object[] { 308, "HEAD", "HEAD" }, - }; - + public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() + { + foreach (int statusCode in new[] { 300, 301, 302, 303, 307, 308 }) + { + yield return new object[] { statusCode, "GET", "GET" }; + yield return new object[] { statusCode, "POST", statusCode <= 303 ? "GET" : "POST" }; + yield return new object[] { statusCode, "HEAD", "HEAD" }; + } + } public HttpClientHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } [OuterLoop("Uses external server")] @@ -112,7 +95,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => { var request = new HttpRequestMessage(new HttpMethod(oldMethod), origUrl) { Version = UseVersion }; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => { @@ -157,7 +140,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => request.Content = new StringContent(ExpectedContent); request.Headers.TransferEncodingChunked = true; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs index 453aa8d7b98ed..ac77cd7e258a2 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -4,11 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; -using System.DirectoryServices.Protocols; using System.IO; using System.Linq; using System.Net.Test.Common; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -28,7 +26,7 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes { public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } - [ConditionalTheory] + [Theory] [InlineData(false, CancellationMode.Token)] [InlineData(true, CancellationMode.Token)] public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode) @@ -39,12 +37,10 @@ public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(b return; } -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif var serverRelease = new TaskCompletionSource(); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => @@ -62,10 +58,13 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => req.Content = new ByteAtATimeContent(int.MaxValue, waitToSend.Task, contentSending, millisecondDelayBetweenBytes: 1); req.Headers.TransferEncodingChunked = chunkedTransfer; - Task resp = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task resp = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); waitToSend.SetResult(true); - await contentSending.Task; - Cancel(mode, client, cts); + await Task.WhenAny(contentSending.Task, resp); + if (!resp.IsCompleted) + { + Cancel(mode, client, cts); + } await ValidateClientCancellationAsync(() => resp); } } @@ -83,7 +82,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => }); } - [ConditionalTheory] + [Theory] [MemberData(nameof(OneBoolAndCancellationMode))] public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode) { @@ -93,12 +92,10 @@ public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuick return; } -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif using (HttpClient client = CreateHttpClient()) { @@ -124,7 +121,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await partialResponseHeadersSent.Task; Cancel(mode, client, cts); await getResponse; @@ -180,7 +177,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(req, HttpCompletionOption.ResponseContentRead, cts.Token); + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseContentRead, cts.Token); await responseHeadersSent.Task; await Task.Delay(1); // make it more likely that client will have started processing response body Cancel(mode, client, cts); @@ -196,7 +193,7 @@ await ValidateClientCancellationAsync(async () => } } - [ConditionalTheory] + [Theory] [MemberData(nameof(ThreeBools))] public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) { @@ -205,13 +202,11 @@ public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCance // There is no chunked encoding or connection header in HTTP/2 and later return; } - -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif using (HttpClient client = CreateHttpClient()) { @@ -238,7 +233,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await ValidateClientCancellationAsync(async () => { HttpResponseMessage resp = await getResponse; @@ -258,19 +253,18 @@ await ValidateClientCancellationAsync(async () => }); } } - [ConditionalTheory] + [Theory] [InlineData(CancellationMode.CancelPendingRequests, false)] [InlineData(CancellationMode.DisposeHttpClient, false)] [InlineData(CancellationMode.CancelPendingRequests, true)] [InlineData(CancellationMode.DisposeHttpClient, true)] public async Task GetAsync_CancelPendingRequests_DoesntCancelReadAsyncOnResponseStream(CancellationMode mode, bool copyToAsync) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + using (HttpClient client = CreateHttpClient()) { client.Timeout = Timeout.InfiniteTimeSpan; @@ -335,7 +329,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => } } - [ConditionalFact] + [Fact] public async Task MaxConnectionsPerServer_WaitingConnectionsAreCancelable() { if (LoopbackServerFactory.Version >= HttpVersion20.Value) @@ -536,7 +530,7 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo #if !NETFRAMEWORK [OuterLoop("Uses Task.Delay")] - [ConditionalTheory] + [Theory] [MemberData(nameof(PostAsync_Cancel_CancellationTokenPassedToContent_MemberData))] public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent content, CancellationTokenSource cancellationTokenSource) { @@ -544,6 +538,12 @@ public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent { return; } + // Skipping test for a sync scenario becasue DelegateStream drops the original cancellationToken when it calls Read/Write methods. + // As a result, ReadAsyncFunc receives default in cancellationToken, which will never get signaled through the cancellationTokenSource. + if (!TestAsync) + { + return; + } await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -552,7 +552,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( using (var req = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content, Version = UseVersion }) try { - using (HttpResponseMessage resp = await invoker.SendAsync(req, cancellationTokenSource.Token)) + using (HttpResponseMessage resp = await invoker.SendAsync(TestAsync, req, cancellationTokenSource.Token)) { Assert.Equal("Hello World", await resp.Content.ReadAsStringAsync()); } @@ -611,23 +611,21 @@ public enum CancellationMode DisposeHttpClient = 0x4 } - private static readonly bool[] s_bools = new[] { true, false }; - public static IEnumerable OneBoolAndCancellationMode() => - from first in s_bools + from first in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { first, mode }; public static IEnumerable TwoBoolsAndCancellationMode() => - from first in s_bools - from second in s_bools + from first in BoolValues + from second in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { first, second, mode }; public static IEnumerable ThreeBools() => - from first in s_bools - from second in s_bools - from third in s_bools + from first in BoolValues + from second in BoolValues + from third in BoolValues select new object[] { first, second, third }; } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs index 3fa53fe5a9be2..861f7db438460 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -137,7 +137,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; requestMessage.Headers.Add("Cookie", s_customCookieHeaderValue); - await client.SendAsync(requestMessage); + await client.SendAsync(TestAsync, requestMessage); } }, async server => @@ -160,7 +160,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( requestMessage.Headers.Add("Cookie", "B=2"); requestMessage.Headers.Add("Cookie", "C=3"); - await client.SendAsync(requestMessage); + await client.SendAsync(TestAsync, requestMessage); } }, async server => @@ -211,7 +211,7 @@ private string GetCookieValue(HttpRequestData request) return cookieHeaderValue; } - [ConditionalFact] + [Fact] public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent() { await LoopbackServerFactory.CreateServerAsync(async (server, url) => @@ -224,7 +224,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => var requestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; requestMessage.Headers.Add("Cookie", s_customCookieHeaderValue); - Task getResponseTask = client.SendAsync(requestMessage); + Task getResponseTask = client.SendAsync(TestAsync, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -238,7 +238,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => }); } - [ConditionalFact] + [Fact] public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent() { await LoopbackServerFactory.CreateServerAsync(async (server, url) => @@ -252,7 +252,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => requestMessage.Headers.Add("Cookie", "A=1"); requestMessage.Headers.Add("Cookie", "B=2"); - Task getResponseTask = client.SendAsync(requestMessage); + Task getResponseTask = client.SendAsync(TestAsync, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -602,7 +602,7 @@ private static string GenerateCookie(string name, char repeat, int overallHeader public static IEnumerable CookieNamesValuesAndUseCookies() { - foreach (bool useCookies in new[] { true, false }) + foreach (bool useCookies in BoolValues) { yield return new object[] { "ABC", "123", useCookies }; yield return new object[] { "Hello", "World", useCookies }; diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs index fa0b630594b7f..921cf6d76cb4c 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs @@ -198,7 +198,7 @@ public async Task GetAsync_SetAutomaticDecompression_HeadersRemoved(Configuratio } [Theory] -#if NETCORE +#if NETCOREAPP [InlineData(DecompressionMethods.Brotli, "br", "")] [InlineData(DecompressionMethods.Brotli, "br", "br")] [InlineData(DecompressionMethods.Brotli, "br", "gzip")] diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.MaxResponseHeadersLength.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.MaxResponseHeadersLength.cs index dd6daf3e35957..bc5f7d2a16e52 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.MaxResponseHeadersLength.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.MaxResponseHeadersLength.cs @@ -47,15 +47,14 @@ public void ValidValue_SetGet_Roundtrips(int validValue) } } - [ConditionalFact] + [Fact] public async Task SetAfterUse_Throws() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using HttpClientHandler handler = CreateHttpClientHandler(); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs index 83443ae607a44..e3cb15208b4f6 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs @@ -26,15 +26,14 @@ public abstract class HttpClientHandler_Proxy_Test : HttpClientHandlerTestBase { public HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { } - [ConditionalFact] + [Fact] public async Task Dispose_HandlerWithProxy_ProxyNotDisposed() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + var proxy = new TrackDisposalProxy(); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => @@ -386,7 +385,7 @@ public static IEnumerable BypassedProxies() public static IEnumerable CredentialsForProxy() { yield return new object[] { null, false }; - foreach (bool wrapCredsInCache in new[] { true, false }) + foreach (bool wrapCredsInCache in BoolValues) { yield return new object[] { new NetworkCredential("username", "password"), wrapCredsInCache }; yield return new object[] { new NetworkCredential("username", "password", "domain"), wrapCredsInCache }; diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs index 85e33a9976759..188050b7cb8aa 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -31,15 +31,14 @@ public abstract partial class HttpClientHandler_ServerCertificates_Test : HttpCl public HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : base(output) { } - [ConditionalFact] + [Fact] public void Ctor_ExpectedDefaultValues() { -#if WINHTTPHANDLER_TEST - if (UseVersion > HttpVersion.Version11) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + using (HttpClientHandler handler = CreateHttpClientHandler()) { Assert.Null(handler.ServerCertificateCustomValidationCallback); @@ -47,15 +46,13 @@ public void Ctor_ExpectedDefaultValues() } } - [ConditionalFact] + [Fact] public void ServerCertificateCustomValidationCallback_SetGet_Roundtrips() { -#if WINHTTPHANDLER_TEST - if (UseVersion > HttpVersion.Version11) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif using (HttpClientHandler handler = CreateHttpClientHandler()) { @@ -182,7 +179,7 @@ public static IEnumerable UseCallback_ValidCertificate_ExpectedValuesD { if (remoteServer.IsSecure) { - foreach (bool checkRevocation in new[] { true, false }) + foreach (bool checkRevocation in BoolValues) { yield return new object[] { remoteServer, diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 3852af097f944..979a3f495845d 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -223,15 +223,14 @@ public async Task SendAsync_SimpleGet_Success(Configuration.Http.RemoteServer re } } - [ConditionalFact] + [Fact] public async Task GetAsync_IPv6LinkLocalAddressUri_Success() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + using (HttpClient client = CreateHttpClient()) { var options = new GenericLoopbackOptions { Address = TestHelper.GetIPv6LinkLocalAddress() }; @@ -250,16 +249,15 @@ await TestHelper.WhenAllCompletedOrAnyFailed( } } - [ConditionalTheory] + [Theory] [MemberData(nameof(GetAsync_IPBasedUri_Success_MemberData))] public async Task GetAsync_IPBasedUri_Success(IPAddress address) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + using (HttpClient client = CreateHttpClient()) { var options = new GenericLoopbackOptions { Address = address }; @@ -484,10 +482,10 @@ await LoopbackServer.CreateClientAndServerAsync(async proxyUri => public static IEnumerable SecureAndNonSecure_IPBasedUri_MemberData() => from address in new[] { IPAddress.Loopback, IPAddress.IPv6Loopback } - from useSsl in new[] { true, false } + from useSsl in BoolValues select new object[] { address, useSsl }; - [ConditionalTheory] + [Theory] [MemberData(nameof(SecureAndNonSecure_IPBasedUri_MemberData))] public async Task GetAsync_SecureAndNonSecureIPBasedUri_CorrectlyFormatted(IPAddress address, bool useSsl) { @@ -569,17 +567,16 @@ public async Task GetAsync_ServerNeedsAuthAndNoCredential_StatusCodeUnauthorized } } - [ConditionalTheory] + [Theory] [InlineData("WWW-Authenticate", "CustomAuth")] [InlineData("", "")] // RFC7235 requires servers to send this header with 401 but some servers don't. public async Task GetAsync_ServerNeedsNonStandardAuthAndSetCredential_StatusCodeUnauthorized(string authHeadrName, string authHeaderValue) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateServerAsync(async (server, url) => { HttpClientHandler handler = CreateHttpClientHandler(); @@ -754,15 +751,14 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => server.AcceptConnectionSendCustomResponseAndCloseAsync("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhe")); } - [ConditionalFact] + [Fact] public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly() { -#if WINHTTPHANDLER_TEST - if (UseVersion > HttpVersion.Version11) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + const string content = "hello world"; // Using examples from https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields @@ -839,7 +835,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => request.Headers.Add("X-Underscore_Name", "X-Underscore_Name"); request.Headers.Add("X-End", "End"); - (await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); + (await client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); } }, async server => { @@ -925,7 +921,7 @@ public static IEnumerable GetAsync_ManyDifferentResponseHeaders_Parsed from dribble in new[] { false, true } select new object[] { newline, fold, dribble }; - [ConditionalTheory] + [Theory] [MemberData(nameof(GetAsync_ManyDifferentResponseHeaders_ParsedCorrectly_MemberData))] public async Task GetAsync_ManyDifferentResponseHeaders_ParsedCorrectly(string newline, string fold, bool dribble) { @@ -1059,7 +1055,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => dribble ? new LoopbackServer.Options { StreamWrapper = s => new DribbleStream(s) } : null); } - [ConditionalFact] + [Fact] public async Task GetAsync_NonTraditionalChunkSizes_Accepted() { if (LoopbackServerFactory.Version >= HttpVersion20.Value) @@ -1198,7 +1194,7 @@ public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws() req.Headers.TransferEncodingChunked = true; using (HttpClient c = CreateHttpClient()) { - HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(req)); + HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(TestAsync, req)); Assert.IsType(error.InnerException); } } @@ -1232,10 +1228,16 @@ public async Task GetAsync_ResponseHeadersRead_ReadFromEachIterativelyDoesntDead [Theory, MemberData(nameof(RemoteServersMemberData))] public async Task SendAsync_HttpRequestMsgResponseHeadersRead_StatusCodeOK(Configuration.Http.RemoteServer remoteServer) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && remoteServer.HttpVersion.Major >= 2) + { + return; + } + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, remoteServer.EchoUri) { Version = remoteServer.HttpVersion }; using (HttpClient client = CreateHttpClientForRemoteServer(remoteServer)) { - using (HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead)) { string responseContent = await response.Content.ReadAsStringAsync(); _output.WriteLine(responseContent); @@ -1282,18 +1284,17 @@ await connection.ReadRequestHeaderAndSendCustomResponseAsync( }); } - [ConditionalTheory] + [Theory] [InlineData(true)] [InlineData(false)] [InlineData(null)] public async Task ReadAsStreamAsync_HandlerProducesWellBehavedResponseStream(bool? chunked) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunked == true) { // Chunking is not supported on HTTP/2 and later. @@ -1304,7 +1305,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) - using (HttpResponseMessage response = await client.SendAsync(request, CancellationToken.None)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request, CancellationToken.None)) { using (Stream responseStream = await response.Content.ReadAsStreamAsync()) { @@ -1445,22 +1446,21 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] + [Fact] public async Task ReadAsStreamAsync_EmptyResponseBody_HandlerProducesWellBehavedResponseStream() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) { var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; - using (HttpResponseMessage response = await client.SendAsync(request, CancellationToken.None)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request, CancellationToken.None)) using (Stream responseStream = await response.Content.ReadAsStreamAsync()) { // Boolean properties returning correct values @@ -1536,15 +1536,15 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => }, server => server.AcceptConnectionSendResponseAndCloseAsync()); } - [ConditionalFact] + + [Fact] public async Task Dispose_DisposingHandlerCancelsActiveOperationsWithoutResponses() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateServerAsync(async (server1, url1) => { await LoopbackServerFactory.CreateServerAsync(async (server2, url2) => @@ -1734,7 +1734,7 @@ public static IEnumerable VerifyUploadServersStreamsAndExpectedData get { foreach (Configuration.Http.RemoteServer remoteServer in Configuration.Http.RemoteServers) // target server - foreach (bool syncCopy in new[] { true, false }) // force the content copy to happen via Read/Write or ReadAsync/WriteAsync + foreach (bool syncCopy in BoolValues) // force the content copy to happen via Read/Write or ReadAsync/WriteAsync { byte[] data = new byte[1234]; new Random(42).NextBytes(data); @@ -1866,36 +1866,42 @@ public async Task PostAsync_CallMethod_EmptyContent(Configuration.Http.RemoteSer } } + public static IEnumerable ExpectContinueVersion() + { + return + from expect in new bool?[] {true, false, null} + from version in new Version[] {new Version(1, 0), new Version(1, 1), new Version(2, 0)} + select new object[] {expect, version}; + } + [OuterLoop("Uses external server")] [Theory] - [InlineData(false, "1.0")] - [InlineData(true, "1.0")] - [InlineData(null, "1.0")] - [InlineData(false, "1.1")] - [InlineData(true, "1.1")] - [InlineData(null, "1.1")] - [InlineData(false, "2.0")] - [InlineData(true, "2.0")] - [InlineData(null, "2.0")] - public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string version) + [MemberData(nameof(ExpectContinueVersion))] + public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, Version version) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && version.Major >= 2) + { + return; + } + using (HttpClient client = CreateHttpClient()) { - var req = new HttpRequestMessage(HttpMethod.Post, version == "2.0" ? Configuration.Http.Http2RemoteEchoServer : Configuration.Http.RemoteEchoServer) + var req = new HttpRequestMessage(HttpMethod.Post, version.Major == 2 ? Configuration.Http.Http2RemoteEchoServer : Configuration.Http.RemoteEchoServer) { Content = new StringContent("Test String", Encoding.UTF8), - Version = new Version(version) + Version = version }; req.Headers.ExpectContinue = expectContinue; - using (HttpResponseMessage response = await client.SendAsync(req)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, req)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); if (!IsWinHttpHandler) { const string ExpectedReqHeader = "\"Expect\": \"100-continue\""; - if (expectContinue == true && (version == "1.1" || version == "2.0")) + if (expectContinue == true && (version >= new Version(1, 1))) { Assert.Contains(ExpectedReqHeader, await response.Content.ReadAsStringAsync()); } @@ -1908,15 +1914,14 @@ public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string } } - [ConditionalFact] + [Fact] public async Task GetAsync_ExpectContinueTrue_NoContent_StillSendsHeader() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + const string ExpectedContent = "Hello, expecting and continuing world."; var clientCompleted = new TaskCompletionSource(); await LoopbackServerFactory.CreateClientAndServerAsync(async uri => @@ -1953,16 +1958,15 @@ public static IEnumerable Interim1xxStatusCode() yield return new object[] { (HttpStatusCode) 199 }; } - [ConditionalTheory] + [Theory] [MemberData(nameof(Interim1xxStatusCode))] public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(HttpStatusCode responseStatusCode) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + var clientFinished = new TaskCompletionSource(); const string TestString = "test"; const string CookieHeaderExpected = "yummy_cookie=choco"; @@ -1980,7 +1984,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => HttpRequestMessage initialMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Version = UseVersion }; initialMessage.Content = new StringContent(TestString); initialMessage.Headers.ExpectContinue = true; - HttpResponseMessage response = await client.SendAsync(initialMessage); + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); // Verify status code. Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -2023,16 +2027,15 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalTheory] + [Theory] [MemberData(nameof(Interim1xxStatusCode))] public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(HttpStatusCode responseStatusCode) { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + var clientFinished = new TaskCompletionSource(); const string TestString = "test"; @@ -2044,7 +2047,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => initialMessage.Content = new StringContent(TestString); // No ExpectContinue header. initialMessage.Headers.ExpectContinue = false; - HttpResponseMessage response = await client.SendAsync(initialMessage); + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2069,15 +2072,14 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] + [Fact] public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + var clientFinished = new TaskCompletionSource(); const string TestString = "test"; @@ -2088,7 +2090,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => HttpRequestMessage initialMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Version = UseVersion }; initialMessage.Content = new StringContent(TestString); initialMessage.Headers.ExpectContinue = true; - HttpResponseMessage response = await client.SendAsync(initialMessage); + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2114,15 +2116,14 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] + [Fact] public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + var clientFinished = new TaskCompletionSource(); const string RequestString = "request"; const string ResponseString = "response"; @@ -2134,7 +2135,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => HttpRequestMessage initialMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Version = UseVersion }; initialMessage.Content = new StringContent(RequestString); initialMessage.Headers.ExpectContinue = true; - using (HttpResponseMessage response = await client.SendAsync(initialMessage)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(ResponseString, await response.Content.ReadAsStringAsync()); @@ -2160,7 +2161,7 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] + [Fact] public async Task SendAsync_101SwitchingProtocolsResponse_Success() { // WinHttpHandler and CurlHandler will hang, waiting for additional response. @@ -2377,7 +2378,7 @@ public async Task SendAsync_SendRequestUsingMethodToEchoServerWithNoContent_Meth new HttpMethod(method), serverUri) { Version = UseVersion }; - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); TestHelper.VerifyRequestMethod(response, method); @@ -2397,7 +2398,7 @@ public async Task SendAsync_SendRequestUsingMethodToEchoServerWithContent_Succes new HttpMethod(method), serverUri) { Version = UseVersion }; request.Content = new StringContent(ExpectedContent); - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); TestHelper.VerifyRequestMethod(response, method); @@ -2430,7 +2431,7 @@ public async Task SendAsync_SendSameRequestMultipleTimesDirectlyOnHandler_Succes for (int iter = 0; iter < 2; iter++) { - using (HttpResponseMessage response = await handler.SendAsync(request, CancellationToken.None)) + using (HttpResponseMessage response = await handler.SendAsync(TestAsync, request, CancellationToken.None)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -2464,7 +2465,7 @@ public async Task SendAsync_SendRequestUsingNoBodyMethodToEchoServerWithContent_ Version = UseVersion }; - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { if (method == "TRACE") { @@ -2491,7 +2492,7 @@ public async Task SendAsync_SendRequestUsingNoBodyMethodToEchoServerWithContent_ public async Task SendAsync_RequestVersion10_ServerReceivesVersion10Request() { // Test is not supported for WinHttpHandler and HTTP/2 - if(IsWinHttpHandler && UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { return; } @@ -2529,6 +2530,12 @@ public async Task SendAsync_RequestVersionNotSpecified_ServerReceivesVersion11Re [MemberData(nameof(Http2Servers))] public async Task SendAsync_RequestVersion20_ResponseVersion20IfHttp2Supported(Uri server) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync) + { + return; + } + // We don't currently have a good way to test whether HTTP/2 is supported without // using the same mechanism we're trying to test, so for now we allow both 2.0 and 1.1 responses. var request = new HttpRequestMessage(HttpMethod.Get, server); @@ -2539,7 +2546,7 @@ public async Task SendAsync_RequestVersion20_ResponseVersion20IfHttp2Supported(U // It is generally expected that the test hosts will be trusted, so we don't register a validation // callback in the usual case. - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True( @@ -2550,20 +2557,25 @@ public async Task SendAsync_RequestVersion20_ResponseVersion20IfHttp2Supported(U } } - [ConditionalFact] + [Fact] public async Task SendAsync_RequestVersion20_HttpNotHttps_NoUpgradeRequest() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + + // Sync API supported only up to HTTP/1.1 + if (!TestAsync) + { + return; + } + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using (HttpClient client = CreateHttpClient()) { - (await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri) { Version = new Version(2, 0) })).Dispose(); + (await client.SendAsync(TestAsync, new HttpRequestMessage(HttpMethod.Get, uri) { Version = new Version(2, 0) })).Dispose(); } }, async server => { @@ -2576,13 +2588,19 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => [ConditionalTheory(nameof(IsWindows10Version1607OrGreater)), MemberData(nameof(Http2NoPushServers))] public async Task SendAsync_RequestVersion20_ResponseVersion20(Uri server) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync) + { + return; + } + _output.WriteLine(server.AbsoluteUri.ToString()); var request = new HttpRequestMessage(HttpMethod.Get, server); request.Version = new Version(2, 0); using (HttpClient client = CreateHttpClient()) { - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(new Version(2, 0), response.Version); @@ -2601,7 +2619,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => using (HttpClient client = CreateHttpClient()) { - Task getResponse = client.SendAsync(request); + Task getResponse = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponse, serverTask); @@ -2624,7 +2642,6 @@ await LoopbackServer.CreateServerAsync(async (server, url) => { Assert.True(false, "Invalid HTTP request version"); } - } }); @@ -2633,15 +2650,14 @@ await LoopbackServer.CreateServerAsync(async (server, url) => #endregion #region Uri wire transmission encoding tests - [ConditionalFact] + [Fact] public async Task SendRequest_UriPathHasReservedChars_ServerReceivedExpectedPath() { -#if WINHTTPHANDLER_TEST - if (UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServerFactory.CreateServerAsync(async (server, rootUrl) => { var uri = new Uri($"{rootUrl.Scheme}://{rootUrl.Host}:{rootUrl.Port}/test[]"); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 003e344d74042..e59fd00ed5268 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net.Test.Common; using System.Threading; @@ -24,6 +25,8 @@ public abstract partial class HttpClientHandlerTestBase : FileCleanupTestBase protected virtual Version UseVersion => HttpVersion.Version11; + protected virtual bool TestAsync => true; + public HttpClientHandlerTestBase(ITestOutputHelper output) { _output = output; @@ -69,8 +72,9 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) }; } - // For use by remote server tests + public static readonly bool[] BoolValues = new[] { true, false }; + // For use by remote server tests public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; protected HttpClient CreateHttpClientForRemoteServer(Configuration.Http.RemoteServer remoteServer) @@ -124,4 +128,49 @@ protected override async Task SendAsync(HttpRequestMessage } } } + + public static class HttpClientExtensions + { + public static Task SendAsync(this HttpClient client, bool async, HttpRequestMessage request, HttpCompletionOption completionOption = default, CancellationToken cancellationToken = default) + { + if (async) + { + return client.SendAsync(request, completionOption, cancellationToken); + } + else + { +#if NETCOREAPP + // Note that the sync call must be done on a different thread because it blocks until the server replies. + // However, the server-side of the request handling is in many cases invoked after the client, thus deadlocking the test. + return Task.Run(() => client.Send(request, completionOption, cancellationToken)); +#else + // Framework won't ever have the sync API. + // This shouldn't be called due to AsyncBoolValues returning only true on Framework. + Debug.Fail("Framework doesn't have Sync API and it shouldn't be attempted to be tested."); + throw new Exception("Shouldn't be reachable"); +#endif + } + } + + public static Task SendAsync(this HttpMessageInvoker invoker, bool async, HttpRequestMessage request, CancellationToken cancellationToken = default) + { + if (async) + { + return invoker.SendAsync(request, cancellationToken); + } + else + { +#if NETCOREAPP + // Note that the sync call must be done on a different thread because it blocks until the server replies. + // However, the server-side of the request handling is in many cases invoked after the client, thus deadlocking the test. + return Task.Run(() => invoker.Send(request, cancellationToken)); +#else + // Framework won't ever have the sync API. + // This shouldn't be called due to AsyncBoolValues returning only true on Framework. + Debug.Fail("Framework doesn't have Sync API and it shouldn't be attempted to be tested."); + throw new Exception("Shouldn't be reachable"); +#endif + } + } + } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs index 635cb2bed65c1..a7eaa9c77d51c 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs @@ -20,15 +20,14 @@ public abstract class HttpProtocolTests : HttpClientHandlerTestBase public HttpProtocolTests(ITestOutputHelper output) : base(output) { } - [ConditionalFact] + [Fact] public async Task GetAsync_RequestVersion10_Success() { -#if WINHTTPHANDLER_TEST - if (UseVersion > HttpVersion.Version11) + if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) { - throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); + return; } -#endif + await LoopbackServer.CreateServerAsync(async (server, url) => { using (HttpClient client = CreateHttpClient()) @@ -36,7 +35,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version10; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -57,7 +56,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version11; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -81,7 +80,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = new Version(0, minorVersion); - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); if (IsWinHttpHandler) @@ -92,7 +91,8 @@ await LoopbackServer.CreateServerAsync(async (server, url) => } else { - await Assert.ThrowsAsync(() => TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask)); + // Await only client side that will throw. Nothing will get to the server side due to this exception thus do not await it at all. + await Assert.ThrowsAsync(() => getResponseTask); } } }, new LoopbackServer.Options { StreamWrapper = GetStream_ClientDisconnectOk}); @@ -108,6 +108,12 @@ await LoopbackServer.CreateServerAsync(async (server, url) => [InlineData(4, 2)] public async Task GetAsync_UnknownRequestVersion_ThrowsOrDegradesTo11(int majorVersion, int minorVersion) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && majorVersion >= 2) + { + return; + } + Type exceptionType = null; await LoopbackServer.CreateServerAsync(async (server, url) => @@ -117,7 +123,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = new Version(majorVersion, minorVersion); - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); if (exceptionType == null) @@ -146,7 +152,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version11; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendCustomResponseAndCloseAsync( $"HTTP/1.{responseMinorVersion} 200 OK\r\nConnection: close\r\nDate: {DateTimeOffset.UtcNow:R}\r\nContent-Length: 0\r\n\r\n"); @@ -174,7 +180,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version11; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendCustomResponseAndCloseAsync( $"HTTP/1.{responseMinorVersion} 200 OK\r\nConnection: close\r\nDate: {DateTimeOffset.UtcNow:R}\r\nContent-Length: 0\r\n\r\n"); @@ -213,7 +219,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version11; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendCustomResponseAndCloseAsync( $"HTTP/0.{responseMinorVersion} 200 OK\r\nConnection: close\r\nDate: {DateTimeOffset.UtcNow:R}\r\nContent-Length: 0\r\n\r\n"); @@ -250,7 +256,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Version = HttpVersion.Version11; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendCustomResponseAndCloseAsync( $"HTTP/{responseMajorVersion}.{responseMinorVersion} 200 OK\r\nConnection: close\r\nDate: {DateTimeOffset.UtcNow:R}\r\nContent-Length: 0\r\n\r\n"); @@ -460,7 +466,7 @@ public async Task GetAsync_Chunked_VaryingSizeChunks_ReceivedCorrectly(int maxCh await LoopbackServer.CreateClientAndServerAsync(async uri => { using (HttpMessageInvoker client = new HttpMessageInvoker(CreateHttpClientHandler())) - using (HttpResponseMessage resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri) { Version = base.UseVersion }, CancellationToken.None)) + using (HttpResponseMessage resp = await client.SendAsync(TestAsync, new HttpRequestMessage(HttpMethod.Get, uri) { Version = base.UseVersion }, CancellationToken.None)) using (Stream respStream = await resp.Content.ReadAsStreamAsync()) { var actualData = new MemoryStream(); @@ -512,6 +518,12 @@ await server.AcceptConnectionAsync(async connection => [InlineData("head", "HEAD")] [InlineData("post", "POST")] [InlineData("put", "PUT")] + [InlineData("delete", "DELETE")] + [InlineData("options", "OPTIONS")] + [InlineData("trace", "TRACE")] +#if !WINHTTPHANDLER_TEST + [InlineData("patch", "PATCH")] +#endif [InlineData("other", "other")] [InlineData("SometHING", "SometHING")] public async Task CustomMethod_SentUppercasedIfKnown(string specifiedMethod, string expectedMethod) @@ -521,7 +533,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => using (HttpClient client = CreateHttpClient()) { var m = new HttpRequestMessage(new HttpMethod(specifiedMethod), uri) { Version = UseVersion }; - (await client.SendAsync(m)).Dispose(); + (await client.SendAsync(TestAsync, m)).Dispose(); } }, async server => { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs index 8f6808e17da10..dda79c80b0562 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Net.Test.Common; using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -89,7 +90,7 @@ await LoopbackServer.CreateClientAndServerAsync(async url => var request = new HttpRequestMessage(HttpMethod.Post, url) { Version = UseVersion }; request.Headers.ExpectContinue = true; request.Content = new SynchronizedSendContent(contentSending, connectionClosed.Task); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + await Assert.ThrowsAsync(() => client.SendAsync(TestAsync, request)); } }, async server => @@ -126,6 +127,11 @@ public SynchronizedSendContent(TaskCompletionSource sendingContent, Task c _sendingContent = sendingContent; } +#if NETCOREAPP + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) => + SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); +#endif + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { _sendingContent.SetResult(true); diff --git a/src/libraries/Common/tests/System/Net/Http/IdnaProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/IdnaProtocolTests.cs index 0d4167f7d7ce6..cebf9c9bfacee 100644 --- a/src/libraries/Common/tests/System/Net/Http/IdnaProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/IdnaProtocolTests.cs @@ -74,7 +74,7 @@ await LoopbackServer.CreateServerAsync(async (server, serverUrl) => var request = new HttpRequestMessage(HttpMethod.Get, serverUrl) { Version = UseVersion }; request.Headers.Host = hostname; request.Headers.Referrer = uri; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.SendAsync(TestAsync, request); Task> serverTask = server.AcceptConnectionSendResponseAndCloseAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); diff --git a/src/libraries/Common/tests/System/Net/Http/PostScenarioTest.cs b/src/libraries/Common/tests/System/Net/Http/PostScenarioTest.cs index 34de8fffac58f..f3a8e42a1af67 100644 --- a/src/libraries/Common/tests/System/Net/Http/PostScenarioTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/PostScenarioTest.cs @@ -181,6 +181,12 @@ public async Task PostRewindableContentUsingAuth_NoPreAuthenticate_Success(Confi [Theory, MemberData(nameof(RemoteServersMemberData))] public async Task PostNonRewindableContentUsingAuth_NoPreAuthenticate_ThrowsHttpRequestException(Configuration.Http.RemoteServer remoteServer) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && remoteServer.HttpVersion.Major >= 2) + { + return; + } + HttpContent content = new StreamContent(new CustomContent.CustomStream(Encoding.UTF8.GetBytes(ExpectedContent), false)); var credential = new NetworkCredential(UserName, Password); await Assert.ThrowsAsync(() => @@ -191,6 +197,12 @@ await Assert.ThrowsAsync(() => [Theory, MemberData(nameof(RemoteServersMemberData))] public async Task PostNonRewindableContentUsingAuth_PreAuthenticate_Success(Configuration.Http.RemoteServer remoteServer) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && remoteServer.HttpVersion.Major >= 2) + { + return; + } + HttpContent content = new StreamContent(new CustomContent.CustomStream(Encoding.UTF8.GetBytes(ExpectedContent), false)); var credential = new NetworkCredential(UserName, Password); await PostUsingAuthHelper(remoteServer, ExpectedContent, content, credential, preAuthenticate: true); @@ -265,7 +277,7 @@ private async Task PostUsingAuthHelper( // Send HEAD request to help bypass the 401 auth challenge for the latter POST assuming // that the authentication will be cached and re-used later when PreAuthenticate is true. var request = new HttpRequestMessage(HttpMethod.Head, serverUri) { Version = remoteServer.HttpVersion }; - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -276,7 +288,7 @@ private async Task PostUsingAuthHelper( requestContent.Headers.ContentLength = null; request.Headers.TransferEncodingChunked = true; - using (HttpResponseMessage response = await client.SendAsync(request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string responseContent = await response.Content.ReadAsStringAsync(); diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 91d4676d3ab99..352762dd03512 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -12,6 +12,7 @@ public partial class ByteArrayContent : System.Net.Http.HttpContent public ByteArrayContent(byte[] content) { } public ByteArrayContent(byte[] content, int offset, int count) { } protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } @@ -28,6 +29,7 @@ protected DelegatingHandler(System.Net.Http.HttpMessageHandler innerHandler) { } [System.Diagnostics.CodeAnalysis.DisallowNullAttribute] public System.Net.Http.HttpMessageHandler? InnerHandler { get { throw null; } set { } } protected override void Dispose(bool disposing) { } + protected internal override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class FormUrlEncodedContent : System.Net.Http.ByteArrayContent @@ -84,6 +86,10 @@ protected override void Dispose(bool disposing) { } public System.Threading.Tasks.Task PutAsync(string? requestUri, System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task PutAsync(System.Uri? requestUri, System.Net.Http.HttpContent content) { throw null; } public System.Threading.Tasks.Task PutAsync(System.Uri? requestUri, System.Net.Http.HttpContent content, System.Threading.CancellationToken cancellationToken) { throw null; } + public System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request) { throw null; } + public System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption) { throw null; } + public System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request) { throw null; } public System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption) { throw null; } public System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) { throw null; } @@ -117,6 +123,7 @@ public HttpClientHandler() { } public bool UseDefaultCredentials { get { throw null; } set { } } public bool UseProxy { get { throw null; } set { } } protected override void Dispose(bool disposing) { } + protected internal override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public enum HttpCompletionOption @@ -128,6 +135,7 @@ public abstract partial class HttpContent : System.IDisposable { protected HttpContent() { } public System.Net.Http.Headers.HttpContentHeaders Headers { get { throw null; } } + public void CopyTo(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream) { throw null; } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } @@ -144,6 +152,7 @@ protected virtual void Dispose(bool disposing) { } public System.Threading.Tasks.Task ReadAsStreamAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task ReadAsStringAsync() { throw null; } public System.Threading.Tasks.Task ReadAsStringAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + protected virtual void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected abstract System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context); protected virtual System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal abstract bool TryComputeLength(out long length); @@ -153,6 +162,7 @@ public abstract partial class HttpMessageHandler : System.IDisposable protected HttpMessageHandler() { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + protected internal virtual System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal abstract System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken); } public partial class HttpMessageInvoker : System.IDisposable @@ -161,6 +171,7 @@ public HttpMessageInvoker(System.Net.Http.HttpMessageHandler handler) { } public HttpMessageInvoker(System.Net.Http.HttpMessageHandler handler, bool disposeHandler) { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + public virtual System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class HttpMethod : System.IEquatable @@ -229,6 +240,7 @@ protected MessageProcessingHandler() { } protected MessageProcessingHandler(System.Net.Http.HttpMessageHandler innerHandler) { } protected abstract System.Net.Http.HttpRequestMessage ProcessRequest(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken); protected abstract System.Net.Http.HttpResponseMessage ProcessResponse(System.Net.Http.HttpResponseMessage response, System.Threading.CancellationToken cancellationToken); + protected internal sealed override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal sealed override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class MultipartContent : System.Net.Http.HttpContent, System.Collections.Generic.IEnumerable, System.Collections.IEnumerable @@ -241,6 +253,7 @@ public virtual void Add(System.Net.Http.HttpContent content) { } protected override System.Threading.Tasks.Task CreateContentReadStreamAsync(System.Threading.CancellationToken cancellationToken) { throw null; } protected override void Dispose(bool disposing) { } public System.Collections.Generic.IEnumerator GetEnumerator() { throw null; } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } @@ -259,6 +272,7 @@ public sealed partial class ReadOnlyMemoryContent : System.Net.Http.HttpContent { public ReadOnlyMemoryContent(System.ReadOnlyMemory content) { } protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } @@ -289,6 +303,7 @@ public SocketsHttpHandler() { } public bool UseCookies { get { throw null; } set { } } public bool UseProxy { get { throw null; } set { } } protected override void Dispose(bool disposing) { } + protected internal override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class StreamContent : System.Net.Http.HttpContent @@ -297,6 +312,7 @@ public StreamContent(System.IO.Stream content) { } public StreamContent(System.IO.Stream content, int bufferSize) { } protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } protected override void Dispose(bool disposing) { } + protected override void SerializeToStream(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context) { throw null; } protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal override bool TryComputeLength(out long length) { throw null; } diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 4074b8e08edaa..3ddc08d1c397b 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -555,4 +555,10 @@ Stream aborted by peer ({0}). - \ No newline at end of file + + The synchronous method is not supported by '{0}'. If you're using a custom '{1}' and wish to use synchronous HTTP methods, you must override its '{2}' virtual method. + + + The synchronous method is not supported by '{0}' for HTTP/2 or higher. Either use an asynchronous method or downgrade the request version to HTTP/1.1 or lower. + + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index f00e794b13c72..94acc221fa938 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -19,6 +19,7 @@ + @@ -51,6 +52,7 @@ + @@ -115,16 +117,14 @@ Link="Common\System\Text\SimpleRegex.cs" /> - - Common\System\Net\ArrayBuffer.cs - + - @@ -702,7 +702,6 @@ - - - diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs index e4d9cca3c1bb4..cf54774fa329b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs @@ -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.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -46,6 +47,9 @@ public ByteArrayContent(byte[] content, int offset, int count) _count = count; } + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + stream.Write(_content, _offset, _count); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsyncCore(stream, default); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CancellationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs similarity index 100% rename from src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CancellationHelper.cs rename to src/libraries/System.Net.Http/src/System/Net/Http/CancellationHelper.cs diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/DelegatingHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/DelegatingHandler.cs index 7fb7b5e395936..2aced67d2f1e0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/DelegatingHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/DelegatingHandler.cs @@ -46,6 +46,16 @@ protected DelegatingHandler(HttpMessageHandler innerHandler) InnerHandler = innerHandler; } + protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); + } + SetOperationStarted(); + return _innerHandler!.Send(request, cancellationToken); + } + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs index 3e4cf7f2cffb1..2708f88414f9b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs @@ -31,7 +31,20 @@ internal static bool IsEnabled() return s_enableActivityPropagation && (Activity.Current != null || s_diagnosticListener.IsEnabled()); } - protected internal override async Task SendAsync(HttpRequestMessage request, + // SendAsyncCore returns already completed ValueTask for when async: false is passed. + // Internally, it calls the synchronous Send method of the base class. + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + ValueTask sendTask = SendAsyncCore(request, async: false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); + } + + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + SendAsyncCore(request, async: true, cancellationToken).AsTask(); + + private async ValueTask SendAsyncCore(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { // HttpClientHandler is responsible to call static DiagnosticsHandler.IsEnabled() before forwarding request here. @@ -57,7 +70,9 @@ protected internal override async Task SendAsync(HttpReques try { - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + return async ? + await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : + base.Send(request, cancellationToken); } finally { @@ -98,20 +113,26 @@ protected internal override async Task SendAsync(HttpReques InjectHeaders(currentActivity, request); } - Task? responseTask = null; + HttpResponseMessage? response = null; + TaskStatus taskStatus = TaskStatus.RanToCompletion; try { - responseTask = base.SendAsync(request, cancellationToken); - - return await responseTask.ConfigureAwait(false); + response = async ? + await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : + base.Send(request, cancellationToken); + return response; } catch (OperationCanceledException) { + taskStatus = TaskStatus.Canceled; + // we'll report task status in HttpRequestOut.Stop throw; } catch (Exception ex) { + taskStatus = TaskStatus.Faulted; + if (s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ExceptionEventName)) { // If request was initially instrumented, Activity.Current has all necessary context for logging @@ -127,12 +148,12 @@ protected internal override async Task SendAsync(HttpReques if (activity != null) { s_diagnosticListener.StopActivity(activity, new ActivityStopData( - responseTask?.Status == TaskStatus.RanToCompletion ? responseTask.Result : null, + response, // If request is failed or cancelled, there is no response, therefore no information about request; // pass the request in the payload, so consumers can have it in Stop for failed/canceled requests // and not retain all requests in Start request, - responseTask?.Status ?? TaskStatus.Faulted)); + taskStatus)); } // Try to write System.Net.Http.Response event (deprecated) if (s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated)) @@ -140,10 +161,10 @@ protected internal override async Task SendAsync(HttpReques long timestamp = Stopwatch.GetTimestamp(); s_diagnosticListener.Write(DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated, new ResponseData( - responseTask?.Status == TaskStatus.RanToCompletion ? responseTask.Result : null, + response, loggingRequestId, timestamp, - responseTask?.Status ?? TaskStatus.Faulted)); + taskStatus)); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs index 728229337c675..d626b207af81c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs @@ -17,6 +17,9 @@ protected internal override bool TryComputeLength(out long length) return true; } + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => Task.CompletedTask; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs index c48875d356729..ee1b7768e1410 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs @@ -445,6 +445,30 @@ public Task DeleteAsync(Uri? requestUri, CancellationToken #region Advanced Send Overloads + public HttpResponseMessage Send(HttpRequestMessage request) + { + return Send(request, defaultCompletionOption, cancellationToken: default); + } + + public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption) + { + return Send(request, completionOption, cancellationToken: default); + } + + public override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + return Send(request, defaultCompletionOption, cancellationToken); + } + + public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption, + CancellationToken cancellationToken) + { + ValueTask sendTask = SendAsyncCore(request, completionOption, async: false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); + } + public Task SendAsync(HttpRequestMessage request) { return SendAsync(request, defaultCompletionOption, CancellationToken.None); @@ -463,6 +487,12 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl public Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) + { + return SendAsyncCore(request, completionOption, async: true, cancellationToken).AsTask(); + } + + private ValueTask SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, + bool async, CancellationToken cancellationToken) { if (request == null) { @@ -475,40 +505,20 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl PrepareRequestMessage(request); // PrepareRequestMessage will resolve the request address against the base address. - // We need a CancellationTokenSource to use with the request. We always have the global - // _pendingRequestsCts to use, plus we may have a token provided by the caller, and we may - // have a timeout. If we have a timeout or a caller-provided token, we need to create a new - // CTS (we can't, for example, timeout the pending requests CTS, as that could cancel other - // unrelated operations). Otherwise, we can use the pending requests CTS directly. - CancellationTokenSource cts; - bool disposeCts; - bool hasTimeout = _timeout != s_infiniteTimeout; - long timeoutTime = long.MaxValue; - if (hasTimeout || cancellationToken.CanBeCanceled) - { - disposeCts = true; - cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _pendingRequestsCts.Token); - if (hasTimeout) - { - timeoutTime = Environment.TickCount64 + (_timeout.Ticks / TimeSpan.TicksPerMillisecond); - cts.CancelAfter(_timeout); - } - } - else - { - disposeCts = false; - cts = _pendingRequestsCts; - } + // Combines given cancellationToken with the global one and the timeout. + CancellationTokenSource cts = PrepareCancellationTokenSource(cancellationToken, out bool disposeCts, out long timeoutTime); // Initiate the send. - Task sendTask; + ValueTask responseTask; try { - sendTask = base.SendAsync(request, cts.Token); + responseTask = async ? + new ValueTask(base.SendAsync(request, cts.Token)) : + new ValueTask(base.Send(request, cts.Token)); } catch (Exception e) { - HandleFinishSendAsyncCleanup(cts, disposeCts); + HandleFinishSendCleanup(cts, disposeCts); if (e is OperationCanceledException operationException && TimeoutFired(cancellationToken, timeoutTime)) { @@ -518,17 +528,21 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl throw; } - return completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ? - FinishSendAsyncBuffered(sendTask, request, cts, disposeCts, cancellationToken, timeoutTime) : - FinishSendAsyncUnbuffered(sendTask, request, cts, disposeCts, cancellationToken, timeoutTime); + bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && + !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); + + return FinishSendAsync(responseTask, request, cts, disposeCts, buffered, async, cancellationToken, timeoutTime); } - private async Task FinishSendAsyncBuffered( - Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime) + private async ValueTask FinishSendAsync(ValueTask sendTask, HttpRequestMessage request, CancellationTokenSource cts, + bool disposeCts, bool buffered, bool async, CancellationToken callerToken, long timeoutTime) { HttpResponseMessage? response = null; try { + // In sync scenario the ValueTask must already contains the result. + Debug.Assert(async || sendTask.IsCompleted, "In synchronous scenario, the sendTask must be already completed."); + // Wait for the send request to complete, getting back the response. response = await sendTask.ConfigureAwait(false); if (response == null) @@ -537,9 +551,17 @@ private async Task FinishSendAsyncBuffered( } // Buffer the response content if we've been asked to and we have a Content to buffer. - if (response.Content != null) + if (buffered && response.Content != null) { - await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false); + if (async) + { + await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false); + + } + else + { + response.Content.LoadIntoBuffer(_maxResponseContentBufferSize, cts.Token); + } } if (NetEventSource.IsEnabled) NetEventSource.ClientSendCompleted(this, response, request); @@ -551,7 +573,7 @@ private async Task FinishSendAsyncBuffered( if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime)) { - HandleSendAsyncTimeout(operationException); + HandleSendTimeout(operationException); throw CreateTimeoutException(operationException); } @@ -560,38 +582,7 @@ private async Task FinishSendAsyncBuffered( } finally { - HandleFinishSendAsyncCleanup(cts, disposeCts); - } - } - - private async Task FinishSendAsyncUnbuffered( - Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime) - { - try - { - HttpResponseMessage response = await sendTask.ConfigureAwait(false); - if (response == null) - { - throw new InvalidOperationException(SR.net_http_handler_noresponse); - } - - if (NetEventSource.IsEnabled) NetEventSource.ClientSendCompleted(this, response, request); - return response; - } - catch (Exception e) - { - if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime)) - { - HandleSendAsyncTimeout(operationException); - throw CreateTimeoutException(operationException); - } - - HandleFinishSendAsyncError(e, cts); - throw; - } - finally - { - HandleFinishSendAsyncCleanup(cts, disposeCts); + HandleFinishSendCleanup(cts, disposeCts); } } @@ -616,7 +607,7 @@ private void HandleFinishSendAsyncError(Exception e, CancellationTokenSource cts } } - private void HandleSendAsyncTimeout(OperationCanceledException e) + private void HandleSendTimeout(OperationCanceledException e) { if (NetEventSource.IsEnabled) { @@ -625,7 +616,7 @@ private void HandleSendAsyncTimeout(OperationCanceledException e) } } - private void HandleFinishSendAsyncCleanup(CancellationTokenSource cts, bool disposeCts) + private void HandleFinishSendCleanup(CancellationTokenSource cts, bool disposeCts) { // Dispose of the CancellationTokenSource if it was created specially for this request // rather than being used across multiple requests. @@ -769,6 +760,32 @@ private void PrepareRequestMessage(HttpRequestMessage request) } } + private CancellationTokenSource PrepareCancellationTokenSource(CancellationToken cancellationToken, out bool disposeCts, out long timeoutTime) + { + // We need a CancellationTokenSource to use with the request. We always have the global + // _pendingRequestsCts to use, plus we may have a token provided by the caller, and we may + // have a timeout. If we have a timeout or a caller-provided token, we need to create a new + // CTS (we can't, for example, timeout the pending requests CTS, as that could cancel other + // unrelated operations). Otherwise, we can use the pending requests CTS directly. + bool hasTimeout = _timeout != s_infiniteTimeout; + timeoutTime = long.MaxValue; + if (hasTimeout || cancellationToken.CanBeCanceled) + { + disposeCts = true; + CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _pendingRequestsCts.Token); + if (hasTimeout) + { + timeoutTime = Environment.TickCount64 + (_timeout.Ticks / TimeSpan.TicksPerMillisecond); + cts.CancelAfter(_timeout); + } + + return cts; + } + + disposeCts = false; + return _pendingRequestsCts; + } + private static void CheckBaseAddress(Uri? baseAddress, string parameterName) { if (baseAddress == null) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs index 41111f54a8415..b3d03a50568f9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs @@ -235,6 +235,14 @@ public SslProtocols SslProtocols public IDictionary Properties => _underlyingHandler.Properties; + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + return DiagnosticsHandler.IsEnabled() ? + _diagnosticsHandler.Send(request, cancellationToken) : + _underlyingHandler.Send(request, cancellationToken); + } + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 23abf0afebfec..06cf2f4030d88 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -316,6 +316,14 @@ public Task ReadAsStreamAsync(CancellationToken cancellationToken) protected abstract Task SerializeToStreamAsync(Stream stream, TransportContext? context); + // We cannot add abstract member to a public class in order to not to break already established contract of this class. + // So we add virtual method, override it everywhere internally and provide proper implementation. + // Unfortunately we cannot force everyone to implement so in such case we throw NSE. + protected virtual void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + throw new NotSupportedException(SR.Format(SR.net_http_missing_sync_implementation, GetType(), nameof(HttpContent), nameof(SerializeToStream))); + } + protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context); @@ -327,6 +335,31 @@ protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext? c // on all known HttpContent types, waiting for the request content to complete before completing the SendAsync task. internal virtual bool AllowDuplex => true; + public void CopyTo(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + CheckDisposed(); + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + try + { + if (TryGetBuffer(out ArraySegment buffer)) + { + stream.Write(buffer.Array!, buffer.Offset, buffer.Count); + } + else + { + SerializeToStream(stream, context, cancellationToken); + } + } + catch (Exception e) when (StreamCopyExceptionNeedsWrapping(e)) + { + throw GetStreamCopyException(e); + } + } + public Task CopyToAsync(Stream stream) => CopyToAsync(stream, CancellationToken.None); @@ -378,6 +411,56 @@ internal ValueTask InternalCopyToAsync(Stream stream, TransportContext? context, return new ValueTask(task); } + internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationToken) + { + CheckDisposed(); + + if (!CreateTemporaryBuffer(maxBufferSize, out MemoryStream? tempBuffer, out Exception? error)) + { + // If we already buffered the content, just return. + return; + } + + if (tempBuffer == null) + { + throw error!; + } + + // Register for cancellation and tear down the underlying stream in case of cancellation/timeout. + // We're only comfortable disposing of the HttpContent instance like this because LoadIntoBuffer is internal and + // we're only using it on content instances we get back from a handler's Send call that haven't been given out to the user yet. + // If we were to ever make LoadIntoBuffer public, we'd need to rethink this. + CancellationTokenRegistration cancellationRegistration = cancellationToken.Register(s => ((HttpContent)s!).Dispose(), this); + + try + { + SerializeToStream(tempBuffer, null, cancellationToken); + tempBuffer.Seek(0, SeekOrigin.Begin); // Rewind after writing data. + _bufferedContent = tempBuffer; + } + catch (Exception e) + { + if (NetEventSource.IsEnabled) NetEventSource.Error(this, e); + + if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) + { + throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); + } + + if (StreamCopyExceptionNeedsWrapping(e)) + { + throw GetStreamCopyException(e); + } + + throw; + } + finally + { + // Clean up the cancellation registration. + cancellationRegistration.Dispose(); + } + } + public Task LoadIntoBufferAsync() => LoadIntoBufferAsync(MaxBufferSize); @@ -393,22 +476,13 @@ internal Task LoadIntoBufferAsync(CancellationToken cancellationToken) => internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancellationToken) { CheckDisposed(); - if (maxBufferSize > HttpContent.MaxBufferSize) - { - // This should only be hit when called directly; HttpClient/HttpClientHandler - // will not exceed this limit. - throw new ArgumentOutOfRangeException(nameof(maxBufferSize), maxBufferSize, - SR.Format(System.Globalization.CultureInfo.InvariantCulture, - SR.net_http_content_buffersize_limit, HttpContent.MaxBufferSize)); - } - if (IsBuffered) + if (!CreateTemporaryBuffer(maxBufferSize, out MemoryStream? tempBuffer, out Exception? error)) { // If we already buffered the content, just return a completed task. return Task.CompletedTask; } - MemoryStream? tempBuffer = CreateMemoryStream(maxBufferSize, out Exception? error); if (tempBuffer == null) { // We don't throw in LoadIntoBufferAsync(): return a faulted task. @@ -505,6 +579,29 @@ protected virtual Task CreateContentReadStreamAsync(CancellationToken ca return null; } + private bool CreateTemporaryBuffer(long maxBufferSize, out MemoryStream? tempBuffer, out Exception? error) + { + if (maxBufferSize > HttpContent.MaxBufferSize) + { + // This should only be hit when called directly; HttpClient/HttpClientHandler + // will not exceed this limit. + throw new ArgumentOutOfRangeException(nameof(maxBufferSize), maxBufferSize, + SR.Format(System.Globalization.CultureInfo.InvariantCulture, + SR.net_http_content_buffersize_limit, HttpContent.MaxBufferSize)); + } + + if (IsBuffered) + { + // If we already buffered the content, just return false. + tempBuffer = default; + error = default; + return false; + } + + tempBuffer = CreateMemoryStream(maxBufferSize, out error); + return true; + } + private MemoryStream? CreateMemoryStream(long maxBufferSize, out Exception? error) { error = null; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs index 959cc395ce431..ccff5f0946a85 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageHandler.cs @@ -17,6 +17,14 @@ protected HttpMessageHandler() if (NetEventSource.IsEnabled) NetEventSource.Info(this); } + // We cannot add abstract member to a public class in order to not to break already established contract of this class. + // So we add virtual method, override it everywhere internally and provide proper implementation. + // Unfortunately we cannot force everyone to implement so in such case we throw NSE. + protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + throw new NotSupportedException(SR.Format(SR.net_http_missing_sync_implementation, GetType(), nameof(HttpMessageHandler), nameof(Send))); + } + protected internal abstract Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); #region IDisposable Members diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs index f5d64a8a04799..17d0e0bd84e1b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpMessageInvoker.cs @@ -37,6 +37,24 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) if (NetEventSource.IsEnabled) NetEventSource.Exit(this); } + public virtual HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + CheckDisposed(); + + if (NetEventSource.IsEnabled) NetEventSource.Enter(this, request); + + HttpResponseMessage response = _handler.Send(request, cancellationToken); + + if (NetEventSource.IsEnabled) NetEventSource.Exit(this, response); + + return response; + } + public virtual Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs index 78f9cc06603d7..62384f9a208b5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MessageProcessingHandler.cs @@ -28,6 +28,21 @@ protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken); + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); + } + + // Since most of the SendAsync code is just Task handling, there's no reason to share the code. + HttpRequestMessage newRequestMessage = ProcessRequest(request, cancellationToken); + HttpResponseMessage response = base.Send(newRequestMessage, cancellationToken); + HttpResponseMessage newResponseMessage = ProcessResponse(response, cancellationToken); + return newResponseMessage; + } + protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs index 20aacaab72cf9..20c4cc37dff8f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs @@ -158,6 +158,42 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() #region Serialization + // for-each content + // write "--" + boundary + // for-each content header + // write header: header-value + // write content.CopyTo[Async] + // write "--" + boundary + "--" + // Can't be canceled directly by the user. If the overall request is canceled + // then the stream will be closed an exception thrown. + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + Debug.Assert(stream != null); + try + { + // Write start boundary. + EncodeStringToStream(stream, "--" + _boundary + CrLf); + + // Write each nested content. + var output = new StringBuilder(); + for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) + { + // Write divider, headers, and content. + HttpContent content = _nestedContent[contentIndex]; + EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, content)); + content.CopyTo(stream, context, cancellationToken); + } + + // Write footer boundary. + EncodeStringToStream(stream, CrLf + "--" + _boundary + "--" + CrLf); + } + catch (Exception ex) + { + if (NetEventSource.IsEnabled) NetEventSource.Error(this, ex); + throw; + } + } + // for-each content // write "--" + boundary // for-each content header @@ -290,6 +326,12 @@ private string SerializeHeadersToString(StringBuilder scratch, int contentIndex, return scratch.ToString(); } + private static void EncodeStringToStream(Stream stream, string input) + { + byte[] buffer = HttpRuleParser.DefaultHttpEncoding.GetBytes(input); + stream.Write(buffer); + } + private static ValueTask EncodeStringToStreamAsync(Stream stream, string input, CancellationToken cancellationToken) { byte[] buffer = HttpRuleParser.DefaultHttpEncoding.GetBytes(input); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs index d4b91c29ad15f..c4abdcb13d470 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs @@ -15,6 +15,11 @@ public sealed class ReadOnlyMemoryContent : HttpContent public ReadOnlyMemoryContent(ReadOnlyMemory content) => _content = content; + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + stream.Write(_content.Span); + } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => stream.WriteAsync(_content).AsTask(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs index 68f2c4a5d7ddb..2c4d5d125d452 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs @@ -14,11 +14,11 @@ namespace System.Net.Http { internal partial class AuthenticationHelper { - private static Task InnerSendAsync(HttpRequestMessage request, bool isProxyAuth, HttpConnectionPool pool, HttpConnection connection, CancellationToken cancellationToken) + private static Task InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth, HttpConnectionPool pool, HttpConnection connection, CancellationToken cancellationToken) { return isProxyAuth ? - connection.SendAsyncCore(request, cancellationToken) : - pool.SendWithNtProxyAuthAsync(connection, request, cancellationToken); + connection.SendAsyncCore(request, async, cancellationToken) : + pool.SendWithNtProxyAuthAsync(connection, request, async, cancellationToken); } private static bool ProxySupportsConnectionAuth(HttpResponseMessage response) @@ -39,9 +39,9 @@ private static bool ProxySupportsConnectionAuth(HttpResponseMessage response) return false; } - private static async Task SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + private static async Task SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { - HttpResponseMessage response = await InnerSendAsync(request, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); + HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response)) { // Proxy didn't indicate that it supports connection-based auth, so we can't proceed. @@ -65,7 +65,7 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe { // Server is closing the connection and asking us to authenticate on a new connection. #pragma warning disable CS8600 // expression returns null connection on error, was not able to use '!' for the expression - (connection, response) = await connectionPool.CreateHttp11ConnectionAsync(request, cancellationToken).ConfigureAwait(false); + (connection, response) = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); #pragma warning restore CS8600 if (response != null) { @@ -142,7 +142,7 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth); - response = await InnerSendAsync(request, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); + response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); if (authContext.IsCompleted || !TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out challengeData)) { break; @@ -169,15 +169,15 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe return response!; } - public static Task SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, ICredentials proxyCredentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + public static Task SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { - return SendWithNtAuthAsync(request, proxyUri, proxyCredentials, isProxyAuth: true, connection, connectionPool, cancellationToken); + return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, isProxyAuth: true, connection, connectionPool, cancellationToken); } - public static Task SendWithNtConnectionAuthAsync(HttpRequestMessage request, ICredentials credentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + public static Task SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { Debug.Assert(request.RequestUri != null); - return SendWithNtAuthAsync(request, request.RequestUri, credentials, isProxyAuth: false, connection, connectionPool, cancellationToken); + return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, isProxyAuth: false, connection, connectionPool, cancellationToken); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs index 4f99031134ba8..b5942c6077dd9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs @@ -204,14 +204,14 @@ private static async ValueTask TrySetDigestAuthToken(HttpRequestMessage re return true; } - private static Task InnerSendAsync(HttpRequestMessage request, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) + private static ValueTask InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) { return isProxyAuth ? - pool.SendWithRetryAsync(request, doRequestAuth, cancellationToken) : - pool.SendWithProxyAuthAsync(request, doRequestAuth, cancellationToken); + pool.SendWithRetryAsync(request, async, doRequestAuth, cancellationToken) : + pool.SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken); } - private static async Task SendWithAuthAsync(HttpRequestMessage request, Uri authUri, ICredentials credentials, bool preAuthenticate, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) + private static async ValueTask SendWithAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool preAuthenticate, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) { // If preauth is enabled and this isn't proxy auth, try to get a basic credential from the // preauth credentials cache, and if successful, set an auth header for it onto the request. @@ -238,7 +238,7 @@ private static async Task SendWithAuthAsync(HttpRequestMess } } - HttpResponseMessage response = await InnerSendAsync(request, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); + HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge challenge)) { @@ -249,7 +249,7 @@ private static async Task SendWithAuthAsync(HttpRequestMess if (await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).ConfigureAwait(false)) { response.Dispose(); - response = await InnerSendAsync(request, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); + response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); // Retry in case of nonce timeout in server. if (TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out string? challengeData)) @@ -259,7 +259,7 @@ private static async Task SendWithAuthAsync(HttpRequestMess await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).ConfigureAwait(false)) { response.Dispose(); - response = await InnerSendAsync(request, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); + response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); } } } @@ -277,7 +277,7 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro response.Dispose(); SetBasicAuthToken(request, challenge.Credential, isProxyAuth); - response = await InnerSendAsync(request, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); + response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false); if (preAuthenticate) { @@ -326,15 +326,15 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro return response; } - public static Task SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, ICredentials proxyCredentials, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) + public static ValueTask SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) { - return SendWithAuthAsync(request, proxyUri, proxyCredentials, preAuthenticate: false, isProxyAuth: true, doRequestAuth, pool, cancellationToken); + return SendWithAuthAsync(request, proxyUri, async, proxyCredentials, preAuthenticate: false, isProxyAuth: true, doRequestAuth, pool, cancellationToken); } - public static Task SendWithRequestAuthAsync(HttpRequestMessage request, ICredentials credentials, bool preAuthenticate, HttpConnectionPool pool, CancellationToken cancellationToken) + public static ValueTask SendWithRequestAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, bool preAuthenticate, HttpConnectionPool pool, CancellationToken cancellationToken) { Debug.Assert(request.RequestUri != null); - return SendWithAuthAsync(request, request.RequestUri, credentials, preAuthenticate, isProxyAuth: false, doRequestAuth: true, pool, cancellationToken); + return SendWithAuthAsync(request, request.RequestUri, async, credentials, preAuthenticate, isProxyAuth: false, doRequestAuth: true, pool, cancellationToken); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs index fb9cd49d7e545..1e137ff3a677e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs @@ -170,7 +170,7 @@ private async ValueTask ReadAsyncCore(Memory buffer, CancellationToke } // We're only here if we need more data to make forward progress. - await _connection.FillAsync().ConfigureAwait(false); + await _connection.FillAsync(async: true).ConfigureAwait(false); // Now that we have more, see if we can get any response data, and if // we can we're done. @@ -224,7 +224,7 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell return; } - await _connection.FillAsync().ConfigureAwait(false); + await _connection.FillAsync(async: true).ConfigureAwait(false); } } catch (Exception exc) when (CancellationHelper.ShouldWrapInOperationCanceledException(exc, cancellationToken)) @@ -473,7 +473,7 @@ public override async ValueTask DrainAsync(int maxDrainBytes) } } - await _connection.FillAsync().ConfigureAwait(false); + await _connection.FillAsync(async: true).ConfigureAwait(false); } } finally diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs index 0f2f5e1fe4531..d4799c3657c2c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs @@ -18,6 +18,26 @@ public ChunkedEncodingWriteStream(HttpConnection connection) : base(connection) { } + public override void Write(ReadOnlySpan buffer) + { + HttpConnection connection = GetConnectionOrThrow(); + Debug.Assert(connection._currentRequest != null); + + if (buffer.Length == 0) + { + connection.Flush(); + return; + } + + // Write chunk length in hex followed by \r\n + connection.WriteHexInt32Async(buffer.Length, async: false).GetAwaiter().GetResult(); + connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: false).GetAwaiter().GetResult(); + + // Write chunk contents followed by \r\n + connection.Write(buffer); + connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: false).GetAwaiter().GetResult(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ignored) { HttpConnection connection = GetConnectionOrThrow(); @@ -30,7 +50,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo ValueTask task = buffer.Length == 0 ? // Don't write if nothing was given, especially since we don't want to accidentally send a 0 chunk, // which would indicate end of body. Instead, just ensure no content is stuck in the buffer. - connection.FlushAsync() : + connection.FlushAsync(async: true) : WriteChunkAsync(connection, buffer); return task; @@ -38,21 +58,21 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo static async ValueTask WriteChunkAsync(HttpConnection connection, ReadOnlyMemory buffer) { // Write chunk length in hex followed by \r\n - await connection.WriteHexInt32Async(buffer.Length).ConfigureAwait(false); - await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await connection.WriteHexInt32Async(buffer.Length, async: true).ConfigureAwait(false); + await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: true).ConfigureAwait(false); // Write chunk contents followed by \r\n - await connection.WriteAsync(buffer).ConfigureAwait(false); - await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await connection.WriteAsync(buffer, async: true).ConfigureAwait(false); + await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: true).ConfigureAwait(false); } } - public override async ValueTask FinishAsync() + public override async ValueTask FinishAsync(bool async) { // Send 0 byte chunk to indicate end, then final CrLf HttpConnection connection = GetConnectionOrThrow(); _connection = null; - await connection.WriteBytesAsync(s_finalChunkBytes).ConfigureAwait(false); + await connection.WriteBytesAsync(s_finalChunkBytes, async).ConfigureAwait(false); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs index 288022837e832..867a281168797 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs @@ -34,7 +34,12 @@ public CertificateCallbackMapper(Func ConnectAsync(string host, int port, CancellationToken cancellationToken) + public static ValueTask ConnectAsync(string host, int port, bool async, CancellationToken cancellationToken) + { + return async ? ConnectAsync(host, port, cancellationToken) : new ValueTask(Connect(host, port, cancellationToken)); + } + + private static async ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken) { // 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 @@ -80,6 +85,34 @@ public static async ValueTask ConnectAsync(string host, int port, Cancel } } + private static Stream Connect(string host, int port, CancellationToken cancellationToken) + { + // For synchronous connections, we can just create a socket and make the connection. + cancellationToken.ThrowIfCancellationRequested(); + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + try + { + socket.NoDelay = true; + using (cancellationToken.UnsafeRegister(s => ((Socket)s!).Dispose(), socket)) + { + socket.Connect(new DnsEndPoint(host, port)); + } + } + catch (Exception e) + { + socket.Dispose(); + + if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) + { + throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); + } + + throw; + } + + return new NetworkStream(socket, ownsSocket: true); + } + /// SocketAsyncEventArgs that carries with it additional state for a Task builder and a CancellationToken. private sealed class ConnectEventArgs : SocketAsyncEventArgs { @@ -125,7 +158,7 @@ protected override void OnCompleted(SocketAsyncEventArgs _) } } - public static ValueTask EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Stream stream, CancellationToken cancellationToken) + public static ValueTask EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, bool async, Stream stream, CancellationToken cancellationToken) { // If there's a cert validation callback, and if it came from HttpClientHandler, // wrap the original delegate in order to change the sender to be the request message (expected by HttpClientHandler's delegate). @@ -145,16 +178,26 @@ public static ValueTask EstablishSslConnectionAsync(SslClientAuthenti } // Create the SslStream, authenticate, and return it. - return EstablishSslConnectionAsyncCore(stream, sslOptions, cancellationToken); + return EstablishSslConnectionAsyncCore(async, stream, sslOptions, cancellationToken); } - private static async ValueTask EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken) + private static async ValueTask EstablishSslConnectionAsyncCore(bool async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken) { SslStream sslStream = new SslStream(stream); try { - await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); + if (async) + { + await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); + } + else + { + using (cancellationToken.UnsafeRegister(s => ((Stream)s!).Dispose(), stream)) + { + sslStream.AuthenticateAsClient(sslOptions); + } + } } catch (Exception e) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs index c199c65f19a9f..0db6a2782fae3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs @@ -106,7 +106,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.CompletedTask; } - Task copyTask = connection.CopyToUntilEofAsync(destination, bufferSize, cancellationToken); + Task copyTask = connection.CopyToUntilEofAsync(destination, async: true, bufferSize, cancellationToken); if (copyTask.IsCompletedSuccessfully) { Finish(connection); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs index ad6bff5783e31..5d6e78a623723 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs @@ -134,7 +134,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.CompletedTask; } - Task copyTask = _connection.CopyToContentLengthAsync(destination, _contentBytesRemaining, bufferSize, cancellationToken); + Task copyTask = _connection.CopyToContentLengthAsync(destination, async: true, _contentBytesRemaining, bufferSize, cancellationToken); if (copyTask.IsCompletedSuccessfully) { Finish(); @@ -225,7 +225,7 @@ public override async ValueTask DrainAsync(int maxDrainBytes) { while (true) { - await _connection.FillAsync().ConfigureAwait(false); + await _connection.FillAsync(async: true).ConfigureAwait(false); ReadFromConnectionBuffer(int.MaxValue); if (_contentBytesRemaining == 0) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs index 393271ea6b6f8..b0c8d3e15f2ff 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs @@ -16,6 +16,16 @@ public ContentLengthWriteStream(HttpConnection connection) : base(connection) { } + public override void Write(ReadOnlySpan buffer) + { + // Have the connection write the data, skipping the buffer. Importantly, this will + // force a flush of anything already in the buffer, i.e. any remaining request headers + // that are still buffered. + HttpConnection connection = GetConnectionOrThrow(); + Debug.Assert(connection._currentRequest != null); + connection.Write(buffer); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ignored) // token ignored as it comes from SendAsync { // Have the connection write the data, skipping the buffer. Importantly, this will @@ -23,10 +33,10 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo // that are still buffered. HttpConnection connection = GetConnectionOrThrow(); Debug.Assert(connection._currentRequest != null); - return connection.WriteAsync(buffer); + return connection.WriteAsync(buffer, async: true); } - public override ValueTask FinishAsync() + public override ValueTask FinishAsync(bool async) { _connection = null; return default; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs index 5eb74ef169ff4..47f567bb3769f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs @@ -12,9 +12,9 @@ namespace System.Net.Http { - internal sealed class DecompressionHandler : HttpMessageHandler + internal sealed class DecompressionHandler : HttpMessageHandlerStage { - private readonly HttpMessageHandler _innerHandler; + private readonly HttpMessageHandlerStage _innerHandler; private readonly DecompressionMethods _decompressionMethods; private const string Gzip = "gzip"; @@ -24,7 +24,7 @@ internal sealed class DecompressionHandler : HttpMessageHandler private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new StringWithQualityHeaderValue(Deflate); private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new StringWithQualityHeaderValue(Brotli); - public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandler innerHandler) + public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandlerStage innerHandler) { Debug.Assert(decompressionMethods != DecompressionMethods.None); Debug.Assert(innerHandler != null); @@ -37,7 +37,7 @@ public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessa internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0; internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0; - protected internal override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + internal override async ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { if (GZipEnabled && !request.Headers.AcceptEncoding.Contains(s_gzipHeaderValue)) { @@ -54,7 +54,7 @@ protected internal override async Task SendAsync(HttpReques request.Headers.AcceptEncoding.Add(s_brotliHeaderValue); } - HttpResponseMessage response = await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false); + HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false); Debug.Assert(response.Content != null); ICollection contentEncodings = response.Content.Headers.ContentEncoding; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 3069f59ba46be..5a7b7d63d4606 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -1679,7 +1679,7 @@ private enum FlushTiming // Note that this is safe to be called concurrently by multiple threads. - public sealed override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public sealed override async Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { if (NetEventSource.IsEnabled) Trace($"{request}"); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs index 50cd701c4cf0b..627aa06e3bb03 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs @@ -163,7 +163,7 @@ private void CheckForShutdown() } } - public override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public override async Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { // Wait for an available stream (based on QUIC MAX_STREAMS) if there isn't one available yet. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpAuthenticatedConnectionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpAuthenticatedConnectionHandler.cs index 7ad432fdf5bcc..df4ba9ef4bcba 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpAuthenticatedConnectionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpAuthenticatedConnectionHandler.cs @@ -7,7 +7,7 @@ namespace System.Net.Http { - internal sealed class HttpAuthenticatedConnectionHandler : HttpMessageHandler + internal sealed class HttpAuthenticatedConnectionHandler : HttpMessageHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -16,9 +16,9 @@ public HttpAuthenticatedConnectionHandler(HttpConnectionPoolManager poolManager) _poolManager = poolManager; } - protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + internal override ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - return _poolManager.SendAsync(request, doRequestAuth: true, cancellationToken); + return _poolManager.SendAsync(request, async, doRequestAuth: true, cancellationToken); } protected override void Dispose(bool disposing) @@ -31,5 +31,4 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } - } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 028502357a628..d649989fd03ec 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -194,7 +194,7 @@ private void ConsumeFromRemainingBuffer(int bytesToConsume) _readOffset += bytesToConsume; } - private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFromContainer) + private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFromContainer, bool async) { if (headers.HeaderStore != null) { @@ -202,24 +202,24 @@ private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFr { if (header.Key.KnownHeader != null) { - await WriteBytesAsync(header.Key.KnownHeader.AsciiBytesWithColonSpace).ConfigureAwait(false); + await WriteBytesAsync(header.Key.KnownHeader.AsciiBytesWithColonSpace, async).ConfigureAwait(false); } else { - await WriteAsciiStringAsync(header.Key.Name).ConfigureAwait(false); - await WriteTwoBytesAsync((byte)':', (byte)' ').ConfigureAwait(false); + await WriteAsciiStringAsync(header.Key.Name, async).ConfigureAwait(false); + await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false); } int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues); Debug.Assert(headerValuesCount > 0, "No values for header??"); if (headerValuesCount > 0) { - await WriteStringAsync(_headerValues[0]).ConfigureAwait(false); + await WriteStringAsync(_headerValues[0], async).ConfigureAwait(false); if (cookiesFromContainer != null && header.Key.KnownHeader == KnownHeaders.Cookie) { - await WriteTwoBytesAsync((byte)';', (byte)' ').ConfigureAwait(false); - await WriteStringAsync(cookiesFromContainer).ConfigureAwait(false); + await WriteTwoBytesAsync((byte)';', (byte)' ', async).ConfigureAwait(false); + await WriteStringAsync(cookiesFromContainer, async).ConfigureAwait(false); cookiesFromContainer = null; } @@ -236,33 +236,33 @@ private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFr for (int i = 1; i < headerValuesCount; i++) { - await WriteAsciiStringAsync(separator).ConfigureAwait(false); - await WriteStringAsync(_headerValues[i]).ConfigureAwait(false); + await WriteAsciiStringAsync(separator, async).ConfigureAwait(false); + await WriteStringAsync(_headerValues[i], async).ConfigureAwait(false); } } } - await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); } } if (cookiesFromContainer != null) { - await WriteAsciiStringAsync(HttpKnownHeaderNames.Cookie).ConfigureAwait(false); - await WriteTwoBytesAsync((byte)':', (byte)' ').ConfigureAwait(false); - await WriteStringAsync(cookiesFromContainer).ConfigureAwait(false); - await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await WriteAsciiStringAsync(HttpKnownHeaderNames.Cookie, async).ConfigureAwait(false); + await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false); + await WriteStringAsync(cookiesFromContainer, async).ConfigureAwait(false); + await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); } } - private async ValueTask WriteHostHeaderAsync(Uri uri) + private async ValueTask WriteHostHeaderAsync(Uri uri, bool async) { - await WriteBytesAsync(KnownHeaders.Host.AsciiBytesWithColonSpace).ConfigureAwait(false); + await WriteBytesAsync(KnownHeaders.Host.AsciiBytesWithColonSpace, async).ConfigureAwait(false); if (_pool.HostHeaderValueBytes != null) { Debug.Assert(Kind != HttpConnectionKind.Proxy); - await WriteBytesAsync(_pool.HostHeaderValueBytes).ConfigureAwait(false); + await WriteBytesAsync(_pool.HostHeaderValueBytes, async).ConfigureAwait(false); } else { @@ -273,26 +273,26 @@ private async ValueTask WriteHostHeaderAsync(Uri uri) // So, we need to add them manually for now. if (uri.HostNameType == UriHostNameType.IPv6) { - await WriteByteAsync((byte)'[').ConfigureAwait(false); - await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false); - await WriteByteAsync((byte)']').ConfigureAwait(false); + await WriteByteAsync((byte)'[', async).ConfigureAwait(false); + await WriteAsciiStringAsync(uri.IdnHost, async).ConfigureAwait(false); + await WriteByteAsync((byte)']', async).ConfigureAwait(false); } else { - await WriteAsciiStringAsync(uri.IdnHost).ConfigureAwait(false); + await WriteAsciiStringAsync(uri.IdnHost, async).ConfigureAwait(false); } if (!uri.IsDefaultPort) { - await WriteByteAsync((byte)':').ConfigureAwait(false); - await WriteDecimalInt32Async(uri.Port).ConfigureAwait(false); + await WriteByteAsync((byte)':', async).ConfigureAwait(false); + await WriteDecimalInt32Async(uri.Port, async).ConfigureAwait(false); } } - await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); } - private Task WriteDecimalInt32Async(int value) + private Task WriteDecimalInt32Async(int value, bool async) { // Try to format into our output buffer directly. if (Utf8Formatter.TryFormat(value, new Span(_writeBuffer, _writeOffset, _writeBuffer.Length - _writeOffset), out int bytesWritten)) @@ -302,10 +302,10 @@ private Task WriteDecimalInt32Async(int value) } // If we don't have enough room, do it the slow way. - return WriteAsciiStringAsync(value.ToString()); + return WriteAsciiStringAsync(value.ToString(), async); } - private Task WriteHexInt32Async(int value) + private Task WriteHexInt32Async(int value, bool async) { // Try to format into our output buffer directly. if (Utf8Formatter.TryFormat(value, new Span(_writeBuffer, _writeOffset, _writeBuffer.Length - _writeOffset), out int bytesWritten, 'X')) @@ -315,10 +315,10 @@ private Task WriteHexInt32Async(int value) } // If we don't have enough room, do it the slow way. - return WriteAsciiStringAsync(value.ToString("X", CultureInfo.InvariantCulture)); + return WriteAsciiStringAsync(value.ToString("X", CultureInfo.InvariantCulture), async); } - public async Task SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken) + public async Task SendAsyncCore(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { TaskCompletionSource? allowExpect100ToContinue = null; Task? sendRequestContentTask = null; @@ -338,8 +338,8 @@ public async Task SendAsyncCore(HttpRequestMessage request, { Debug.Assert(request.RequestUri != null); // Write request line - await WriteStringAsync(normalizedMethod.Method).ConfigureAwait(false); - await WriteByteAsync((byte)' ').ConfigureAwait(false); + await WriteStringAsync(normalizedMethod.Method, async).ConfigureAwait(false); + await WriteByteAsync((byte)' ', async).ConfigureAwait(false); if (ReferenceEquals(normalizedMethod, HttpMethod.Connect)) { @@ -349,7 +349,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, { throw new HttpRequestException(SR.net_http_request_no_host); } - await WriteAsciiStringAsync(request.Headers.Host).ConfigureAwait(false); + await WriteAsciiStringAsync(request.Headers.Host, async).ConfigureAwait(false); } else { @@ -357,35 +357,35 @@ public async Task SendAsyncCore(HttpRequestMessage request, { // Proxied requests contain full URL Debug.Assert(request.RequestUri.Scheme == Uri.UriSchemeHttp); - await WriteBytesAsync(s_httpSchemeAndDelimiter).ConfigureAwait(false); + await WriteBytesAsync(s_httpSchemeAndDelimiter, async).ConfigureAwait(false); // TODO https://github.com/dotnet/runtime/issues/25782: // Uri.IdnHost is missing '[', ']' characters around IPv6 address. // So, we need to add them manually for now. if (request.RequestUri.HostNameType == UriHostNameType.IPv6) { - await WriteByteAsync((byte)'[').ConfigureAwait(false); - await WriteAsciiStringAsync(request.RequestUri.IdnHost).ConfigureAwait(false); - await WriteByteAsync((byte)']').ConfigureAwait(false); + await WriteByteAsync((byte)'[', async).ConfigureAwait(false); + await WriteAsciiStringAsync(request.RequestUri.IdnHost, async).ConfigureAwait(false); + await WriteByteAsync((byte)']', async).ConfigureAwait(false); } else { - await WriteAsciiStringAsync(request.RequestUri.IdnHost).ConfigureAwait(false); + await WriteAsciiStringAsync(request.RequestUri.IdnHost, async).ConfigureAwait(false); } if (!request.RequestUri.IsDefaultPort) { - await WriteByteAsync((byte)':').ConfigureAwait(false); - await WriteDecimalInt32Async(request.RequestUri.Port).ConfigureAwait(false); + await WriteByteAsync((byte)':', async).ConfigureAwait(false); + await WriteDecimalInt32Async(request.RequestUri.Port, async).ConfigureAwait(false); } } - await WriteStringAsync(request.RequestUri.PathAndQuery).ConfigureAwait(false); + await WriteStringAsync(request.RequestUri.PathAndQuery, async).ConfigureAwait(false); } // Fall back to 1.1 for all versions other than 1.0 Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class bool isHttp10 = request.Version.Minor == 0 && request.Version.Major == 1; - await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11NewlineAsciiBytes).ConfigureAwait(false); + await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11NewlineAsciiBytes, async).ConfigureAwait(false); // Determine cookies to send string? cookiesFromContainer = null; @@ -402,13 +402,13 @@ public async Task SendAsyncCore(HttpRequestMessage request, // wasn't sent, so as it's required by HTTP 1.1 spec, send one based on the Request Uri. if (!request.HasHeaders || request.Headers.Host == null) { - await WriteHostHeaderAsync(request.RequestUri).ConfigureAwait(false); + await WriteHostHeaderAsync(request.RequestUri, async).ConfigureAwait(false); } // Write request headers if (request.HasHeaders || cookiesFromContainer != null) { - await WriteHeadersAsync(request.Headers, cookiesFromContainer).ConfigureAwait(false); + await WriteHeadersAsync(request.Headers, cookiesFromContainer, async).ConfigureAwait(false); } if (request.Content == null) @@ -417,22 +417,22 @@ public async Task SendAsyncCore(HttpRequestMessage request, // unless this is a method that never has a body. if (normalizedMethod.MustHaveRequestBody) { - await WriteBytesAsync(s_contentLength0NewlineAsciiBytes).ConfigureAwait(false); + await WriteBytesAsync(s_contentLength0NewlineAsciiBytes, async).ConfigureAwait(false); } } else { // Write content headers - await WriteHeadersAsync(request.Content.Headers, cookiesFromContainer: null).ConfigureAwait(false); + await WriteHeadersAsync(request.Content.Headers, cookiesFromContainer: null, async).ConfigureAwait(false); } // CRLF for end of headers. - await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false); + await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); if (request.Content == null) { // We have nothing more to send, so flush out any headers we haven't yet sent. - await FlushAsync().ConfigureAwait(false); + await FlushAsync(async).ConfigureAwait(false); } else { @@ -444,14 +444,14 @@ public async Task SendAsyncCore(HttpRequestMessage request, // to run concurrently until we receive the final status line, at which point we wait for it. if (!hasExpectContinueHeader) { - await SendRequestContentAsync(request, CreateRequestContentStream(request), cancellationToken).ConfigureAwait(false); + await SendRequestContentAsync(request, CreateRequestContentStream(request), async, cancellationToken).ConfigureAwait(false); } else { // We're sending an Expect: 100-continue header. We need to flush headers so that the server receives // all of them, and we need to do so before initiating the send, as once we do that, it effectively // owns the right to write, and we don't want to concurrently be accessing the write buffer. - await FlushAsync().ConfigureAwait(false); + await FlushAsync(async).ConfigureAwait(false); // Create a TCS we'll use to block the request content from being sent, and create a timer that's used // as a fail-safe to unblock the request content if we don't hear back from the server in a timely manner. @@ -461,7 +461,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, s => ((TaskCompletionSource)s!).TrySetResult(true), allowExpect100ToContinue, _pool.Settings._expect100ContinueTimeout, Timeout.InfiniteTimeSpan); sendRequestContentTask = SendRequestContentWithExpect100ContinueAsync( - request, allowExpect100ToContinue.Task, CreateRequestContentStream(request), expect100Timer, cancellationToken); + request, allowExpect100ToContinue.Task, CreateRequestContentStream(request), expect100Timer, async, cancellationToken); } } @@ -477,7 +477,16 @@ public async Task SendAsyncCore(HttpRequestMessage request, ValueTask? t = ConsumeReadAheadTask(); if (t != null) { - int bytesRead = await t.GetValueOrDefault().ConfigureAwait(false); + // Handle the pre-emptive read. For the async==false case, hopefully the read has + // already completed and this will be a nop, but if it hasn't, we're forced to block + // waiting for the async operation to complete. We will only hit this case for proxied HTTPS + // requests that use a pooled connection, as in that case we don't have a Socket we + // can poll and are forced to issue an async read. + ValueTask vt = t.GetValueOrDefault(); + int bytesRead = + vt.IsCompletedSuccessfully ? vt.Result : + async ? await vt.ConfigureAwait(false) : + vt.AsTask().GetAwaiter().GetResult(); if (NetEventSource.IsEnabled) Trace($"Received {bytesRead} bytes."); if (bytesRead == 0) @@ -496,7 +505,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, // Parse the response status line. var response = new HttpResponseMessage() { RequestMessage = request, Content = new HttpConnectionResponseContent() }; - ParseStatusLine(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false), response); + ParseStatusLine(await ReadNextResponseHeaderLineAsync(async).ConfigureAwait(false), response); // Multiple 1xx responses handling. // RFC 7231: A client MUST be able to parse one or more 1xx responses received prior to a final response, @@ -528,16 +537,16 @@ public async Task SendAsyncCore(HttpRequestMessage request, // Discard headers that come with the interim 1xx responses. // RFC7231: 1xx responses are terminated by the first empty line after the status-line. - while (!IsLineEmpty(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false))); + while (!IsLineEmpty(await ReadNextResponseHeaderLineAsync(async).ConfigureAwait(false))); // Parse the status line for next response. - ParseStatusLine(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false), response); + ParseStatusLine(await ReadNextResponseHeaderLineAsync(async).ConfigureAwait(false), response); } // Parse the response headers. Logic after this point depends on being able to examine headers in the response object. while (true) { - ArraySegment line = await ReadNextResponseHeaderLineAsync(foldedHeadersAllowed: true).ConfigureAwait(false); + ArraySegment line = await ReadNextResponseHeaderLineAsync(async, foldedHeadersAllowed: true).ConfigureAwait(false); if (IsLineEmpty(line)) { break; @@ -589,7 +598,14 @@ public async Task SendAsyncCore(HttpRequestMessage request, { Task sendTask = sendRequestContentTask; sendRequestContentTask = null; - await sendTask.ConfigureAwait(false); + if (async) + { + await sendTask.ConfigureAwait(false); + } + else + { + sendTask.GetAwaiter().GetResult(); + } } // Now we are sure that the request was fully sent. @@ -718,10 +734,8 @@ public async Task SendAsyncCore(HttpRequestMessage request, } } - public sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return SendAsyncCore(request, cancellationToken); - } + public sealed override Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) => + SendAsyncCore(request, async, cancellationToken); private HttpContentWriteStream CreateRequestContentStream(HttpRequestMessage request) { @@ -755,25 +769,32 @@ private CancellationTokenRegistration RegisterCancellation(CancellationToken can private static bool IsLineEmpty(ArraySegment line) => line.Count == 0; - private async ValueTask SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, CancellationToken cancellationToken) + private async ValueTask SendRequestContentAsync(HttpRequestMessage request, HttpContentWriteStream stream, bool async, CancellationToken cancellationToken) { // Now that we're sending content, prohibit retries on this connection. _canRetry = false; // Copy all of the data to the server. - await request.Content!.CopyToAsync(stream, _transportContext, cancellationToken).ConfigureAwait(false); + if (async) + { + await request.Content!.CopyToAsync(stream, _transportContext, cancellationToken).ConfigureAwait(false); + } + else + { + request.Content!.CopyTo(stream, _transportContext, cancellationToken); + } // Finish the content; with a chunked upload, this includes writing the terminating chunk. - await stream.FinishAsync().ConfigureAwait(false); + await stream.FinishAsync(async).ConfigureAwait(false); // Flush any content that might still be buffered. - await FlushAsync().ConfigureAwait(false); + await FlushAsync(async).ConfigureAwait(false); if (NetEventSource.IsEnabled) Trace("Finished sending request content."); } private async Task SendRequestContentWithExpect100ContinueAsync( - HttpRequestMessage request, Task allowExpect100ToContinueTask, HttpContentWriteStream stream, Timer expect100Timer, CancellationToken cancellationToken) + HttpRequestMessage request, Task allowExpect100ToContinueTask, HttpContentWriteStream stream, Timer expect100Timer, bool async, CancellationToken cancellationToken) { // Wait until we receive a trigger notification that it's ok to continue sending content. // This will come either when the timer fires or when we receive a response status line from the server. @@ -786,7 +807,7 @@ private async Task SendRequestContentWithExpect100ContinueAsync( if (sendRequestContent) { if (NetEventSource.IsEnabled) Trace($"Sending request content for Expect: 100-continue."); - await SendRequestContentAsync(request, stream, cancellationToken).ConfigureAwait(false); + await SendRequestContentAsync(request, stream, async, cancellationToken).ConfigureAwait(false); } else { @@ -972,7 +993,7 @@ private void WriteToBuffer(ReadOnlyMemory source) _writeOffset += source.Length; } - private async ValueTask WriteAsync(ReadOnlyMemory source) + private void Write(ReadOnlySpan source) { int remaining = _writeBuffer.Length - _writeOffset; @@ -988,13 +1009,44 @@ private async ValueTask WriteAsync(ReadOnlyMemory source) // Fit what we can in the current write buffer and flush it. WriteToBuffer(source.Slice(0, remaining)); source = source.Slice(remaining); - await FlushAsync().ConfigureAwait(false); + Flush(); } if (source.Length >= _writeBuffer.Length) { // Large write. No sense buffering this. Write directly to stream. - await WriteToStreamAsync(source).ConfigureAwait(false); + WriteToStream(source); + } + else + { + // Copy remainder into buffer + WriteToBuffer(source); + } + } + + private async ValueTask WriteAsync(ReadOnlyMemory source, bool async) + { + int remaining = _writeBuffer.Length - _writeOffset; + + if (source.Length <= remaining) + { + // Fits in current write buffer. Just copy and return. + WriteToBuffer(source); + return; + } + + if (_writeOffset != 0) + { + // Fit what we can in the current write buffer and flush it. + WriteToBuffer(source.Slice(0, remaining)); + source = source.Slice(remaining); + await FlushAsync(async).ConfigureAwait(false); + } + + if (source.Length >= _writeBuffer.Length) + { + // Large write. No sense buffering this. Write directly to stream. + await WriteToStreamAsync(source, async).ConfigureAwait(false); } else { @@ -1027,13 +1079,13 @@ private void WriteWithoutBuffering(ReadOnlySpan source) WriteToStream(source); } - private ValueTask WriteWithoutBufferingAsync(ReadOnlyMemory source) + private ValueTask WriteWithoutBufferingAsync(ReadOnlyMemory source, bool async) { if (_writeOffset == 0) { // There's nothing in the write buffer we need to flush. // Just write the supplied data out to the stream. - return WriteToStreamAsync(source); + return WriteToStreamAsync(source, async); } int remaining = _writeBuffer.Length - _writeOffset; @@ -1044,40 +1096,40 @@ private ValueTask WriteWithoutBufferingAsync(ReadOnlyMemory source) // the content to the write buffer and then flush it, so that we // can do a single send rather than two. WriteToBuffer(source); - return FlushAsync(); + return FlushAsync(async); } // There's data in the write buffer and the data we're writing doesn't fit after it. // Do two writes, one to flush the buffer and then another to write the supplied content. - return FlushThenWriteWithoutBufferingAsync(source); + return FlushThenWriteWithoutBufferingAsync(source, async); } - private async ValueTask FlushThenWriteWithoutBufferingAsync(ReadOnlyMemory source) + private async ValueTask FlushThenWriteWithoutBufferingAsync(ReadOnlyMemory source, bool async) { - await FlushAsync().ConfigureAwait(false); - await WriteToStreamAsync(source).ConfigureAwait(false); + await FlushAsync(async).ConfigureAwait(false); + await WriteToStreamAsync(source, async).ConfigureAwait(false); } - private Task WriteByteAsync(byte b) + private Task WriteByteAsync(byte b, bool async) { if (_writeOffset < _writeBuffer.Length) { _writeBuffer[_writeOffset++] = b; return Task.CompletedTask; } - return WriteByteSlowAsync(b); + return WriteByteSlowAsync(b, async); } - private async Task WriteByteSlowAsync(byte b) + private async Task WriteByteSlowAsync(byte b, bool async) { Debug.Assert(_writeOffset == _writeBuffer.Length); - await WriteToStreamAsync(_writeBuffer).ConfigureAwait(false); + await WriteToStreamAsync(_writeBuffer, async).ConfigureAwait(false); _writeBuffer[0] = b; _writeOffset = 1; } - private Task WriteTwoBytesAsync(byte b1, byte b2) + private Task WriteTwoBytesAsync(byte b1, byte b2, bool async) { if (_writeOffset <= _writeBuffer.Length - 2) { @@ -1086,16 +1138,16 @@ private Task WriteTwoBytesAsync(byte b1, byte b2) buffer[_writeOffset++] = b2; return Task.CompletedTask; } - return WriteTwoBytesSlowAsync(b1, b2); + return WriteTwoBytesSlowAsync(b1, b2, async); } - private async Task WriteTwoBytesSlowAsync(byte b1, byte b2) + private async Task WriteTwoBytesSlowAsync(byte b1, byte b2, bool async) { - await WriteByteAsync(b1).ConfigureAwait(false); - await WriteByteAsync(b2).ConfigureAwait(false); + await WriteByteAsync(b1, async).ConfigureAwait(false); + await WriteByteAsync(b2, async).ConfigureAwait(false); } - private Task WriteBytesAsync(byte[] bytes) + private Task WriteBytesAsync(byte[] bytes, bool async) { if (_writeOffset <= _writeBuffer.Length - bytes.Length) { @@ -1103,10 +1155,10 @@ private Task WriteBytesAsync(byte[] bytes) _writeOffset += bytes.Length; return Task.CompletedTask; } - return WriteBytesSlowAsync(bytes); + return WriteBytesSlowAsync(bytes, async); } - private async Task WriteBytesSlowAsync(byte[] bytes) + private async Task WriteBytesSlowAsync(byte[] bytes, bool async) { int offset = 0; while (true) @@ -1125,13 +1177,13 @@ private async Task WriteBytesSlowAsync(byte[] bytes) } else if (_writeOffset == _writeBuffer.Length) { - await WriteToStreamAsync(_writeBuffer).ConfigureAwait(false); + await WriteToStreamAsync(_writeBuffer, async).ConfigureAwait(false); _writeOffset = 0; } } } - private Task WriteStringAsync(string s) + private Task WriteStringAsync(string s, bool async) { // If there's enough space in the buffer to just copy all of the string's bytes, do so. // Unlike WriteAsciiStringAsync, validate each char along the way. @@ -1153,10 +1205,10 @@ private Task WriteStringAsync(string s) // Otherwise, fall back to doing a normal slow string write; we could optimize away // the extra checks later, but the case where we cross a buffer boundary should be rare. - return WriteStringAsyncSlow(s); + return WriteStringAsyncSlow(s, async); } - private Task WriteAsciiStringAsync(string s) + private Task WriteAsciiStringAsync(string s, bool async) { // If there's enough space in the buffer to just copy all of the string's bytes, do so. int offset = _writeOffset; @@ -1173,10 +1225,10 @@ private Task WriteAsciiStringAsync(string s) // Otherwise, fall back to doing a normal slow string write; we could optimize away // the extra checks later, but the case where we cross a buffer boundary should be rare. - return WriteStringAsyncSlow(s); + return WriteStringAsyncSlow(s, async); } - private async Task WriteStringAsyncSlow(string s) + private async Task WriteStringAsyncSlow(string s, bool async) { for (int i = 0; i < s.Length; i++) { @@ -1185,7 +1237,7 @@ private async Task WriteStringAsyncSlow(string s) { throw new HttpRequestException(SR.net_http_request_invalid_char_encoding); } - await WriteByteAsync((byte)c).ConfigureAwait(false); + await WriteByteAsync((byte)c, async).ConfigureAwait(false); } } @@ -1198,11 +1250,11 @@ private void Flush() } } - private ValueTask FlushAsync() + private ValueTask FlushAsync(bool async) { if (_writeOffset > 0) { - ValueTask t = WriteToStreamAsync(new ReadOnlyMemory(_writeBuffer, 0, _writeOffset)); + ValueTask t = WriteToStreamAsync(new ReadOnlyMemory(_writeBuffer, 0, _writeOffset), async); _writeOffset = 0; return t; } @@ -1215,10 +1267,19 @@ private void WriteToStream(ReadOnlySpan source) _stream.Write(source); } - private ValueTask WriteToStreamAsync(ReadOnlyMemory source) + private ValueTask WriteToStreamAsync(ReadOnlyMemory source, bool async) { if (NetEventSource.IsEnabled) Trace($"Writing {source.Length} bytes."); - return _stream.WriteAsync(source); + + if (async) + { + return _stream.WriteAsync(source); + } + else + { + _stream.Write(source.Span); + return default; + } } private bool TryReadNextLine(out ReadOnlySpan line) @@ -1245,7 +1306,7 @@ private bool TryReadNextLine(out ReadOnlySpan line) return true; } - private async ValueTask> ReadNextResponseHeaderLineAsync(bool foldedHeadersAllowed = false) + private async ValueTask> ReadNextResponseHeaderLineAsync(bool async, bool foldedHeadersAllowed = false) { int previouslyScannedBytes = 0; while (true) @@ -1282,7 +1343,7 @@ private async ValueTask> ReadNextResponseHeaderLineAsync(bool previouslyScannedBytes = backPos - _readOffset; _allowedReadLineBytes -= backPos - scanOffset; ThrowIfExceededAllowedReadLineBytes(); - await FillAsync().ConfigureAwait(false); + await FillAsync(async).ConfigureAwait(false); continue; } @@ -1332,7 +1393,7 @@ private async ValueTask> ReadNextResponseHeaderLineAsync(bool previouslyScannedBytes = _readLength - _readOffset; _allowedReadLineBytes -= _readLength - scanOffset; ThrowIfExceededAllowedReadLineBytes(); - await FillAsync().ConfigureAwait(false); + await FillAsync(async).ConfigureAwait(false); } } @@ -1344,54 +1405,15 @@ private void ThrowIfExceededAllowedReadLineBytes() } } - // Throws IOException on EOF. This is only called when we expect more data. private void Fill() { - Debug.Assert(_readAheadTask == null); - - int remaining = _readLength - _readOffset; - Debug.Assert(remaining >= 0); - - if (remaining == 0) - { - // No data in the buffer. Simply reset the offset and length to 0 to allow - // the whole buffer to be filled. - _readOffset = _readLength = 0; - } - else if (_readOffset > 0) - { - // There's some data in the buffer but it's not at the beginning. Shift it - // down to make room for more. - Buffer.BlockCopy(_readBuffer, _readOffset, _readBuffer, 0, remaining); - _readOffset = 0; - _readLength = remaining; - } - else if (remaining == _readBuffer.Length) - { - // The whole buffer is full, but the caller is still requesting more data, - // so increase the size of the buffer. - Debug.Assert(_readOffset == 0); - Debug.Assert(_readLength == _readBuffer.Length); - - var newReadBuffer = new byte[_readBuffer.Length * 2]; - Buffer.BlockCopy(_readBuffer, 0, newReadBuffer, 0, remaining); - _readBuffer = newReadBuffer; - _readOffset = 0; - _readLength = remaining; - } - - int bytesRead = _stream.Read(_readBuffer, _readLength, _readBuffer.Length - _readLength); - if (NetEventSource.IsEnabled) Trace($"Received {bytesRead} bytes."); - if (bytesRead == 0) - { - throw new IOException(SR.net_http_invalid_response_premature_eof); - } - - _readLength += bytesRead; + ValueTask fillTask = FillAsync(async: false); + Debug.Assert(fillTask.IsCompleted); + fillTask.GetAwaiter().GetResult(); } // Throws IOException on EOF. This is only called when we expect more data. - private async ValueTask FillAsync() + private async ValueTask FillAsync(bool async) { Debug.Assert(_readAheadTask == null); @@ -1426,7 +1448,9 @@ private async ValueTask FillAsync() _readLength = remaining; } - int bytesRead = await _stream.ReadAsync(new Memory(_readBuffer, _readLength, _readBuffer.Length - _readLength)).ConfigureAwait(false); + int bytesRead = async ? + await _stream.ReadAsync(new Memory(_readBuffer, _readLength, _readBuffer.Length - _readLength)).ConfigureAwait(false) : + _stream.Read(_readBuffer, _readLength, _readBuffer.Length - _readLength); if (NetEventSource.IsEnabled) Trace($"Received {bytesRead} bytes."); if (bytesRead == 0) @@ -1584,38 +1608,62 @@ private async ValueTask ReadBufferedAsyncCore(Memory destination) return bytesToCopy; } - private async ValueTask CopyFromBufferAsync(Stream destination, int count, CancellationToken cancellationToken) + private async ValueTask CopyFromBufferAsync(Stream destination, bool async, int count, CancellationToken cancellationToken) { Debug.Assert(count <= _readLength - _readOffset); if (NetEventSource.IsEnabled) Trace($"Copying {count} bytes to stream."); - await destination.WriteAsync(new ReadOnlyMemory(_readBuffer, _readOffset, count), cancellationToken).ConfigureAwait(false); + if (async) + { + await destination.WriteAsync(new ReadOnlyMemory(_readBuffer, _readOffset, count), cancellationToken).ConfigureAwait(false); + } + else + { + destination.Write(_readBuffer, _readOffset, count); + } _readOffset += count; } - private Task CopyToUntilEofAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + private Task CopyToUntilEofAsync(Stream destination, bool async, int bufferSize, CancellationToken cancellationToken) { Debug.Assert(destination != null); int remaining = _readLength - _readOffset; - return remaining > 0 ? - CopyToUntilEofWithExistingBufferedDataAsync(destination, bufferSize, cancellationToken) : - _stream.CopyToAsync(destination, bufferSize, cancellationToken); + + if (remaining > 0) + { + return CopyToUntilEofWithExistingBufferedDataAsync(destination, async, bufferSize, cancellationToken); + } + + if (async) + { + return _stream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + _stream.CopyTo(destination, bufferSize); + return Task.CompletedTask; } - private async Task CopyToUntilEofWithExistingBufferedDataAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + private async Task CopyToUntilEofWithExistingBufferedDataAsync(Stream destination, bool async, int bufferSize, CancellationToken cancellationToken) { int remaining = _readLength - _readOffset; Debug.Assert(remaining > 0); - await CopyFromBufferAsync(destination, remaining, cancellationToken).ConfigureAwait(false); + await CopyFromBufferAsync(destination, async, remaining, cancellationToken).ConfigureAwait(false); _readLength = _readOffset = 0; - await _stream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false); + if (async) + { + await _stream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false); + } + else + { + _stream.CopyTo(destination, bufferSize); + } } // Copy *exactly* [length] bytes into destination; throws on end of stream. - private async Task CopyToContentLengthAsync(Stream destination, ulong length, int bufferSize, CancellationToken cancellationToken) + private async Task CopyToContentLengthAsync(Stream destination, bool async, ulong length, int bufferSize, CancellationToken cancellationToken) { Debug.Assert(destination != null); Debug.Assert(length > 0); @@ -1628,7 +1676,7 @@ private async Task CopyToContentLengthAsync(Stream destination, ulong length, in { remaining = (int)length; } - await CopyFromBufferAsync(destination, remaining, cancellationToken).ConfigureAwait(false); + await CopyFromBufferAsync(destination, async, remaining, cancellationToken).ConfigureAwait(false); length -= (ulong)remaining; if (length == 0) @@ -1651,10 +1699,10 @@ private async Task CopyToContentLengthAsync(Stream destination, ulong length, in { while (true) { - await FillAsync().ConfigureAwait(false); + await FillAsync(async).ConfigureAwait(false); remaining = (ulong)_readLength < length ? _readLength : (int)length; - await CopyFromBufferAsync(destination, remaining, cancellationToken).ConfigureAwait(false); + await CopyFromBufferAsync(destination, async, remaining, cancellationToken).ConfigureAwait(false); length -= (ulong)remaining; if (length == 0) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs index a516450542eb7..4b11b74d5f0d4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs @@ -39,7 +39,7 @@ static string GetOrAddCachedValue([NotNull] ref string? cache, HeaderDescriptor } } - public abstract Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); + public abstract Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); public abstract void Trace(string message, [CallerMemberName] string? memberName = null); protected void TraceConnection(Stream stream) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionHandler.cs index e0aac9fa4c61a..a29e2710570aa 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionHandler.cs @@ -7,7 +7,7 @@ namespace System.Net.Http { - internal sealed class HttpConnectionHandler : HttpMessageHandler + internal sealed class HttpConnectionHandler : HttpMessageHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -16,9 +16,9 @@ public HttpConnectionHandler(HttpConnectionPoolManager poolManager) _poolManager = poolManager; } - protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + internal override ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - return _poolManager.SendAsync(request, doRequestAuth: false, cancellationToken); + return _poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken); } protected override void Dispose(bool disposing) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 450974b63421f..4e4b90d214d36 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -334,7 +334,7 @@ public byte[] Http2AltSvcOriginUri private object SyncObj => _idleConnections; private ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> - GetConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) + GetConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { if (_http3Enabled && request.Version.Major >= 3) { @@ -347,13 +347,13 @@ public byte[] Http2AltSvcOriginUri if (_http2Enabled && request.Version.Major >= 2) { - return GetHttp2ConnectionAsync(request, cancellationToken); + return GetHttp2ConnectionAsync(request, async, cancellationToken); } - return GetHttpConnectionAsync(request, cancellationToken); + return GetHttpConnectionAsync(request, async, cancellationToken); } - private ValueTask GetOrReserveHttp11ConnectionAsync(CancellationToken cancellationToken) + private ValueTask GetOrReserveHttp11ConnectionAsync(bool async, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -412,12 +412,27 @@ public byte[] Http2AltSvcOriginUri } HttpConnection conn = cachedConnection._connection; - if (!conn.LifetimeExpired(nowTicks, pooledConnectionLifetime) && - !conn.EnsureReadAheadAndPollRead()) + if (!conn.LifetimeExpired(nowTicks, pooledConnectionLifetime)) { - // We found a valid connection. Return it. - if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool."); - return new ValueTask(conn); + // Check to see if we've received anything on the connection; if we have, that's + // either erroneous data (we shouldn't have received anything yet) or the connection + // has been closed; either way, we can't use it. If this is an async request, we + // perform an async read on the stream, since we're going to need to read from it + // anyway, and in doing so we can avoid the extra syscall. For sync requests, we + // try to directly poll the socket rather than doing an async read, so that we can + // issue an appropriate sync read when we actually need it. We don't have the + // underlying socket in all cases, though, so PollRead may fall back to an async + // read in some cases. + bool validConnection = async ? + !conn.EnsureReadAheadAndPollRead() : + !conn.PollRead(); + + if (validConnection) + { + // We found a valid connection. Return it. + if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool."); + return new ValueTask(conn); + } } // We got a connection, but it was already closed by the server or the @@ -429,13 +444,15 @@ public byte[] Http2AltSvcOriginUri // We are at the connection limit. Wait for an available connection or connection count (indicated by null). if (NetEventSource.IsEnabled) Trace("Connection limit reached, waiting for available connection."); - return waiter.WaitWithCancellationAsync(cancellationToken); + return async ? + waiter.WaitWithCancellationAsync(cancellationToken) : + new ValueTask(waiter.Task.GetAwaiter().GetResult()); } private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> - GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) + GetHttpConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - HttpConnection? connection = await GetOrReserveHttp11ConnectionAsync(cancellationToken).ConfigureAwait(false); + HttpConnection? connection = await GetOrReserveHttp11ConnectionAsync(async, cancellationToken).ConfigureAwait(false); if (connection != null) { return (connection, false, null); @@ -446,7 +463,7 @@ public byte[] Http2AltSvcOriginUri try { HttpResponseMessage? failureResponse; - (connection, failureResponse) = await CreateHttp11ConnectionAsync(request, cancellationToken).ConfigureAwait(false); + (connection, failureResponse) = await CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); if (connection == null) { Debug.Assert(failureResponse != null); @@ -462,7 +479,7 @@ public byte[] Http2AltSvcOriginUri } private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> - GetHttp2ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) + GetHttp2ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http); @@ -531,7 +548,7 @@ public byte[] Http2AltSvcOriginUri Stream? stream; HttpResponseMessage? failureResponse; (socket, stream, transportContext, failureResponse) = - await ConnectAsync(request, true, cancellationToken).ConfigureAwait(false); + await ConnectAsync(request, async, true, cancellationToken).ConfigureAwait(false); if (failureResponse != null) { return (null, true, failureResponse); @@ -629,7 +646,7 @@ public byte[] Http2AltSvcOriginUri } // If we reach this point, it means we need to fall back to a (new or existing) HTTP/1.1 connection. - return await GetHttpConnectionAsync(request, cancellationToken).ConfigureAwait(false); + return await GetHttpConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); } private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> @@ -717,13 +734,13 @@ public byte[] Http2AltSvcOriginUri } } - public async Task SendWithRetryAsync(HttpRequestMessage request, bool doRequestAuth, CancellationToken cancellationToken) + public async ValueTask SendWithRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { while (true) { // Loop on connection failures and retry if possible. - (HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse) = await GetConnectionAsync(request, cancellationToken).ConfigureAwait(false); + (HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse) = await GetConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); if (failureResponse != null) { // Proxy tunnel failure; return proxy response @@ -742,8 +759,8 @@ public async Task SendWithRetryAsync(HttpRequestMessage req try { response = await (doRequestAuth && Settings._credentials != null ? - AuthenticationHelper.SendWithNtConnectionAuthAsync(request, Settings._credentials, (HttpConnection)connection, this, cancellationToken) : - SendWithNtProxyAuthAsync((HttpConnection)connection, request, cancellationToken)).ConfigureAwait(false); + AuthenticationHelper.SendWithNtConnectionAuthAsync(request, async, Settings._credentials, (HttpConnection)connection, this, cancellationToken) : + SendWithNtProxyAuthAsync((HttpConnection)connection, request, async, cancellationToken)).ConfigureAwait(false); } finally { @@ -752,7 +769,7 @@ public async Task SendWithRetryAsync(HttpRequestMessage req } else { - response = await connection!.SendAsync(request, cancellationToken).ConfigureAwait(false); + response = await connection!.SendAsync(request, async, cancellationToken).ConfigureAwait(false); } } catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnLowerHttpVersion) @@ -1016,17 +1033,17 @@ public void OnNetworkChanged() } } - public async Task SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, bool doRequestAuth, CancellationToken cancellationToken) + public async Task SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { connection.Acquire(); try { if (doRequestAuth && Settings._credentials != null) { - return await AuthenticationHelper.SendWithNtConnectionAuthAsync(request, Settings._credentials, connection, this, cancellationToken).ConfigureAwait(false); + return await AuthenticationHelper.SendWithNtConnectionAuthAsync(request, async, Settings._credentials, connection, this, cancellationToken).ConfigureAwait(false); } - return await SendWithNtProxyAuthAsync(connection, request, cancellationToken).ConfigureAwait(false); + return await SendWithNtProxyAuthAsync(connection, request, async, cancellationToken).ConfigureAwait(false); } finally { @@ -1034,41 +1051,41 @@ public async Task SendWithNtConnectionAuthAsync(HttpConnect } } - public Task SendWithNtProxyAuthAsync(HttpConnection connection, HttpRequestMessage request, CancellationToken cancellationToken) + public Task SendWithNtProxyAuthAsync(HttpConnection connection, HttpRequestMessage request, bool async, CancellationToken cancellationToken) { if (AnyProxyKind && ProxyCredentials != null) { - return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, ProxyCredentials, connection, this, cancellationToken); + return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, connection, this, cancellationToken); } - return connection.SendAsync(request, cancellationToken); + return connection.SendAsync(request, async, cancellationToken); } - public Task SendWithProxyAuthAsync(HttpRequestMessage request, bool doRequestAuth, CancellationToken cancellationToken) + public ValueTask SendWithProxyAuthAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { if ((_kind == HttpConnectionKind.Proxy || _kind == HttpConnectionKind.ProxyConnect) && _poolManager.ProxyCredentials != null) { - return AuthenticationHelper.SendWithProxyAuthAsync(request, _proxyUri!, _poolManager.ProxyCredentials, doRequestAuth, this, cancellationToken); + return AuthenticationHelper.SendWithProxyAuthAsync(request, _proxyUri!, async, _poolManager.ProxyCredentials, doRequestAuth, this, cancellationToken); } - return SendWithRetryAsync(request, doRequestAuth, cancellationToken); + return SendWithRetryAsync(request, async, doRequestAuth, cancellationToken); } - public Task SendAsync(HttpRequestMessage request, bool doRequestAuth, CancellationToken cancellationToken) + public ValueTask SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { if (doRequestAuth && Settings._credentials != null) { - return AuthenticationHelper.SendWithRequestAuthAsync(request, Settings._credentials, Settings._preAuthenticate, this, cancellationToken); + return AuthenticationHelper.SendWithRequestAuthAsync(request, async, Settings._credentials, Settings._preAuthenticate, this, cancellationToken); } - return SendWithProxyAuthAsync(request, doRequestAuth, cancellationToken); + return SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken); } - private async ValueTask<(Socket?, Stream?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool allowHttp2, CancellationToken cancellationToken) + private async ValueTask<(Socket?, Stream?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool async, bool allowHttp2, CancellationToken cancellationToken) { - // If a non-infinite connect timeout has been set, create and use a new CancellationToken that'll be canceled + // If a non-infinite connect timeout has been set, create and use a new CancellationToken that will be canceled // when either the original token is canceled or a connect timeout occurs. CancellationTokenSource? cancellationWithConnectTimeout = null; if (Settings._connectTimeout != Timeout.InfiniteTimeSpan) @@ -1087,17 +1104,17 @@ public Task SendAsync(HttpRequestMessage request, bool doRe case HttpConnectionKind.Https: case HttpConnectionKind.ProxyConnect: Debug.Assert(_originAuthority != null); - stream = await ConnectHelper.ConnectAsync(_originAuthority.IdnHost, _originAuthority.Port, cancellationToken).ConfigureAwait(false); + stream = await ConnectHelper.ConnectAsync(_originAuthority.IdnHost, _originAuthority.Port, async, cancellationToken).ConfigureAwait(false); break; case HttpConnectionKind.Proxy: - stream = await ConnectHelper.ConnectAsync(_proxyUri!.IdnHost, _proxyUri.Port, cancellationToken).ConfigureAwait(false); + stream = await ConnectHelper.ConnectAsync(_proxyUri!.IdnHost, _proxyUri.Port, async, cancellationToken).ConfigureAwait(false); break; case HttpConnectionKind.ProxyTunnel: case HttpConnectionKind.SslProxyTunnel: HttpResponseMessage? response; - (stream, response) = await EstablishProxyTunnel(request.HasHeaders ? request.Headers : null, cancellationToken).ConfigureAwait(false); + (stream, response) = await EstablishProxyTunnel(async, request.HasHeaders ? request.Headers : null, cancellationToken).ConfigureAwait(false); if (response != null) { // Return non-success response from proxy. @@ -1112,7 +1129,7 @@ public Task SendAsync(HttpRequestMessage request, bool doRe TransportContext? transportContext = null; if (_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel) { - SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(allowHttp2 ? _sslOptionsHttp2! : _sslOptionsHttp11!, request, stream!, cancellationToken).ConfigureAwait(false); + SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(allowHttp2 ? _sslOptionsHttp2! : _sslOptionsHttp11!, request, async, stream!, cancellationToken).ConfigureAwait(false); stream = sslStream; transportContext = sslStream.TransportContext; } @@ -1125,10 +1142,10 @@ public Task SendAsync(HttpRequestMessage request, bool doRe } } - internal async ValueTask<(HttpConnection?, HttpResponseMessage?)> CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) + internal async ValueTask<(HttpConnection?, HttpResponseMessage?)> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { (Socket? socket, Stream? stream, TransportContext? transportContext, HttpResponseMessage? failureResponse) = - await ConnectAsync(request, false, cancellationToken).ConfigureAwait(false); + await ConnectAsync(request, async, false, cancellationToken).ConfigureAwait(false); if (failureResponse != null) { @@ -1146,7 +1163,7 @@ private HttpConnection ConstructHttp11Connection(Socket? socket, Stream stream, } // Returns the established stream or an HttpResponseMessage from the proxy indicating failure. - private async ValueTask<(Stream?, HttpResponseMessage?)> EstablishProxyTunnel(HttpRequestHeaders? headers, CancellationToken cancellationToken) + private async ValueTask<(Stream?, HttpResponseMessage?)> EstablishProxyTunnel(bool async, HttpRequestHeaders? headers, CancellationToken cancellationToken) { Debug.Assert(_originAuthority != null); // Send a CONNECT request to the proxy server to establish a tunnel. @@ -1158,7 +1175,7 @@ private HttpConnection ConstructHttp11Connection(Socket? socket, Stream stream, tunnelRequest.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.UserAgent, values); } - HttpResponseMessage tunnelResponse = await _poolManager.SendProxyConnectAsync(tunnelRequest, _proxyUri!, cancellationToken).ConfigureAwait(false); + HttpResponseMessage tunnelResponse = await _poolManager.SendProxyConnectAsync(tunnelRequest, _proxyUri!, async, cancellationToken).ConfigureAwait(false); if (tunnelResponse.StatusCode != HttpStatusCode.OK) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index e90475264620a..e9bf7fcc29058 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -294,7 +294,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox } } - public Task SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken) + public ValueTask SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken) { HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect); @@ -330,15 +330,15 @@ public Task SendAsyncCore(HttpRequestMessage request, Uri? // that need to be closed. } - return pool.SendAsync(request, doRequestAuth, cancellationToken); + return pool.SendAsync(request, async, doRequestAuth, cancellationToken); } - public Task SendProxyConnectAsync(HttpRequestMessage request, Uri proxyUri, CancellationToken cancellationToken) + public ValueTask SendProxyConnectAsync(HttpRequestMessage request, Uri proxyUri, bool async, CancellationToken cancellationToken) { - return SendAsyncCore(request, proxyUri, doRequestAuth: false, isProxyConnect: true, cancellationToken); + return SendAsyncCore(request, proxyUri, async, doRequestAuth: false, isProxyConnect: true, cancellationToken); } - public Task SendAsync(HttpRequestMessage request, bool doRequestAuth, CancellationToken cancellationToken) + public ValueTask SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { return HttpTelemetry.IsEnabled && request.RequestUri != null ? SendAsyncWithLogging(request, doRequestAuth, cancellationToken) : @@ -380,7 +380,7 @@ private Task SendAsyncHelper(HttpRequestMessage request, bo { if (_proxy == null) { - return SendAsyncCore(request, null, doRequestAuth, isProxyConnect: false, cancellationToken); + return SendAsyncCore(request, null, async, doRequestAuth, isProxyConnect: false, cancellationToken); } // Do proxy lookup. @@ -396,7 +396,7 @@ private Task SendAsyncHelper(HttpRequestMessage request, bo if (multiProxy.ReadNext(out proxyUri, out bool isFinalProxy) && !isFinalProxy) { - return SendAsyncMultiProxy(request, doRequestAuth, multiProxy, proxyUri, cancellationToken); + return SendAsyncMultiProxy(request, async, doRequestAuth, multiProxy, proxyUri, cancellationToken); } } else @@ -417,18 +417,19 @@ private Task SendAsyncHelper(HttpRequestMessage request, bo throw new NotSupportedException(SR.net_http_invalid_proxy_scheme); } - return SendAsyncCore(request, proxyUri, doRequestAuth, isProxyConnect: false, cancellationToken); + return SendAsyncCore(request, proxyUri, async, doRequestAuth, isProxyConnect: false, cancellationToken); } /// /// Iterates a request over a set of proxies until one works, or all proxies have failed. /// /// The request message. + /// Whether to execute the request synchronously or asynchronously. /// Whether to perform request authentication. /// The set of proxies to use. /// The first proxy try. /// The cancellation token to use for the operation. - private async Task SendAsyncMultiProxy(HttpRequestMessage request, bool doRequestAuth, MultiProxy multiProxy, Uri? firstProxy, CancellationToken cancellationToken) + private async ValueTask SendAsyncMultiProxy(HttpRequestMessage request, bool async, bool doRequestAuth, MultiProxy multiProxy, Uri? firstProxy, CancellationToken cancellationToken) { HttpRequestException rethrowException; @@ -436,7 +437,7 @@ private async Task SendAsyncMultiProxy(HttpRequestMessage r { try { - return await SendAsyncCore(request, firstProxy, doRequestAuth, isProxyConnect: false, cancellationToken).ConfigureAwait(false); + return await SendAsyncCore(request, firstProxy, async, doRequestAuth, isProxyConnect: false, cancellationToken).ConfigureAwait(false); } catch (HttpRequestException ex) when (ex.AllowRetry != RequestRetryType.NoRetry) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionResponseContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionResponseContent.cs index 7ed050b6990ab..30fddc858a469 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionResponseContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionResponseContent.cs @@ -34,12 +34,30 @@ private Stream ConsumeStream() return _stream; } + protected override void SerializeToStream(Stream stream, TransportContext? context, + CancellationToken cancellationToken) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + using (Stream contentStream = ConsumeStream()) + { + const int BufferSize = 8192; + contentStream.CopyTo(stream, BufferSize); + } + } + protected sealed override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsync(stream, context, CancellationToken.None); protected sealed override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - Debug.Assert(stream != null); + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } using (Stream contentStream = ConsumeStream()) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs index 319523dc36d5c..c15024518ecb3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs @@ -18,6 +18,11 @@ public HttpContentStream(HttpConnection connection) _connection = connection; } + public override void Write(byte[] buffer, int offset, int count) + { + Write(new ReadOnlySpan(buffer, offset, count)); + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs index 9cdd9a6977999..6bbf141be649c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs @@ -26,7 +26,7 @@ public sealed override Task FlushAsync(CancellationToken ignored) { HttpConnection? connection = _connection; return connection != null ? - connection.FlushAsync().AsTask() : + connection.FlushAsync(async: true).AsTask() : default!; } @@ -36,7 +36,7 @@ public sealed override Task FlushAsync(CancellationToken ignored) public sealed override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => throw new NotSupportedException(); - public abstract ValueTask FinishAsync(); + public abstract ValueTask FinishAsync(bool async); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs new file mode 100644 index 0000000000000..126962427eb67 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + internal abstract class HttpMessageHandlerStage : HttpMessageHandler + { + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + ValueTask sendTask = SendAsync(request, async:false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); + } + + protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + SendAsync(request, async: true, cancellationToken).AsTask(); + + internal abstract ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs index 07b88022c560d..082e4b9463fcf 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs @@ -106,7 +106,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.CompletedTask; } - Task copyTask = connection.CopyToUntilEofAsync(destination, bufferSize, cancellationToken); + Task copyTask = connection.CopyToUntilEofAsync(destination, async: true, bufferSize, cancellationToken); if (copyTask.IsCompletedSuccessfully) { Finish(connection); @@ -149,12 +149,6 @@ private void Finish(HttpConnection connection) _connection = null; } - public override void Write(byte[] buffer, int offset, int count) - { - ValidateBufferArgs(buffer, offset, count); - Write(buffer.AsSpan(offset, count)); - } - public override void Write(ReadOnlySpan buffer) { HttpConnection? connection = _connection; @@ -187,7 +181,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return default; } - ValueTask writeTask = connection.WriteWithoutBufferingAsync(buffer); + ValueTask writeTask = connection.WriteWithoutBufferingAsync(buffer, async: true); return writeTask.IsCompleted ? writeTask : new ValueTask(WaitWithConnectionCancellationAsync(writeTask, connection, cancellationToken)); @@ -208,7 +202,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - ValueTask flushTask = connection.FlushAsync(); + ValueTask flushTask = connection.FlushAsync(async: true); return flushTask.IsCompleted ? flushTask.AsTask() : WaitWithConnectionCancellationAsync(flushTask, connection, cancellationToken); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs index 8917d85ef9e90..f3b93ba881fe9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs @@ -9,13 +9,13 @@ namespace System.Net.Http { - internal sealed partial class RedirectHandler : HttpMessageHandler + internal sealed class RedirectHandler : HttpMessageHandlerStage { - private readonly HttpMessageHandler _initialInnerHandler; // Used for initial request - private readonly HttpMessageHandler _redirectInnerHandler; // Used for redirects; this allows disabling auth + private readonly HttpMessageHandlerStage _initialInnerHandler; // Used for initial request + private readonly HttpMessageHandlerStage _redirectInnerHandler; // Used for redirects; this allows disabling auth private readonly int _maxAutomaticRedirections; - public RedirectHandler(int maxAutomaticRedirections, HttpMessageHandler initialInnerHandler, HttpMessageHandler redirectInnerHandler) + public RedirectHandler(int maxAutomaticRedirections, HttpMessageHandlerStage initialInnerHandler, HttpMessageHandlerStage redirectInnerHandler) { Debug.Assert(initialInnerHandler != null); Debug.Assert(redirectInnerHandler != null); @@ -26,11 +26,11 @@ public RedirectHandler(int maxAutomaticRedirections, HttpMessageHandler initialI _redirectInnerHandler = redirectInnerHandler; } - protected internal override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + internal override async ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { if (NetEventSource.IsEnabled) NetEventSource.Enter(this, request, cancellationToken); - HttpResponseMessage response = await _initialInnerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false); + HttpResponseMessage response = await _initialInnerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false); uint redirectCount = 0; Uri? redirectUri; @@ -79,7 +79,7 @@ protected internal override async Task SendAsync(HttpReques } // Issue the redirected request. - response = await _redirectInnerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false); + response = await _redirectInnerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false); } if (NetEventSource.IsEnabled) NetEventSource.Exit(this); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index 3f6ec70d07da9..b82e2d7ca7352 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; using System.Net.Security; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace System.Net.Http public sealed class SocketsHttpHandler : HttpMessageHandler { private readonly HttpConnectionSettings _settings = new HttpConnectionSettings(); - private HttpMessageHandler? _handler; + private HttpMessageHandlerStage? _handler; private bool _disposed; private void CheckDisposed() @@ -287,7 +288,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private HttpMessageHandler SetupHandlerChain() + private HttpMessageHandlerStage SetupHandlerChain() { // Clone the settings to get a relatively consistent view that won't change after this point. // (This isn't entirely complete, as some of the collections it contains aren't currently deeply cloned.) @@ -295,7 +296,7 @@ private HttpMessageHandler SetupHandlerChain() HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings); - HttpMessageHandler handler; + HttpMessageHandlerStage handler; if (settings._credentials == null) { @@ -311,7 +312,7 @@ private HttpMessageHandler SetupHandlerChain() // Just as with WinHttpHandler, for security reasons, we do not support authentication on redirects // if the credential is anything other than a CredentialCache. // We allow credentials in a CredentialCache since they are specifically tied to URIs. - HttpMessageHandler redirectHandler = + HttpMessageHandlerStage redirectHandler = (settings._credentials == null || settings._credentials is CredentialCache) ? handler : new HttpConnectionHandler(poolManager); // will not authenticate @@ -333,8 +334,27 @@ private HttpMessageHandler SetupHandlerChain() return _handler; } - protected internal override Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (request.Version.Major >= 2) + { + throw new NotSupportedException(SR.Format(SR.net_http_http2_sync_not_supported, GetType())); + } + + CheckDisposed(); + HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain(); + + Exception? error = ValidateAndNormalizeRequest(request); + if (error != null) + { + throw error; + } + + return handler.Send(request, cancellationToken); + } + + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { CheckDisposed(); HttpMessageHandler handler = _handler ?? SetupHandlerChain(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/StreamContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/StreamContent.cs index 9ccda2b0ee14a..9bd8544ac861b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/StreamContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/StreamContent.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -54,6 +55,14 @@ private void InitializeContent(Stream content, int bufferSize) if (NetEventSource.IsEnabled) NetEventSource.Associate(this, content); } + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + Debug.Assert(stream != null); + PrepareContent(); + // If the stream can't be re-read, make sure that it gets disposed once it is consumed. + StreamToStreamCopy.Copy(_content, stream, _bufferSize, !_content.CanSeek); + } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsyncCore(stream, default); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/StreamToStreamCopy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/StreamToStreamCopy.cs index 5b3abea16ab58..bb7f68bf4a72f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/StreamToStreamCopy.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/StreamToStreamCopy.cs @@ -16,6 +16,32 @@ namespace System.Net.Http /// internal static class StreamToStreamCopy { + /// Copies the source stream from its current position to the destination stream at its current position. + /// The source stream from which to copy. + /// The destination stream to which to copy. + /// The size of the buffer to allocate if one needs to be allocated. If zero, use the default buffer size. + /// Whether to dispose of the source stream after the copy has finished successfully. + public static void Copy(Stream source, Stream destination, int bufferSize, bool disposeSource) + { + Debug.Assert(source != null); + Debug.Assert(destination != null); + Debug.Assert(bufferSize >= 0); + + if (bufferSize == 0) + { + source.CopyTo(destination); + } + else + { + source.CopyTo(destination, bufferSize); + } + + if (disposeSource) + { + DisposeSource(source); + } + } + /// Copies the source stream from its current position to the destination stream at its current position. /// The source stream from which to copy. /// The destination stream to which to copy. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs index 8ab8c7f5886e0..fa0588d241343 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs @@ -602,7 +602,7 @@ public void SendAsync_ExpectedDiagnosticSourceActivityLoggingDoesNotOverwriteHea public void SendAsync_ExpectedDiagnosticSourceActivityLoggingDoesNotOverwriteW3CTraceParentHeader() { Assert.True(UseVersion.Major < 2, "The test currently only supports HTTP/1."); - RemoteExecutor.Invoke(useVersionString => + RemoteExecutor.Invoke((useVersionString, testAsyncString) => { bool activityStartLogged = false; bool activityStopLogged = false; @@ -640,7 +640,7 @@ public void SendAsync_ExpectedDiagnosticSourceActivityLoggingDoesNotOverwriteW3C using (HttpClient client = CreateHttpClient(useVersionString)) { request.Headers.Add("traceparent", customTraceParentHeader); - client.SendAsync(request).Result.Dispose(); + client.SendAsync(bool.Parse(testAsyncString), request).Result.Dispose(); } Assert.True(activityStartLogged, "HttpRequestOut.Start was not logged."); @@ -650,7 +650,7 @@ public void SendAsync_ExpectedDiagnosticSourceActivityLoggingDoesNotOverwriteW3C "HttpRequestOut.Stop was not logged within 1 second timeout."); diagnosticListenerObserver.Disable(); } - }, UseVersion.ToString()).Dispose(); + }, UseVersion.ToString(), TestAsync.ToString()).Dispose(); } [OuterLoop("Uses external server")] @@ -751,7 +751,7 @@ public void SendAsync_ExpectedDiagnosticExceptionActivityLogging() [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void SendAsync_ExpectedDiagnosticSynchronousExceptionActivityLogging() { - RemoteExecutor.Invoke(useVersionString => + RemoteExecutor.Invoke((useVersionString , testAsyncString)=> { bool exceptionLogged = false; bool activityStopLogged = false; @@ -795,7 +795,14 @@ public void SendAsync_ExpectedDiagnosticSynchronousExceptionActivityLogging() // modifier, and returns Task. If the call is not awaited, the current test method will continue // run before the call is completed, thus Assert.Throws() will not capture the exception. // We need to wait for the Task to complete synchronously, to validate the exception. - Task sendTask = client.SendAsync(request); + bool testAsync = bool.Parse(testAsyncString); + Task sendTask = client.SendAsync(testAsync, request); + if (!testAsync) + { + // In sync test case we execute client.Send(...) in separate thread to prevent deadlocks, + // so it will never finish immediately and we need to wait for it. + ((IAsyncResult)sendTask).AsyncWaitHandle.WaitOne(); + } Assert.True(sendTask.IsFaulted); Assert.IsType(sendTask.Exception.InnerException); } @@ -806,7 +813,7 @@ public void SendAsync_ExpectedDiagnosticSynchronousExceptionActivityLogging() Assert.True(exceptionLogged, "Exception was not logged"); diagnosticListenerObserver.Disable(); } - }, UseVersion.ToString()).Dispose(); + }, UseVersion.ToString(), TestAsync.ToString()).Dispose(); } [OuterLoop("Uses external server")] diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs new file mode 100644 index 0000000000000..38d9bd759fad5 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs @@ -0,0 +1,97 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Test.Common; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + public abstract class HttpClientHandler_Http11_Cancellation_Test : HttpClientHandler_Cancellation_Test + { + protected HttpClientHandler_Http11_Cancellation_Test(ITestOutputHelper output) : base(output) { } + + [OuterLoop] + [Fact] + public async Task ConnectTimeout_TimesOutSSLAuth_Throws() + { + var releaseServer = new TaskCompletionSource(); + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var handler = new SocketsHttpHandler()) + using (var invoker = new HttpMessageInvoker(handler)) + { + handler.ConnectTimeout = TimeSpan.FromSeconds(1); + + var sw = Stopwatch.StartNew(); + await Assert.ThrowsAnyAsync(() => + invoker.SendAsync(TestAsync, new HttpRequestMessage(HttpMethod.Get, + new UriBuilder(uri) { Scheme = "https" }.ToString()) { Version = UseVersion }, default)); + sw.Stop(); + + Assert.InRange(sw.ElapsedMilliseconds, 500, 60_000); + releaseServer.SetResult(); + } + }, server => releaseServer.Task); // doesn't establish SSL connection + } + + [OuterLoop("Incurs significant delay")] + [Fact] + public async Task Expect100Continue_WaitsExpectedPeriodOfTimeBeforeSendingContent() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var handler = new SocketsHttpHandler()) + using (var invoker = new HttpMessageInvoker(handler)) + { + TimeSpan delay = TimeSpan.FromSeconds(3); + handler.Expect100ContinueTimeout = delay; + + var tcs = new TaskCompletionSource(); + var content = new SetTcsContent(new MemoryStream(new byte[1]), tcs); + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content, Version = UseVersion }; + request.Headers.ExpectContinue = true; + + var sw = Stopwatch.StartNew(); + (await invoker.SendAsync(TestAsync, request, default)).Dispose(); + sw.Stop(); + + Assert.InRange(sw.Elapsed, delay - TimeSpan.FromSeconds(.5), delay * 20); // arbitrary wiggle room + } + }, async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAsync(); + await connection.ReadAsync(new byte[1], 0, 1); + await connection.SendResponseAsync(); + }); + }); + } + + private sealed class SetTcsContent : StreamContent + { + private readonly TaskCompletionSource _tcs; + + public SetTcsContent(Stream stream, TaskCompletionSource tcs) : base(stream) => _tcs = tcs; + + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) => + SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + _tcs.SetResult(true); + return base.SerializeToStreamAsync(stream, context); + } + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs new file mode 100644 index 0000000000000..db195df96b773 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Test.Common; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + public abstract class HttpClientHandler_Connect_Test : HttpClientHandlerTestBase + { + public HttpClientHandler_Connect_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task ConnectMethod_Success() + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClient client = CreateHttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion }; + request.Headers.Host = "foo.com:345"; + + // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. + Task responseTask = client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead); + + await server.AcceptConnectionAsync(async connection => + { + // Verify that Host header exist and has same value and URI authority. + List lines = await connection.ReadRequestHeaderAsync().ConfigureAwait(false); + string authority = lines[0].Split()[1]; + foreach (string line in lines) + { + if (line.StartsWith("Host:",StringComparison.InvariantCultureIgnoreCase)) + { + Assert.Equal("Host: foo.com:345", line); + break; + } + } + + Task serverTask = connection.SendResponseAsync(HttpStatusCode.OK); + await TestHelper.WhenAllCompletedOrAnyFailed(responseTask, serverTask).ConfigureAwait(false); + + using (Stream clientStream = await (await responseTask).Content.ReadAsStreamAsync()) + { + Assert.True(clientStream.CanWrite); + Assert.True(clientStream.CanRead); + Assert.False(clientStream.CanSeek); + + TextReader clientReader = new StreamReader(clientStream); + TextWriter clientWriter = new StreamWriter(clientStream) { AutoFlush = true }; + TextWriter serverWriter = connection.Writer; + + const string helloServer = "hello server"; + const string helloClient = "hello client"; + const string goodbyeServer = "goodbye server"; + const string goodbyeClient = "goodbye client"; + + clientWriter.WriteLine(helloServer); + Assert.Equal(helloServer, connection.ReadLine()); + serverWriter.WriteLine(helloClient); + Assert.Equal(helloClient, clientReader.ReadLine()); + clientWriter.WriteLine(goodbyeServer); + Assert.Equal(goodbyeServer, connection.ReadLine()); + serverWriter.WriteLine(goodbyeClient); + Assert.Equal(goodbyeClient, clientReader.ReadLine()); + } + }); + } + }); + } + + [Fact] + public async Task ConnectMethod_Fails() + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClient client = CreateHttpClient()) + { + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion }; + request.Headers.Host = "foo.com:345"; + // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. + Task responseTask = client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead); + await server.AcceptConnectionAsync(async connection => + { + Task> serverTask = connection.ReadRequestHeaderAndSendResponseAsync(HttpStatusCode.Forbidden, content: "error"); + + await TestHelper.WhenAllCompletedOrAnyFailed(responseTask, serverTask); + HttpResponseMessage response = await responseTask; + + Assert.True(response.StatusCode == HttpStatusCode.Forbidden); + }); + } + }); + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Headers.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Headers.cs index 48cc1cee5f2d6..625988baee711 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Headers.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Headers.cs @@ -33,7 +33,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; message.Headers.TryAddWithoutValidation("User-Agent", userAgent); - (await client.SendAsync(message).ConfigureAwait(false)).Dispose(); + (await client.SendAsync(TestAsync, message).ConfigureAwait(false)).Dispose(); } }, async server => @@ -58,7 +58,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => client.DefaultRequestHeaders.TryAddWithoutValidation("x-ms-version", Version); client.DefaultRequestHeaders.Add("x-ms-blob-type", Blob); var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; - (await client.SendAsync(message).ConfigureAwait(false)).Dispose(); + (await client.SendAsync(TestAsync, message).ConfigureAwait(false)).Dispose(); } }, async server => @@ -85,7 +85,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; Assert.True(request.Headers.TryAddWithoutValidation("bad", value)); - await Assert.ThrowsAsync(() => client.SendAsync(request)); + await Assert.ThrowsAsync(() => client.SendAsync(TestAsync, request)); } }, @@ -119,7 +119,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => message.Content = new StringContent(""); contentHeader = message.Content.Headers.TryAddWithoutValidation(key, value); } - (await client.SendAsync(message).ConfigureAwait(false)).Dispose(); + (await client.SendAsync(TestAsync, message).ConfigureAwait(false)).Dispose(); } // Validate our test by validating our understanding of a header's parsability. @@ -223,7 +223,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => using (HttpClient client = CreateHttpClient()) { var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; - HttpResponseMessage response = await client.SendAsync(message); + HttpResponseMessage response = await client.SendAsync(TestAsync, message); Assert.NotNull(response.Content.Headers.Expires); // Invalid date should be converted to MinValue so everything is expired. Assert.Equal(isValid ? DateTime.Parse(value) : DateTimeOffset.MinValue, response.Content.Headers.Expires); @@ -261,7 +261,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => using (HttpClient client = CreateHttpClient()) { var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; - HttpResponseMessage response = await client.SendAsync(message); + HttpResponseMessage response = await client.SendAsync(TestAsync, message); Assert.Equal(value, response.Headers.GetValues(name).First()); } @@ -284,7 +284,7 @@ public async Task SendAsync_GetWithValidHostHeader_Success(bool withPort) m.Headers.Host = withPort ? Configuration.Http.SecureHost + ":443" : Configuration.Http.SecureHost; using (HttpClient client = CreateHttpClient()) - using (HttpResponseMessage response = await client.SendAsync(m)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, m)) { string responseContent = await response.Content.ReadAsStringAsync(); _output.WriteLine(responseContent); @@ -312,7 +312,7 @@ public async Task SendAsync_GetWithInvalidHostHeader_ThrowsException() using (HttpClient client = CreateHttpClient()) { - await Assert.ThrowsAsync(() => client.SendAsync(m)); + await Assert.ThrowsAsync(() => client.SendAsync(TestAsync, m)); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index 9e9343dba8875..211bdfca3fb06 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -2707,7 +2707,7 @@ public async Task SendAsync_ConcurentSendReceive_Ok(bool shouldWaitForRequestBod (int streamId, HttpRequestData requestData) = await connection.ReadAndParseRequestHeaderAsync(readBody : false); // Client finished sending request headers and we received them. - // Send reqquest body. + // Send request body. await requestStream.WriteAsync(Encoding.UTF8.GetBytes(requestContent)); duplexContent.Complete(); @@ -2717,7 +2717,7 @@ public async Task SendAsync_ConcurentSendReceive_Ok(bool shouldWaitForRequestBod await connection.ReadBodyAsync(); } - // Send response headers + // Send response headers await connection.SendResponseHeadersAsync(streamId, endStream: false, responseCode); HttpResponseMessage response = await responseTask; diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 62ff7ace2e248..2f50789b34f58 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.DotNet.RemoteExecutor; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Test.Common; @@ -329,38 +331,6 @@ public async Task GetAsync_CustomException_Asynchronous_ThrowsException() } } - [Fact] - public void SendAsync_NullRequest_ThrowsException() - { - using (var client = new HttpClient(new CustomResponseHandler((r,c) => Task.FromResult(null)))) - { - AssertExtensions.Throws("request", () => { client.SendAsync(null); }); - } - } - - [Fact] - public async Task SendAsync_DuplicateRequest_ThrowsException() - { - using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) - using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) - { - (await client.SendAsync(request)).Dispose(); - Assert.Throws(() => { client.SendAsync(request); }); - } - } - - [Fact] - public async Task SendAsync_RequestContentNotDisposed() - { - var content = new ByteArrayContent(new byte[1]); - using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri()) { Content = content }) - using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) - { - await client.SendAsync(request); - await content.ReadAsStringAsync(); // no exception - } - } - [Fact] public async Task GetStringAsync_Success() { @@ -583,6 +553,7 @@ public void Dispose_UseAfterDispose_Throws() Assert.Throws(() => { client.PostAsync(CreateFakeUri(), new ByteArrayContent(new byte[1])); }); Assert.Throws(() => { client.PutAsync(CreateFakeUri(), new ByteArrayContent(new byte[1])); }); Assert.Throws(() => { client.SendAsync(new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())); }); + Assert.Throws(() => { client.Send(new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())); }); Assert.Throws(() => { client.Timeout = TimeSpan.FromSeconds(1); }); } @@ -756,6 +727,281 @@ public void Dispose_UsePatchAfterDispose_Throws() Assert.Throws(() => { client.PatchAsync(CreateFakeUri(), new ByteArrayContent(new byte[1])); }); } + [Theory] + [InlineData(HttpCompletionOption.ResponseContentRead)] + [InlineData(HttpCompletionOption.ResponseHeadersRead)] + public async Task Send_SingleThread_Succeeds(HttpCompletionOption completionOption) + { + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + + var client = new HttpClient(new CustomResponseHandler((r, c) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + return Task.FromResult(new HttpResponseMessage() + { + Content = new CustomContent(stream => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + }) + }); + })); + using (client) + { + HttpResponseMessage response = client.Send(new HttpRequestMessage(HttpMethod.Get, CreateFakeUri()) + { + Content = new CustomContent(stream => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + }) + }, completionOption); + // ToDo: use synchronous ReadAsStream once it's been implemented. + if (completionOption == HttpCompletionOption.ResponseContentRead) + { + // Should be buffered thus return completed task and execute synchronously. + Stream contentStream = await response.Content.ReadAsStreamAsync(); + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + } + } + } + + [Theory] + [InlineData(HttpCompletionOption.ResponseContentRead)] + [InlineData(HttpCompletionOption.ResponseHeadersRead)] + public async Task Send_SingleThread_Loopback_Succeeds(HttpCompletionOption completionOption) + { + string content = "Test content"; + + ManualResetEventSlim mres = new ManualResetEventSlim(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + try + { + // To prevent deadlock + await Task.Yield(); + + int currentThreadId = Thread.CurrentThread.ManagedThreadId; + + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { + Content = new CustomContent(stream => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + stream.Write(Encoding.UTF8.GetBytes(content)); + }) + }, completionOption); + + // ToDo: use synchronous ReadAsStream once it's been implemented. + if (completionOption == HttpCompletionOption.ResponseContentRead) + { + // Should be buffered thus return completed task and execute synchronously. + Stream contentStream = await response.Content.ReadAsStreamAsync(); + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + using (StreamReader sr = new StreamReader(contentStream)) + { + Assert.Equal(content, sr.ReadToEnd()); + } + } + } + finally + { + mres.Set(); + } + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(content: content); + + // To keep the connection open until the response is fully read. + mres.Wait(); + }); + }); + } + + [Fact] + [OuterLoop] + public async Task Send_CancelledRequestContent_Throws() + { + CancellationTokenSource cts = new CancellationTokenSource(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { + Content = new CustomContent(new Action(stream => + { + while (true) + { + stream.Write(new byte[] { 0xff }); + Thread.Sleep(TimeSpan.FromSeconds(0.1)); + } + })) + }, cts.Token); + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsNotType(ex.InnerException); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + try + { + await connection.ReadRequestHeaderAsync(); + cts.Cancel(); + await connection.ReadRequestBodyAsync(); + } + catch { } + }); + }); + } + + [Fact] + [OuterLoop] + public async Task Send_TimeoutRequestContent_Throws() + { + ManualResetEventSlim mres = new ManualResetEventSlim(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(0.5); + + try + { + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { + Content = new CustomContent(new Action(stream => + { + while (true) + { + stream.Write(new byte[] { 0xff }); + Thread.Sleep(TimeSpan.FromSeconds(0.1)); + } + })) + }); + } + finally + { + mres.Set(); + } + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + try + { + await connection.ReadRequestHeaderAsync(); + mres.Wait(); + } + catch { } + }); + }); + } + + [Fact] + [OuterLoop] + public async Task Send_CancelledResponseContent_Throws() + { + string content = "Test content"; + + CancellationTokenSource cts = new CancellationTokenSource(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { + Content = new CustomContent(stream => + { + stream.Write(Encoding.UTF8.GetBytes(content)); + }) + }, cts.Token); + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsNotType(ex.InnerException); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + try + { + await connection.ReadRequestDataAsync(); + await connection.SendResponseAsync(headers: new List() { + new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) + }); + await Task.Delay(TimeSpan.FromSeconds(0.5)); + cts.Cancel(); + await connection.Writer.WriteLineAsync(content); + } + catch { } + }); + }); + } + + [Fact] + [OuterLoop] + public async Task Send_TimeoutResponseContent_Throws() + { + string content = "Test content"; + + ManualResetEventSlim mres = new ManualResetEventSlim(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(0.5); + try + { + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri)); + } + finally + { + mres.Set(); + } + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + try + { + await connection.ReadRequestDataAsync(); + await connection.SendResponseAsync(headers: new List() { + new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) + }); + mres.Wait(); + await connection.Writer.WriteLineAsync(content); + } + catch { } + }); + }); + } + [Fact] public void DefaultRequestVersion_InitialValueExpected() { @@ -844,17 +1090,32 @@ protected override Task SendAsync(HttpRequestMessage reques { return _func(request, cancellationToken); } + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _func(request, cancellationToken).GetAwaiter().GetResult(); + } } private sealed class CustomContent : HttpContent { - private readonly Func _func; + private readonly Func _serializeAsync; + private readonly Action _serializeSync; + + public CustomContent(Func serializeAsync) { _serializeAsync = serializeAsync; } - public CustomContent(Func func) { _func = func; } + public CustomContent(Action serializeSync) { _serializeSync = serializeSync; } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { - return _func(stream); + Debug.Assert(_serializeAsync != null); + return _serializeAsync(stream); + } + + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + Debug.Assert(_serializeSync != null); + _serializeSync(stream); } protected override bool TryComputeLength(out long length) @@ -863,5 +1124,54 @@ protected override bool TryComputeLength(out long length) return false; } } + + public abstract class HttpClientSendTest : HttpClientHandlerTestBase + { + protected HttpClientSendTest(ITestOutputHelper output) : base(output) { } + + + [Fact] + public async Task Send_NullRequest_ThrowsException() + { + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(null)))) + { + await AssertExtensions.ThrowsAsync("request", () => client.SendAsync(TestAsync, null)); + } + } + + [Fact] + public async Task Send_DuplicateRequest_ThrowsException() + { + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) + using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) + { + (await client.SendAsync(TestAsync, request)).Dispose(); + await Assert.ThrowsAsync(() => client.SendAsync(TestAsync, request)); + } + } + + [Fact] + public async Task Send_RequestContentNotDisposed() + { + var content = new ByteArrayContent(new byte[1]); + using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri()) { Content = content }) + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) + { + await client.SendAsync(TestAsync, request); + await content.ReadAsStringAsync(); // no exception + } + } + } + } + + public sealed class HttpClientSendTest_Async : HttpClientTest.HttpClientSendTest + { + public HttpClientSendTest_Async(ITestOutputHelper output) : base(output) { } + } + + public sealed class HttpClientSendTest_Sync : HttpClientTest.HttpClientSendTest + { + public HttpClientSendTest_Sync(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index de27cbc5d92c1..5ed66fc3f4469 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -109,14 +109,6 @@ private sealed class SetOnFinalized public sealed class SocketsHttpHandler_HttpProtocolTests : HttpProtocolTests { public SocketsHttpHandler_HttpProtocolTests(ITestOutputHelper output) : base(output) { } - - [Theory] - [InlineData("delete", "DELETE")] - [InlineData("options", "OPTIONS")] - [InlineData("trace", "TRACE")] - [InlineData("patch", "PATCH")] - public Task CustomMethod_SentUppercasedIfKnown_Additional(string specifiedMethod, string expectedMethod) => - CustomMethod_SentUppercasedIfKnown(specifiedMethod, expectedMethod); } public sealed class SocketsHttpHandler_HttpProtocolTests_Dribble : HttpProtocolTests_Dribble @@ -984,7 +976,7 @@ public sealed class SocketsHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTes public SocketsHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } } - public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Cancellation_Test + public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Http11_Cancellation_Test { public SocketsHttpHandler_HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } @@ -1036,31 +1028,6 @@ public void ConnectTimeout_SetAfterUse_Throws() } } - [OuterLoop] - [Fact] - public async Task ConnectTimeout_TimesOutSSLAuth_Throws() - { - var releaseServer = new TaskCompletionSource(); - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - using (var handler = new SocketsHttpHandler()) - using (var invoker = new HttpMessageInvoker(handler)) - { - handler.ConnectTimeout = TimeSpan.FromSeconds(1); - - var sw = Stopwatch.StartNew(); - await Assert.ThrowsAnyAsync(() => - invoker.SendAsync(new HttpRequestMessage(HttpMethod.Get, - new UriBuilder(uri) { Scheme = "https" }.ToString()) { Version = UseVersion }, default)); - sw.Stop(); - - Assert.InRange(sw.ElapsedMilliseconds, 500, 60_000); - releaseServer.SetResult(); - } - }, server => releaseServer.Task); // doesn't establish SSL connection - } - - [Fact] public void Expect100ContinueTimeout_Default() { @@ -1107,53 +1074,6 @@ public void Expect100ContinueTimeout_SetAfterUse_Throws() Assert.Throws(() => handler.Expect100ContinueTimeout = TimeSpan.FromMilliseconds(1)); } } - - [OuterLoop("Incurs significant delay")] - [Fact] - public async Task Expect100Continue_WaitsExpectedPeriodOfTimeBeforeSendingContent() - { - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - using (var handler = new SocketsHttpHandler()) - using (var invoker = new HttpMessageInvoker(handler)) - { - TimeSpan delay = TimeSpan.FromSeconds(3); - handler.Expect100ContinueTimeout = delay; - - var tcs = new TaskCompletionSource(); - var content = new SetTcsContent(new MemoryStream(new byte[1]), tcs); - var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content, Version = UseVersion }; - request.Headers.ExpectContinue = true; - - var sw = Stopwatch.StartNew(); - (await invoker.SendAsync(request, default)).Dispose(); - sw.Stop(); - - Assert.InRange(sw.Elapsed, delay - TimeSpan.FromSeconds(.5), delay * 20); // arbitrary wiggle room - } - }, async server => - { - await server.AcceptConnectionAsync(async connection => - { - await connection.ReadRequestHeaderAsync(); - await connection.ReadAsync(new byte[1], 0, 1); - await connection.SendResponseAsync(); - }); - }); - } - - private sealed class SetTcsContent : StreamContent - { - private readonly TaskCompletionSource _tcs; - - public SetTcsContent(Stream stream, TaskCompletionSource tcs) : base(stream) => _tcs = tcs; - - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - _tcs.SetResult(true); - return base.SerializeToStreamAsync(stream, context); - } - } } public sealed class SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test : HttpClientHandler_MaxResponseHeadersLength_Test @@ -1164,43 +1084,6 @@ public SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test(ITestO public sealed class SocketsHttpHandler_HttpClientHandler_Authentication_Test : HttpClientHandler_Authentication_Test { public SocketsHttpHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { } - - [Theory] - [MemberData(nameof(Authentication_SocketsHttpHandler_TestData))] - public async Task SocketsHttpHandler_Authentication_Succeeds(string authenticateHeader, bool result) - { - await HttpClientHandler_Authentication_Succeeds(authenticateHeader, result); - } - - public static IEnumerable Authentication_SocketsHttpHandler_TestData() - { - // These test cases successfully authenticate on SocketsHttpHandler but fail on the other handlers. - // These are legal as per the RFC, so authenticating is the expected behavior. - // See https://github.com/dotnet/runtime/issues/25643 for details. - yield return new object[] { "Basic realm=\"testrealm1\" basic realm=\"testrealm1\"", true }; - yield return new object[] { "Basic something digest something", true }; - yield return new object[] { "Digest realm=\"api@example.org\", qop=\"auth\", algorithm=MD5-sess, nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", " + - "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true", true }; - yield return new object[] { "dIgEsT realm=\"api@example.org\", qop=\"auth\", algorithm=MD5-sess, nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", " + - "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true", true }; - - // These cases fail on WinHttpHandler because of a behavior in WinHttp that causes requests to be duplicated - // when the digest header has certain parameters. See https://github.com/dotnet/runtime/issues/25644 for details. - yield return new object[] { "Digest ", false }; - yield return new object[] { "Digest realm=\"testrealm\", nonce=\"testnonce\", algorithm=\"myown\"", false }; - - // These cases fail to authenticate on SocketsHttpHandler, but succeed on the other handlers. - // they are all invalid as per the RFC, so failing is the expected behavior. See https://github.com/dotnet/runtime/issues/25645 for details. - yield return new object[] { "Digest realm=withoutquotes, nonce=withoutquotes", false }; - yield return new object[] { "Digest realm=\"testrealm\" nonce=\"testnonce\"", false }; - yield return new object[] { "Digest realm=\"testrealm1\", nonce=\"testnonce1\" Digest realm=\"testrealm2\", nonce=\"testnonce2\"", false }; - - // These tests check that the algorithm parameter is treated in case insensitive way. - // WinHTTP only supports plain MD5, so other algorithms are included here. - yield return new object[] { $"Digest realm=\"testrealm\", algorithm=md5-Sess, nonce=\"testnonce\", qop=\"auth\"", true }; - yield return new object[] { $"Digest realm=\"testrealm\", algorithm=sha-256, nonce=\"testnonce\"", true }; - yield return new object[] { $"Digest realm=\"testrealm\", algorithm=sha-256-SESS, nonce=\"testnonce\", qop=\"auth\"", true }; - } } public sealed class SocketsHttpHandler_ConnectionUpgrade_Test : HttpClientHandlerTestBase @@ -1322,92 +1205,9 @@ await server.AcceptConnectionAsync(async connection => } } - public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandlerTestBase + public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandler_Connect_Test { public SocketsHttpHandler_Connect_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task ConnectMethod_Success() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClient client = CreateHttpClient()) - { - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion }; - request.Headers.Host = "foo.com:345"; - - // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. - Task responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - - await server.AcceptConnectionAsync(async connection => - { - // Verify that Host header exist and has same value and URI authority. - List lines = await connection.ReadRequestHeaderAsync().ConfigureAwait(false); - string authority = lines[0].Split()[1]; - foreach (string line in lines) - { - if (line.StartsWith("Host:",StringComparison.InvariantCultureIgnoreCase)) - { - Assert.Equal("Host: foo.com:345", line); - break; - } - } - - Task serverTask = connection.SendResponseAsync(HttpStatusCode.OK); - await TestHelper.WhenAllCompletedOrAnyFailed(responseTask, serverTask).ConfigureAwait(false); - - using (Stream clientStream = await (await responseTask).Content.ReadAsStreamAsync()) - { - Assert.True(clientStream.CanWrite); - Assert.True(clientStream.CanRead); - Assert.False(clientStream.CanSeek); - - TextReader clientReader = new StreamReader(clientStream); - TextWriter clientWriter = new StreamWriter(clientStream) { AutoFlush = true }; - TextWriter serverWriter = connection.Writer; - - const string helloServer = "hello server"; - const string helloClient = "hello client"; - const string goodbyeServer = "goodbye server"; - const string goodbyeClient = "goodbye client"; - - clientWriter.WriteLine(helloServer); - Assert.Equal(helloServer, connection.ReadLine()); - serverWriter.WriteLine(helloClient); - Assert.Equal(helloClient, clientReader.ReadLine()); - clientWriter.WriteLine(goodbyeServer); - Assert.Equal(goodbyeServer, connection.ReadLine()); - serverWriter.WriteLine(goodbyeClient); - Assert.Equal(goodbyeClient, clientReader.ReadLine()); - } - }); - } - }); - } - - [Fact] - public async Task ConnectMethod_Fails() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClient client = CreateHttpClient()) - { - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("CONNECT"), url) { Version = UseVersion }; - request.Headers.Host = "foo.com:345"; - // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. - Task responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - await server.AcceptConnectionAsync(async connection => - { - Task> serverTask = connection.ReadRequestHeaderAndSendResponseAsync(HttpStatusCode.Forbidden, content: "error"); - - await TestHelper.WhenAllCompletedOrAnyFailed(responseTask, serverTask); - HttpResponseMessage response = await responseTask; - - Assert.True(response.StatusCode == HttpStatusCode.Forbidden); - }); - } - }); - } } public sealed class SocketsHttpHandler_HttpClientHandler_ConnectionPooling_Test : HttpClientHandlerTestBase diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs new file mode 100644 index 0000000000000..a9930d48a0bff --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs @@ -0,0 +1,93 @@ +// 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 Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + public sealed class SyncHttpHandler_HttpProtocolTests : HttpProtocolTests + { + public SyncHttpHandler_HttpProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_HttpProtocolTests_Dribble : HttpProtocolTests_Dribble + { + public SyncHttpHandler_HttpProtocolTests_Dribble(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_DiagnosticsTest : DiagnosticsTest + { + public SyncHttpHandler_DiagnosticsTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_PostScenarioTest : PostScenarioTest + { + public SyncHttpHandler_PostScenarioTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest + { + public SyncHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandlerTest_AutoRedirect : HttpClientHandlerTest_AutoRedirect + { + public SyncHttpHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_IdnaProtocolTests : IdnaProtocolTests + { + public SyncHttpHandler_IdnaProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + protected override bool SupportsIdna => true; + } + + public sealed class SyncHttpHandler_HttpRetryProtocolTests : HttpRetryProtocolTests + { + public SyncHttpHandler_HttpRetryProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies + { + public SyncHttpHandlerTest_Cookies(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTest_Cookies_Http11 + { + public SyncHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Http11_Cancellation_Test + { + public SyncHttpHandler_HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_HttpClientHandler_Authentication_Test : HttpClientHandler_Authentication_Test + { + public SyncHttpHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandler_Connect_Test : HttpClientHandler_Connect_Test + { + public SyncHttpHandler_Connect_Test(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } + + public sealed class SyncHttpHandlerTest_HttpClientHandlerTest_Headers : HttpClientHandlerTest_Headers + { + public SyncHttpHandlerTest_HttpClientHandlerTest_Headers(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index d05f66c3ce992..688498d7d9c69 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -168,6 +168,8 @@ + + + +