From 1e11d423e2e2877b032092743916a8b2b6e9e692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 29 Jan 2020 19:45:57 +0100 Subject: [PATCH 01/60] Moved and fixed code from https://github.com/stephentoub/corefx/tree/socketshttphandlersync https://github.com/stephentoub/corefx/commit/0e4d6401e76c79f8ef81b5064322b8de18a115a2 Little compilation fix. Compilaris Test compilaris Fixed buffer sending not passing offset and count. --- .../tests/System/Net/Http/LoopbackServer.cs | 4 +- .../System.Net.Http/ref/System.Net.Http.cs | 5 + .../src/System.Net.Http.csproj | 1 + .../src/System/Net/Http/ByteArrayContent.cs | 6 + .../src/System/Net/Http/HttpContent.cs | 57 ++- .../src/System/Net/Http/MultipartContent.cs | 42 ++ .../System/Net/Http/ReadOnlyMemoryContent.cs | 3 + .../AuthenticationHelper.NtAuth.cs | 22 +- .../AuthenticationHelper.cs | 24 +- .../ChunkedEncodingReadStream.cs | 6 +- .../ChunkedEncodingWriteStream.cs | 34 +- .../Http/SocketsHttpHandler/ConnectHelper.cs | 41 +- .../ConnectionCloseReadStream.cs | 2 +- .../ContentLengthReadStream.cs | 4 +- .../ContentLengthWriteStream.cs | 14 +- .../Http/SocketsHttpHandler/CreditManager.cs | 31 ++ .../DecompressionHandler.cs | 20 +- .../SocketsHttpHandler/Http2Connection.cs | 8 +- .../Http/SocketsHttpHandler/Http2Stream.cs | 13 + .../HttpAuthenticatedConnectionHandler.cs | 17 +- .../Http/SocketsHttpHandler/HttpBaseStream.cs | 11 +- .../Http/SocketsHttpHandler/HttpConnection.cs | 369 ++++++++++-------- .../SocketsHttpHandler/HttpConnectionBase.cs | 2 +- .../HttpConnectionHandler.cs | 16 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 99 +++-- .../HttpConnectionPoolManager.cs | 20 +- .../HttpContentWriteStream.cs | 4 +- .../SocketsHttpHandler/RawConnectionStream.cs | 12 +- .../SocketsHttpHandler/RedirectHandler.cs | 25 +- .../SocketsHttpHandler/SocketsHttpHandler.cs | 39 +- .../SocketsHttpHandlerStage.cs | 16 + .../src/System/Net/Http/StreamContent.cs | 10 + .../src/System/Net/Http/StreamToStreamCopy.cs | 18 + .../FunctionalTests/SocketsHttpHandlerTest.cs | 23 ++ 34 files changed, 670 insertions(+), 348 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 2092e6e57ce3c..d8b57d4b23200 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -77,8 +77,8 @@ public static Task CreateClientAndServerAsync(Func clientFunc, Func { - Task clientTask = clientFunc(server.Uri); - Task serverTask = serverFunc(server); + Task clientTask = Task.Run(() => clientFunc(server.Uri)); + Task serverTask = Task.Run(() => serverFunc(server)); await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed().ConfigureAwait(false); }, options); 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 bca3746a9ff8c..e14e7e4fe7e2b 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -127,6 +127,8 @@ 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) { } + public void CopyTo(System.IO.Stream stream, System.Net.TransportContext context) { } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream) { throw null; } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken) { throw null; } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Net.TransportContext context) { throw null; } @@ -143,6 +145,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) { } 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); @@ -283,6 +286,8 @@ public SocketsHttpHandler() { } public bool UseCookies { get { throw null; } set { } } public bool UseProxy { get { throw null; } set { } } protected override void Dispose(bool disposing) { } + public System.Net.Http.HttpResponseMessage SendDirect(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Threading.Tasks.Task SendDirectAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default) { 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 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 455731cfd4aaa..0114b9d5afa06 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -41,6 +41,7 @@ + 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 c6e85c0db157d..775f432c6af4b 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 @@ -63,6 +63,12 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext c private protected Task SerializeToStreamAsyncCore(Stream stream, CancellationToken cancellationToken) => stream.WriteAsync(_content, _offset, _count, cancellationToken); + protected override void SerializeToStream(Stream stream, TransportContext context) + { + Debug.Assert(stream != null); + stream.Write(_content, _offset, _count); + } + protected internal override bool TryComputeLength(out long length) { length = _count; 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 41e08361dc4bb..d9a897d91372e 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 @@ -319,6 +319,14 @@ internal Stream TryReadAsStream() protected abstract Task SerializeToStreamAsync(Stream stream, TransportContext context); + protected virtual void SerializeToStream(Stream stream, TransportContext context) + { + // No choice but to do sync-over-async. Derived types should override this whenever possible. + // Thankfully most CreateContentReadStreamAsync implementations actually complete synchronously. + Stream source = CreateContentReadStreamAsync().GetAwaiter().GetResult(); + source.CopyTo(stream); + } + protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context); @@ -330,6 +338,34 @@ protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext co // 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) => + CopyTo(stream, null); + + public void CopyTo(Stream stream, TransportContext context) + { + 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); + } + } + catch (Exception e) when (StreamCopyExceptionNeedsWrapping(e)) + { + throw GetStreamCopyException(e); + } + } + public Task CopyToAsync(Stream stream) => CopyToAsync(stream, CancellationToken.None); @@ -349,8 +385,7 @@ public Task CopyToAsync(Stream stream, TransportContext context, CancellationTok try { - ArraySegment buffer; - if (TryGetBuffer(out buffer)) + if (TryGetBuffer(out ArraySegment buffer)) { return CopyToAsyncCore(stream.WriteAsync(new ReadOnlyMemory(buffer.Array, buffer.Offset, buffer.Count), cancellationToken)); } @@ -365,17 +400,17 @@ public Task CopyToAsync(Stream stream, TransportContext context, CancellationTok { return Task.FromException(GetStreamCopyException(e)); } - } - private static async Task CopyToAsyncCore(ValueTask copyTask) - { - try + static async Task CopyToAsyncCore(ValueTask copyTask) { - await copyTask.ConfigureAwait(false); - } - catch (Exception e) when (StreamCopyExceptionNeedsWrapping(e)) - { - throw WrapStreamCopyException(e); + try + { + await copyTask.ConfigureAwait(false); + } + catch (Exception e) when (StreamCopyExceptionNeedsWrapping(e)) + { + throw GetStreamCopyException(e); + } } } 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 0f832a73f7c24..7dc9b7c7f97ea 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 @@ -161,6 +161,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) + { + 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); + } + + // 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 @@ -293,6 +329,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 e73e0d007f056..112e6e17a49ce 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 @@ -27,6 +27,9 @@ public ReadOnlyMemoryContent(ReadOnlyMemory content) protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => stream.WriteAsync(_content).AsTask(); + protected override void SerializeToStream(Stream stream, TransportContext context) => + stream.Write(_content.Span); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) => stream.WriteAsync(_content, cancellationToken).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 b1d6e8b30884d..d4ddd38c706d7 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. @@ -64,7 +64,7 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe if (response.Headers.ConnectionClose.GetValueOrDefault()) { // Server is closing the connection and asking us to authenticate on a new connection. - (connection, response) = await connectionPool.CreateHttp11ConnectionAsync(request, cancellationToken).ConfigureAwait(false); + (connection, response) = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); if (response != null) { return response; @@ -140,7 +140,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; @@ -167,14 +167,14 @@ 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) { - 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 b6855e39ba2b4..dcdb575a19c89 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 @@ -203,14 +203,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. @@ -237,7 +237,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)) { @@ -248,7 +248,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)) @@ -258,7 +258,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); } } } @@ -276,7 +276,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) { @@ -325,14 +325,14 @@ 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) { - 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 32cba801895c7..586621c94a815 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 @@ -169,7 +169,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. @@ -223,7 +223,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)) @@ -471,7 +471,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 9d466cc7f6104..db242768e50e9 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 @@ -33,8 +33,32 @@ public CertificateCallbackMapper(Func ConnectAsync(string host, int port, CancellationToken cancellationToken) + public static async ValueTask ConnectAsync(string host, int port, bool async, CancellationToken cancellationToken) { + // For synchronous connections, we can just create a socket and make the connection. + // (For async, we need to do more gymnastics with SocketAsyncEventArgs.) + if (!async) + { + cancellationToken.ThrowIfCancellationRequested(); + Socket socket = null; + try + { + socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; + using (cancellationToken.UnsafeRegister(s => ((Socket)s).Dispose(), socket)) + { + socket.Connect(new DnsEndPoint(host, port)); + } + } + catch + { + socket?.Dispose(); + throw; + } + + return new NetworkStream(socket, ownsSocket: true); + } + // Rather than creating a new Socket and calling ConnectAsync on it, we use the static // Socket.ConnectAsync with a SocketAsyncEventArgs, as we can then use Socket.CancelConnectAsync // to cancel it if needed. @@ -126,7 +150,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). @@ -141,16 +165,23 @@ 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 + { + sslStream.AuthenticateAsClient(sslOptions.TargetHost, sslOptions.ClientCertificates, sslOptions.EnabledSslProtocols, sslOptions.CertificateRevocationCheckMode == X509RevocationMode.Online); + } } 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 262ab203e6188..3f2f5edf135c4 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 @@ -104,7 +104,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 7aba584ba448d..4b96d31afa43f 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 @@ -132,7 +132,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(); @@ -220,7 +220,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/CreditManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs index f1bbf346a22fc..15ce033bfb653 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs @@ -40,6 +40,37 @@ private object SyncObject } } + public int RequestCredit(int amount) + { + Waiter waiter; + + lock (SyncObject) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(CreditManager)); + } + + if (_current > 0) + { + Debug.Assert(_waiters == null || _waiters.Count == 0, "Shouldn't have waiters when credit is available"); + + int granted = Math.Min(amount, _current); + if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, current={_current}, granted={granted}"); + _current -= granted; + return granted; + } + + if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, no credit available."); + + waiter = new Waiter { Amount = amount }; + (_waiters ??= new Queue()).Enqueue(waiter); + } + + // TODO: This is sync-over-async... don't see a good way around this. + return waiter.Task.GetAwaiter().GetResult(); + } + public ValueTask RequestCreditAsync(int amount, CancellationToken cancellationToken) { lock (SyncObject) 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 1a4e8a669c012..cd2b396874ca8 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 : SocketsHttpHandlerStage { - private readonly HttpMessageHandler _innerHandler; + private readonly SocketsHttpHandlerStage _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, SocketsHttpHandlerStage 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); ICollection contentEncodings = response.Content.Headers.ContentEncoding; if (contentEncodings.Count > 0) @@ -82,15 +82,7 @@ protected internal override async Task SendAsync(HttpReques return response; } - protected override void Dispose(bool disposing) - { - if (disposing) - { - _innerHandler.Dispose(); - } - - base.Dispose(disposing); - } + public override void Dispose() => _innerHandler.Dispose(); private abstract class DecompressedContent : HttpContent { 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 7a985da7ad76f..647f816f7e5a2 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 @@ -1641,9 +1641,15 @@ private enum FlushTiming Now } + // TODO: Can we do better for async=false? Right now the caller will block waiting for this to complete. + // But this is also sharing the connection with many other requests, which makes it challenging. At a minimum + // a request may need to block while waiting for other threads to finish their work: the question is just + // whether we could make it so that at least one of the request's threads is not blocked, which would + // mean that the connection processing loop would need to be entirely restructured. + // 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/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index dd0af4b3a675c..8019877cd9025 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -1204,6 +1204,19 @@ protected override void Dispose(bool disposing) public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) + { + ValidateBufferArgs(buffer, offset, count); + Http2Stream http2Stream = _http2Stream; + if (http2Stream == null) + { + throw new ObjectDisposedException(nameof(Http2WriteStream)); + } + + // Sync-over-async. See comment on Http2Connection.SendAsync. + http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), default).GetAwaiter().GetResult(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { Http2Stream http2Stream = _http2Stream; 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..d746bdc565b9b 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 : SocketsHttpHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -16,20 +16,11 @@ 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) - { - if (disposing) - { - _poolManager.Dispose(); - } - - base.Dispose(disposing); - } + public override void Dispose() => _poolManager.Dispose(); } - } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs index db0398d120ffa..1a9082190d0a8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpBaseStream.cs @@ -99,11 +99,8 @@ public sealed override Task ReadAsync(byte[] buffer, int offset, int count, public override void Write(byte[] buffer, int offset, int count) { - // This does sync-over-async, but it also should only end up being used in strange - // situations. Either a derived stream overrides this anyway, so the implementation won't be used, - // or it's being called as part of HttpContent.SerializeToStreamAsync, which means custom - // content is explicitly choosing to make a synchronous call as part of an asynchronous method. - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + ValidateBufferArgs(buffer, offset, count); + Write(new ReadOnlySpan(buffer, offset, count)); } public sealed override void WriteByte(byte value) => @@ -130,5 +127,9 @@ protected static Task NopAsync(CancellationToken cancellationToken) => public abstract override int Read(Span buffer); public abstract override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken); public abstract override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken); + + // Either Write(byte[], int, int) or Write(ReadOnlySpan) should also be overridden. If the former is overridden, + // the default implementation of the latter will call the former; if the latter is overridden, this type's override + // of the former will call the latter. If neither is overridden, stack overflows will result. } } 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 bc6502d9cd766..482c6ac15b23c 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 @@ -192,7 +192,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) { @@ -200,24 +200,24 @@ private async ValueTask WriteHeadersAsync(HttpHeaders headers, string cookiesFro { 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; } @@ -234,33 +234,33 @@ private async ValueTask WriteHeadersAsync(HttpHeaders headers, string cookiesFro 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 { @@ -271,26 +271,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)) @@ -300,10 +300,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')) @@ -313,10 +313,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; @@ -335,8 +335,8 @@ public async Task SendAsyncCore(HttpRequestMessage request, try { // 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)) { @@ -346,7 +346,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 { @@ -354,35 +354,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/corefx/issues/28863: // 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; @@ -399,13 +399,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) @@ -414,22 +414,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 { @@ -441,14 +441,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. @@ -474,7 +474,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 asyc==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) @@ -493,7 +502,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, @@ -525,16 +534,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; @@ -586,7 +595,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. @@ -713,10 +729,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) { @@ -750,19 +764,27 @@ 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 + { + cancellationToken.ThrowIfCancellationRequested(); + request.Content.CopyTo(stream, _transportContext); + } // 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."); } @@ -772,7 +794,7 @@ private async Task SendRequestContentWithExpect100ContinueAsync( { // 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. - bool sendRequestContent = await allowExpect100ToContinueTask.ConfigureAwait(false); + bool sendRequestContent = await allowExpect100ToContinueTask.ConfigureAwait(false); // TODO: decide what to do about this // Clean up the timer; it's no longer needed. expect100Timer.Dispose(); @@ -781,7 +803,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: true, cancellationToken).ConfigureAwait(false); // TODO: is async: true correct here? } else { @@ -963,7 +985,38 @@ private void WriteToBuffer(ReadOnlyMemory source) _writeOffset += source.Length; } - private async ValueTask WriteAsync(ReadOnlyMemory source) + private void Write(ReadOnlySpan source) + { + 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); + Flush(); + } + + if (source.Length >= _writeBuffer.Length) + { + // Large write. No sense buffering this. Write directly to stream. + WriteToStream(source); + } + else + { + // Copy remainder into buffer + WriteToBuffer(source); + } + } + + private async ValueTask WriteAsync(ReadOnlyMemory source, bool async) { int remaining = _writeBuffer.Length - _writeOffset; @@ -979,13 +1032,13 @@ 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); + await FlushAsync(async).ConfigureAwait(false); } if (source.Length >= _writeBuffer.Length) { // Large write. No sense buffering this. Write directly to stream. - await WriteToStreamAsync(source).ConfigureAwait(false); + await WriteToStreamAsync(source, async).ConfigureAwait(false); } else { @@ -1018,13 +1071,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; @@ -1035,40 +1088,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) { @@ -1077,16 +1130,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) { @@ -1094,10 +1147,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) @@ -1116,13 +1169,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. @@ -1144,10 +1197,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; @@ -1164,10 +1217,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++) { @@ -1176,7 +1229,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); } } @@ -1189,11 +1242,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; } @@ -1206,10 +1259,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) @@ -1236,7 +1298,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) @@ -1273,7 +1335,7 @@ private async ValueTask> ReadNextResponseHeaderLineAsync(bool previouslyScannedBytes = backPos - _readOffset; _allowedReadLineBytes -= backPos - scanOffset; ThrowIfExceededAllowedReadLineBytes(); - await FillAsync().ConfigureAwait(false); + await FillAsync(async).ConfigureAwait(false); continue; } @@ -1323,7 +1385,7 @@ private async ValueTask> ReadNextResponseHeaderLineAsync(bool previouslyScannedBytes = _readLength - _readOffset; _allowedReadLineBytes -= _readLength - scanOffset; ThrowIfExceededAllowedReadLineBytes(); - await FillAsync().ConfigureAwait(false); + await FillAsync(async).ConfigureAwait(false); } } @@ -1335,54 +1397,10 @@ 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; - } + private void Fill() => FillAsync(async: false).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); @@ -1417,7 +1435,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) @@ -1575,38 +1595,65 @@ 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 + { + cancellationToken.ThrowIfCancellationRequested(); + 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); + } + + cancellationToken.ThrowIfCancellationRequested(); + _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 + { + cancellationToken.ThrowIfCancellationRequested(); + _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); @@ -1619,7 +1666,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) @@ -1642,10 +1689,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 51d6ad0f395e2..45b0260a02e41 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 @@ -12,7 +12,7 @@ namespace System.Net.Http { internal abstract class HttpConnectionBase : IHttpTrace { - 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..b9232142534d3 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 : SocketsHttpHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -16,19 +16,11 @@ 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) - { - if (disposing) - { - _poolManager.Dispose(); - } - - base.Dispose(disposing); - } + public override void Dispose() => _poolManager.Dispose(); } } 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 4b9bdb7dde93e..a69ed03b9169c 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 @@ -225,17 +225,17 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection 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 (_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) { @@ -294,12 +294,27 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation } 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 @@ -311,13 +326,15 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation // 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); @@ -328,7 +345,7 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation 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); @@ -344,7 +361,7 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation } 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); @@ -413,7 +430,7 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation 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); @@ -511,16 +528,16 @@ private ValueTask GetOrReserveHttp11ConnectionAsync(Cancellation } // 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); } - 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 @@ -533,11 +550,11 @@ public async Task SendWithRetryAsync(HttpRequestMessage req { if (connection is HttpConnection) { - return await SendWithNtConnectionAuthAsync((HttpConnection)connection, request, doRequestAuth, cancellationToken).ConfigureAwait(false); + return await SendWithNtConnectionAuthAsync((HttpConnection)connection, request, async, doRequestAuth, cancellationToken).ConfigureAwait(false); } else { - return await connection.SendAsync(request, cancellationToken).ConfigureAwait(false); + return await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false); } } catch (HttpRequestException e) when (!isNewConnection && e.AllowRetry == RequestRetryType.RetryOnSameOrNextProxy) @@ -552,17 +569,17 @@ public async Task SendWithRetryAsync(HttpRequestMessage req } } - 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 { @@ -570,39 +587,39 @@ 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 // when either the original token is canceled or a connect timeout occurs. @@ -622,17 +639,17 @@ public Task SendAsync(HttpRequestMessage request, bool doRe case HttpConnectionKind.Http: case HttpConnectionKind.Https: case HttpConnectionKind.ProxyConnect: - stream = await ConnectHelper.ConnectAsync(_host, _port, cancellationToken).ConfigureAwait(false); + stream = await ConnectHelper.ConnectAsync(_host, _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. @@ -647,7 +664,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; } @@ -660,10 +677,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) { @@ -681,7 +698,7 @@ private HttpConnection ConstructHttp11Connection(Socket socket, Stream stream, T } // 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) { // Send a CONNECT request to the proxy server to establish a tunnel. HttpRequestMessage tunnelRequest = new HttpRequestMessage(HttpMethod.Connect, _proxyUri); @@ -692,7 +709,7 @@ private HttpConnection ConstructHttp11Connection(Socket socket, Stream stream, T 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 ffdf082f6d532..d924ea32af003 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 @@ -223,7 +223,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri proxy } } - 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); @@ -264,19 +264,19 @@ public Task SendAsyncCore(HttpRequestMessage request, Uri p // 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) { if (_proxy == null) { - return SendAsyncCore(request, null, doRequestAuth, isProxyConnect: false, cancellationToken); + return SendAsyncCore(request, null, async, doRequestAuth, isProxyConnect: false, cancellationToken); } // Do proxy lookup. @@ -291,7 +291,7 @@ public Task SendAsync(HttpRequestMessage request, bool doRe 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 @@ -312,7 +312,7 @@ public Task SendAsync(HttpRequestMessage request, bool doRe 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); } /// @@ -320,7 +320,7 @@ public Task SendAsync(HttpRequestMessage request, bool doRe /// /// The set of proxies to use. /// The first proxy try. - 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 = null; @@ -328,7 +328,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/HttpContentWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs index 3c53400ac2751..ab5b66747ef06 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/RawConnectionStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs index 05ec6f462b40a..293d6dbb4d218 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 @@ -104,7 +104,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); @@ -146,12 +146,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; @@ -184,7 +178,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)); @@ -205,7 +199,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 4ee923de6c080..89ee5a50acb2b 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 partial class RedirectHandler : SocketsHttpHandlerStage { - private readonly HttpMessageHandler _initialInnerHandler; // Used for initial request - private readonly HttpMessageHandler _redirectInnerHandler; // Used for redirects; this allows disabling auth + private readonly SocketsHttpHandlerStage _initialInnerHandler; // Used for initial request + private readonly SocketsHttpHandlerStage _redirectInnerHandler; // Used for redirects; this allows disabling auth private readonly int _maxAutomaticRedirections; - public RedirectHandler(int maxAutomaticRedirections, HttpMessageHandler initialInnerHandler, HttpMessageHandler redirectInnerHandler) + public RedirectHandler(int maxAutomaticRedirections, SocketsHttpHandlerStage initialInnerHandler, SocketsHttpHandlerStage 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; @@ -75,7 +75,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); @@ -151,15 +151,10 @@ private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMetho } } - protected override void Dispose(bool disposing) + public override void Dispose() { - if (disposing) - { - _initialInnerHandler.Dispose(); - _redirectInnerHandler.Dispose(); - } - - base.Dispose(disposing); + _initialInnerHandler.Dispose(); + _redirectInnerHandler.Dispose(); } internal void Trace(string message, int requestId, [CallerMemberName] string memberName = null) => 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 161d8b40fffdb..145c721ccb073 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; @@ -12,7 +13,7 @@ namespace System.Net.Http public sealed class SocketsHttpHandler : HttpMessageHandler { private readonly HttpConnectionSettings _settings = new HttpConnectionSettings(); - private HttpMessageHandler _handler; + private SocketsHttpHandlerStage _handler; private bool _disposed; private void CheckDisposed() @@ -284,7 +285,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private HttpMessageHandler SetupHandlerChain() + private SocketsHttpHandlerStage 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.) @@ -292,7 +293,7 @@ private HttpMessageHandler SetupHandlerChain() HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings); - HttpMessageHandler handler; + SocketsHttpHandlerStage handler; if (settings._credentials == null) { @@ -308,7 +309,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 = + SocketsHttpHandlerStage redirectHandler = (settings._credentials == null || settings._credentials is CredentialCache) ? handler : new HttpConnectionHandler(poolManager); // will not authenticate @@ -330,11 +331,30 @@ private HttpMessageHandler SetupHandlerChain() return _handler; } - protected internal override Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + // TODO: The good Send/SendAsync names are taken by the base protected internal methods, and we + // can't change visibility as part of overriding. What names should we use instead? + // SendDirect{Async}? Invoke{Async}? + + public HttpResponseMessage SendDirect(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + CheckDisposed(); + SocketsHttpHandlerStage handler = _handler ?? SetupHandlerChain(); + + Exception error = ValidateAndNormalizeRequest(request); + if (error != null) + { + throw error; + } + + ValueTask response = handler.SendAsync(request, async: false, cancellationToken); + Debug.Assert(response.IsCompleted); + return response.AsTask().GetAwaiter().GetResult(); + } + + public Task SendDirectAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { CheckDisposed(); - HttpMessageHandler handler = _handler ?? SetupHandlerChain(); + SocketsHttpHandlerStage handler = _handler ?? SetupHandlerChain(); Exception error = ValidateAndNormalizeRequest(request); if (error != null) @@ -342,9 +362,12 @@ protected internal override Task SendAsync( return Task.FromException(error); } - return handler.SendAsync(request, cancellationToken); + return handler.SendAsync(request, async: true, cancellationToken).AsTask(); } + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + SendDirectAsync(request, cancellationToken); + private Exception ValidateAndNormalizeRequest(HttpRequestMessage request) { if (request.Version.Major == 0) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs new file mode 100644 index 0000000000000..39f1991c31634 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs @@ -0,0 +1,16 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + internal abstract class SocketsHttpHandlerStage : IDisposable + { + internal abstract ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); + + public abstract void Dispose(); + } +} 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 b0e6852e66507..bb4e6649285f2 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 @@ -52,6 +52,16 @@ private void InitializeContent(Stream content, int bufferSize) if (NetEventSource.IsEnabled) NetEventSource.Associate(this, content); } + protected override void SerializeToStream(Stream stream, TransportContext context) + { + 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..95fd4d30b857f 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,24 @@ 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); + + 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/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 69581052b9e6f..0c8dfbae1351f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -1302,6 +1302,29 @@ await server.AcceptConnectionAsync(async connection => } } + public sealed class SocketsHttpHandler_Sync_Test : HttpClientHandlerTestBase + { + public SocketsHttpHandler_Sync_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task SendDirect_Get_StaysOnSameThread() + { + string content = new string(Enumerable.Range(0, 1000000).Select(i => (char)('a' + (i % 26))).ToArray()); + await LoopbackServer.CreateClientAndServerAsync(uri => + { + using (var handler = new SocketsHttpHandler()) + using (HttpResponseMessage response = handler.SendDirect(new HttpRequestMessage(HttpMethod.Get, uri))) + using (Stream responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + { + var ms = new MemoryStream(); + responseStream.CopyTo(ms); + Assert.Equal(content, Encoding.ASCII.GetString(ms.ToArray())); + } + return Task.CompletedTask; + }, server => server.AcceptConnectionSendResponseAndCloseAsync(content: content)); + } + } + public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandlerTestBase { public SocketsHttpHandler_Connect_Test(ITestOutputHelper output) : base(output) { } From 8a96eab7acc3b68b783433f3eec4b2f7f0d472ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 4 Feb 2020 18:46:39 +0100 Subject: [PATCH 02/60] Sync on HttpMessageInvoker instead of SocketHandler --- .../System.Net.Http/ref/System.Net.Http.cs | 20 +++++--- .../src/System.Net.Http.csproj | 2 +- .../src/System/Net/Http/DelegatingHandler.cs | 10 ++++ .../src/System/Net/Http/DiagnosticsHandler.cs | 46 +++++++++++++++---- .../src/System/Net/Http/HttpClientHandler.cs | 7 +++ .../src/System/Net/Http/HttpMessageHandler.cs | 9 ++++ .../src/System/Net/Http/HttpMessageInvoker.cs | 17 +++++++ .../Net/Http/MessageProcessingHandler.cs | 14 ++++++ .../DecompressionHandler.cs | 16 +++++-- .../HttpAuthenticatedConnectionHandler.cs | 12 ++++- .../HttpConnectionHandler.cs | 12 ++++- .../HttpMessageHandlerStage.cs | 18 ++++++++ .../SocketsHttpHandler/RedirectHandler.cs | 19 +++++--- .../SocketsHttpHandler/SocketsHttpHandler.cs | 29 ++++-------- .../SocketsHttpHandlerStage.cs | 16 ------- .../FunctionalTests/SocketsHttpHandlerTest.cs | 23 ---------- 16 files changed, 180 insertions(+), 90 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs delete mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs 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 e14e7e4fe7e2b..6fe72aedfb314 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) { } 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; } @@ -27,6 +28,7 @@ protected DelegatingHandler() { } protected DelegatingHandler(System.Net.Http.HttpMessageHandler innerHandler) { } 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) { 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 @@ -116,6 +118,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) { throw null; } protected internal override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } } public enum HttpCompletionOption @@ -130,9 +133,9 @@ protected HttpContent() { } public void CopyTo(System.IO.Stream stream) { } public void CopyTo(System.IO.Stream stream, System.Net.TransportContext context) { } public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream) { throw null; } - public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken) { 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; } + public System.Threading.Tasks.Task CopyToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken) { throw null; } protected virtual System.Threading.Tasks.Task CreateContentReadStreamAsync() { throw null; } protected virtual System.Threading.Tasks.Task CreateContentReadStreamAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public void Dispose() { } @@ -155,6 +158,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) { 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 @@ -163,6 +167,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) { 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 @@ -228,6 +233,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) { 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 @@ -240,6 +246,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) { } 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; } @@ -258,6 +265,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) { } 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; } @@ -286,8 +294,7 @@ public SocketsHttpHandler() { } public bool UseCookies { get { throw null; } set { } } public bool UseProxy { get { throw null; } set { } } protected override void Dispose(bool disposing) { } - public System.Net.Http.HttpResponseMessage SendDirect(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default) { throw null; } - public System.Threading.Tasks.Task SendDirectAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default) { throw null; } + protected internal override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request) { 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 @@ -296,6 +303,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) { } 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; } @@ -546,9 +554,9 @@ public NameValueHeaderValue(string name, string value) { } } public partial class NameValueWithParametersHeaderValue : System.Net.Http.Headers.NameValueHeaderValue, System.ICloneable { - protected NameValueWithParametersHeaderValue(System.Net.Http.Headers.NameValueWithParametersHeaderValue source) : base (default(System.Net.Http.Headers.NameValueHeaderValue)) { } - public NameValueWithParametersHeaderValue(string name) : base (default(System.Net.Http.Headers.NameValueHeaderValue)) { } - public NameValueWithParametersHeaderValue(string name, string value) : base (default(System.Net.Http.Headers.NameValueHeaderValue)) { } + protected NameValueWithParametersHeaderValue(System.Net.Http.Headers.NameValueWithParametersHeaderValue source) : base (default(string)) { } + public NameValueWithParametersHeaderValue(string name) : base (default(string)) { } + public NameValueWithParametersHeaderValue(string name, string value) : base (default(string)) { } public System.Collections.Generic.ICollection Parameters { get { throw null; } } public override bool Equals(object obj) { throw null; } public override int GetHashCode() { throw null; } 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 0114b9d5afa06..04767fe31e3c5 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -41,7 +41,7 @@ - + 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 eb3651b5f7696..889f530b36e8a 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 @@ -44,6 +44,16 @@ protected DelegatingHandler(HttpMessageHandler innerHandler) InnerHandler = innerHandler; } + protected internal override HttpResponseMessage Send(HttpRequestMessage request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); + } + SetOperationStarted(); + return _innerHandler.Send(request); + } + 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 682e91c7ecbd8..07b51927cab46 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,16 @@ 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) => + SendAsyncCore(request, false, default).GetAwaiter().GetResult(); + + protected internal override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) => + SendAsyncCore(request, 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 +66,14 @@ protected internal override async Task SendAsync(HttpReques try { - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (async) + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + else + { + return base.Send(request); + } } finally { @@ -98,12 +114,22 @@ protected internal override async Task SendAsync(HttpReques InjectHeaders(currentActivity, request); } - Task responseTask = null; + HttpResponseMessage response = null; + TaskStatus taskStatus = TaskStatus.Faulted; try { - responseTask = base.SendAsync(request, cancellationToken); - - return await responseTask.ConfigureAwait(false); + if (async) + { + Task responseTask = base.SendAsync(request, cancellationToken); + response = await responseTask.ConfigureAwait(false); + taskStatus = responseTask.Status; + } + else + { + response = base.Send(request); + taskStatus = TaskStatus.RanToCompletion; + } + return response; } catch (OperationCanceledException) { @@ -127,12 +153,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 +166,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/HttpClientHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs index 280b506bc113d..8dad6b2ee4218 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 @@ -214,6 +214,13 @@ public SslProtocols SslProtocols public IDictionary Properties => _socketsHttpHandler.Properties; + protected internal override HttpResponseMessage Send(HttpRequestMessage request) + { + return DiagnosticsHandler.IsEnabled() ? + _diagnosticsHandler.Send(request) : + _socketsHttpHandler.Send(request); + } + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 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..2182ab7e96deb 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,15 @@ 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 have no other option that to do sync-over-async. + protected internal virtual HttpResponseMessage Send(HttpRequestMessage request) + { + if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Doing sync-over-async due to lack of {nameof(Send)} override"); + return SendAsync(request, default).GetAwaiter().GetResult(); + } + 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..07acf0440596e 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,23 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) if (NetEventSource.IsEnabled) NetEventSource.Exit(this); } + public virtual HttpResponseMessage Send(HttpRequestMessage request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + CheckDisposed(); + + if (NetEventSource.IsEnabled) NetEventSource.Enter(this, request); + + HttpResponseMessage response = _handler.Send(request); + + 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 55db3c6ce4b44..0928d6acb466f 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,20 @@ protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken); + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request) + { + 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, default); + HttpResponseMessage response = base.Send(newRequestMessage); + HttpResponseMessage newResponseMessage = ProcessResponse(response, default); + return newResponseMessage; + } + protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 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 cd2b396874ca8..c495ef7819958 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 : SocketsHttpHandlerStage + internal sealed class DecompressionHandler : HttpMessageHandlerStage { - private readonly SocketsHttpHandlerStage _innerHandler; + private readonly HttpMessageHandlerStage _innerHandler; private readonly DecompressionMethods _decompressionMethods; private const string Gzip = "gzip"; @@ -24,7 +24,7 @@ internal sealed class DecompressionHandler : SocketsHttpHandlerStage private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new StringWithQualityHeaderValue(Deflate); private static readonly StringWithQualityHeaderValue s_brotliHeaderValue = new StringWithQualityHeaderValue(Brotli); - public DecompressionHandler(DecompressionMethods decompressionMethods, SocketsHttpHandlerStage innerHandler) + public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandlerStage innerHandler) { Debug.Assert(decompressionMethods != DecompressionMethods.None); Debug.Assert(innerHandler != null); @@ -82,7 +82,15 @@ internal override async ValueTask SendAsync(HttpRequestMess return response; } - public override void Dispose() => _innerHandler.Dispose(); + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerHandler.Dispose(); + } + + base.Dispose(disposing); + } private abstract class DecompressedContent : HttpContent { 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 d746bdc565b9b..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 : SocketsHttpHandlerStage + internal sealed class HttpAuthenticatedConnectionHandler : HttpMessageHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -21,6 +21,14 @@ internal override ValueTask SendAsync(HttpRequestMessage re return _poolManager.SendAsync(request, async, doRequestAuth: true, cancellationToken); } - public override void Dispose() => _poolManager.Dispose(); + protected override void Dispose(bool disposing) + { + if (disposing) + { + _poolManager.Dispose(); + } + + base.Dispose(disposing); + } } } 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 b9232142534d3..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 : SocketsHttpHandlerStage + internal sealed class HttpConnectionHandler : HttpMessageHandlerStage { private readonly HttpConnectionPoolManager _poolManager; @@ -21,6 +21,14 @@ internal override ValueTask SendAsync(HttpRequestMessage re return _poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken); } - public override void Dispose() => _poolManager.Dispose(); + protected override void Dispose(bool disposing) + { + if (disposing) + { + _poolManager.Dispose(); + } + + base.Dispose(disposing); + } } } 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..510f337b32f60 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + internal abstract class HttpMessageHandlerStage : HttpMessageHandler + { + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request) => + SendAsync(request, false, default).GetAwaiter().GetResult(); + + protected internal sealed override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) => + SendAsync(request, 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/RedirectHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs index 89ee5a50acb2b..ea4a13ef04338 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 : SocketsHttpHandlerStage + internal sealed class RedirectHandler : HttpMessageHandlerStage { - private readonly SocketsHttpHandlerStage _initialInnerHandler; // Used for initial request - private readonly SocketsHttpHandlerStage _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, SocketsHttpHandlerStage initialInnerHandler, SocketsHttpHandlerStage redirectInnerHandler) + public RedirectHandler(int maxAutomaticRedirections, HttpMessageHandlerStage initialInnerHandler, HttpMessageHandlerStage redirectInnerHandler) { Debug.Assert(initialInnerHandler != null); Debug.Assert(redirectInnerHandler != null); @@ -151,10 +151,15 @@ private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMetho } } - public override void Dispose() + protected override void Dispose(bool disposing) { - _initialInnerHandler.Dispose(); - _redirectInnerHandler.Dispose(); + if (disposing) + { + _initialInnerHandler.Dispose(); + _redirectInnerHandler.Dispose(); + } + + base.Dispose(disposing); } internal void Trace(string message, int requestId, [CallerMemberName] string memberName = null) => 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 145c721ccb073..3b63ee2a2e8fd 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 @@ -13,7 +13,7 @@ namespace System.Net.Http public sealed class SocketsHttpHandler : HttpMessageHandler { private readonly HttpConnectionSettings _settings = new HttpConnectionSettings(); - private SocketsHttpHandlerStage _handler; + private HttpMessageHandlerStage _handler; private bool _disposed; private void CheckDisposed() @@ -285,7 +285,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private SocketsHttpHandlerStage 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.) @@ -293,7 +293,7 @@ private SocketsHttpHandlerStage SetupHandlerChain() HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings); - SocketsHttpHandlerStage handler; + HttpMessageHandlerStage handler; if (settings._credentials == null) { @@ -309,7 +309,7 @@ private SocketsHttpHandlerStage 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. - SocketsHttpHandlerStage redirectHandler = + HttpMessageHandlerStage redirectHandler = (settings._credentials == null || settings._credentials is CredentialCache) ? handler : new HttpConnectionHandler(poolManager); // will not authenticate @@ -331,14 +331,10 @@ private SocketsHttpHandlerStage SetupHandlerChain() return _handler; } - // TODO: The good Send/SendAsync names are taken by the base protected internal methods, and we - // can't change visibility as part of overriding. What names should we use instead? - // SendDirect{Async}? Invoke{Async}? - - public HttpResponseMessage SendDirect(HttpRequestMessage request, CancellationToken cancellationToken = default) + protected internal override HttpResponseMessage Send(HttpRequestMessage request) { CheckDisposed(); - SocketsHttpHandlerStage handler = _handler ?? SetupHandlerChain(); + HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain(); Exception error = ValidateAndNormalizeRequest(request); if (error != null) @@ -346,15 +342,13 @@ public HttpResponseMessage SendDirect(HttpRequestMessage request, CancellationTo throw error; } - ValueTask response = handler.SendAsync(request, async: false, cancellationToken); - Debug.Assert(response.IsCompleted); - return response.AsTask().GetAwaiter().GetResult(); + return handler.Send(request); } - public Task SendDirectAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { CheckDisposed(); - SocketsHttpHandlerStage handler = _handler ?? SetupHandlerChain(); + HttpMessageHandler handler = _handler ?? SetupHandlerChain(); Exception error = ValidateAndNormalizeRequest(request); if (error != null) @@ -362,12 +356,9 @@ public Task SendDirectAsync(HttpRequestMessage request, Can return Task.FromException(error); } - return handler.SendAsync(request, async: true, cancellationToken).AsTask(); + return handler.SendAsync(request, cancellationToken); } - protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => - SendDirectAsync(request, cancellationToken); - private Exception ValidateAndNormalizeRequest(HttpRequestMessage request) { if (request.Version.Major == 0) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs deleted file mode 100644 index 39f1991c31634..0000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandlerStage.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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.Threading; -using System.Threading.Tasks; - -namespace System.Net.Http -{ - internal abstract class SocketsHttpHandlerStage : IDisposable - { - internal abstract ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); - - public abstract void Dispose(); - } -} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 0c8dfbae1351f..69581052b9e6f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -1302,29 +1302,6 @@ await server.AcceptConnectionAsync(async connection => } } - public sealed class SocketsHttpHandler_Sync_Test : HttpClientHandlerTestBase - { - public SocketsHttpHandler_Sync_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task SendDirect_Get_StaysOnSameThread() - { - string content = new string(Enumerable.Range(0, 1000000).Select(i => (char)('a' + (i % 26))).ToArray()); - await LoopbackServer.CreateClientAndServerAsync(uri => - { - using (var handler = new SocketsHttpHandler()) - using (HttpResponseMessage response = handler.SendDirect(new HttpRequestMessage(HttpMethod.Get, uri))) - using (Stream responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) - { - var ms = new MemoryStream(); - responseStream.CopyTo(ms); - Assert.Equal(content, Encoding.ASCII.GetString(ms.ToArray())); - } - return Task.CompletedTask; - }, server => server.AcceptConnectionSendResponseAndCloseAsync(content: content)); - } - } - public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandlerTestBase { public SocketsHttpHandler_Connect_Test(ITestOutputHelper output) : base(output) { } From 8b9baeb34d8a645798503cbb87828ae43d156aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 6 Feb 2020 15:02:23 +0100 Subject: [PATCH 03/60] Fix after merge --- .../src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 afa94e542f128..408d73dffd38f 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 @@ -139,7 +139,7 @@ private void CheckForShutdown() } } - public override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public override Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { Task waitTask = WaitForAvailableRequestStreamAsync(cancellationToken); From 3eeff26ce4b5eea50734db8fd714346161f5825b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 7 Feb 2020 11:56:22 +0100 Subject: [PATCH 04/60] Sync HttpClient.Send, including timeout and cancellation --- .../System.Net.Http/ref/System.Net.Http.cs | 29 +++-- .../src/System/Net/Http/ByteArrayContent.cs | 5 +- .../src/System/Net/Http/DelegatingHandler.cs | 4 +- .../src/System/Net/Http/DiagnosticsHandler.cs | 9 +- .../src/System/Net/Http/HttpClient.cs | 118 +++++++++++++++--- .../src/System/Net/Http/HttpClientHandler.cs | 7 +- .../src/System/Net/Http/HttpContent.cs | 64 ++++++++-- .../src/System/Net/Http/HttpMessageHandler.cs | 4 +- .../src/System/Net/Http/HttpMessageInvoker.cs | 5 +- .../Net/Http/MessageProcessingHandler.cs | 5 +- .../src/System/Net/Http/MultipartContent.cs | 6 +- .../System/Net/Http/ReadOnlyMemoryContent.cs | 6 +- .../Http/SocketsHttpHandler/HttpConnection.cs | 2 +- .../HttpMessageHandlerStage.cs | 5 +- .../SocketsHttpHandler/SocketsHttpHandler.cs | 5 +- .../src/System/Net/Http/StreamContent.cs | 6 +- 16 files changed, 212 insertions(+), 68 deletions(-) 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 6fe72aedfb314..31f123323fde2 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -12,7 +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) { } + 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,7 +28,7 @@ protected DelegatingHandler() { } protected DelegatingHandler(System.Net.Http.HttpMessageHandler innerHandler) { } 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) { throw null; } + 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 @@ -85,6 +85,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; } @@ -118,7 +122,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) { throw null; } + 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 @@ -130,8 +134,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) { } - public void CopyTo(System.IO.Stream stream, System.Net.TransportContext context) { } + 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; } @@ -148,7 +151,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) { } + 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); @@ -158,7 +161,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) { throw null; } + 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 @@ -167,7 +170,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) { throw null; } + 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 @@ -233,7 +236,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) { throw null; } + 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 @@ -246,7 +249,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) { } + 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; } @@ -265,7 +268,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) { } + 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; } @@ -294,7 +297,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) { throw null; } + 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 @@ -303,7 +306,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) { } + 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/System/Net/Http/ByteArrayContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs index 775f432c6af4b..ede896777bc8d 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 @@ -63,9 +63,12 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext c private protected Task SerializeToStreamAsyncCore(Stream stream, CancellationToken cancellationToken) => stream.WriteAsync(_content, _offset, _count, cancellationToken); - protected override void SerializeToStream(Stream stream, TransportContext context) + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) { Debug.Assert(stream != null); + + // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. + cancellationToken.ThrowIfCancellationRequested(); stream.Write(_content, _offset, _count); } 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 889f530b36e8a..93ecf32f0c5fb 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 @@ -44,14 +44,14 @@ protected DelegatingHandler(HttpMessageHandler innerHandler) InnerHandler = innerHandler; } - protected internal override HttpResponseMessage Send(HttpRequestMessage request) + 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); + return _innerHandler.Send(request, cancellationToken); } protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 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 07b51927cab46..a5eed8ac09239 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 @@ -33,8 +33,9 @@ internal static bool IsEnabled() // 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) => - SendAsyncCore(request, false, default).GetAwaiter().GetResult(); + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) => + SendAsyncCore(request, false, cancellationToken).GetAwaiter().GetResult(); protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => @@ -72,7 +73,7 @@ private async ValueTask SendAsyncCore(HttpRequestMessage re } else { - return base.Send(request); + return base.Send(request, cancellationToken); } } finally @@ -126,7 +127,7 @@ private async ValueTask SendAsyncCore(HttpRequestMessage re } else { - response = base.Send(request); + response = base.Send(request, cancellationToken); taskStatus = TaskStatus.RanToCompletion; } return response; 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 05288da777fec..479679abec63a 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 @@ -453,6 +453,77 @@ public Task DeleteAsync(Uri requestUri, CancellationToken c #region Advanced Send Overloads + public HttpResponseMessage Send(HttpRequestMessage request) + { + return Send(request, defaultCompletionOption, default); + } + + public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption) + { + return Send(request, completionOption, default); + } + + public override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) + { + return Send(request, defaultCompletionOption, cancellationToken); + } + + public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + CheckDisposed(); + CheckRequestMessage(request); + + SetOperationStarted(); + 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; + if (hasTimeout) + { + disposeCts = true; + cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _pendingRequestsCts.Token); + if (hasTimeout) + { + cts.CancelAfter(_timeout); + } + } + else + { + disposeCts = false; + cts = _pendingRequestsCts; + } + + // Initiate the send. + HttpResponseMessage response; + try + { + response = base.Send(request, cts.Token); + } + catch + { + HandleFinishSendCleanup(cts, disposeCts); + throw; + } + + bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && + !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); + + return FinishSend(response, request, cts, disposeCts, buffered); + } + public Task SendAsync(HttpRequestMessage request) { return SendAsync(request, defaultCompletionOption, CancellationToken.None); @@ -514,32 +585,30 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl } catch { - HandleFinishSendAsyncCleanup(cts, disposeCts); + HandleFinishSendCleanup(cts, disposeCts); throw; } - return completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ? - FinishSendAsyncBuffered(sendTask, request, cts, disposeCts) : - FinishSendAsyncUnbuffered(sendTask, request, cts, disposeCts); + bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && + !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); + + return FinishSendAsync(sendTask, request, cts, disposeCts, buffered); } - private async Task FinishSendAsyncBuffered( - Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts) + private HttpResponseMessage FinishSend(HttpResponseMessage response, HttpRequestMessage request, CancellationTokenSource cts, + bool disposeCts, bool buffered) { - HttpResponseMessage response = null; try { - // Wait for the send request to complete, getting back the response. - response = await sendTask.ConfigureAwait(false); if (response == null) { throw new InvalidOperationException(SR.net_http_handler_noresponse); } // 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); + response.Content.LoadIntoBuffer(_maxResponseContentBufferSize, cts.Token); } if (NetEventSource.IsEnabled) NetEventSource.ClientSendCompleted(this, response, request); @@ -548,41 +617,50 @@ private async Task FinishSendAsyncBuffered( catch (Exception e) { response?.Dispose(); - HandleFinishSendAsyncError(e, cts); + HandleFinishSendError(e, cts); throw; } finally { - HandleFinishSendAsyncCleanup(cts, disposeCts); + HandleFinishSendCleanup(cts, disposeCts); } } - private async Task FinishSendAsyncUnbuffered( - Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts) + private async Task FinishSendAsync(Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, + bool disposeCts, bool buffered) { + HttpResponseMessage response = null; try { - HttpResponseMessage response = await sendTask.ConfigureAwait(false); + // Wait for the send request to complete, getting back the response. + response = await sendTask.ConfigureAwait(false); if (response == null) { throw new InvalidOperationException(SR.net_http_handler_noresponse); } + // Buffer the response content if we've been asked to and we have a Content to buffer. + if (buffered && response.Content != null) + { + await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false); + } + if (NetEventSource.IsEnabled) NetEventSource.ClientSendCompleted(this, response, request); return response; } catch (Exception e) { - HandleFinishSendAsyncError(e, cts); + response?.Dispose(); + HandleFinishSendError(e, cts); throw; } finally { - HandleFinishSendAsyncCleanup(cts, disposeCts); + HandleFinishSendCleanup(cts, disposeCts); } } - private void HandleFinishSendAsyncError(Exception e, CancellationTokenSource cts) + private void HandleFinishSendError(Exception e, CancellationTokenSource cts) { if (NetEventSource.IsEnabled) NetEventSource.Error(this, e); @@ -595,7 +673,7 @@ private void HandleFinishSendAsyncError(Exception e, CancellationTokenSource cts } } - 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. 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 8dad6b2ee4218..3f7985043f463 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 @@ -214,11 +214,12 @@ public SslProtocols SslProtocols public IDictionary Properties => _socketsHttpHandler.Properties; - protected internal override HttpResponseMessage Send(HttpRequestMessage request) + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) { return DiagnosticsHandler.IsEnabled() ? - _diagnosticsHandler.Send(request) : - _socketsHttpHandler.Send(request); + _diagnosticsHandler.Send(request, cancellationToken) : + _socketsHttpHandler.Send(request, cancellationToken); } protected internal override Task SendAsync(HttpRequestMessage request, 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 d9a897d91372e..b9f5ce1ecf6b8 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 @@ -319,11 +319,14 @@ internal Stream TryReadAsStream() protected abstract Task SerializeToStreamAsync(Stream stream, TransportContext context); - protected virtual void SerializeToStream(Stream stream, TransportContext context) + protected virtual void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) { - // No choice but to do sync-over-async. Derived types should override this whenever possible. + // No choice but to do sync-over-async. Derived types should override this whenever possible. // Thankfully most CreateContentReadStreamAsync implementations actually complete synchronously. - Stream source = CreateContentReadStreamAsync().GetAwaiter().GetResult(); + Stream source = CreateContentReadStreamAsync(cancellationToken).GetAwaiter().GetResult(); + + // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. + cancellationToken.ThrowIfCancellationRequested(); source.CopyTo(stream); } @@ -338,10 +341,7 @@ protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext co // 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) => - CopyTo(stream, null); - - public void CopyTo(Stream stream, TransportContext context) + public void CopyTo(Stream stream, TransportContext context, CancellationToken cancellationToken) { CheckDisposed(); if (stream == null) @@ -353,11 +353,13 @@ public void CopyTo(Stream stream, TransportContext context) { if (TryGetBuffer(out ArraySegment buffer)) { + // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. + cancellationToken.ThrowIfCancellationRequested(); stream.Write(buffer.Array, buffer.Offset, buffer.Count); } else { - SerializeToStream(stream, context); + SerializeToStream(stream, context, cancellationToken); } } catch (Exception e) when (StreamCopyExceptionNeedsWrapping(e)) @@ -414,6 +416,49 @@ static async Task CopyToAsyncCore(ValueTask copyTask) } } + internal void LoadIntoBuffer(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 we already buffered the content, just return a completed task. + return; + } + + MemoryStream tempBuffer = CreateMemoryStream(maxBufferSize, out Exception error); + if (tempBuffer == null) + { + throw error; + } + + 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 (StreamCopyExceptionNeedsWrapping(e)) + { + throw GetStreamCopyException(e); + } + + throw; + } + } + public Task LoadIntoBufferAsync() { return LoadIntoBufferAsync(MaxBufferSize); @@ -446,8 +491,7 @@ internal Task LoadIntoBufferAsync(long maxBufferSize, CancellationToken cancella return Task.CompletedTask; } - Exception error = null; - MemoryStream tempBuffer = CreateMemoryStream(maxBufferSize, out error); + MemoryStream tempBuffer = CreateMemoryStream(maxBufferSize, out Exception error); if (tempBuffer == null) { // We don't throw in LoadIntoBufferAsync(): return a faulted task. 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 2182ab7e96deb..e7d3b45654c52 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 @@ -20,10 +20,10 @@ protected HttpMessageHandler() // 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 have no other option that to do sync-over-async. - protected internal virtual HttpResponseMessage Send(HttpRequestMessage request) + protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Doing sync-over-async due to lack of {nameof(Send)} override"); - return SendAsync(request, default).GetAwaiter().GetResult(); + return SendAsync(request, cancellationToken).GetAwaiter().GetResult(); } protected internal abstract Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); 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 07acf0440596e..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,7 +37,8 @@ public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler) if (NetEventSource.IsEnabled) NetEventSource.Exit(this); } - public virtual HttpResponseMessage Send(HttpRequestMessage request) + public virtual HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) { if (request == null) { @@ -47,7 +48,7 @@ public virtual HttpResponseMessage Send(HttpRequestMessage request) if (NetEventSource.IsEnabled) NetEventSource.Enter(this, request); - HttpResponseMessage response = _handler.Send(request); + HttpResponseMessage response = _handler.Send(request, cancellationToken); if (NetEventSource.IsEnabled) NetEventSource.Exit(this, response); 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 0928d6acb466f..94833362df47e 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,7 +28,8 @@ protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken); - protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request) + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) { if (request == null) { @@ -37,7 +38,7 @@ protected internal sealed override HttpResponseMessage Send(HttpRequestMessage r // Since most of the SendAsync code is just Task handling, there's no reason to share the code. HttpRequestMessage newRequestMessage = ProcessRequest(request, default); - HttpResponseMessage response = base.Send(newRequestMessage); + HttpResponseMessage response = base.Send(newRequestMessage, cancellationToken); HttpResponseMessage newResponseMessage = ProcessResponse(response, default); return newResponseMessage; } 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 7dc9b7c7f97ea..5ffd4689195f3 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 @@ -169,7 +169,7 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() // 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) + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) { Debug.Assert(stream != null); try @@ -181,10 +181,12 @@ protected override void SerializeToStream(Stream stream, TransportContext contex var output = new StringBuilder(); for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) { + cancellationToken.ThrowIfCancellationRequested(); + // Write divider, headers, and content. HttpContent content = _nestedContent[contentIndex]; EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, content)); - content.CopyTo(stream, context); + content.CopyTo(stream, context, cancellationToken); } // Write footer boundary. 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 112e6e17a49ce..ba18daceb4b04 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 @@ -27,8 +27,12 @@ public ReadOnlyMemoryContent(ReadOnlyMemory content) protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => stream.WriteAsync(_content).AsTask(); - protected override void SerializeToStream(Stream stream, TransportContext context) => + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. + cancellationToken.ThrowIfCancellationRequested(); stream.Write(_content.Span); + } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) => stream.WriteAsync(_content, cancellationToken).AsTask(); 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 482c6ac15b23c..00feff45f63fa 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 @@ -777,7 +777,7 @@ private async ValueTask SendRequestContentAsync(HttpRequestMessage request, Http else { cancellationToken.ThrowIfCancellationRequested(); - request.Content.CopyTo(stream, _transportContext); + request.Content.CopyTo(stream, _transportContext, cancellationToken); } // Finish the content; with a chunked upload, this includes writing the terminating chunk. 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 index 510f337b32f60..657846cc1b370 100644 --- 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 @@ -5,8 +5,9 @@ namespace System.Net.Http { internal abstract class HttpMessageHandlerStage : HttpMessageHandler { - protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request) => - SendAsync(request, false, default).GetAwaiter().GetResult(); + protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) => + SendAsync(request, false, cancellationToken).GetAwaiter().GetResult(); protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => 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 3b63ee2a2e8fd..5538aa6353650 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 @@ -331,7 +331,8 @@ private HttpMessageHandlerStage SetupHandlerChain() return _handler; } - protected internal override HttpResponseMessage Send(HttpRequestMessage request) + protected internal override HttpResponseMessage Send(HttpRequestMessage request, + CancellationToken cancellationToken) { CheckDisposed(); HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain(); @@ -342,7 +343,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request) throw error; } - return handler.Send(request); + return handler.Send(request, cancellationToken); } protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 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 bb4e6649285f2..5d37ce68c5c5e 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 @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -52,12 +53,15 @@ private void InitializeContent(Stream content, int bufferSize) if (NetEventSource.IsEnabled) NetEventSource.Associate(this, content); } - protected override void SerializeToStream(Stream stream, TransportContext context) + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) { Debug.Assert(stream != null); PrepareContent(); + // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. + cancellationToken.ThrowIfCancellationRequested(); + // 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); } From 13b7d33b270f7a98384508415844150b2b5d63e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 18 Mar 2020 21:53:09 +0100 Subject: [PATCH 05/60] Fixed compilation after merge. --- .../System.Net.Http/ref/System.Net.Http.cs | 22 +++++++++-- .../src/System/Net/Http/ByteArrayContent.cs | 2 +- .../src/System/Net/Http/DelegatingHandler.cs | 2 +- .../src/System/Net/Http/DiagnosticsHandler.cs | 2 +- .../src/System/Net/Http/HttpClient.cs | 39 +++++++++++++------ .../src/System/Net/Http/HttpContent.cs | 10 ++--- .../src/System/Net/Http/MultipartContent.cs | 2 +- .../System/Net/Http/ReadOnlyMemoryContent.cs | 2 +- .../Http/SocketsHttpHandler/ConnectHelper.cs | 6 +-- .../Http/SocketsHttpHandler/CreditManager.cs | 31 ++++++++++----- .../Http/SocketsHttpHandler/Http2Stream.cs | 2 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../SocketsHttpHandler/SocketsHttpHandler.cs | 2 +- .../src/System/Net/Http/StreamContent.cs | 2 +- 14 files changed, 86 insertions(+), 40 deletions(-) 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 131f048c0d847..779468bc39f34 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 @@ -228,6 +239,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 @@ -240,6 +252,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; } @@ -258,6 +271,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; } @@ -268,7 +282,7 @@ public SocketsHttpHandler() { } public bool AllowAutoRedirect { get { throw null; } set { } } public System.Net.DecompressionMethods AutomaticDecompression { get { throw null; } set { } } public System.TimeSpan ConnectTimeout { get { throw null; } set { } } - [System.Diagnostics.CodeAnalysis.AllowNull] + [System.Diagnostics.CodeAnalysis.AllowNullAttribute] public System.Net.CookieContainer CookieContainer { get { throw null; } set { } } public System.Net.ICredentials? Credentials { get { throw null; } set { } } public System.Net.ICredentials? DefaultProxyCredentials { get { throw null; } set { } } @@ -288,6 +302,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 @@ -296,6 +311,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; } @@ -347,7 +363,7 @@ public CacheControlHeaderValue() { } public static System.Net.Http.Headers.CacheControlHeaderValue Parse(string? input) { throw null; } object System.ICloneable.Clone() { throw null; } public override string ToString() { throw null; } - public static bool TryParse(string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.Http.Headers.CacheControlHeaderValue? parsedValue) { throw null; } + public static bool TryParse(string? input, out System.Net.Http.Headers.CacheControlHeaderValue? parsedValue) { throw null; } } public partial class ContentDispositionHeaderValue : System.ICloneable { @@ -569,7 +585,7 @@ public ProductHeaderValue(string name, string? version) { } public static System.Net.Http.Headers.ProductHeaderValue Parse(string? input) { throw null; } object System.ICloneable.Clone() { throw null; } public override string ToString() { throw null; } - public static bool TryParse(string? input, out System.Net.Http.Headers.ProductHeaderValue? parsedValue) { throw null; } + public static bool TryParse(string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.Http.Headers.ProductHeaderValue? parsedValue) { throw null; } } public partial class ProductInfoHeaderValue : System.ICloneable { 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 afa2fa4eff8e0..5af3749d61f05 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 @@ -63,7 +63,7 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext? private protected Task SerializeToStreamAsyncCore(Stream stream, CancellationToken cancellationToken) => stream.WriteAsync(_content, _offset, _count, cancellationToken); - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { Debug.Assert(stream != null); 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 54b734a180898..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 @@ -53,7 +53,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); } SetOperationStarted(); - return _innerHandler.Send(request, cancellationToken); + return _innerHandler!.Send(request, cancellationToken); } protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 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 4632ac6f9c1f1..0e2ae7bedc0e4 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 @@ -115,7 +115,7 @@ private async ValueTask SendAsyncCore(HttpRequestMessage re InjectHeaders(currentActivity, request); } - HttpResponseMessage response = null; + HttpResponseMessage? response = null; TaskStatus taskStatus = TaskStatus.Faulted; try { 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 94527ccbe9f4d..fcf298a0368da 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 @@ -492,12 +492,14 @@ public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption CancellationTokenSource cts; bool disposeCts; bool hasTimeout = _timeout != s_infiniteTimeout; - if (hasTimeout) + 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); } } @@ -513,16 +515,22 @@ public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption { response = base.Send(request, cts.Token); } - catch + catch (Exception e) { HandleFinishSendCleanup(cts, disposeCts); + + if (e is OperationCanceledException operationException && TimeoutFired(cancellationToken, timeoutTime)) + { + throw CreateTimeoutException(operationException); + } + throw; } bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); - return FinishSend(response, request, cts, disposeCts, buffered); + return FinishSend(response, request, cts, disposeCts, buffered, cancellationToken, timeoutTime); } public Task SendAsync(HttpRequestMessage request) @@ -601,11 +609,11 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); - return FinishSendAsync(sendTask, request, cts, disposeCts, buffered); + return FinishSendAsync(sendTask, request, cts, disposeCts, buffered, cancellationToken, timeoutTime); } private HttpResponseMessage FinishSend(HttpResponseMessage response, HttpRequestMessage request, CancellationTokenSource cts, - bool disposeCts, bool buffered) + bool disposeCts, bool buffered, CancellationToken callerToken, long timeoutTime) { try { @@ -626,7 +634,14 @@ private HttpResponseMessage FinishSend(HttpResponseMessage response, HttpRequest catch (Exception e) { response?.Dispose(); - HandleFinishSendError(e, cts); + + if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime)) + { + HandleSendTimeout(operationException); + throw CreateTimeoutException(operationException); + } + + HandleFinishSendAsyncError(e, cts); throw; } finally @@ -636,9 +651,9 @@ private HttpResponseMessage FinishSend(HttpResponseMessage response, HttpRequest } private async Task FinishSendAsync(Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, - bool disposeCts, bool buffered) + bool disposeCts, bool buffered, CancellationToken callerToken, long timeoutTime) { - HttpResponseMessage response = null; + HttpResponseMessage? response = null; try { // Wait for the send request to complete, getting back the response. @@ -659,9 +674,11 @@ private async Task FinishSendAsync(Task ReadAsStreamAsync(CancellationToken cancellationToken) protected abstract Task SerializeToStreamAsync(Stream stream, TransportContext? context); - protected virtual void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + protected virtual void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { // No choice but to do sync-over-async. Derived types should override this whenever possible. // Thankfully most CreateContentReadStreamAsync implementations actually complete synchronously. @@ -343,7 +343,7 @@ 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) + public void CopyTo(Stream stream, TransportContext? context, CancellationToken cancellationToken) { CheckDisposed(); if (stream == null) @@ -357,7 +357,7 @@ public void CopyTo(Stream stream, TransportContext context, CancellationToken ca { // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. cancellationToken.ThrowIfCancellationRequested(); - stream.Write(buffer.Array, buffer.Offset, buffer.Count); + stream.Write(buffer.Array!, buffer.Offset, buffer.Count); } else { @@ -436,10 +436,10 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT return; } - MemoryStream tempBuffer = CreateMemoryStream(maxBufferSize, out Exception error); + MemoryStream? tempBuffer = CreateMemoryStream(maxBufferSize, out Exception? error); if (tempBuffer == null) { - throw error; + throw error!; } try 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 db302c594b634..cafad28f49331 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 @@ -166,7 +166,7 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() // 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) + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { Debug.Assert(stream != null); try 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 8f7e7045c63fb..69006e5d44950 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 @@ -27,7 +27,7 @@ public ReadOnlyMemoryContent(ReadOnlyMemory content) protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => stream.WriteAsync(_content).AsTask(); - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. cancellationToken.ThrowIfCancellationRequested(); 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 f7a44aeae0604..9af907e399a53 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 @@ -41,12 +41,12 @@ public static async ValueTask ConnectAsync(string host, int port, bool a if (!async) { cancellationToken.ThrowIfCancellationRequested(); - Socket socket = null; + Socket? socket = null; try { socket = new Socket(SocketType.Stream, ProtocolType.Tcp); socket.NoDelay = true; - using (cancellationToken.UnsafeRegister(s => ((Socket)s).Dispose(), socket)) + using (cancellationToken.UnsafeRegister(s => ((Socket)s!).Dispose(), socket)) { socket.Connect(new DnsEndPoint(host, port)); } @@ -179,7 +179,7 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a } else { - sslStream.AuthenticateAsClient(sslOptions.TargetHost, sslOptions.ClientCertificates, sslOptions.EnabledSslProtocols, sslOptions.CertificateRevocationCheckMode == X509RevocationMode.Online); + sslStream.AuthenticateAsClient(sslOptions.TargetHost!, sslOptions.ClientCertificates, sslOptions.EnabledSslProtocols, sslOptions.CertificateRevocationCheckMode == X509RevocationMode.Online); } } catch (Exception e) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs index 75a7fb1ef81fc..94b5baeed7b60 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs @@ -36,10 +36,8 @@ private object SyncObject get => this; } - public int RequestCredit(int amount) + public int RequestCredit(int amount, CancellationToken cancellationToken) { - Waiter waiter; - lock (SyncObject) { if (_disposed) @@ -49,7 +47,7 @@ public int RequestCredit(int amount) if (_current > 0) { - Debug.Assert(_waiters == null || _waiters.Count == 0, "Shouldn't have waiters when credit is available"); + Debug.Assert(_waitersTail is null, "Shouldn't have waiters when credit is available"); int granted = Math.Min(amount, _current); if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, current={_current}, granted={granted}"); @@ -59,12 +57,27 @@ public int RequestCredit(int amount) if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, no credit available."); - waiter = new Waiter { Amount = amount }; - (_waiters ??= new Queue()).Enqueue(waiter); - } + // Otherwise, create a new waiter. + CreditWaiter waiter = cancellationToken.CanBeCanceled ? + new CancelableCreditWaiter(SyncObject, cancellationToken) : + new CreditWaiter(); + waiter.Amount = amount; - // TODO: This is sync-over-async... don't see a good way around this. - return waiter.Task.GetAwaiter().GetResult(); + // Add the waiter at the tail of the queue. + if (_waitersTail is null) + { + _waitersTail = waiter.Next = waiter; + } + else + { + waiter.Next = _waitersTail.Next; + _waitersTail.Next = waiter; + _waitersTail = waiter; + } + + // TODO: This is sync-over-async... don't see a good way around this. + return waiter.AsValueTask().GetAwaiter().GetResult(); + } } public ValueTask RequestCreditAsync(int amount, CancellationToken cancellationToken) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index 6a74679baaefb..fddf19a239649 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -1367,7 +1367,7 @@ protected override void Dispose(bool disposing) public override void Write(byte[] buffer, int offset, int count) { ValidateBufferArgs(buffer, offset, count); - Http2Stream http2Stream = _http2Stream; + Http2Stream? http2Stream = _http2Stream; if (http2Stream == null) { throw new ObjectDisposedException(nameof(Http2WriteStream)); 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 cd406b6178de8..0fb6acaa06ec9 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 @@ -440,7 +440,7 @@ public byte[] Http2AltSvcOriginUri if (NetEventSource.IsEnabled) Trace("Connection limit reached, waiting for available connection."); return async ? waiter.WaitWithCancellationAsync(cancellationToken) : - new ValueTask(waiter.Task.GetAwaiter().GetResult()); + new ValueTask(waiter.Task.GetAwaiter().GetResult()); } private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> 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 e615a76ae6200..7fae1d7e617b6 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 @@ -340,7 +340,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, CheckDisposed(); HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain(); - Exception error = ValidateAndNormalizeRequest(request); + Exception? error = ValidateAndNormalizeRequest(request); if (error != null) { throw error; 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 906f757981899..31dd6c4b4837f 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 @@ -53,7 +53,7 @@ private void InitializeContent(Stream content, int bufferSize) if (NetEventSource.IsEnabled) NetEventSource.Associate(this, content); } - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { Debug.Assert(stream != null); From 836c6a3fb783f734ec2ebda9aee34c18699f784d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 23 Mar 2020 14:21:58 +0100 Subject: [PATCH 06/60] HttpClient Send tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index ef0257e1cc56e..3b0ae22df83fb 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -326,6 +326,38 @@ public async Task GetAsync_CustomException_Asynchronous_ThrowsException() } } + [Fact] + public void Send_NullRequest_ThrowsException() + { + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(null)))) + { + AssertExtensions.Throws("request", () => { client.Send(null); }); + } + } + + [Fact] + public void Send_DuplicateRequest_ThrowsException() + { + using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) + using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) + { + (client.Send(request)).Dispose(); + Assert.Throws(() => { client.Send(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())))) + { + client.Send(request); + await content.ReadAsStringAsync(); // no exception + } + } + [Fact] public void SendAsync_NullRequest_ThrowsException() { @@ -580,6 +612,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); }); } @@ -841,6 +874,11 @@ 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 From c9b5907892547b575c053ed0c33d34a82fa94746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 1 Apr 2020 14:55:17 +0200 Subject: [PATCH 07/60] Post merge fixes. --- .../src/System/Net/Http/DiagnosticsHandler.cs | 8 ++++++-- .../System/Net/Http/SocketsHttpHandler/CreditManager.cs | 2 +- .../System/Net/Http/SocketsHttpHandler/Http2Stream.cs | 2 +- .../System/Net/Http/SocketsHttpHandler/HttpConnection.cs | 7 ++++++- .../Http/SocketsHttpHandler/HttpMessageHandlerStage.cs | 9 +++++++-- .../tests/FunctionalTests/HttpClientHandlerTest.Http2.cs | 4 ++-- 6 files changed, 23 insertions(+), 9 deletions(-) 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 0e2ae7bedc0e4..05cdacbe47fd4 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 @@ -34,8 +34,12 @@ internal static bool IsEnabled() // 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) => - SendAsyncCore(request, false, cancellationToken).GetAwaiter().GetResult(); + CancellationToken cancellationToken) + { + ValueTask sendTask = SendAsyncCore(request, false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); + } protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs index 94b5baeed7b60..0cd537d6d560b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs @@ -76,7 +76,7 @@ public int RequestCredit(int amount, CancellationToken cancellationToken) } // TODO: This is sync-over-async... don't see a good way around this. - return waiter.AsValueTask().GetAwaiter().GetResult(); + return waiter.AsValueTask().AsTask().GetAwaiter().GetResult(); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index fddf19a239649..c40a6060385b4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -1374,7 +1374,7 @@ public override void Write(byte[] buffer, int offset, int count) } // Sync-over-async. See comment on Http2Connection.SendAsync. - http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), default).GetAwaiter().GetResult(); + http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), default).AsTask().GetAwaiter().GetResult(); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) 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 b19b025218c4e..bd1f90850818c 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 @@ -1400,7 +1400,12 @@ private void ThrowIfExceededAllowedReadLineBytes() } } - private void Fill() => FillAsync(async: false).GetAwaiter().GetResult(); + private void Fill() + { + 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(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 index 657846cc1b370..84b6936fb1200 100644 --- 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 @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -6,8 +7,12 @@ namespace System.Net.Http internal abstract class HttpMessageHandlerStage : HttpMessageHandler { protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, - CancellationToken cancellationToken) => - SendAsync(request, false, cancellationToken).GetAwaiter().GetResult(); + CancellationToken cancellationToken) + { + ValueTask sendTask = SendAsync(request, false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); + } protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => 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 034ab35b739dd..e946c5a1c4724 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -2706,7 +2706,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(); @@ -2716,7 +2716,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; From 8e6abc8a7a2393bf9508cc351402a1f905d85107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 2 Apr 2020 13:39:34 +0200 Subject: [PATCH 08/60] AutoRedirect and Authenticaton sync tests done. --- .../HttpClientHandlerTest.Authentication.cs | 7 +-- .../HttpClientHandlerTest.AutoRedirect.cs | 48 +++++++++-------- .../Net/Http/HttpClientHandlerTestBase.cs | 23 ++++++++ .../tests/FunctionalTests/HttpClientTest.cs | 52 ++++--------------- 4 files changed, 63 insertions(+), 67 deletions(-) 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..8181f9f763107 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs @@ -551,8 +551,9 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_Success() } } - [ConditionalFact(nameof(IsDomainJoinedServerAvailable))] - public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success() + [ConditionalTheory(nameof(IsDomainJoinedServerAvailable))] + [MemberData(nameof(Async))] + public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success(bool async) { using (HttpClientHandler handler = CreateHttpClientHandler()) using (HttpClient client = CreateHttpClient(handler)) @@ -568,7 +569,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.Send(async, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string body = await response.Content.ReadAsStringAsync(); 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..7f521bb0068ed 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs @@ -40,31 +40,35 @@ public static IEnumerable RemoteServersAndRedirectStatusCodes() } } - public static readonly object[][] RedirectStatusCodesOldMethodsNewMethods = { - new object[] { 300, "GET", "GET" }, - new object[] { 300, "POST", "GET" }, - new object[] { 300, "HEAD", "HEAD" }, + public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() + { + foreach (bool value in new[] {true, false}) + { + yield return new object[] {300, "GET", "GET", value}; + yield return new object[] {300, "POST", "GET", value}; + yield return new object[] {300, "HEAD", "HEAD", value}; - new object[] { 301, "GET", "GET" }, - new object[] { 301, "POST", "GET" }, - new object[] { 301, "HEAD", "HEAD" }, + yield return new object[] {301, "GET", "GET", value}; + yield return new object[] {301, "POST", "GET", value}; + yield return new object[] {301, "HEAD", "HEAD", value}; - new object[] { 302, "GET", "GET" }, - new object[] { 302, "POST", "GET" }, - new object[] { 302, "HEAD", "HEAD" }, + yield return new object[] {302, "GET", "GET", value}; + yield return new object[] {302, "POST", "GET", value}; + yield return new object[] {302, "HEAD", "HEAD", value}; - new object[] { 303, "GET", "GET" }, - new object[] { 303, "POST", "GET" }, - new object[] { 303, "HEAD", "HEAD" }, + yield return new object[] {303, "GET", "GET", value}; + yield return new object[] {303, "POST", "GET", value}; + yield return new object[] {303, "HEAD", "HEAD", value}; - new object[] { 307, "GET", "GET" }, - new object[] { 307, "POST", "POST" }, - new object[] { 307, "HEAD", "HEAD" }, + yield return new object[] {307, "GET", "GET", value}; + yield return new object[] {307, "POST", "POST", value}; + yield return new object[] {307, "HEAD", "HEAD", value}; - new object[] { 308, "GET", "GET" }, - new object[] { 308, "POST", "POST" }, - new object[] { 308, "HEAD", "HEAD" }, - }; + yield return new object[] {308, "GET", "GET", value}; + yield return new object[] {308, "POST", "POST", value}; + yield return new object[] {308, "HEAD", "HEAD", value}; + } + } public HttpClientHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } @@ -97,7 +101,7 @@ public async Task GetAsync_AllowAutoRedirectFalse_RedirectFromHttpToHttp_StatusC [Theory, MemberData(nameof(RedirectStatusCodesOldMethodsNewMethods))] public async Task AllowAutoRedirect_True_ValidateNewMethodUsedOnRedirection( - int statusCode, string oldMethod, string newMethod) + int statusCode, string oldMethod, string newMethod, bool async) { if (statusCode == 308 && (IsWinHttpHandler && PlatformDetection.WindowsVersion < 10)) { @@ -112,7 +116,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.Send(async, request); await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 003e344d74042..e2fc01f346ea1 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -69,6 +69,12 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) }; } + public static IEnumerable Async() + { + yield return new object[] { true }; + yield return new object[] { false }; + } + // For use by remote server tests public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; @@ -124,4 +130,21 @@ protected override async Task SendAsync(HttpRequestMessage } } } + + public static class HttpClientExtensions + { + public static Task Send(this HttpClient client, bool async, HttpRequestMessage request) + { + if (async) + { + return client.SendAsync(request); + } + else + { + // 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)); + } + } + } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 3b0ae22df83fb..9da387de4a4eb 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -326,66 +326,34 @@ public async Task GetAsync_CustomException_Asynchronous_ThrowsException() } } - [Fact] - public void Send_NullRequest_ThrowsException() + [Theory, MemberData(nameof(Async))] + public async Task Send_NullRequest_ThrowsException(bool async) { using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(null)))) { - AssertExtensions.Throws("request", () => { client.Send(null); }); + await AssertExtensions.ThrowsAsync("request", () => client.Send(async, null)); } } - [Fact] - public void Send_DuplicateRequest_ThrowsException() + [Theory, MemberData(nameof(Async))] + public async Task Send_DuplicateRequest_ThrowsException(bool async) { using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) { - (client.Send(request)).Dispose(); - Assert.Throws(() => { client.Send(request); }); + (await client.Send(async, request)).Dispose(); + await Assert.ThrowsAsync(() => client.Send(async, request)); } } - [Fact] - public async Task Send_RequestContentNotDisposed() + [Theory, MemberData(nameof(Async))] + public async Task Send_RequestContentNotDisposed(bool async) { 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())))) { - client.Send(request); - await content.ReadAsStringAsync(); // no exception - } - } - - [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 client.Send(async, request); await content.ReadAsStringAsync(); // no exception } } From f930e5f321b1613a6f2bc42053ace24c2dd6a829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 2 Apr 2020 20:44:35 +0200 Subject: [PATCH 09/60] Cancellation sync tests --- .../System/Net/Http/ByteAtATimeContent.cs | 17 ++++++ .../HttpClientHandlerTest.AutoRedirect.cs | 54 ++++++++++--------- .../HttpClientHandlerTest.Cancellation.cs | 46 ++++++++++------ .../Net/Http/HttpClientHandlerTestBase.cs | 6 +-- .../Http/SocketsHttpHandler/ConnectHelper.cs | 3 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../HttpMessageHandlerStage.cs | 5 +- 7 files changed, 86 insertions(+), 47 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs index 67a95370cbde1..561c2f1e8baa2 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,22 @@ public ByteAtATimeContent(int length, Task waitToSend, TaskCompletionSource RedirectStatusCodesOldMethodsNewMethods() { foreach (bool value in new[] {true, false}) { - yield return new object[] {300, "GET", "GET", value}; - yield return new object[] {300, "POST", "GET", value}; - yield return new object[] {300, "HEAD", "HEAD", value}; + yield return new object[] {value, 300, "GET", "GET"}; + yield return new object[] {value, 300, "POST", "GET"}; + yield return new object[] {value, 300, "HEAD", "HEAD"}; - yield return new object[] {301, "GET", "GET", value}; - yield return new object[] {301, "POST", "GET", value}; - yield return new object[] {301, "HEAD", "HEAD", value}; + yield return new object[] {value, 301, "GET", "GET"}; + yield return new object[] {value, 301, "POST", "GET"}; + yield return new object[] {value, 301, "HEAD", "HEAD"}; - yield return new object[] {302, "GET", "GET", value}; - yield return new object[] {302, "POST", "GET", value}; - yield return new object[] {302, "HEAD", "HEAD", value}; + yield return new object[] {value, 302, "GET", "GET"}; + yield return new object[] {value, 302, "POST", "GET"}; + yield return new object[] {value, 302, "HEAD", "HEAD"}; - yield return new object[] {303, "GET", "GET", value}; - yield return new object[] {303, "POST", "GET", value}; - yield return new object[] {303, "HEAD", "HEAD", value}; + yield return new object[] {value, 303, "GET", "GET"}; + yield return new object[] {value, 303, "POST", "GET"}; + yield return new object[] {value, 303, "HEAD", "HEAD"}; - yield return new object[] {307, "GET", "GET", value}; - yield return new object[] {307, "POST", "POST", value}; - yield return new object[] {307, "HEAD", "HEAD", value}; + yield return new object[] {value, 307, "GET", "GET"}; + yield return new object[] {value, 307, "POST", "POST"}; + yield return new object[] {value, 307, "HEAD", "HEAD"}; - yield return new object[] {308, "GET", "GET", value}; - yield return new object[] {308, "POST", "POST", value}; - yield return new object[] {308, "HEAD", "HEAD", value}; + yield return new object[] {value, 308, "GET", "GET"}; + yield return new object[] {value, 308, "POST", "POST"}; + yield return new object[] {value, 308, "HEAD", "HEAD"}; } } @@ -101,7 +101,7 @@ public async Task GetAsync_AllowAutoRedirectFalse_RedirectFromHttpToHttp_StatusC [Theory, MemberData(nameof(RedirectStatusCodesOldMethodsNewMethods))] public async Task AllowAutoRedirect_True_ValidateNewMethodUsedOnRedirection( - int statusCode, string oldMethod, string newMethod, bool async) + bool async, int statusCode, string oldMethod, string newMethod) { if (statusCode == 308 && (IsWinHttpHandler && PlatformDetection.WindowsVersion < 10)) { @@ -146,11 +146,15 @@ await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => } [Theory] - [InlineData(300)] - [InlineData(301)] - [InlineData(302)] - [InlineData(303)] - public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(int statusCode) + [InlineData(true, 300)] + [InlineData(true, 301)] + [InlineData(true, 302)] + [InlineData(true, 303)] + [InlineData(false, 300)] + [InlineData(false, 301)] + [InlineData(false, 302)] + [InlineData(false, 303)] + public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(bool async, int statusCode) { HttpClientHandler handler = CreateHttpClientHandler(); using (HttpClient client = CreateHttpClient(handler)) @@ -161,7 +165,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => request.Content = new StringContent(ExpectedContent); request.Headers.TransferEncodingChunked = true; - Task getResponseTask = client.SendAsync(request); + Task getResponseTask = client.Send(async, 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 b248f3470bbdd..d5068109a9fa8 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -29,9 +29,8 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } [ConditionalTheory] - [InlineData(false, CancellationMode.Token)] - [InlineData(true, CancellationMode.Token)] - public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode) + [MemberData(nameof(TwoBoolsAndCancellationMode))] + public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool async, bool chunkedTransfer, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer) { @@ -62,10 +61,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.Send(async, 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); } } @@ -84,8 +86,8 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => } [ConditionalTheory] - [MemberData(nameof(OneBoolAndCancellationMode))] - public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode) + [MemberData(nameof(TwoBoolsAndCancellationMode))] + public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool async, bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && connectionClose) { @@ -124,7 +126,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.Send(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await partialResponseHeadersSent.Task; Cancel(mode, client, cts); await getResponse; @@ -141,8 +143,8 @@ await ValidateClientCancellationAsync(async () => [Theory] [ActiveIssue("https://github.com/dotnet/runtime/issues/25760")] - [MemberData(nameof(TwoBoolsAndCancellationMode))] - public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, CancellationMode mode) + [MemberData(nameof(ThreeBoolsAndCancellationMode))] + public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) { @@ -180,7 +182,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.Send(async, 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); @@ -197,8 +199,8 @@ await ValidateClientCancellationAsync(async () => } [ConditionalTheory] - [MemberData(nameof(ThreeBools))] - public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) + [MemberData(nameof(FourBools))] + public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) { @@ -238,7 +240,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.Send(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await ValidateClientCancellationAsync(async () => { HttpResponseMessage resp = await getResponse; @@ -607,10 +609,24 @@ from second in s_bools from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { first, second, mode }; + public static IEnumerable ThreeBoolsAndCancellationMode() => + from first in s_bools + from second in s_bools + from third in s_bools + from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } + select new object[] { first, second, third, mode }; + public static IEnumerable ThreeBools() => from first in s_bools from second in s_bools from third in s_bools select new object[] { first, second, third }; + + public static IEnumerable FourBools() => + from first in s_bools + from second in s_bools + from third in s_bools + from fourth in s_bools + select new object[] { first, second, third, fourth }; } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index e2fc01f346ea1..c0d27c82f6400 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -133,17 +133,17 @@ protected override async Task SendAsync(HttpRequestMessage public static class HttpClientExtensions { - public static Task Send(this HttpClient client, bool async, HttpRequestMessage request) + public static Task Send(this HttpClient client, bool async, HttpRequestMessage request, HttpCompletionOption completionOption = default, CancellationToken cancellationToken = default) { if (async) { - return client.SendAsync(request); + return client.SendAsync(request, completionOption, cancellationToken); } else { // 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)); + return Task.Run(() => client.Send(request, completionOption, cancellationToken)); } } } 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 9af907e399a53..db9cab4949eba 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 @@ -179,7 +179,8 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a } else { - sslStream.AuthenticateAsClient(sslOptions.TargetHost!, sslOptions.ClientCertificates, sslOptions.EnabledSslProtocols, sslOptions.CertificateRevocationCheckMode == X509RevocationMode.Online); + // ToDo: [ActiveIssue("https://github.com/dotnet/runtime/issues/34638")] + sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).GetAwaiter().GetResult(); } } catch (Exception e) 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 0fb6acaa06ec9..d6400a831a216 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 @@ -1069,7 +1069,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool 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) 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 index 84b6936fb1200..19196f9e27120 100644 --- 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 @@ -10,8 +10,9 @@ protected internal sealed override HttpResponseMessage Send(HttpRequestMessage r CancellationToken cancellationToken) { ValueTask sendTask = SendAsync(request, false, cancellationToken); - Debug.Assert(sendTask.IsCompleted); - return sendTask.GetAwaiter().GetResult(); + // At least for HTTP 1.1 we must support fully sync Send + Debug.Assert(request.Version.Major >= 2 || sendTask.IsCompleted); + return sendTask.IsCompleted ? sendTask.GetAwaiter().GetResult() : sendTask.AsTask().GetAwaiter().GetResult(); } protected internal sealed override Task SendAsync(HttpRequestMessage request, From 25ecee672fe988e92f91fd1b04a043fcac6b60c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 9 Apr 2020 15:15:37 +0200 Subject: [PATCH 10/60] Sync Cookie tests. --- .../Net/Http/HttpClientHandlerTest.Cookies.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) 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..7dcf1638aa0b8 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -126,8 +126,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync( }); } - [Fact] - public async Task GetAsync_AddCookieHeader_CookieHeaderSent() + [Theory] + [MemberData(nameof(Async))] + public async Task GetAsync_AddCookieHeader_CookieHeaderSent(bool async) { await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -137,7 +138,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.Send(async, requestMessage); } }, async server => @@ -147,8 +148,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync( }); } - [Fact] - public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent() + [Theory] + [MemberData(nameof(Async))] + public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent(bool async) { await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -160,7 +162,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( requestMessage.Headers.Add("Cookie", "B=2"); requestMessage.Headers.Add("Cookie", "C=3"); - await client.SendAsync(requestMessage); + await client.Send(async, requestMessage); } }, async server => @@ -211,8 +213,9 @@ private string GetCookieValue(HttpRequestData request) return cookieHeaderValue; } - [ConditionalFact] - public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent() + [Theory] + [MemberData(nameof(Async))] + public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent(bool async) { await LoopbackServerFactory.CreateServerAsync(async (server, url) => { @@ -224,7 +227,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.Send(async, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -238,8 +241,9 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => }); } - [ConditionalFact] - public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent() + [Theory] + [MemberData(nameof(Async))] + public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent(bool async) { await LoopbackServerFactory.CreateServerAsync(async (server, url) => { @@ -252,7 +256,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.Send(async, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); From dd6ca30d4ca0a8037fea50607438399aa0663259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 9 Apr 2020 17:25:04 +0200 Subject: [PATCH 11/60] Sync HttpClientHandlerTest --- .../HttpClientHandlerTest.AutoRedirect.cs | 2 +- .../HttpClientHandlerTest.Cancellation.cs | 32 +++---- .../Net/Http/HttpClientHandlerTest.Cookies.cs | 2 +- .../Net/Http/HttpClientHandlerTest.Proxy.cs | 2 +- ...ttpClientHandlerTest.ServerCertificates.cs | 2 +- .../System/Net/Http/HttpClientHandlerTest.cs | 88 +++++++++++-------- .../Net/Http/HttpClientHandlerTestBase.cs | 14 +-- 7 files changed, 76 insertions(+), 66 deletions(-) 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 afca58d7ec21b..287a4f0a1bcbc 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs @@ -42,7 +42,7 @@ public static IEnumerable RemoteServersAndRedirectStatusCodes() public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() { - foreach (bool value in new[] {true, false}) + foreach (bool value in Bools) { yield return new object[] {value, 300, "GET", "GET"}; yield return new object[] {value, 300, "POST", "GET"}; 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 d5068109a9fa8..dfeecad7afbe2 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; @@ -595,38 +593,36 @@ public enum CancellationMode CancelPendingRequests = 0x2, DisposeHttpClient = 0x4 } - - private static readonly bool[] s_bools = new[] { true, false }; - + public static IEnumerable OneBoolAndCancellationMode() => - from first in s_bools + from first in Bools 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 Bools + from second in Bools from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { first, second, mode }; public static IEnumerable ThreeBoolsAndCancellationMode() => - from first in s_bools - from second in s_bools - from third in s_bools + from first in Bools + from second in Bools + from third in Bools from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { first, second, third, mode }; public static IEnumerable ThreeBools() => - from first in s_bools - from second in s_bools - from third in s_bools + from first in Bools + from second in Bools + from third in Bools select new object[] { first, second, third }; public static IEnumerable FourBools() => - from first in s_bools - from second in s_bools - from third in s_bools - from fourth in s_bools + from first in Bools + from second in Bools + from third in Bools + from fourth in Bools select new object[] { first, second, third, fourth }; } } 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 7dcf1638aa0b8..a56c5ada283e6 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -606,7 +606,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 Bools) { 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.Proxy.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs index 80ec9e25b244d..75950153caa43 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs @@ -385,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 Bools) { 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 5849f3c425497..21e654b2d35a3 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -182,7 +182,7 @@ public static IEnumerable UseCallback_ValidCertificate_ExpectedValuesD { if (remoteServer.IsSecure) { - foreach (bool checkRevocation in new[] { true, false }) + foreach (bool checkRevocation in Bools) { 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 a866a5d076a99..35ac60f0e016e 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -484,7 +484,7 @@ 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 Bools select new object[] { address, useSsl }; [ConditionalTheory] @@ -753,8 +753,9 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => server.AcceptConnectionSendCustomResponseAndCloseAsync("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhe")); } - [ConditionalFact] - public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly() + [ConditionalTheory] + [MemberData(nameof(Async))] + public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly(bool async) { #if WINHTTPHANDLER_TEST if (UseVersion > HttpVersion.Version11) @@ -838,7 +839,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.Send(async, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); } }, async server => { @@ -1187,14 +1188,15 @@ await LoopbackServer.CreateServerAsync(async (server, url) => }); } - [Fact] - public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws() + [Theory] + [MemberData(nameof(Async))] + public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws(bool async) { var req = new HttpRequestMessage(HttpMethod.Post, "http://bing.com") { Version = UseVersion }; req.Headers.TransferEncodingChunked = true; using (HttpClient c = CreateHttpClient()) { - HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(req)); + HttpRequestException error = await Assert.ThrowsAsync(() => c.Send(async, req)); Assert.IsType(error.InnerException); } } @@ -1225,13 +1227,13 @@ public async Task GetAsync_ResponseHeadersRead_ReadFromEachIterativelyDoesntDead } [OuterLoop("Uses external server")] - [Theory, MemberData(nameof(RemoteServersMemberData))] - public async Task SendAsync_HttpRequestMsgResponseHeadersRead_StatusCodeOK(Configuration.Http.RemoteServer remoteServer) + [Theory, MemberData(nameof(AsyncRemoteServersMemberData))] + public async Task SendAsync_HttpRequestMsgResponseHeadersRead_StatusCodeOK(bool async, Configuration.Http.RemoteServer remoteServer) { 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.Send(async, request, HttpCompletionOption.ResponseHeadersRead)) { string responseContent = await response.Content.ReadAsStringAsync(); _output.WriteLine(responseContent); @@ -1728,7 +1730,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 Bools) // force the content copy to happen via Read/Write or ReadAsync/WriteAsync { byte[] data = new byte[1234]; new Random(42).NextBytes(data); @@ -1860,18 +1862,21 @@ public async Task PostAsync_CallMethod_EmptyContent(Configuration.Http.RemoteSer } } + public static IEnumerable ExpectContinueVersion() + { + var versions = new string[] {"1.0", "1.1", "2.0"}; + var expectContinue = new bool?[] {true, false, null}; + return + from async in Bools + from expect in expectContinue + from version in versions + select new object[] {async, 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 async, bool? expectContinue, string version) { using (HttpClient client = CreateHttpClient()) { @@ -1882,7 +1887,7 @@ public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string }; req.Headers.ExpectContinue = expectContinue; - using (HttpResponseMessage response = await client.SendAsync(req)) + using (HttpResponseMessage response = await client.Send(async, req)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -1938,18 +1943,21 @@ await server.AcceptConnectionAsync(async connection => public static IEnumerable Interim1xxStatusCode() { - yield return new object[] { (HttpStatusCode) 100 }; // 100 Continue. - // 101 SwitchingProtocols will be treated as a final status code. - yield return new object[] { (HttpStatusCode) 102 }; // 102 Processing. - yield return new object[] { (HttpStatusCode) 103 }; // 103 EarlyHints. - yield return new object[] { (HttpStatusCode) 150 }; - yield return new object[] { (HttpStatusCode) 180 }; - yield return new object[] { (HttpStatusCode) 199 }; + foreach (var async in Bools) + { + yield return new object[] {async, (HttpStatusCode)100}; // 100 Continue. + // 101 SwitchingProtocols will be treated as a final status code. + yield return new object[] {async, (HttpStatusCode)102}; // 102 Processing. + yield return new object[] {async, (HttpStatusCode)103}; // 103 EarlyHints. + yield return new object[] {async, (HttpStatusCode)150}; + yield return new object[] {async, (HttpStatusCode)180}; + yield return new object[] {async, (HttpStatusCode)199}; + } } [ConditionalTheory] [MemberData(nameof(Interim1xxStatusCode))] - public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(HttpStatusCode responseStatusCode) + public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(bool async, HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -1974,7 +1982,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.Send(async, initialMessage); // Verify status code. Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -2019,7 +2027,7 @@ await server.AcceptConnectionAsync(async connection => [ConditionalTheory] [MemberData(nameof(Interim1xxStatusCode))] - public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(HttpStatusCode responseStatusCode) + public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(bool async, HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2038,7 +2046,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.Send(async, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2063,8 +2071,9 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] - public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse() + [ConditionalTheory] + [MemberData(nameof(Async))] + public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse(bool async) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2082,7 +2091,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.Send(async, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2108,8 +2117,9 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] - public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually() + [ConditionalTheory] + [MemberData(nameof(Async))] + public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually(bool async) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2128,7 +2138,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.Send(async, initialMessage)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(ResponseString, await response.Content.ReadAsStringAsync()); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index c0d27c82f6400..726a30bc628d6 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -69,16 +70,19 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) }; } - public static IEnumerable Async() - { - yield return new object[] { true }; - yield return new object[] { false }; - } + public static readonly bool[] Bools = new[] { true, false }; + + public static IEnumerable Async() => Bools.Select(b => new object[] { b }); // For use by remote server tests public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; + public static IEnumerable AsyncRemoteServersMemberData() => + from async in Bools + from remoteServer in Configuration.Http.RemoteServers + select new object[] { async, remoteServer }; + protected HttpClient CreateHttpClientForRemoteServer(Configuration.Http.RemoteServer remoteServer) { return CreateHttpClientForRemoteServer(remoteServer, CreateHttpClientHandler()); From ecee08f564f7a4c68ff3d81b5ab88e3afecafdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 15 Apr 2020 20:10:10 +0200 Subject: [PATCH 12/60] net472 compilaris --- .../System/Net/Http/ByteAtATimeContent.cs | 2 + .../HttpClientHandlerTest.AutoRedirect.cs | 2 +- .../HttpClientHandlerTest.Cancellation.cs | 50 ++++++++----------- .../Net/Http/HttpClientHandlerTest.Cookies.cs | 2 +- .../Net/Http/HttpClientHandlerTest.Proxy.cs | 2 +- ...ttpClientHandlerTest.ServerCertificates.cs | 2 +- .../System/Net/Http/HttpClientHandlerTest.cs | 8 +-- .../Net/Http/HttpClientHandlerTestBase.cs | 20 ++++++-- 8 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs index 561c2f1e8baa2..c2700909dcd95 100644 --- a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs +++ b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs @@ -25,6 +25,7 @@ public ByteAtATimeContent(int length, Task waitToSend, TaskCompletionSource RemoteServersAndRedirectStatusCodes() public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() { - foreach (bool value in Bools) + foreach (bool value in AsyncBoolValues) { yield return new object[] {value, 300, "GET", "GET"}; yield return new object[] {value, 300, "POST", "GET"}; 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 dfeecad7afbe2..52b08a7324e63 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -532,15 +532,20 @@ await LoopbackServerFactory.CreateClientAndServerAsync( async uri => { using (var invoker = new HttpMessageInvoker(CreateHttpClientHandler())) - using (var req = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content, Version = UseVersion }) - try + using (var req = new HttpRequestMessage(HttpMethod.Post, uri) {Content = content, Version = UseVersion}) { - using (HttpResponseMessage resp = await invoker.SendAsync(req, cancellationTokenSource.Token)) + try + { + using (HttpResponseMessage resp = + await invoker.SendAsync(req, cancellationTokenSource.Token)) + { + Assert.Equal("Hello World", await resp.Content.ReadAsStringAsync()); + } + } + catch (OperationCanceledException) { - Assert.Equal("Hello World", await resp.Content.ReadAsStringAsync()); } } - catch (OperationCanceledException) { } }, async server => { @@ -593,36 +598,25 @@ public enum CancellationMode CancelPendingRequests = 0x2, DisposeHttpClient = 0x4 } - - public static IEnumerable OneBoolAndCancellationMode() => - from first in Bools - 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 Bools - from second in Bools + from async in AsyncBoolValues + from second in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } - select new object[] { first, second, mode }; + select new object[] { async, second, mode }; public static IEnumerable ThreeBoolsAndCancellationMode() => - from first in Bools - from second in Bools - from third in Bools + from async in AsyncBoolValues + from second in BoolValues + from third in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } - select new object[] { first, second, third, mode }; - - public static IEnumerable ThreeBools() => - from first in Bools - from second in Bools - from third in Bools - select new object[] { first, second, third }; + select new object[] { async, second, third, mode }; public static IEnumerable FourBools() => - from first in Bools - from second in Bools - from third in Bools - from fourth in Bools - select new object[] { first, second, third, fourth }; + from async in AsyncBoolValues + from second in BoolValues + from third in BoolValues + from fourth in BoolValues + select new object[] { async, second, third, fourth }; } } 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 a56c5ada283e6..9c9a275a94729 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -606,7 +606,7 @@ private static string GenerateCookie(string name, char repeat, int overallHeader public static IEnumerable CookieNamesValuesAndUseCookies() { - foreach (bool useCookies in Bools) + 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.Proxy.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs index 75950153caa43..7eff4098da860 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs @@ -385,7 +385,7 @@ public static IEnumerable BypassedProxies() public static IEnumerable CredentialsForProxy() { yield return new object[] { null, false }; - foreach (bool wrapCredsInCache in Bools) + 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 21e654b2d35a3..eb9be0a7ecacb 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ServerCertificates.cs @@ -182,7 +182,7 @@ public static IEnumerable UseCallback_ValidCertificate_ExpectedValuesD { if (remoteServer.IsSecure) { - foreach (bool checkRevocation in Bools) + 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 35ac60f0e016e..c8df5f1a65ca0 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -484,7 +484,7 @@ await LoopbackServer.CreateClientAndServerAsync(async proxyUri => public static IEnumerable SecureAndNonSecure_IPBasedUri_MemberData() => from address in new[] { IPAddress.Loopback, IPAddress.IPv6Loopback } - from useSsl in Bools + from useSsl in BoolValues select new object[] { address, useSsl }; [ConditionalTheory] @@ -1730,7 +1730,7 @@ public static IEnumerable VerifyUploadServersStreamsAndExpectedData get { foreach (Configuration.Http.RemoteServer remoteServer in Configuration.Http.RemoteServers) // target server - foreach (bool syncCopy in Bools) // 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); @@ -1867,7 +1867,7 @@ public static IEnumerable ExpectContinueVersion() var versions = new string[] {"1.0", "1.1", "2.0"}; var expectContinue = new bool?[] {true, false, null}; return - from async in Bools + from async in AsyncBoolValues from expect in expectContinue from version in versions select new object[] {async, expect, version}; @@ -1943,7 +1943,7 @@ await server.AcceptConnectionAsync(async connection => public static IEnumerable Interim1xxStatusCode() { - foreach (var async in Bools) + foreach (var async in BoolValues) { yield return new object[] {async, (HttpStatusCode)100}; // 100 Continue. // 101 SwitchingProtocols will be treated as a final status code. diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 726a30bc628d6..99dfe98ff80b5 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -70,16 +70,24 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) }; } - public static readonly bool[] Bools = new[] { true, false }; + public static readonly bool[] BoolValues = new[] { true, false }; + public static readonly bool[] AsyncBoolValues = + new[] + { + true, +#if NETCORE + false +#endif + }; - public static IEnumerable Async() => Bools.Select(b => new object[] { b }); + public static IEnumerable Async() => AsyncBoolValues.Select(b => new object[] { b }); // For use by remote server tests public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; public static IEnumerable AsyncRemoteServersMemberData() => - from async in Bools + from async in AsyncBoolValues from remoteServer in Configuration.Http.RemoteServers select new object[] { async, remoteServer }; @@ -145,9 +153,15 @@ public static Task Send(this HttpClient client, bool async, } 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 + // Do just async for framework, since it'll never have the sync API and + // it shouldn't even be called due to AsyncBoolValues returning only true. + return client.SendAsync(request, completionOption, cancellationToken); +#endif } } } From 34809a54b6be782536908d883509ebbfc545e9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 17 Apr 2020 16:08:30 +0200 Subject: [PATCH 13/60] Removed offloading of client/server tasks to another thread in LoopbackServer. --- src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index c6107bca8eef0..97a93b34af282 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -77,8 +77,8 @@ public static Task CreateClientAndServerAsync(Func clientFunc, Func { - Task clientTask = Task.Run(() => clientFunc(server.Address)); - Task serverTask = Task.Run(() => serverFunc(server)); + Task clientTask = clientFunc(server.Address); + Task serverTask = serverFunc(server); await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed().ConfigureAwait(false); }, options); From a2a653ecf380063f9b61e57dd7070b1dc264ebb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 23 Apr 2020 11:20:49 +0200 Subject: [PATCH 14/60] Removed unused method. --- .../Http/SocketsHttpHandler/CreditManager.cs | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs index 0cd537d6d560b..f49d61bc4c0b3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs @@ -36,50 +36,6 @@ private object SyncObject get => this; } - public int RequestCredit(int amount, CancellationToken cancellationToken) - { - lock (SyncObject) - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(CreditManager)); - } - - if (_current > 0) - { - Debug.Assert(_waitersTail is null, "Shouldn't have waiters when credit is available"); - - int granted = Math.Min(amount, _current); - if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, current={_current}, granted={granted}"); - _current -= granted; - return granted; - } - - if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, no credit available."); - - // Otherwise, create a new waiter. - CreditWaiter waiter = cancellationToken.CanBeCanceled ? - new CancelableCreditWaiter(SyncObject, cancellationToken) : - new CreditWaiter(); - waiter.Amount = amount; - - // Add the waiter at the tail of the queue. - if (_waitersTail is null) - { - _waitersTail = waiter.Next = waiter; - } - else - { - waiter.Next = _waitersTail.Next; - _waitersTail.Next = waiter; - _waitersTail = waiter; - } - - // TODO: This is sync-over-async... don't see a good way around this. - return waiter.AsValueTask().AsTask().GetAwaiter().GetResult(); - } - } - public ValueTask RequestCreditAsync(int amount, CancellationToken cancellationToken) { lock (SyncObject) From 724241a747359732568362cb67e880f3a059376c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 23 Apr 2020 21:39:16 +0200 Subject: [PATCH 15/60] Cory's comments. Nits, asserts, default parameters --- .../HttpClientHandlerTest.AutoRedirect.cs | 21 +++++++++++-------- .../System/Net/Http/HttpClientHandlerTest.cs | 2 +- .../Net/Http/HttpClientHandlerTestBase.cs | 6 +++--- .../src/System/Net/Http/ByteArrayContent.cs | 5 ++++- .../src/System/Net/Http/HttpClient.cs | 4 ++-- .../Net/Http/MessageProcessingHandler.cs | 4 ++-- .../src/System/Net/Http/MultipartContent.cs | 6 +++++- .../ConnectionCloseReadStream.cs | 2 +- .../ContentLengthReadStream.cs | 2 +- .../Http/SocketsHttpHandler/Http2Stream.cs | 2 +- .../src/System/Net/Http/StreamContent.cs | 5 ++++- 11 files changed, 36 insertions(+), 23 deletions(-) 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 50770c1e29de6..c7496ea552c3f 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs @@ -70,6 +70,17 @@ public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() } } + public static IEnumerable RedirectStatusCodesAsync() + { + foreach (bool value in AsyncBoolValues) + { + yield return new object[] { value, 300 }; + yield return new object[] { value, 301 }; + yield return new object[] { value, 302 }; + yield return new object[] { value, 303 }; + } + } + public HttpClientHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } [OuterLoop("Uses external server")] @@ -145,15 +156,7 @@ await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => } } - [Theory] - [InlineData(true, 300)] - [InlineData(true, 301)] - [InlineData(true, 302)] - [InlineData(true, 303)] - [InlineData(false, 300)] - [InlineData(false, 301)] - [InlineData(false, 302)] - [InlineData(false, 303)] + [Theory, MemberData(nameof(RedirectStatusCodesAsync))] public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(bool async, int statusCode) { HttpClientHandler handler = CreateHttpClientHandler(); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index cb2d16f1fccba..034c6e8ce869b 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1943,7 +1943,7 @@ await server.AcceptConnectionAsync(async connection => public static IEnumerable Interim1xxStatusCode() { - foreach (var async in BoolValues) + foreach (var async in AsyncBoolValues) { yield return new object[] {async, (HttpStatusCode)100}; // 100 Continue. // 101 SwitchingProtocols will be treated as a final status code. diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 99dfe98ff80b5..98c0aa97937a2 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -158,9 +158,9 @@ public static Task Send(this HttpClient client, bool async, // 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 - // Do just async for framework, since it'll never have the sync API and - // it shouldn't even be called due to AsyncBoolValues returning only true. - return client.SendAsync(request, completionOption, cancellationToken); + // Framework won't ever have the sync API. + // This shouldn't be called due to AsyncBoolValues returning only true on Framework. + Debug.Assert(async); #endif } } 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 d985b92d6e688..29c3a16532f6e 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 @@ -61,7 +61,10 @@ private protected Task SerializeToStreamAsyncCore(Stream stream, CancellationTok protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - Debug.Assert(stream != null); + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. cancellationToken.ThrowIfCancellationRequested(); 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 06df9a9d019f1..fc13dfeb54a3d 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 @@ -447,12 +447,12 @@ public Task DeleteAsync(Uri? requestUri, CancellationToken public HttpResponseMessage Send(HttpRequestMessage request) { - return Send(request, defaultCompletionOption, default); + return Send(request, defaultCompletionOption, cancellationToken: default); } public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption) { - return Send(request, completionOption, default); + return Send(request, completionOption, cancellationToken: default); } public override HttpResponseMessage Send(HttpRequestMessage request, 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 a63b3115b07d1..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 @@ -37,9 +37,9 @@ protected internal sealed override HttpResponseMessage Send(HttpRequestMessage r } // Since most of the SendAsync code is just Task handling, there's no reason to share the code. - HttpRequestMessage newRequestMessage = ProcessRequest(request, default); + HttpRequestMessage newRequestMessage = ProcessRequest(request, cancellationToken); HttpResponseMessage response = base.Send(newRequestMessage, cancellationToken); - HttpResponseMessage newResponseMessage = ProcessResponse(response, default); + HttpResponseMessage newResponseMessage = ProcessResponse(response, cancellationToken); return newResponseMessage; } 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 cafad28f49331..75bafeec23e22 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 @@ -168,7 +168,11 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() // then the stream will be closed an exception thrown. protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - Debug.Assert(stream != null); + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + try { // Write start boundary. 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 38575ba371550..c9fa2b37a91ae 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 @@ -104,7 +104,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.CompletedTask; } - Task copyTask = connection.CopyToUntilEofAsync(destination, async:true, 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 66c3e5cd97899..49afe0cbc7a0c 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 @@ -132,7 +132,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.CompletedTask; } - Task copyTask = _connection.CopyToContentLengthAsync(destination, async:true, _contentBytesRemaining, bufferSize, cancellationToken); + Task copyTask = _connection.CopyToContentLengthAsync(destination, async: true, _contentBytesRemaining, bufferSize, cancellationToken); if (copyTask.IsCompletedSuccessfully) { Finish(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index dc2c8d634dfd8..b67d38d07b7da 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -1375,7 +1375,7 @@ public override void Write(byte[] buffer, int offset, int count) } // Sync-over-async. See comment on Http2Connection.SendAsync. - http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), default).AsTask().GetAwaiter().GetResult(); + http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken: default).AsTask().GetAwaiter().GetResult(); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) 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 31dd6c4b4837f..d8b5872a70471 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 @@ -55,7 +55,10 @@ private void InitializeContent(Stream content, int bufferSize) protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - Debug.Assert(stream != null); + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } PrepareContent(); From 07699aa8235843b48e53be69e368104046196b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 24 Apr 2020 13:42:43 +0200 Subject: [PATCH 16/60] Stephen's nits, renames, comments. --- .../HttpClientHandlerTest.Authentication.cs | 4 +- .../HttpClientHandlerTest.AutoRedirect.cs | 46 ++++++++----------- .../HttpClientHandlerTest.Cancellation.cs | 22 ++++----- .../Net/Http/HttpClientHandlerTest.Cookies.cs | 16 +++---- .../System/Net/Http/HttpClientHandlerTest.cs | 34 +++++++------- .../Net/Http/HttpClientHandlerTestBase.cs | 8 ++-- .../src/System/Net/Http/DiagnosticsHandler.cs | 15 ++---- .../Http/SocketsHttpHandler/ConnectHelper.cs | 5 +- .../Http/SocketsHttpHandler/HttpConnection.cs | 2 +- .../HttpMessageHandlerStage.cs | 4 +- .../tests/FunctionalTests/HttpClientTest.cs | 14 +++--- 11 files changed, 78 insertions(+), 92 deletions(-) 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 8181f9f763107..6255cbdc05cf5 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs @@ -552,7 +552,7 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_Success() } [ConditionalTheory(nameof(IsDomainJoinedServerAvailable))] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success(bool async) { using (HttpClientHandler handler = CreateHttpClientHandler()) @@ -569,7 +569,7 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHost _output.WriteLine(request.RequestUri.AbsoluteUri.ToString()); _output.WriteLine($"Host: {request.Headers.Host}"); - using (HttpResponseMessage response = await client.Send(async, request)) + using (HttpResponseMessage response = await client.SendAsync(async, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string body = await response.Content.ReadAsStringAsync(); 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 c7496ea552c3f..04e26c15d1dc7 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AutoRedirect.cs @@ -40,37 +40,27 @@ public static IEnumerable RemoteServersAndRedirectStatusCodes() } } - public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() + public static IEnumerable AsyncRedirectStatusCodesOldMethodsNewMethods() { foreach (bool value in AsyncBoolValues) { - yield return new object[] {value, 300, "GET", "GET"}; - yield return new object[] {value, 300, "POST", "GET"}; - yield return new object[] {value, 300, "HEAD", "HEAD"}; - - yield return new object[] {value, 301, "GET", "GET"}; - yield return new object[] {value, 301, "POST", "GET"}; - yield return new object[] {value, 301, "HEAD", "HEAD"}; - - yield return new object[] {value, 302, "GET", "GET"}; - yield return new object[] {value, 302, "POST", "GET"}; - yield return new object[] {value, 302, "HEAD", "HEAD"}; - - yield return new object[] {value, 303, "GET", "GET"}; - yield return new object[] {value, 303, "POST", "GET"}; - yield return new object[] {value, 303, "HEAD", "HEAD"}; - - yield return new object[] {value, 307, "GET", "GET"}; - yield return new object[] {value, 307, "POST", "POST"}; - yield return new object[] {value, 307, "HEAD", "HEAD"}; + foreach (int statusCode in new[] { 300, 301, 302, 303 }) + { + yield return new object[] { value, statusCode, "GET", "GET" }; + yield return new object[] { value, statusCode, "POST", "GET" }; + yield return new object[] { value, statusCode, "HEAD", "HEAD" }; + } - yield return new object[] {value, 308, "GET", "GET"}; - yield return new object[] {value, 308, "POST", "POST"}; - yield return new object[] {value, 308, "HEAD", "HEAD"}; + foreach (int statusCode in new[] {307, 308}) + { + yield return new object[] {value, statusCode, "GET", "GET"}; + yield return new object[] {value, statusCode, "POST", "POST"}; + yield return new object[] {value, 307, "HEAD", "HEAD"}; + } } } - public static IEnumerable RedirectStatusCodesAsync() + public static IEnumerable AsyncRedirectStatusCodes() { foreach (bool value in AsyncBoolValues) { @@ -110,7 +100,7 @@ public async Task GetAsync_AllowAutoRedirectFalse_RedirectFromHttpToHttp_StatusC } } - [Theory, MemberData(nameof(RedirectStatusCodesOldMethodsNewMethods))] + [Theory, MemberData(nameof(AsyncRedirectStatusCodesOldMethodsNewMethods))] public async Task AllowAutoRedirect_True_ValidateNewMethodUsedOnRedirection( bool async, int statusCode, string oldMethod, string newMethod) { @@ -127,7 +117,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => { var request = new HttpRequestMessage(new HttpMethod(oldMethod), origUrl) { Version = UseVersion }; - Task getResponseTask = client.Send(async, request); + Task getResponseTask = client.SendAsync(async, request); await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => { @@ -156,7 +146,7 @@ await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => } } - [Theory, MemberData(nameof(RedirectStatusCodesAsync))] + [Theory, MemberData(nameof(AsyncRedirectStatusCodes))] public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(bool async, int statusCode) { HttpClientHandler handler = CreateHttpClientHandler(); @@ -168,7 +158,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => request.Content = new StringContent(ExpectedContent); request.Headers.TransferEncodingChunked = true; - Task getResponseTask = client.Send(async, request); + Task getResponseTask = client.SendAsync(async, 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 52b08a7324e63..42367da8f783f 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -27,7 +27,7 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } [ConditionalTheory] - [MemberData(nameof(TwoBoolsAndCancellationMode))] + [MemberData(nameof(AsyncAndBoolAndCancellationMode))] public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool async, bool chunkedTransfer, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer) @@ -59,7 +59,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => req.Content = new ByteAtATimeContent(int.MaxValue, waitToSend.Task, contentSending, millisecondDelayBetweenBytes: 1); req.Headers.TransferEncodingChunked = chunkedTransfer; - Task resp = client.Send(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task resp = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); waitToSend.SetResult(true); await Task.WhenAny(contentSending.Task, resp); if (!resp.IsCompleted) @@ -84,7 +84,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => } [ConditionalTheory] - [MemberData(nameof(TwoBoolsAndCancellationMode))] + [MemberData(nameof(AsyncAndBoolAndCancellationMode))] public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool async, bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && connectionClose) @@ -124,7 +124,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.Send(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await partialResponseHeadersSent.Task; Cancel(mode, client, cts); await getResponse; @@ -141,7 +141,7 @@ await ValidateClientCancellationAsync(async () => [Theory] [ActiveIssue("https://github.com/dotnet/runtime/issues/25760")] - [MemberData(nameof(ThreeBoolsAndCancellationMode))] + [MemberData(nameof(AsyncAndTwoBoolsAndCancellationMode))] public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) @@ -180,7 +180,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.Send(async, req, HttpCompletionOption.ResponseContentRead, cts.Token); + Task getResponse = client.SendAsync(async, 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); @@ -197,7 +197,7 @@ await ValidateClientCancellationAsync(async () => } [ConditionalTheory] - [MemberData(nameof(FourBools))] + [MemberData(nameof(AsyncAndThreeBools))] public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) @@ -238,7 +238,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.Send(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await ValidateClientCancellationAsync(async () => { HttpResponseMessage resp = await getResponse; @@ -599,20 +599,20 @@ public enum CancellationMode DisposeHttpClient = 0x4 } - public static IEnumerable TwoBoolsAndCancellationMode() => + public static IEnumerable AsyncAndBoolAndCancellationMode() => from async in AsyncBoolValues from second in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { async, second, mode }; - public static IEnumerable ThreeBoolsAndCancellationMode() => + public static IEnumerable AsyncAndTwoBoolsAndCancellationMode() => from async in AsyncBoolValues from second in BoolValues from third in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } select new object[] { async, second, third, mode }; - public static IEnumerable FourBools() => + public static IEnumerable AsyncAndThreeBools() => from async in AsyncBoolValues from second in BoolValues from third in BoolValues 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 9c9a275a94729..eaa7c26ab01f6 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -127,7 +127,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( } [Theory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task GetAsync_AddCookieHeader_CookieHeaderSent(bool async) { await LoopbackServerFactory.CreateClientAndServerAsync( @@ -138,7 +138,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; requestMessage.Headers.Add("Cookie", s_customCookieHeaderValue); - await client.Send(async, requestMessage); + await client.SendAsync(async, requestMessage); } }, async server => @@ -149,7 +149,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( } [Theory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent(bool async) { await LoopbackServerFactory.CreateClientAndServerAsync( @@ -162,7 +162,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( requestMessage.Headers.Add("Cookie", "B=2"); requestMessage.Headers.Add("Cookie", "C=3"); - await client.Send(async, requestMessage); + await client.SendAsync(async, requestMessage); } }, async server => @@ -214,7 +214,7 @@ private string GetCookieValue(HttpRequestData request) } [Theory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent(bool async) { await LoopbackServerFactory.CreateServerAsync(async (server, url) => @@ -227,7 +227,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.Send(async, requestMessage); + Task getResponseTask = client.SendAsync(async, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -242,7 +242,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => } [Theory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent(bool async) { await LoopbackServerFactory.CreateServerAsync(async (server, url) => @@ -256,7 +256,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => requestMessage.Headers.Add("Cookie", "A=1"); requestMessage.Headers.Add("Cookie", "B=2"); - Task getResponseTask = client.Send(async, requestMessage); + Task getResponseTask = client.SendAsync(async, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 034c6e8ce869b..8a57ad9c95013 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -754,7 +754,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => } [ConditionalTheory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly(bool async) { #if WINHTTPHANDLER_TEST @@ -839,7 +839,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => request.Headers.Add("X-Underscore_Name", "X-Underscore_Name"); request.Headers.Add("X-End", "End"); - (await client.Send(async, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); + (await client.SendAsync(async, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); } }, async server => { @@ -1189,14 +1189,14 @@ await LoopbackServer.CreateServerAsync(async (server, url) => } [Theory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws(bool async) { var req = new HttpRequestMessage(HttpMethod.Post, "http://bing.com") { Version = UseVersion }; req.Headers.TransferEncodingChunked = true; using (HttpClient c = CreateHttpClient()) { - HttpRequestException error = await Assert.ThrowsAsync(() => c.Send(async, req)); + HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(async, req)); Assert.IsType(error.InnerException); } } @@ -1233,7 +1233,7 @@ public async Task SendAsync_HttpRequestMsgResponseHeadersRead_StatusCodeOK(bool HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, remoteServer.EchoUri) { Version = remoteServer.HttpVersion }; using (HttpClient client = CreateHttpClientForRemoteServer(remoteServer)) { - using (HttpResponseMessage response = await client.Send(async, request, HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await client.SendAsync(async, request, HttpCompletionOption.ResponseHeadersRead)) { string responseContent = await response.Content.ReadAsStringAsync(); _output.WriteLine(responseContent); @@ -1862,7 +1862,7 @@ public async Task PostAsync_CallMethod_EmptyContent(Configuration.Http.RemoteSer } } - public static IEnumerable ExpectContinueVersion() + public static IEnumerable AsyncExpectContinueVersion() { var versions = new string[] {"1.0", "1.1", "2.0"}; var expectContinue = new bool?[] {true, false, null}; @@ -1875,7 +1875,7 @@ from version in versions [OuterLoop("Uses external server")] [Theory] - [MemberData(nameof(ExpectContinueVersion))] + [MemberData(nameof(AsyncExpectContinueVersion))] public async Task PostAsync_ExpectContinue_Success(bool async, bool? expectContinue, string version) { using (HttpClient client = CreateHttpClient()) @@ -1887,7 +1887,7 @@ public async Task PostAsync_ExpectContinue_Success(bool async, bool? expectConti }; req.Headers.ExpectContinue = expectContinue; - using (HttpResponseMessage response = await client.Send(async, req)) + using (HttpResponseMessage response = await client.SendAsync(async, req)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -1941,7 +1941,7 @@ await server.AcceptConnectionAsync(async connection => }); } - public static IEnumerable Interim1xxStatusCode() + public static IEnumerable AsyncInterim1xxStatusCode() { foreach (var async in AsyncBoolValues) { @@ -1956,7 +1956,7 @@ public static IEnumerable Interim1xxStatusCode() } [ConditionalTheory] - [MemberData(nameof(Interim1xxStatusCode))] + [MemberData(nameof(AsyncInterim1xxStatusCode))] public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(bool async, HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST @@ -1982,7 +1982,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.Send(async, initialMessage); + HttpResponseMessage response = await client.SendAsync(async, initialMessage); // Verify status code. Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -2026,7 +2026,7 @@ await server.AcceptConnectionAsync(async connection => } [ConditionalTheory] - [MemberData(nameof(Interim1xxStatusCode))] + [MemberData(nameof(AsyncInterim1xxStatusCode))] public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(bool async, HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST @@ -2046,7 +2046,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => initialMessage.Content = new StringContent(TestString); // No ExpectContinue header. initialMessage.Headers.ExpectContinue = false; - HttpResponseMessage response = await client.Send(async, initialMessage); + HttpResponseMessage response = await client.SendAsync(async, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2072,7 +2072,7 @@ await server.AcceptConnectionAsync(async connection => } [ConditionalTheory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse(bool async) { #if WINHTTPHANDLER_TEST @@ -2091,7 +2091,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.Send(async, initialMessage); + HttpResponseMessage response = await client.SendAsync(async, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2118,7 +2118,7 @@ await server.AcceptConnectionAsync(async connection => } [ConditionalTheory] - [MemberData(nameof(Async))] + [MemberData(nameof(AsyncBoolMemberData))] public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually(bool async) { #if WINHTTPHANDLER_TEST @@ -2138,7 +2138,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.Send(async, initialMessage)) + using (HttpResponseMessage response = await client.SendAsync(async, initialMessage)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(ResponseString, await response.Content.ReadAsStringAsync()); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 98c0aa97937a2..3bdc2e176437b 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.Linq; using System.Net.Test.Common; @@ -80,7 +81,7 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) #endif }; - public static IEnumerable Async() => AsyncBoolValues.Select(b => new object[] { b }); + public static IEnumerable AsyncBoolMemberData() => AsyncBoolValues.Select(b => new object[] { b }); // For use by remote server tests @@ -145,7 +146,7 @@ protected override async Task SendAsync(HttpRequestMessage public static class HttpClientExtensions { - public static Task Send(this HttpClient client, bool async, HttpRequestMessage request, HttpCompletionOption completionOption = default, CancellationToken cancellationToken = default) + public static Task SendAsync(this HttpClient client, bool async, HttpRequestMessage request, HttpCompletionOption completionOption = default, CancellationToken cancellationToken = default) { if (async) { @@ -160,7 +161,8 @@ public static Task Send(this HttpClient client, bool async, #else // Framework won't ever have the sync API. // This shouldn't be called due to AsyncBoolValues returning only true on Framework. - Debug.Assert(async); + Debug.Fail("Framework doesn't have Sync API and it shouldn't be attempted to be tested."); + return Task.FromResult(null); #endif } } 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 05cdacbe47fd4..41e99127aaf2a 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 @@ -36,14 +36,14 @@ internal static bool IsEnabled() protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - ValueTask sendTask = SendAsyncCore(request, false, 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, true, cancellationToken).AsTask(); + SendAsyncCore(request, async: true, cancellationToken).AsTask(); private async ValueTask SendAsyncCore(HttpRequestMessage request, bool async, CancellationToken cancellationToken) @@ -71,14 +71,9 @@ private async ValueTask SendAsyncCore(HttpRequestMessage re try { - if (async) - { - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - } - else - { - return base.Send(request, cancellationToken); - } + return async ? + await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : + base.Send(request, cancellationToken); } finally { 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 db9cab4949eba..9b579e81af81b 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 @@ -41,10 +41,9 @@ public static async ValueTask ConnectAsync(string host, int port, bool a if (!async) { cancellationToken.ThrowIfCancellationRequested(); - Socket? socket = null; + Socket? socket = new Socket(SocketType.Stream, ProtocolType.Tcp); try { - socket = new Socket(SocketType.Stream, ProtocolType.Tcp); socket.NoDelay = true; using (cancellationToken.UnsafeRegister(s => ((Socket)s!).Dispose(), socket)) { @@ -53,7 +52,7 @@ public static async ValueTask ConnectAsync(string host, int port, bool a } catch { - socket?.Dispose(); + socket.Dispose(); throw; } 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 4a6d9bfccd6b9..491f450d79305 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 @@ -477,7 +477,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, ValueTask? t = ConsumeReadAheadTask(); if (t != null) { - // Handle the pre-emptive read. For the asyc==false case, hopefully the read has + // 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 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 index 19196f9e27120..2a8ed849d9622 100644 --- 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 @@ -9,7 +9,7 @@ internal abstract class HttpMessageHandlerStage : HttpMessageHandler protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - ValueTask sendTask = SendAsync(request, false, cancellationToken); + ValueTask sendTask = SendAsync(request, async:false, cancellationToken); // At least for HTTP 1.1 we must support fully sync Send Debug.Assert(request.Version.Major >= 2 || sendTask.IsCompleted); return sendTask.IsCompleted ? sendTask.GetAwaiter().GetResult() : sendTask.AsTask().GetAwaiter().GetResult(); @@ -17,7 +17,7 @@ protected internal sealed override HttpResponseMessage Send(HttpRequestMessage r protected internal sealed override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => - SendAsync(request, true, cancellationToken).AsTask(); + SendAsync(request, async: true, cancellationToken).AsTask(); internal abstract ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 9da387de4a4eb..51253acc5ad8d 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -326,34 +326,34 @@ public async Task GetAsync_CustomException_Asynchronous_ThrowsException() } } - [Theory, MemberData(nameof(Async))] + [Theory, MemberData(nameof(AsyncBoolMemberData))] public async Task Send_NullRequest_ThrowsException(bool async) { using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(null)))) { - await AssertExtensions.ThrowsAsync("request", () => client.Send(async, null)); + await AssertExtensions.ThrowsAsync("request", () => client.SendAsync(async, null)); } } - [Theory, MemberData(nameof(Async))] + [Theory, MemberData(nameof(AsyncBoolMemberData))] public async Task Send_DuplicateRequest_ThrowsException(bool async) { using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) { - (await client.Send(async, request)).Dispose(); - await Assert.ThrowsAsync(() => client.Send(async, request)); + (await client.SendAsync(async, request)).Dispose(); + await Assert.ThrowsAsync(() => client.SendAsync(async, request)); } } - [Theory, MemberData(nameof(Async))] + [Theory, MemberData(nameof(AsyncBoolMemberData))] public async Task Send_RequestContentNotDisposed(bool async) { 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.Send(async, request); + await client.SendAsync(async, request); await content.ReadAsStringAsync(); // no exception } } From d25d2d6272a6ed9689c1e05e756e2ad3a645a877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 28 Apr 2020 13:47:54 +0200 Subject: [PATCH 17/60] 100-continue todo resolved. --- .../System/Net/Http/SocketsHttpHandler/HttpConnection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 491f450d79305..2acec49423e13 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 @@ -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); } } @@ -793,11 +793,11 @@ private async ValueTask SendRequestContentAsync(HttpRequestMessage request, Http } 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. - bool sendRequestContent = await allowExpect100ToContinueTask.ConfigureAwait(false); // TODO: decide what to do about this + bool sendRequestContent = await allowExpect100ToContinueTask.ConfigureAwait(false); // Clean up the timer; it's no longer needed. expect100Timer.Dispose(); @@ -806,7 +806,7 @@ private async Task SendRequestContentWithExpect100ContinueAsync( if (sendRequestContent) { if (NetEventSource.IsEnabled) Trace($"Sending request content for Expect: 100-continue."); - await SendRequestContentAsync(request, stream, async: true, cancellationToken).ConfigureAwait(false); // TODO: is async: true correct here? + await SendRequestContentAsync(request, stream, async, cancellationToken).ConfigureAwait(false); } else { From c00d8eabc1d420e274b354462e77d850851d2071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 28 Apr 2020 14:54:12 +0200 Subject: [PATCH 18/60] ByteAtATimeContent sync over async for tests. --- .../tests/System/Net/Http/ByteAtATimeContent.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs index c2700909dcd95..7c32624c77830 100644 --- a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs +++ b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs @@ -27,20 +27,8 @@ public ByteAtATimeContent(int length, Task waitToSend, TaskCompletionSource + SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); #endif protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) From 9a78f71eb82a4fcab079a8e8c84aec5f65775512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 30 Apr 2020 16:45:59 +0200 Subject: [PATCH 19/60] HttpClient and HttpContent refactored duplicate code. --- .../src/System/Net/Http/HttpClient.cs | 186 ++++++------------ .../src/System/Net/Http/HttpContent.cs | 48 +++-- 2 files changed, 89 insertions(+), 145 deletions(-) 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 fc13dfeb54a3d..85a183dc7f55d 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 @@ -464,64 +464,9 @@ public override HttpResponseMessage Send(HttpRequestMessage request, public HttpResponseMessage Send(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - CheckDisposed(); - CheckRequestMessage(request); - - SetOperationStarted(); - 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; - } - - // Initiate the send. - HttpResponseMessage response; - try - { - response = base.Send(request, cts.Token); - } - catch (Exception e) - { - HandleFinishSendCleanup(cts, disposeCts); - - if (e is OperationCanceledException operationException && TimeoutFired(cancellationToken, timeoutTime)) - { - throw CreateTimeoutException(operationException); - } - - throw; - } - - bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && - !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); - - return FinishSend(response, request, cts, disposeCts, buffered, cancellationToken, timeoutTime); + ValueTask sendTask = SendAsyncCore(request, completionOption, async: false, cancellationToken); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); } public Task SendAsync(HttpRequestMessage request) @@ -543,6 +488,13 @@ 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) { throw new ArgumentNullException(nameof(request)); @@ -554,36 +506,17 @@ 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) { @@ -600,55 +533,26 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl bool buffered = completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase); - return FinishSendAsync(sendTask, request, cts, disposeCts, buffered, cancellationToken, timeoutTime); + return FinishSendAsync(responseTask, request, cts, disposeCts, buffered, async, cancellationToken, timeoutTime); } - private HttpResponseMessage FinishSend(HttpResponseMessage response, HttpRequestMessage request, CancellationTokenSource cts, - bool disposeCts, bool buffered, 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 { - if (response == null) + if (async) { - throw new InvalidOperationException(SR.net_http_handler_noresponse); - } - - // Buffer the response content if we've been asked to and we have a Content to buffer. - if (buffered && response.Content != null) - { - response.Content.LoadIntoBuffer(_maxResponseContentBufferSize, cts.Token); + // Wait for the send request to complete, getting back the response. + response = await sendTask.ConfigureAwait(false); } - - if (NetEventSource.IsEnabled) NetEventSource.ClientSendCompleted(this, response, request); - return response; - } - catch (Exception e) - { - response?.Dispose(); - - if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime)) + else { - HandleSendTimeout(operationException); - throw CreateTimeoutException(operationException); + // In sync scenario the ValueTask already contains the result, it has been constructed that way in Send method. + response = sendTask.Result; } - HandleFinishSendAsyncError(e, cts); - throw; - } - finally - { - HandleFinishSendCleanup(cts, disposeCts); - } - } - - private async Task FinishSendAsync(Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, - bool disposeCts, bool buffered, CancellationToken callerToken, long timeoutTime) - { - HttpResponseMessage? response = null; - try - { - // Wait for the send request to complete, getting back the response. - response = await sendTask.ConfigureAwait(false); if (response == null) { throw new InvalidOperationException(SR.net_http_handler_noresponse); @@ -657,7 +561,15 @@ private async Task FinishSendAsync(Task 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. + // If we already buffered the content, just return. return; } - MemoryStream? tempBuffer = CreateMemoryStream(maxBufferSize, out Exception? error); if (tempBuffer == null) { throw error!; @@ -473,22 +464,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. @@ -582,9 +564,33 @@ protected virtual Task CreateContentReadStreamAsync(CancellationToken ca // to do so. _canCalculateLength = false; } + 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; From c1de37cd9af83a438843bcf3e343ba7180a09065 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Wed, 13 May 2020 16:43:19 +0200 Subject: [PATCH 20/60] Throwing NSE instead of sync-over-async. --- .../System.Net.Http/src/Resources/Strings.resx | 5 ++++- .../src/System/Net/Http/HttpContent.cs | 11 ++++------- .../src/System/Net/Http/HttpMessageHandler.cs | 5 ++--- .../Net/Http/SocketsHttpHandler/Http2Connection.cs | 6 ------ .../SocketsHttpHandler/HttpMessageHandlerStage.cs | 10 +++++++--- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 4074b8e08edaa..4a5e22fc6b357 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -555,4 +555,7 @@ Stream aborted by peer ({0}). - \ No newline at end of file + + The synchronous API is not supported by '{0}'. If you're using custom '{1}' and wish to use synchronous HTTP APIs, you must override '{2}' virtual method. + + 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 ac63e1a559c12..89989a596b89f 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,15 +316,12 @@ 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) { - // No choice but to do sync-over-async. Derived types should override this whenever possible. - // Thankfully most CreateContentReadStreamAsync implementations actually complete synchronously. - Stream source = CreateContentReadStreamAsync(cancellationToken).GetAwaiter().GetResult(); - - // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. - cancellationToken.ThrowIfCancellationRequested(); - source.CopyTo(stream); + throw new NotSupportedException(SR.Format(SR.net_http_missing_sync_implementation, typeof(HttpContent), GetType(), nameof(SerializeToStream))); } protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => 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 e7d3b45654c52..04a87cfe9be47 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 @@ -19,11 +19,10 @@ protected HttpMessageHandler() // 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 have no other option that to do sync-over-async. + // Unfortunately we cannot force everyone to implement so in such case we throw NSE. protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Doing sync-over-async due to lack of {nameof(Send)} override"); - return SendAsync(request, cancellationToken).GetAwaiter().GetResult(); + throw new NotSupportedException(SR.Format(SR.net_http_missing_sync_implementation, typeof(HttpMessageHandler), GetType(), nameof(Send))); } protected internal abstract Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); 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 d312c47c15b7d..77903c1e6a1eb 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 @@ -1754,12 +1754,6 @@ private enum FlushTiming Now } - // TODO: Can we do better for async=false? Right now the caller will block waiting for this to complete. - // But this is also sharing the connection with many other requests, which makes it challenging. At a minimum - // a request may need to block while waiting for other threads to finish their work: the question is just - // whether we could make it so that at least one of the request's threads is not blocked, which would - // mean that the connection processing loop would need to be entirely restructured. - // Note that this is safe to be called concurrently by multiple threads. public sealed override async Task SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) 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 index 2a8ed849d9622..203cf7638b427 100644 --- 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 @@ -9,10 +9,14 @@ internal abstract class HttpMessageHandlerStage : HttpMessageHandler protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { + if (request.Version.Major >= 2) + { + throw new NotSupportedException(); + } + ValueTask sendTask = SendAsync(request, async:false, cancellationToken); - // At least for HTTP 1.1 we must support fully sync Send - Debug.Assert(request.Version.Major >= 2 || sendTask.IsCompleted); - return sendTask.IsCompleted ? sendTask.GetAwaiter().GetResult() : sendTask.AsTask().GetAwaiter().GetResult(); + Debug.Assert(sendTask.IsCompleted); + return sendTask.GetAwaiter().GetResult(); } protected internal sealed override Task SendAsync(HttpRequestMessage request, From 6de79c245b5ff71f9f9d9cc348ecfdbf37dfc75d Mon Sep 17 00:00:00 2001 From: ManickaP Date: Wed, 13 May 2020 17:52:14 +0200 Subject: [PATCH 21/60] Fixed compiler constant. --- .../System/Net/Http/HttpClientHandlerTest.Decompression.cs | 2 +- .../Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 3bdc2e176437b..36fbc2aef0339 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -76,7 +76,7 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) new[] { true, -#if NETCORE +#if NETCOREAPP false #endif }; From 0a32c8475ac4096df327c74252951d53a9e08e8b Mon Sep 17 00:00:00 2001 From: ManickaP Date: Thu, 14 May 2020 15:52:22 +0200 Subject: [PATCH 22/60] Reworked tests to use inheritance instead of a test parameter. MemberData must be static. Current HttpVersion (virtual property) cannot be used in data generation (i.e. AsyncBoolValue). --- .../HttpClientHandlerTest.AutoRedirect.cs | 48 +- .../HttpClientHandlerTest.Cancellation.cs | 135 +- .../Net/Http/HttpClientHandlerTestBase.cs | 24 +- .../HttpConnectionResponseContent.cs | 20 +- .../FunctionalTests/SocketsHttpHandlerTest.cs | 74 +- .../FunctionalTests/SyncHttpHandlerTest.cs | 2047 +++++++++++++++++ .../System.Net.Http.Functional.Tests.csproj | 1 + 7 files changed, 2209 insertions(+), 140 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs 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 04e26c15d1dc7..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,37 +40,15 @@ public static IEnumerable RemoteServersAndRedirectStatusCodes() } } - public static IEnumerable AsyncRedirectStatusCodesOldMethodsNewMethods() + public static IEnumerable RedirectStatusCodesOldMethodsNewMethods() { - foreach (bool value in AsyncBoolValues) + foreach (int statusCode in new[] { 300, 301, 302, 303, 307, 308 }) { - foreach (int statusCode in new[] { 300, 301, 302, 303 }) - { - yield return new object[] { value, statusCode, "GET", "GET" }; - yield return new object[] { value, statusCode, "POST", "GET" }; - yield return new object[] { value, statusCode, "HEAD", "HEAD" }; - } - - foreach (int statusCode in new[] {307, 308}) - { - yield return new object[] {value, statusCode, "GET", "GET"}; - yield return new object[] {value, statusCode, "POST", "POST"}; - yield return new object[] {value, 307, "HEAD", "HEAD"}; - } - } - } - - public static IEnumerable AsyncRedirectStatusCodes() - { - foreach (bool value in AsyncBoolValues) - { - yield return new object[] { value, 300 }; - yield return new object[] { value, 301 }; - yield return new object[] { value, 302 }; - yield return new object[] { value, 303 }; + 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")] @@ -100,9 +78,9 @@ public async Task GetAsync_AllowAutoRedirectFalse_RedirectFromHttpToHttp_StatusC } } - [Theory, MemberData(nameof(AsyncRedirectStatusCodesOldMethodsNewMethods))] + [Theory, MemberData(nameof(RedirectStatusCodesOldMethodsNewMethods))] public async Task AllowAutoRedirect_True_ValidateNewMethodUsedOnRedirection( - bool async, int statusCode, string oldMethod, string newMethod) + int statusCode, string oldMethod, string newMethod) { if (statusCode == 308 && (IsWinHttpHandler && PlatformDetection.WindowsVersion < 10)) { @@ -117,7 +95,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => { var request = new HttpRequestMessage(new HttpMethod(oldMethod), origUrl) { Version = UseVersion }; - Task getResponseTask = client.SendAsync(async, request); + Task getResponseTask = client.SendAsync(TestAsync, request); await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => { @@ -146,8 +124,12 @@ await LoopbackServer.CreateServerAsync(async (redirServer, redirUrl) => } } - [Theory, MemberData(nameof(AsyncRedirectStatusCodes))] - public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(bool async, int statusCode) + [Theory] + [InlineData(300)] + [InlineData(301)] + [InlineData(302)] + [InlineData(303)] + public async Task AllowAutoRedirect_True_PostToGetDoesNotSendTE(int statusCode) { HttpClientHandler handler = CreateHttpClientHandler(); using (HttpClient client = CreateHttpClient(handler)) @@ -158,7 +140,7 @@ await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => request.Content = new StringContent(ExpectedContent); request.Headers.TransferEncodingChunked = true; - Task getResponseTask = client.SendAsync(async, 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 81298f087e30d..6a7365e8eaa84 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -27,8 +27,9 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } [ConditionalTheory] - [MemberData(nameof(AsyncAndBoolAndCancellationMode))] - public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool async, bool chunkedTransfer, CancellationMode mode) + [InlineData(false, CancellationMode.Token)] + [InlineData(true, CancellationMode.Token)] + public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer) { @@ -59,7 +60,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => req.Content = new ByteAtATimeContent(int.MaxValue, waitToSend.Task, contentSending, millisecondDelayBetweenBytes: 1); req.Headers.TransferEncodingChunked = chunkedTransfer; - Task resp = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task resp = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); waitToSend.SetResult(true); await Task.WhenAny(contentSending.Task, resp); if (!resp.IsCompleted) @@ -84,8 +85,8 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => } [ConditionalTheory] - [MemberData(nameof(AsyncAndBoolAndCancellationMode))] - public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool async, bool connectionClose, CancellationMode mode) + [MemberData(nameof(OneBoolAndCancellationMode))] + public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && connectionClose) { @@ -124,7 +125,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await partialResponseHeadersSent.Task; Cancel(mode, client, cts); await getResponse; @@ -141,8 +142,8 @@ await ValidateClientCancellationAsync(async () => [Theory] [ActiveIssue("https://github.com/dotnet/runtime/issues/25760")] - [MemberData(nameof(AsyncAndTwoBoolsAndCancellationMode))] - public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, CancellationMode mode) + [MemberData(nameof(TwoBoolsAndCancellationMode))] + public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) { @@ -180,7 +181,7 @@ await ValidateClientCancellationAsync(async () => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(async, 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); @@ -197,8 +198,8 @@ await ValidateClientCancellationAsync(async () => } [ConditionalTheory] - [MemberData(nameof(AsyncAndThreeBools))] - public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool async, bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) + [MemberData(nameof(ThreeBools))] + public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose)) { @@ -238,7 +239,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; - Task getResponse = client.SendAsync(async, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await ValidateClientCancellationAsync(async () => { HttpResponseMessage resp = await getResponse; @@ -532,20 +533,15 @@ await LoopbackServerFactory.CreateClientAndServerAsync( async uri => { using (var invoker = new HttpMessageInvoker(CreateHttpClientHandler())) - using (var req = new HttpRequestMessage(HttpMethod.Post, uri) {Content = content, Version = UseVersion}) - { + 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()); } } - catch (OperationCanceledException) - { - } - } + catch (OperationCanceledException) { } }, async server => { @@ -599,24 +595,99 @@ public enum CancellationMode DisposeHttpClient = 0x4 } - public static IEnumerable AsyncAndBoolAndCancellationMode() => - from async in AsyncBoolValues - from second in BoolValues + public static IEnumerable OneBoolAndCancellationMode() => + from first in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } - select new object[] { async, second, mode }; + select new object[] { first, mode }; - public static IEnumerable AsyncAndTwoBoolsAndCancellationMode() => - from async in AsyncBoolValues + public static IEnumerable TwoBoolsAndCancellationMode() => + from first in BoolValues from second in BoolValues - from third in BoolValues from mode in new[] { CancellationMode.Token, CancellationMode.CancelPendingRequests, CancellationMode.DisposeHttpClient, CancellationMode.Token | CancellationMode.CancelPendingRequests } - select new object[] { async, second, third, mode }; + select new object[] { first, second, mode }; - public static IEnumerable AsyncAndThreeBools() => - from async in AsyncBoolValues + public static IEnumerable ThreeBools() => + from first in BoolValues from second in BoolValues from third in BoolValues - from fourth in BoolValues - select new object[] { async, second, third, fourth }; + select new object[] { first, second, third }; + } + + 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(true); + } + }, 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 Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + _tcs.SetResult(true); + return base.SerializeToStreamAsync(stream, context); + } + } } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 36fbc2aef0339..2654cde8de9b2 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -26,6 +26,8 @@ public abstract partial class HttpClientHandlerTestBase : FileCleanupTestBase protected virtual Version UseVersion => HttpVersion.Version11; + protected virtual bool TestAsync => true; + public HttpClientHandlerTestBase(ITestOutputHelper output) { _output = output; @@ -76,7 +78,7 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) new[] { true, -#if NETCOREAPP +#if __NETCOREAPP false #endif }; @@ -163,6 +165,26 @@ public static Task SendAsync(this HttpClient client, bool a // 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."); return Task.FromResult(null); +#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."); + return Task.FromResult(null); #endif } } 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/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index fe7560db7089d..99b0c2c2bb5b0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -984,7 +984,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 +1036,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(true); - } - }, server => releaseServer.Task); // doesn't establish SSL connection - } - - [Fact] public void Expect100ContinueTimeout_Default() { @@ -1107,53 +1082,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 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..1cab9f4c84303 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs @@ -0,0 +1,2047 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Quic; +using System.Net.Security; +using System.Net.Sockets; +using System.Net.Test.Common; +using System.Runtime.CompilerServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + /*public sealed class SocketsHttpHandler_HttpClientHandler_Asynchrony_Test : HttpClientHandler_Asynchrony_Test + { + public SocketsHttpHandler_HttpClientHandler_Asynchrony_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task ExecutionContext_Suppressed_Success() + { + await LoopbackServerFactory.CreateClientAndServerAsync( + uri => Task.Run(() => + { + using (ExecutionContext.SuppressFlow()) + using (HttpClient client = CreateHttpClient()) + { + client.GetStringAsync(uri).GetAwaiter().GetResult(); + } + }), + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(); + }); + } + + [OuterLoop("Relies on finalization")] + [Fact] + public async Task ExecutionContext_HttpConnectionLifetimeDoesntKeepContextAlive() + { + var clientCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + try + { + using (HttpClient client = CreateHttpClient()) + { + (Task completedWhenFinalized, Task getRequest) = MakeHttpRequestWithTcsSetOnFinalizationInAsyncLocal(client, uri); + await getRequest; + + for (int i = 0; i < 3; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + await completedWhenFinalized.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); + } + } + finally + { + clientCompleted.SetResult(true); + } + }, async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(); + await clientCompleted.Task; + }); + }); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // avoid JIT extending lifetime of the finalizable object + private static (Task completedOnFinalized, Task getRequest) MakeHttpRequestWithTcsSetOnFinalizationInAsyncLocal(HttpClient client, Uri uri) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Put something in ExecutionContext, start the HTTP request, then undo the EC change. + var al = new AsyncLocal() { Value = new SetOnFinalized() { _completedWhenFinalized = tcs } }; + Task t = client.GetStringAsync(uri); + al.Value = null; + + // Return a task that will complete when the SetOnFinalized is finalized, + // as well as a task to wait on for the get request; for the get request, + // we return a continuation to avoid any test-altering issues related to + // the state machine holding onto stuff. + t = t.ContinueWith(p => p.GetAwaiter().GetResult()); + return (tcs.Task, t); + } + + private sealed class SetOnFinalized + { + internal TaskCompletionSource _completedWhenFinalized; + ~SetOnFinalized() => _completedWhenFinalized.SetResult(true); + } + } + + 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 + { + public SocketsHttpHandler_HttpProtocolTests_Dribble(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_DiagnosticsTest : DiagnosticsTest + { + public SocketsHttpHandler_DiagnosticsTest(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClient_SelectedSites_Test : HttpClient_SelectedSites_Test + { + public SocketsHttpHandler_HttpClient_SelectedSites_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientEKUTest : HttpClientEKUTest + { + public SocketsHttpHandler_HttpClientEKUTest(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_Decompression_Tests : HttpClientHandler_Decompression_Test + { + public SocketsHttpHandler_HttpClientHandler_Decompression_Tests(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test : HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test + { + public SocketsHttpHandler_HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_ClientCertificates_Test : HttpClientHandler_ClientCertificates_Test + { + public SocketsHttpHandler_HttpClientHandler_ClientCertificates_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_DefaultProxyCredentials_Test : HttpClientHandler_DefaultProxyCredentials_Test + { + public SocketsHttpHandler_HttpClientHandler_DefaultProxyCredentials_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test : HttpClientHandler_Finalization_Test + { + public SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test : HttpClientHandler_Finalization_Test + { + public SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + public sealed class SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test : HttpClientHandler_MaxConnectionsPerServer_Test + { + public SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test(ITestOutputHelper output) : base(output) { } + + [OuterLoop("Incurs a small delay")] + [Theory] + [InlineData(0)] + [InlineData(1)] + public async Task SmallConnectionLifetimeWithMaxConnections_PendingRequestUsesDifferentConnection(int lifetimeMilliseconds) + { + using (var handler = new SocketsHttpHandler()) + { + handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(lifetimeMilliseconds); + handler.MaxConnectionsPerServer = 1; + + using (HttpClient client = CreateHttpClient(handler)) + { + await LoopbackServer.CreateServerAsync(async (server, uri) => + { + Task request1 = client.GetStringAsync(uri); + Task request2 = client.GetStringAsync(uri); + + await server.AcceptConnectionAsync(async connection => + { + Task secondResponse = server.AcceptConnectionAsync(connection2 => + connection2.ReadRequestHeaderAndSendCustomResponseAsync(LoopbackServer.GetConnectionCloseResponse())); + + // Wait a small amount of time before sending the first response, so the connection lifetime will expire. + Debug.Assert(lifetimeMilliseconds < 100); + await Task.Delay(100); + + // Second request should not have completed yet, as we haven't completed the first yet. + Assert.False(request2.IsCompleted); + Assert.False(secondResponse.IsCompleted); + + // Send the first response and wait for the first request to complete. + await connection.ReadRequestHeaderAndSendResponseAsync(); + await request1; + + // Now the second request should complete. + await secondResponse.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); + }); + }); + } + } + } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test : HttpClientHandler_ServerCertificates_Test + { + public SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_ResponseDrain_Test : HttpClientHandler_ResponseDrain_Test + { + protected override void SetResponseDrainTimeout(HttpClientHandler handler, TimeSpan time) + { + SocketsHttpHandler s = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); + Assert.NotNull(s); + s.ResponseDrainTimeout = time; + } + + public SocketsHttpHandler_HttpClientHandler_ResponseDrain_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void MaxResponseDrainSize_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); + + handler.MaxResponseDrainSize = 0; + Assert.Equal(0, handler.MaxResponseDrainSize); + + handler.MaxResponseDrainSize = int.MaxValue; + Assert.Equal(int.MaxValue, handler.MaxResponseDrainSize); + } + } + + [Fact] + public void MaxResponseDrainSize_InvalidArgument_Throws() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); + + AssertExtensions.Throws("value", () => handler.MaxResponseDrainSize = -1); + AssertExtensions.Throws("value", () => handler.MaxResponseDrainSize = int.MinValue); + + Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); + } + } + + [Fact] + public void MaxResponseDrainSize_SetAfterUse_Throws() + { + using (var handler = new SocketsHttpHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + handler.MaxResponseDrainSize = 1; + client.GetAsync("http://" + Guid.NewGuid().ToString("N")); // ignoring failure + Assert.Equal(1, handler.MaxResponseDrainSize); + Assert.Throws(() => handler.MaxResponseDrainSize = 1); + } + } + + [Fact] + public void ResponseDrainTimeout_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); + + handler.ResponseDrainTimeout = TimeSpan.Zero; + Assert.Equal(TimeSpan.Zero, handler.ResponseDrainTimeout); + + handler.ResponseDrainTimeout = TimeSpan.FromTicks(int.MaxValue); + Assert.Equal(TimeSpan.FromTicks(int.MaxValue), handler.ResponseDrainTimeout); + } + } + + [Fact] + public void MaxResponseDraiTime_InvalidArgument_Throws() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); + + AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(-1)); + AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.MaxValue); + AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(int.MaxValue)); + + Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); + } + } + + [Fact] + public void ResponseDrainTimeout_SetAfterUse_Throws() + { + using (var handler = new SocketsHttpHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + handler.ResponseDrainTimeout = TimeSpan.FromSeconds(42); + client.GetAsync("http://" + Guid.NewGuid().ToString("N")); // ignoring failure + Assert.Equal(TimeSpan.FromSeconds(42), handler.ResponseDrainTimeout); + Assert.Throws(() => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(42)); + } + } + + [OuterLoop] + [Theory] + [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 3, LoopbackServer.ContentMode.ContentLength)] + [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 3, LoopbackServer.ContentMode.SingleChunk)] + [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 13, LoopbackServer.ContentMode.BytePerChunk)] + public async Task GetAsyncWithMaxConnections_DisposeBeforeReadingToEnd_DrainsRequestsUnderMaxDrainSizeAndReusesConnection(int totalSize, int readSize, int maxDrainSize, LoopbackServer.ContentMode mode) + { + await LoopbackServer.CreateClientAndServerAsync( + async url => + { + var handler = new SocketsHttpHandler(); + handler.MaxResponseDrainSize = maxDrainSize; + handler.ResponseDrainTimeout = Timeout.InfiniteTimeSpan; + + // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) + handler.MaxConnectionsPerServer = 1; + + using (HttpClient client = CreateHttpClient(handler)) + { + HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + ValidateResponseHeaders(response1, totalSize, mode); + + // Read part but not all of response + Stream responseStream = await response1.Content.ReadAsStreamAsync(); + await ReadToByteCount(responseStream, readSize); + + response1.Dispose(); + + // Issue another request. We'll confirm that it comes on the same connection. + HttpResponseMessage response2 = await client.GetAsync(url); + ValidateResponseHeaders(response2, totalSize, mode); + Assert.Equal(totalSize, (await response2.Content.ReadAsStringAsync()).Length); + } + }, + async server => + { + string content = new string('a', totalSize); + string response = LoopbackServer.GetContentModeResponse(mode, content); + await server.AcceptConnectionAsync(async connection => + { + server.ListenSocket.Close(); // Shut down the listen socket so attempts at additional connections would fail on the client + await connection.ReadRequestHeaderAndSendCustomResponseAsync(response); + await connection.ReadRequestHeaderAndSendCustomResponseAsync(response); + }); + }); + } + + [OuterLoop] + [Theory] + [InlineData(100_000, 0, LoopbackServer.ContentMode.ContentLength)] + [InlineData(100_000, 0, LoopbackServer.ContentMode.SingleChunk)] + [InlineData(100_000, 0, LoopbackServer.ContentMode.BytePerChunk)] + public async Task GetAsyncWithMaxConnections_DisposeLargerThanMaxDrainSize_KillsConnection(int totalSize, int maxDrainSize, LoopbackServer.ContentMode mode) + { + await LoopbackServer.CreateClientAndServerAsync( + async url => + { + var handler = new SocketsHttpHandler(); + handler.MaxResponseDrainSize = maxDrainSize; + handler.ResponseDrainTimeout = Timeout.InfiniteTimeSpan; + + // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) + handler.MaxConnectionsPerServer = 1; + + using (HttpClient client = CreateHttpClient(handler)) + { + HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + ValidateResponseHeaders(response1, totalSize, mode); + response1.Dispose(); + + // Issue another request. We'll confirm that it comes on a new connection. + HttpResponseMessage response2 = await client.GetAsync(url); + ValidateResponseHeaders(response2, totalSize, mode); + Assert.Equal(totalSize, (await response2.Content.ReadAsStringAsync()).Length); + } + }, + async server => + { + string content = new string('a', totalSize); + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAsync(); + try + { + await connection.Writer.WriteAsync(LoopbackServer.GetContentModeResponse(mode, content, connectionClose: false)); + } + catch (Exception) { } // Eat errors from client disconnect. + + await server.AcceptConnectionSendCustomResponseAndCloseAsync(LoopbackServer.GetContentModeResponse(mode, content, connectionClose: true)); + }); + }); + } + + [OuterLoop] + [Theory] + [InlineData(LoopbackServer.ContentMode.ContentLength)] + [InlineData(LoopbackServer.ContentMode.SingleChunk)] + [InlineData(LoopbackServer.ContentMode.BytePerChunk)] + public async Task GetAsyncWithMaxConnections_DrainTakesLongerThanTimeout_KillsConnection(LoopbackServer.ContentMode mode) + { + const int ContentLength = 10_000; + + await LoopbackServer.CreateClientAndServerAsync( + async url => + { + var handler = new SocketsHttpHandler(); + handler.MaxResponseDrainSize = int.MaxValue; + handler.ResponseDrainTimeout = TimeSpan.FromMilliseconds(1); + + // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) + handler.MaxConnectionsPerServer = 1; + + using (HttpClient client = CreateHttpClient(handler)) + { + client.Timeout = Timeout.InfiniteTimeSpan; + + HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + ValidateResponseHeaders(response1, ContentLength, mode); + response1.Dispose(); + + // Issue another request. We'll confirm that it comes on a new connection. + HttpResponseMessage response2 = await client.GetAsync(url); + ValidateResponseHeaders(response2, ContentLength, mode); + Assert.Equal(ContentLength, (await response2.Content.ReadAsStringAsync()).Length); + } + }, + async server => + { + string content = new string('a', ContentLength); + await server.AcceptConnectionAsync(async connection => + { + string response = LoopbackServer.GetContentModeResponse(mode, content, connectionClose: false); + await connection.ReadRequestHeaderAsync(); + try + { + // Write out only part of the response + await connection.Writer.WriteAsync(response.Substring(0, response.Length / 2)); + } + catch (Exception) { } // Eat errors from client disconnect. + + response = LoopbackServer.GetContentModeResponse(mode, content, connectionClose: true); + await server.AcceptConnectionSendCustomResponseAndCloseAsync(response); + }); + }); + } + } + + public sealed class SocketsHttpHandler_PostScenarioTest : PostScenarioTest + { + public SocketsHttpHandler_PostScenarioTest(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DisposeTargetStream_ThrowsObjectDisposedException(bool knownLength) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + try + { + using (HttpClient client = CreateHttpClient()) + { + Task t = client.PostAsync(uri, new DisposeStreamWhileCopyingContent(knownLength)); + Assert.IsType((await Assert.ThrowsAsync(() => t)).InnerException); + } + } + finally + { + tcs.SetResult(0); + } + }, server => tcs.Task); + } + + private sealed class DisposeStreamWhileCopyingContent : HttpContent + { + private readonly bool _knownLength; + + public DisposeStreamWhileCopyingContent(bool knownLength) => _knownLength = knownLength; + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + await stream.WriteAsync(new byte[42], 0, 42); + stream.Dispose(); + } + + protected override bool TryComputeLength(out long length) + { + if (_knownLength) + { + length = 42; + return true; + } + else + { + length = 0; + return false; + } + } + } + } + + public sealed class SocketsHttpHandler_ResponseStreamTest : ResponseStreamTest + { + public SocketsHttpHandler_ResponseStreamTest(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_SslProtocols_Test : HttpClientHandler_SslProtocols_Test + { + public SocketsHttpHandler_HttpClientHandler_SslProtocols_Test(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandler_Proxy_Test : HttpClientHandler_Proxy_Test + { + public SocketsHttpHandler_HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { } + } + + public abstract class SocketsHttpHandler_TrailingHeaders_Test : HttpClientHandlerTestBase + { + public SocketsHttpHandler_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } + + protected static byte[] DataBytes = Encoding.ASCII.GetBytes("data"); + + protected static readonly IList TrailingHeaders = new HttpHeaderData[] { + new HttpHeaderData("MyCoolTrailerHeader", "amazingtrailer"), + new HttpHeaderData("EmptyHeader", ""), + new HttpHeaderData("Accept-Encoding", "identity,gzip"), + new HttpHeaderData("Hello", "World") }; + + protected static Frame MakeDataFrame(int streamId, byte[] data, bool endStream = false) => + new DataFrame(data, (endStream ? FrameFlags.EndStream : FrameFlags.None), 0, streamId); + } + + public class SocketsHttpHandler_Http1_TrailingHeaders_Test : SocketsHttpHandler_TrailingHeaders_Test + { + public SocketsHttpHandler_Http1_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task GetAsyncDefaultCompletionOption_TrailingHeaders_Available(bool includeTrailerHeader) + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + Task getResponseTask = client.GetAsync(url); + await TestHelper.WhenAllCompletedOrAnyFailed( + getResponseTask, + server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + (includeTrailerHeader ? "Trailer: MyCoolTrailerHeader, Hello\r\n" : "") + + "\r\n" + + "4\r\n" + + "data\r\n" + + "0\r\n" + + "MyCoolTrailerHeader: amazingtrailer\r\n" + + "Accept-encoding: identity,gzip\r\n" + + "Hello: World\r\n" + + "\r\n")); + + using (HttpResponseMessage response = await getResponseTask) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); + + // Check the Trailer header. + if (includeTrailerHeader) + { + Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer")); + Assert.Contains("Hello", response.Headers.GetValues("Trailer")); + } + + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + Assert.Contains("identity,gzip", response.TrailingHeaders.GetValues("Accept-encoding")); + + string data = await response.Content.ReadAsStringAsync(); + Assert.Contains("data", data); + // Trailers should not be part of the content data. + Assert.DoesNotContain("MyCoolTrailerHeader", data); + Assert.DoesNotContain("amazingtrailer", data); + Assert.DoesNotContain("Hello", data); + Assert.DoesNotContain("World", data); + } + } + }); + } + + [Fact] + public async Task GetAsyncResponseHeadersReadOption_TrailingHeaders_Available() + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + Task getResponseTask = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + await TestHelper.WhenAllCompletedOrAnyFailed( + getResponseTask, + server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Trailer: MyCoolTrailerHeader\r\n" + + "\r\n" + + "4\r\n" + + "data\r\n" + + "0\r\n" + + "MyCoolTrailerHeader: amazingtrailer\r\n" + + "Hello: World\r\n" + + "\r\n")); + + using (HttpResponseMessage response = await getResponseTask) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); + Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer")); + + // Pending read on the response content. + var trailingHeaders = response.TrailingHeaders; + Assert.Empty(trailingHeaders); + + Stream stream = await response.Content.ReadAsStreamAsync(); + Byte[] data = new Byte[100]; + // Read some data, preferably whole body. + int readBytes = await stream.ReadAsync(data, 0, 4); + + // Intermediate test - haven't reached stream EOF yet. + Assert.Empty(response.TrailingHeaders); + if (readBytes == 4) + { + // If we consumed whole content, check content. + Assert.Contains("data", System.Text.Encoding.Default.GetString(data)); + } + + // Read data until EOF is reached + while (stream.Read(data, 0, data.Length) != 0) + ; + + Assert.Same(trailingHeaders, response.TrailingHeaders); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + }); + } + + [Theory] + [InlineData("Age", "1")] + [InlineData("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l")] + [InlineData("Cache-Control", "no-cache")] + [InlineData("Content-Encoding", "gzip")] + [InlineData("Content-Length", "22")] + [InlineData("Content-type", "foo/bar")] + [InlineData("Content-Range", "bytes 200-1000/67589")] + [InlineData("Date", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("Expect", "100-continue")] + [InlineData("Expires", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("Host", "foo")] + [InlineData("If-Match", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("If-None-Match", "*")] + [InlineData("If-Range", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("If-Unmodified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")] + [InlineData("Location", "/index.html")] + [InlineData("Max-Forwards", "2")] + [InlineData("Pragma", "no-cache")] + [InlineData("Range", "5/10")] + [InlineData("Retry-After", "20")] + [InlineData("Set-Cookie", "foo=bar")] + [InlineData("TE", "boo")] + [InlineData("Transfer-Encoding", "chunked")] + [InlineData("Transfer-Encoding", "gzip")] + [InlineData("Vary", "*")] + [InlineData("Warning", "300 - \"Be Warned!\"")] + public async Task GetAsync_ForbiddenTrailingHeaders_Ignores(string name, string value) + { + await LoopbackServer.CreateClientAndServerAsync(async url => + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + HttpResponseMessage response = await client.GetAsync(url); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.False(response.TrailingHeaders.TryGetValues(name, out IEnumerable values)); + Assert.Contains("Loopback", response.TrailingHeaders.GetValues("Server")); + } + }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + $"Trailer: Set-Cookie, MyCoolTrailerHeader, {name}, Hello\r\n" + + "\r\n" + + "4\r\n" + + "data\r\n" + + "0\r\n" + + "Set-Cookie: yummy\r\n" + + "MyCoolTrailerHeader: amazingtrailer\r\n" + + $"{name}: {value}\r\n" + + "Server: Loopback\r\n" + + $"{name}: {value}\r\n" + + "\r\n")); + } + + [Fact] + public async Task GetAsync_NoTrailingHeaders_EmptyCollection() + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + Task getResponseTask = client.GetAsync(url); + await TestHelper.WhenAllCompletedOrAnyFailed( + getResponseTask, + server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Trailer: MyCoolTrailerHeader\r\n" + + "\r\n" + + "4\r\n" + + "data\r\n" + + "0\r\n" + + "\r\n")); + + using (HttpResponseMessage response = await getResponseTask) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); + + Assert.NotNull(response.TrailingHeaders); + Assert.Equal(0, response.TrailingHeaders.Count()); + Assert.Same(response.TrailingHeaders, response.TrailingHeaders); + } + } + }); + } + } + + // TODO: make generic to support HTTP/2 and HTTP/3. + public sealed class SocketsHttpHandler_Http2_TrailingHeaders_Test : SocketsHttpHandler_TrailingHeaders_Test + { + public SocketsHttpHandler_Http2_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + + // Response data. + await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes, endStream: true)); + + // Server doesn't send trailing header frame. + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.TrailingHeaders); + Assert.Equal(0, response.TrailingHeaders.Count()); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + + // Response data, missing Trailers. + await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); + + // Additional trailing header frame. + await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:true, headers: TrailingHeaders, endStream : true); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_TrailerHeaders_TrailingPseudoHeadersThrow() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); + // Additional trailing header frame with pseudo-headers again.. + await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:false, headers: TrailingHeaders, endStream : true); + + await Assert.ThrowsAsync(() => sendTask); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address, HttpCompletionOption.ResponseHeadersRead); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + + // Response data, missing Trailers. + await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Pending read on the response content. + Assert.Empty(response.TrailingHeaders); + + Stream stream = await response.Content.ReadAsStreamAsync(); + Byte[] data = new Byte[100]; + await stream.ReadAsync(data, 0, data.Length); + + // Intermediate test - haven't reached stream EOF yet. + Assert.Empty(response.TrailingHeaders); + + // Finish data stream and write out trailing headers. + await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); + await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders); + + // Read data until EOF is reached + while (stream.Read(data, 0, data.Length) != 0); + + Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_TrailerHeaders_TrailingHeaderNoBody() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public async Task Http2GetAsync_TrailingHeaders_NoData_EmptyResponseObserved() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + + int streamId = await connection.ReadRequestHeaderAsync(); + + // Response header. + await connection.SendDefaultResponseHeadersAsync(streamId); + + // No data. + + // Response trailing headers + await connection.SendResponseHeadersAsync(streamId, isTrailingHeader: true, headers: TrailingHeaders); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Array.Empty(), await response.Content.ReadAsByteArrayAsync()); + Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); + } + } + } + + public sealed class SocketsHttpHandler_SchSendAuxRecordHttpTest : SchSendAuxRecordHttpTest + { + public SocketsHttpHandler_SchSendAuxRecordHttpTest(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest + { + public SocketsHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { } + }*/ + + public sealed class SyncHttpHandlerTest_AutoRedirect : HttpClientHandlerTest_AutoRedirect + { + protected override bool TestAsync => false; + + public SyncHttpHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } + } + + /*public sealed class SocketsHttpHandler_DefaultCredentialsTest : DefaultCredentialsTest + { + public SocketsHttpHandler_DefaultCredentialsTest(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandler_IdnaProtocolTests : IdnaProtocolTests + { + public SocketsHttpHandler_IdnaProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool SupportsIdna => true; + } + + public sealed class SocketsHttpHandler_HttpRetryProtocolTests : HttpRetryProtocolTests + { + public SocketsHttpHandler_HttpRetryProtocolTests(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies + { + public SocketsHttpHandlerTest_Cookies(ITestOutputHelper output) : base(output) { } + } + + public sealed class SocketsHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTest_Cookies_Http11 + { + public SocketsHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } + }*/ + + public sealed class SyncHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Http11_Cancellation_Test + { + protected override bool TestAsync => false; + + public SyncHttpHandler_HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } + } + /* + public sealed class SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test : HttpClientHandler_MaxResponseHeadersLength_Test + { + public SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test(ITestOutputHelper output) : base(output) { } + } + + 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 + { + public SocketsHttpHandler_ConnectionUpgrade_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task UpgradeConnection_ReturnsReadableAndWritableStream() + { + await LoopbackServer.CreateServerAsync(async (server, url) => + { + using (HttpClient client = CreateHttpClient()) + { + // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. + Task getResponseTask = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + await server.AcceptConnectionAsync(async connection => + { + Task> serverTask = connection.ReadRequestHeaderAndSendCustomResponseAsync($"HTTP/1.1 101 Switching Protocols\r\nDate: {DateTimeOffset.UtcNow:R}\r\n\r\n"); + + await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); + + using (Stream clientStream = await (await getResponseTask).Content.ReadAsStreamAsync()) + { + // Boolean properties returning correct values + Assert.True(clientStream.CanWrite); + Assert.True(clientStream.CanRead); + Assert.False(clientStream.CanSeek); + + // Not supported operations + Assert.Throws(() => clientStream.Length); + Assert.Throws(() => clientStream.Position); + Assert.Throws(() => clientStream.Position = 0); + Assert.Throws(() => clientStream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => clientStream.SetLength(0)); + + // Invalid arguments + var nonWritableStream = new MemoryStream(new byte[1], false); + var disposedStream = new MemoryStream(); + disposedStream.Dispose(); + Assert.Throws(() => clientStream.CopyTo(null)); + Assert.Throws(() => clientStream.CopyTo(Stream.Null, 0)); + Assert.Throws(() => { clientStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, 0, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, -1, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(nonWritableStream, 100, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(disposedStream, 100, default); }); + Assert.Throws(() => clientStream.Read(null, 0, 100)); + Assert.Throws(() => clientStream.Read(new byte[1], -1, 1)); + Assert.ThrowsAny(() => clientStream.Read(new byte[1], 2, 1)); + Assert.Throws(() => clientStream.Read(new byte[1], 0, -1)); + Assert.ThrowsAny(() => clientStream.Read(new byte[1], 0, 2)); + Assert.Throws(() => clientStream.BeginRead(null, 0, 100, null, null)); + Assert.Throws(() => clientStream.BeginRead(new byte[1], -1, 1, null, null)); + Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 2, 1, null, null)); + Assert.Throws(() => clientStream.BeginRead(new byte[1], 0, -1, null, null)); + Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 0, 2, null, null)); + Assert.Throws(() => clientStream.EndRead(null)); + Assert.Throws(() => { clientStream.ReadAsync(null, 0, 100, default); }); + Assert.Throws(() => { clientStream.ReadAsync(new byte[1], -1, 1, default); }); + Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 2, 1, default); }); + Assert.Throws(() => { clientStream.ReadAsync(new byte[1], 0, -1, default); }); + Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 0, 2, default); }); + + // Validate writing APIs on clientStream + + clientStream.WriteByte((byte)'!'); + clientStream.Write(new byte[] { (byte)'\r', (byte)'\n' }, 0, 2); + Assert.Equal("!", await connection.ReadLineAsync()); + + clientStream.Write(new Span(new byte[] { (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'\r', (byte)'\n' })); + Assert.Equal("hello", await connection.ReadLineAsync()); + + await clientStream.WriteAsync(new byte[] { (byte)'w', (byte)'o', (byte)'r', (byte)'l', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 7); + Assert.Equal("world", await connection.ReadLineAsync()); + + await clientStream.WriteAsync(new Memory(new byte[] { (byte)'a', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 5)); + Assert.Equal("and", await connection.ReadLineAsync()); + + await Task.Factory.FromAsync(clientStream.BeginWrite, clientStream.EndWrite, new byte[] { (byte)'b', (byte)'e', (byte)'y', (byte)'o', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 8, null); + Assert.Equal("beyond", await connection.ReadLineAsync()); + + clientStream.Flush(); + await clientStream.FlushAsync(); + + // Validate reading APIs on clientStream + await connection.Stream.WriteAsync(Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz")); + var buffer = new byte[1]; + + Assert.Equal('a', clientStream.ReadByte()); + + Assert.Equal(1, clientStream.Read(buffer, 0, 1)); + Assert.Equal((byte)'b', buffer[0]); + + Assert.Equal(1, clientStream.Read(new Span(buffer, 0, 1))); + Assert.Equal((byte)'c', buffer[0]); + + Assert.Equal(1, await clientStream.ReadAsync(buffer, 0, 1)); + Assert.Equal((byte)'d', buffer[0]); + + Assert.Equal(1, await clientStream.ReadAsync(new Memory(buffer, 0, 1))); + Assert.Equal((byte)'e', buffer[0]); + + Assert.Equal(1, await Task.Factory.FromAsync(clientStream.BeginRead, clientStream.EndRead, buffer, 0, 1, null)); + Assert.Equal((byte)'f', buffer[0]); + + var ms = new MemoryStream(); + Task copyTask = clientStream.CopyToAsync(ms); + + string bigString = string.Concat(Enumerable.Repeat("abcdefghijklmnopqrstuvwxyz", 1000)); + Task lotsOfDataSent = connection.Socket.SendAsync(Encoding.ASCII.GetBytes(bigString), SocketFlags.None); + connection.Socket.Shutdown(SocketShutdown.Send); + await copyTask; + await lotsOfDataSent; + Assert.Equal("ghijklmnopqrstuvwxyz" + bigString, Encoding.ASCII.GetString(ms.ToArray())); + } + }); + } + }); + } + } + + public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandlerTestBase + { + 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 + { + public SocketsHttpHandler_HttpClientHandler_ConnectionPooling_Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task MultipleIterativeRequests_SameConnectionReused() + { + using (HttpClient client = CreateHttpClient()) + using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + var ep = (IPEndPoint)listener.LocalEndPoint; + var uri = new Uri($"http://{ep.Address}:{ep.Port}/"); + + string responseBody = + "HTTP/1.1 200 OK\r\n" + + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + + "Content-Length: 0\r\n" + + "\r\n"; + + Task firstRequest = client.GetStringAsync(uri); + using (Socket server = await listener.AcceptAsync()) + using (var serverStream = new NetworkStream(server, ownsSocket: false)) + using (var serverReader = new StreamReader(serverStream)) + { + while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())); + await server.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None); + await firstRequest; + + Task secondAccept = listener.AcceptAsync(); // shouldn't complete + + Task additionalRequest = client.GetStringAsync(uri); + while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())); + await server.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None); + await additionalRequest; + + Assert.False(secondAccept.IsCompleted, $"Second accept should never complete"); + } + } + } + + [OuterLoop("Incurs a delay")] + [Fact] + public async Task ServerDisconnectsAfterInitialRequest_SubsequentRequestUsesDifferentConnection() + { + using (HttpClient client = CreateHttpClient()) + { + await LoopbackServer.CreateServerAsync(async (server, uri) => + { + // Make multiple requests iteratively. + for (int i = 0; i < 2; i++) + { + Task request = client.GetStringAsync(uri); + await server.AcceptConnectionSendResponseAndCloseAsync(); + await request; + + if (i == 0) + { + await Task.Delay(2000); // give client time to see the closing before next connect + } + } + }); + } + } + + [Fact] + public async Task ServerSendsGarbageAfterInitialRequest_SubsequentRequestUsesDifferentConnection() + { + using (HttpClient client = CreateHttpClient()) + { + await LoopbackServer.CreateServerAsync(async (server, uri) => + { + var releaseServer = new TaskCompletionSource(); + + // Make multiple requests iteratively. + + Task serverTask1 = server.AcceptConnectionAsync(async connection => + { + await connection.Writer.WriteAsync(LoopbackServer.GetHttpResponse(connectionClose: false) + "here is a bunch of garbage"); + await releaseServer.Task; // keep connection alive on the server side + }); + await client.GetStringAsync(uri); + + Task serverTask2 = server.AcceptConnectionSendCustomResponseAndCloseAsync(LoopbackServer.GetHttpResponse(connectionClose: true)); + await new[] { client.GetStringAsync(uri), serverTask2 }.WhenAllOrAnyFailed(); + + releaseServer.SetResult(true); + await serverTask1; + }); + } + } + + [Fact] + public async Task ServerSendsConnectionClose_SubsequentRequestUsesDifferentConnection() + { + using (HttpClient client = CreateHttpClient()) + { + await LoopbackServer.CreateServerAsync(async (server, uri) => + { + string responseBody = + "HTTP/1.1 200 OK\r\n" + + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n" + + "\r\n"; + + // Make first request. + Task request1 = client.GetStringAsync(uri); + await server.AcceptConnectionAsync(async connection1 => + { + await connection1.ReadRequestHeaderAndSendCustomResponseAsync(responseBody); + await request1; + + // Make second request and expect it to be served from a different connection. + Task request2 = client.GetStringAsync(uri); + await server.AcceptConnectionAsync(async connection2 => + { + await connection2.ReadRequestHeaderAndSendCustomResponseAsync(responseBody); + await request2; + }); + }); + }); + } + } + + [Theory] + [InlineData("PooledConnectionLifetime")] + [InlineData("PooledConnectionIdleTimeout")] + public async Task SmallConnectionTimeout_SubsequentRequestUsesDifferentConnection(string timeoutPropertyName) + { + using (var handler = new SocketsHttpHandler()) + { + switch (timeoutPropertyName) + { + case "PooledConnectionLifetime": handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; + case "PooledConnectionIdleTimeout": handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; + default: throw new ArgumentOutOfRangeException(nameof(timeoutPropertyName)); + } + + using (HttpClient client = CreateHttpClient(handler)) + { + await LoopbackServer.CreateServerAsync(async (server, uri) => + { + // Make first request. + Task request1 = client.GetStringAsync(uri); + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(); + await request1; + + // Wait a small amount of time before making the second request, to give the first request time to timeout. + await Task.Delay(100); + + // Make second request and expect it to be served from a different connection. + Task request2 = client.GetStringAsync(uri); + await server.AcceptConnectionAsync(async connection2 => + { + await connection2.ReadRequestHeaderAndSendResponseAsync(); + await request2; + }); + }); + }); + } + } + } + + [Theory] + [InlineData("PooledConnectionLifetime")] + [InlineData("PooledConnectionIdleTimeout")] + public async Task Http2_SmallConnectionTimeout_SubsequentRequestUsesDifferentConnection(string timeoutPropertyName) + { + await Http2LoopbackServerFactory.CreateServerAsync(async (server, url) => + { + HttpClientHandler handler = CreateHttpClientHandler(HttpVersion.Version20); + SocketsHttpHandler s = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); + switch (timeoutPropertyName) + { + case "PooledConnectionLifetime": s.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; + case "PooledConnectionIdleTimeout": s.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; + default: throw new ArgumentOutOfRangeException(nameof(timeoutPropertyName)); + } + + using (HttpClient client = CreateHttpClient(handler)) + { + client.DefaultRequestVersion = HttpVersion.Version20; + Task request1 = client.GetStringAsync(url); + + Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); + int streamId = await connection.ReadRequestHeaderAsync(); + await connection.SendDefaultResponseAsync(streamId); + await request1; + + // Wait a small amount of time before making the second request, to give the first request time to timeout. + await Task.Delay(100); + // Grab reference to underlying socket and stream to make sure they are not disposed and closed. + (Socket socket, Stream stream) = connection.ResetNetwork(); + + // Make second request and expect it to be served from a different connection. + Task request2 = client.GetStringAsync(url); + connection = await server.EstablishConnectionAsync(); + streamId = await connection.ReadRequestHeaderAsync(); + await connection.SendDefaultResponseAsync(streamId); + await request2; + + // Close underlying socket from first connection. + socket.Close(); + } + }); + } + + [OuterLoop] + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ConnectionsPooledThenDisposed_NoUnobservedTaskExceptions(bool secure) + { + RemoteExecutor.Invoke(async (secureString, useVersionString) => + { + var releaseServer = new TaskCompletionSource(); + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var handler = new SocketsHttpHandler()) + using (HttpClient client = CreateHttpClient(handler, useVersionString)) + { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); + + var exceptions = new List(); + TaskScheduler.UnobservedTaskException += (s, e) => exceptions.Add(e.Exception); + + await client.GetStringAsync(uri); + await Task.Delay(10); // any value >= the lifetime + Task ignored = client.GetStringAsync(uri); // force the pool to look for the previous connection and find it's too old + await Task.Delay(100); // give some time for the connection close to fail pending reads + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Note that there are race conditions here such that we may not catch every failure, + // and thus could have some false negatives, but there won't be any false positives. + Assert.True(exceptions.Count == 0, string.Concat(exceptions)); + + releaseServer.SetResult(true); + } + }, server => server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(content: "hello world"); + await releaseServer.Task; + }), + new LoopbackServer.Options { UseSsl = bool.Parse(secureString) }); + }, secure.ToString(), UseVersion.ToString()).Dispose(); + } + + [OuterLoop] + [Fact] + public void HandlerDroppedWithoutDisposal_NotKeptAlive() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + HandlerDroppedWithoutDisposal_NotKeptAliveCore(tcs); + for (int i = 0; i < 10; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + Assert.True(tcs.Task.IsCompleted); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void HandlerDroppedWithoutDisposal_NotKeptAliveCore(TaskCompletionSource setOnFinalized) + { + // This relies on knowing that in order for the connection pool to operate, it needs + // to maintain a reference to the supplied IWebProxy. As such, we provide a proxy + // that when finalized will set our event, so that we can determine the state associated + // with a handler has gone away. + IWebProxy p = new PassthroughProxyWithFinalizerCallback(() => setOnFinalized.TrySetResult(true)); + + // Make a bunch of requests and drop the associated HttpClient instances after making them, without disposal. + Task.WaitAll((from i in Enumerable.Range(0, 10) + select LoopbackServer.CreateClientAndServerAsync( + url => CreateHttpClient(new SocketsHttpHandler { Proxy = p }).GetStringAsync(url), + server => server.AcceptConnectionSendResponseAndCloseAsync())).ToArray()); + } + + private sealed class PassthroughProxyWithFinalizerCallback : IWebProxy + { + private readonly Action _callback; + + public PassthroughProxyWithFinalizerCallback(Action callback) => _callback = callback; + ~PassthroughProxyWithFinalizerCallback() => _callback(); + + public ICredentials Credentials { get; set; } + public Uri GetProxy(Uri destination) => destination; + public bool IsBypassed(Uri host) => true; + } + + [Fact] + public async Task ProxyAuth_SameConnection_Succeeds() + { + Task serverTask = LoopbackServer.CreateServerAsync(async (proxyServer, proxyUrl) => + { + string responseBody = + "HTTP/1.1 407 Proxy Auth Required\r\n" + + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + + "Proxy-Authenticate: Basic\r\n" + + "Content-Length: 0\r\n" + + "\r\n"; + + using (var handler = new HttpClientHandler()) + { + handler.Proxy = new UseSpecifiedUriWebProxy(proxyUrl, new NetworkCredential("abc", "def")); + + using (HttpClient client = CreateHttpClient(handler)) + { + Task request = client.GetStringAsync($"http://notarealserver.com/"); + + await proxyServer.AcceptConnectionAsync(async connection => + { + // Get first request, no body for GET. + await connection.ReadRequestHeaderAndSendCustomResponseAsync(responseBody).ConfigureAwait(false); + // Client should send another request after being rejected with 407. + await connection.ReadRequestHeaderAndSendResponseAsync(content:"OK").ConfigureAwait(false); + }); + + string response = await request; + Assert.Equal("OK", response); + } + } + }); + await serverTask.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); + } + } + + public sealed class SocketsHttpHandler_PublicAPIBehavior_Test + { + [Fact] + public void AllowAutoRedirect_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.True(handler.AllowAutoRedirect); + + handler.AllowAutoRedirect = true; + Assert.True(handler.AllowAutoRedirect); + + handler.AllowAutoRedirect = false; + Assert.False(handler.AllowAutoRedirect); + } + } + + [Fact] + public void AutomaticDecompression_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(DecompressionMethods.None, handler.AutomaticDecompression); + + handler.AutomaticDecompression = DecompressionMethods.GZip; + Assert.Equal(DecompressionMethods.GZip, handler.AutomaticDecompression); + + handler.AutomaticDecompression = DecompressionMethods.Deflate; + Assert.Equal(DecompressionMethods.Deflate, handler.AutomaticDecompression); + + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + Assert.Equal(DecompressionMethods.GZip | DecompressionMethods.Deflate, handler.AutomaticDecompression); + } + } + + [Fact] + public void CookieContainer_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + CookieContainer container = handler.CookieContainer; + Assert.Same(container, handler.CookieContainer); + + var newContainer = new CookieContainer(); + handler.CookieContainer = newContainer; + Assert.Same(newContainer, handler.CookieContainer); + } + } + + [Fact] + public void Credentials_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Null(handler.Credentials); + + var newCredentials = new NetworkCredential("username", "password"); + handler.Credentials = newCredentials; + Assert.Same(newCredentials, handler.Credentials); + } + } + + [Fact] + public void DefaultProxyCredentials_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Null(handler.DefaultProxyCredentials); + + var newCredentials = new NetworkCredential("username", "password"); + handler.DefaultProxyCredentials = newCredentials; + Assert.Same(newCredentials, handler.DefaultProxyCredentials); + } + } + + [Fact] + public void MaxAutomaticRedirections_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(50, handler.MaxAutomaticRedirections); + + handler.MaxAutomaticRedirections = int.MaxValue; + Assert.Equal(int.MaxValue, handler.MaxAutomaticRedirections); + + handler.MaxAutomaticRedirections = 1; + Assert.Equal(1, handler.MaxAutomaticRedirections); + + AssertExtensions.Throws("value", () => handler.MaxAutomaticRedirections = 0); + AssertExtensions.Throws("value", () => handler.MaxAutomaticRedirections = -1); + } + } + + [Fact] + public void MaxConnectionsPerServer_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); + + handler.MaxConnectionsPerServer = int.MaxValue; + Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); + + handler.MaxConnectionsPerServer = 1; + Assert.Equal(1, handler.MaxConnectionsPerServer); + + AssertExtensions.Throws("value", () => handler.MaxConnectionsPerServer = 0); + AssertExtensions.Throws("value", () => handler.MaxConnectionsPerServer = -1); + } + } + + [Fact] + public void MaxResponseHeadersLength_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(64, handler.MaxResponseHeadersLength); + + handler.MaxResponseHeadersLength = int.MaxValue; + Assert.Equal(int.MaxValue, handler.MaxResponseHeadersLength); + + handler.MaxResponseHeadersLength = 1; + Assert.Equal(1, handler.MaxResponseHeadersLength); + + AssertExtensions.Throws("value", () => handler.MaxResponseHeadersLength = 0); + AssertExtensions.Throws("value", () => handler.MaxResponseHeadersLength = -1); + } + } + + [Fact] + public void PreAuthenticate_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.False(handler.PreAuthenticate); + + handler.PreAuthenticate = false; + Assert.False(handler.PreAuthenticate); + + handler.PreAuthenticate = true; + Assert.True(handler.PreAuthenticate); + } + } + + [Fact] + public void PooledConnectionIdleTimeout_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(TimeSpan.FromMinutes(2), handler.PooledConnectionIdleTimeout); + + handler.PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan; + Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionIdleTimeout); + + handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(0); + Assert.Equal(TimeSpan.FromSeconds(0), handler.PooledConnectionIdleTimeout); + + handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1); + Assert.Equal(TimeSpan.FromSeconds(1), handler.PooledConnectionIdleTimeout); + + AssertExtensions.Throws("value", () => handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(-2)); + } + } + + [Fact] + public void PooledConnectionLifetime_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); + + handler.PooledConnectionLifetime = Timeout.InfiniteTimeSpan; + Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); + + handler.PooledConnectionLifetime = TimeSpan.FromSeconds(0); + Assert.Equal(TimeSpan.FromSeconds(0), handler.PooledConnectionLifetime); + + handler.PooledConnectionLifetime = TimeSpan.FromSeconds(1); + Assert.Equal(TimeSpan.FromSeconds(1), handler.PooledConnectionLifetime); + + AssertExtensions.Throws("value", () => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(-2)); + } + } + + [Fact] + public void Properties_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + IDictionary props = handler.Properties; + Assert.NotNull(props); + Assert.Empty(props); + + props.Add("hello", "world"); + Assert.Equal(1, props.Count); + Assert.Equal("world", props["hello"]); + } + } + + [Fact] + public void Proxy_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.Null(handler.Proxy); + + var proxy = new WebProxy(); + handler.Proxy = proxy; + Assert.Same(proxy, handler.Proxy); + } + } + + [Fact] + public void SslOptions_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + SslClientAuthenticationOptions options = handler.SslOptions; + Assert.NotNull(options); + + Assert.True(options.AllowRenegotiation); + Assert.Null(options.ApplicationProtocols); + Assert.Equal(X509RevocationMode.NoCheck, options.CertificateRevocationCheckMode); + Assert.Null(options.ClientCertificates); + Assert.Equal(SslProtocols.None, options.EnabledSslProtocols); + Assert.Equal(EncryptionPolicy.RequireEncryption, options.EncryptionPolicy); + Assert.Null(options.LocalCertificateSelectionCallback); + Assert.Null(options.RemoteCertificateValidationCallback); + Assert.Null(options.TargetHost); + + Assert.Same(options, handler.SslOptions); + + var newOptions = new SslClientAuthenticationOptions(); + handler.SslOptions = newOptions; + Assert.Same(newOptions, handler.SslOptions); + } + } + + [Fact] + public void UseCookies_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.True(handler.UseCookies); + + handler.UseCookies = true; + Assert.True(handler.UseCookies); + + handler.UseCookies = false; + Assert.False(handler.UseCookies); + } + } + + [Fact] + public void UseProxy_GetSet_Roundtrips() + { + using (var handler = new SocketsHttpHandler()) + { + Assert.True(handler.UseProxy); + + handler.UseProxy = false; + Assert.False(handler.UseProxy); + + handler.UseProxy = true; + Assert.True(handler.UseProxy); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AfterDisposeSendAsync_GettersUsable_SettersThrow(bool dispose) + { + using (var handler = new SocketsHttpHandler()) + { + Type expectedExceptionType; + if (dispose) + { + handler.Dispose(); + expectedExceptionType = typeof(ObjectDisposedException); + } + else + { + using (var c = new HttpMessageInvoker(handler, disposeHandler: false)) + await Assert.ThrowsAnyAsync(() => + c.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri("/shouldquicklyfail", UriKind.Relative)), default)); + expectedExceptionType = typeof(InvalidOperationException); + } + + Assert.True(handler.AllowAutoRedirect); + Assert.Equal(DecompressionMethods.None, handler.AutomaticDecompression); + Assert.NotNull(handler.CookieContainer); + Assert.Null(handler.Credentials); + Assert.Null(handler.DefaultProxyCredentials); + Assert.Equal(50, handler.MaxAutomaticRedirections); + Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); + Assert.Equal(64, handler.MaxResponseHeadersLength); + Assert.False(handler.PreAuthenticate); + Assert.Equal(TimeSpan.FromMinutes(2), handler.PooledConnectionIdleTimeout); + Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); + Assert.NotNull(handler.Properties); + Assert.Null(handler.Proxy); + Assert.NotNull(handler.SslOptions); + Assert.True(handler.UseCookies); + Assert.True(handler.UseProxy); + + Assert.Throws(expectedExceptionType, () => handler.AllowAutoRedirect = false); + Assert.Throws(expectedExceptionType, () => handler.AutomaticDecompression = DecompressionMethods.GZip); + Assert.Throws(expectedExceptionType, () => handler.CookieContainer = new CookieContainer()); + Assert.Throws(expectedExceptionType, () => handler.Credentials = new NetworkCredential("anotheruser", "anotherpassword")); + Assert.Throws(expectedExceptionType, () => handler.DefaultProxyCredentials = new NetworkCredential("anotheruser", "anotherpassword")); + Assert.Throws(expectedExceptionType, () => handler.MaxAutomaticRedirections = 2); + Assert.Throws(expectedExceptionType, () => handler.MaxConnectionsPerServer = 2); + Assert.Throws(expectedExceptionType, () => handler.MaxResponseHeadersLength = 2); + Assert.Throws(expectedExceptionType, () => handler.PreAuthenticate = false); + Assert.Throws(expectedExceptionType, () => handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(2)); + Assert.Throws(expectedExceptionType, () => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(2)); + Assert.Throws(expectedExceptionType, () => handler.Proxy = new WebProxy()); + Assert.Throws(expectedExceptionType, () => handler.SslOptions = new SslClientAuthenticationOptions()); + Assert.Throws(expectedExceptionType, () => handler.UseCookies = false); + Assert.Throws(expectedExceptionType, () => handler.UseProxy = false); + } + } + } + + public sealed class SocketsHttpHandlerTest_LocationHeader + { + private static readonly byte[] s_redirectResponseBefore = Encoding.ASCII.GetBytes( + "HTTP/1.1 301 Moved Permanently\r\n" + + "Connection: close\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Location: "); + + private static readonly byte[] s_redirectResponseAfter = Encoding.ASCII.GetBytes( + "\r\n" + + "Server: Loopback\r\n" + + "\r\n" + + "0\r\n\r\n"); + + [Theory] + // US-ASCII only + [InlineData("http://a/", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/' })] + [InlineData("http://a/asdasd", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', (byte)'a', (byte)'s', (byte)'d', (byte)'a', (byte)'s', (byte)'d' })] + // 2, 3, 4 byte UTF-8 characters + [InlineData("http://a/\u00A2", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC2, 0xA2 })] + [InlineData("http://a/\u20AC", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xE2, 0x82, 0xAC })] + [InlineData("http://a/\uD800\uDF48", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xF0, 0x90, 0x8D, 0x88 })] + // 3 Polish letters + [InlineData("http://a/\u0105\u015B\u0107", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC4, 0x85, 0xC5, 0x9B, 0xC4, 0x87 })] + // Negative cases - should be interpreted as ISO-8859-1 + // Invalid utf-8 sequence (continuation without start) + [InlineData("http://a/%C2%80", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0b10000000 })] + // Invalid utf-8 sequence (not allowed character) + [InlineData("http://a/\u00C3\u0028", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC3, 0x28 })] + // Incomplete utf-8 sequence + [InlineData("http://a/\u00C2", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC2 })] + public async Task LocationHeader_DecodesUtf8_Success(string expected, byte[] location) + { + await LoopbackServer.CreateClientAndServerAsync(async url => + { + using (HttpClientHandler handler = new HttpClientHandler()) + { + handler.AllowAutoRedirect = false; + + using (HttpClient client = new HttpClient(handler)) + { + HttpResponseMessage response = await client.GetAsync(url); + Assert.Equal(expected, response.Headers.Location.ToString()); + } + } + }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync(PreperateResponseWithRedirect(location))); + } + + private static byte[] PreperateResponseWithRedirect(byte[] location) + { + return s_redirectResponseBefore.Concat(location).Concat(s_redirectResponseAfter).ToArray(); + } + } + + public sealed class SocketsHttpHandlerTest_Http2 : HttpClientHandlerTest_Http2 + { + public SocketsHttpHandlerTest_Http2(ITestOutputHelper output) : base(output) { } + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandlerTest_Cookies_Http2 : HttpClientHandlerTest_Cookies + { + public SocketsHttpHandlerTest_Cookies_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Http2 : HttpClientHandlerTest + { + public SocketsHttpHandlerTest_HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http11 : HttpClientHandlerTest_Headers + { + public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http11(ITestOutputHelper output) : base(output) { } + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http2 : HttpClientHandlerTest_Headers + { + public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http2 : HttpClientHandler_Cancellation_Test + { + public SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http2(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http3_Test : HttpClientHandler_Finalization_Test + { + public SocketsHttpHandler_HttpClientHandler_Finalization_Http3_Test(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandlerTest_Http3 : HttpClientHandlerTest_Http3 + { + public SocketsHttpHandlerTest_Http3(ITestOutputHelper output) : base(output) { } + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandlerTest_Cookies_Http3 : HttpClientHandlerTest_Cookies + { + public SocketsHttpHandlerTest_Cookies_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Http3 : HttpClientHandlerTest + { + public SocketsHttpHandlerTest_HttpClientHandlerTest_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http3 : HttpClientHandlerTest_Headers + { + public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3 : HttpClientHandler_Cancellation_Test + { + public SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + + [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] + public sealed class SocketsHttpHandler_HttpClientHandler_AltSvc_Test_Http3 : HttpClientHandler_AltSvc_Test + { + public SocketsHttpHandler_HttpClientHandler_AltSvc_Test_Http3(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + }*/ +} 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..7d016af577bea 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 @@ -218,6 +218,7 @@ Link="Common\System\Net\Http\ResponseStreamTest.cs" /> + Date: Mon, 18 May 2020 16:49:34 +0200 Subject: [PATCH 23/60] Fixed throwing NSE and missing sync implementations. --- .../src/Resources/Strings.resx | 3 ++ .../src/System/Net/Http/ByteArrayContent.cs | 29 +++++++++++-------- .../src/System/Net/Http/HttpContent.cs | 2 +- .../src/System/Net/Http/HttpMessageHandler.cs | 2 +- .../src/System/Net/Http/MultipartContent.cs | 14 +++++++-- .../Net/Http/MultipartFormDataContent.cs | 15 ++++++++++ .../System/Net/Http/ReadOnlyMemoryContent.cs | 8 ++--- .../HttpMessageHandlerStage.cs | 5 ---- .../SocketsHttpHandler/SocketsHttpHandler.cs | 5 ++++ .../src/System/Net/Http/StreamContent.cs | 19 ++++++++---- .../src/System/Net/Http/StringContent.cs | 15 ++++++++++ 11 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 4a5e22fc6b357..0f332dd1166ee 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -558,4 +558,7 @@ The synchronous API is not supported by '{0}'. If you're using custom '{1}' and wish to use synchronous HTTP APIs, you must override '{2}' virtual method. + + The synchronous API is not supported by '{0}' for HTTP/2 or higher. Either use an async implementation or downgrade you request version to HTTP/1.1 or lower. + 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 29c3a16532f6e..cb450c3dbddb8 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 @@ -47,6 +47,23 @@ public ByteArrayContent(byte[] content, int offset, int count) _count = count; } + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(ByteArrayContent)) + { + base.SerializeToStream(stream, context, cancellationToken); + } + else + { + SerializeToStreamCore(stream, context, cancellationToken); + } + } + + private protected void SerializeToStreamCore(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + stream.Write(_content, _offset, _count); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => SerializeToStreamAsyncCore(stream, default); @@ -59,18 +76,6 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext? private protected Task SerializeToStreamAsyncCore(Stream stream, CancellationToken cancellationToken) => stream.WriteAsync(_content, _offset, _count, cancellationToken); - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. - cancellationToken.ThrowIfCancellationRequested(); - stream.Write(_content, _offset, _count); - } - protected internal override bool TryComputeLength(out long length) { length = _count; 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 89989a596b89f..f90dc87a2af1b 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 @@ -321,7 +321,7 @@ public Task ReadAsStreamAsync(CancellationToken cancellationToken) // 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, typeof(HttpContent), GetType(), nameof(SerializeToStream))); + 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) => 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 04a87cfe9be47..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 @@ -22,7 +22,7 @@ protected HttpMessageHandler() // 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, typeof(HttpMessageHandler), GetType(), nameof(Send))); + 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); 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 75bafeec23e22..430aa541d82d2 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 @@ -168,11 +168,21 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() // then the stream will be closed an exception thrown. protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - if (stream == null) + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(MultipartContent)) + { + base.SerializeToStream(stream, context, cancellationToken); + } + else { - throw new ArgumentNullException(nameof(stream)); + SerializeToStreamCore(stream, context, cancellationToken); } + } + private protected void SerializeToStreamCore(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + Debug.Assert(stream != null); try { // Write start boundary. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs index 8b579c7821715..97270d70252a5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs @@ -85,6 +85,21 @@ private void AddInternal(HttpContent content, string name, string? fileName) base.Add(content); } + protected override void SerializeToStream(Stream stream, TransportContext? context, + CancellationToken cancellationToken) + { + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(MultipartFormDataContent)) + { + base.SerializeToStream(stream, context, cancellationToken); + } + else + { + SerializeToStreamCore(stream, context, cancellationToken); + } + } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. 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 90e392058d0cb..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,16 +15,14 @@ public sealed class ReadOnlyMemoryContent : HttpContent public ReadOnlyMemoryContent(ReadOnlyMemory content) => _content = content; - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => - stream.WriteAsync(_content).AsTask(); - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. - cancellationToken.ThrowIfCancellationRequested(); stream.Write(_content.Span); } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + stream.WriteAsync(_content).AsTask(); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => stream.WriteAsync(_content, cancellationToken).AsTask(); 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 index 203cf7638b427..d8bd8a9c58546 100644 --- 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 @@ -9,11 +9,6 @@ internal abstract class HttpMessageHandlerStage : HttpMessageHandler protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - if (request.Version.Major >= 2) - { - throw new NotSupportedException(); - } - ValueTask sendTask = SendAsync(request, async:false, cancellationToken); Debug.Assert(sendTask.IsCompleted); return sendTask.GetAwaiter().GetResult(); 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 7fae1d7e617b6..30c8bf5901ca8 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 @@ -337,6 +337,11 @@ private HttpMessageHandlerStage SetupHandlerChain() protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { + if (request.Version.Major >= 2) + { + throw new NotSupportedException(SR.Format(SR.net_http_sync_not_supported, GetType())); + } + CheckDisposed(); HttpMessageHandlerStage 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 d8b5872a70471..59e6eb0f43526 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 @@ -55,16 +55,23 @@ private void InitializeContent(Stream content, int bufferSize) protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { - if (stream == null) + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(StreamContent)) { - throw new ArgumentNullException(nameof(stream)); + base.SerializeToStream(stream, context, cancellationToken); } + else + { + SerializeToStreamCore(stream, context, cancellationToken); + } + } + private void SerializeToStreamCore(Stream stream, TransportContext? context, + CancellationToken cancellationToken) + { + Debug.Assert(stream != null); PrepareContent(); - - // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. - cancellationToken.ThrowIfCancellationRequested(); - // 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); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs index d19d8f2e2f126..fa599c2448d4e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs @@ -54,6 +54,21 @@ private static byte[] GetContentByteArray(string content, Encoding? encoding) return encoding.GetBytes(content); } + protected override void SerializeToStream(Stream stream, TransportContext? context, + CancellationToken cancellationToken) + { + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(StringContent)) + { + base.SerializeToStream(stream, context, cancellationToken); + } + else + { + SerializeToStreamCore(stream, context, cancellationToken); + } + } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. From f1736507d9850ed7b49266b9d2f2f2ebc06d35e7 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Wed, 20 May 2020 14:31:15 +0200 Subject: [PATCH 24/60] Fixed Cancellation tests --- src/libraries/Common/tests/System/IO/DelegateStream.cs | 4 ++-- .../Net/Http/HttpClientHandlerTest.Cancellation.cs | 4 ++++ .../src/System/Net/Http/StreamToStreamCopy.cs | 10 +++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) 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/HttpClientHandlerTest.Cancellation.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs index 6a7365e8eaa84..a42a3001bc6cd 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -683,6 +683,10 @@ private sealed class SetTcsContent : StreamContent 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); 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 95fd4d30b857f..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 @@ -27,7 +27,15 @@ public static void Copy(Stream source, Stream destination, int bufferSize, bool Debug.Assert(destination != null); Debug.Assert(bufferSize >= 0); - source.CopyTo(destination, bufferSize); + if (bufferSize == 0) + { + source.CopyTo(destination); + } + else + { + source.CopyTo(destination, bufferSize); + } + if (disposeSource) { DisposeSource(source); From e0b8c0de71bdbfb883efe040c1d9dc495ffa0f98 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Thu, 21 May 2020 10:22:37 +0200 Subject: [PATCH 25/60] More tests fixed. --- .../System/Net/Http/HttpClientHandlerTest.cs | 122 ++++++++++-------- .../Net/Http/HttpClientHandlerTestBase.cs | 5 - .../FunctionalTests/SyncHttpHandlerTest.cs | 10 +- 3 files changed, 74 insertions(+), 63 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 80c66597cb422..b199f5ea6cc52 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -754,9 +754,8 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => server.AcceptConnectionSendCustomResponseAndCloseAsync("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhe")); } - [ConditionalTheory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly(bool async) + [ConditionalFact] + public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly() { #if WINHTTPHANDLER_TEST if (UseVersion > HttpVersion.Version11) @@ -840,7 +839,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => request.Headers.Add("X-Underscore_Name", "X-Underscore_Name"); request.Headers.Add("X-End", "End"); - (await client.SendAsync(async, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); + (await client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); } }, async server => { @@ -1192,15 +1191,14 @@ await LoopbackServer.CreateServerAsync(async (server, url) => }); } - [Theory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws(bool async) + [Fact] + public async Task SendAsync_TransferEncodingSetButNoRequestContent_Throws() { var req = new HttpRequestMessage(HttpMethod.Post, "http://bing.com") { Version = UseVersion }; req.Headers.TransferEncodingChunked = true; using (HttpClient c = CreateHttpClient()) { - HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(async, req)); + HttpRequestException error = await Assert.ThrowsAsync(() => c.SendAsync(TestAsync, req)); Assert.IsType(error.InnerException); } } @@ -1231,13 +1229,19 @@ public async Task GetAsync_ResponseHeadersRead_ReadFromEachIterativelyDoesntDead } [OuterLoop("Uses external server")] - [Theory, MemberData(nameof(AsyncRemoteServersMemberData))] - public async Task SendAsync_HttpRequestMsgResponseHeadersRead_StatusCodeOK(bool async, Configuration.Http.RemoteServer remoteServer) + [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(async, request, HttpCompletionOption.ResponseHeadersRead)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead)) { string responseContent = await response.Content.ReadAsStringAsync(); _output.WriteLine(responseContent); @@ -1306,7 +1310,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()) { @@ -1462,7 +1466,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { 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 @@ -1867,22 +1871,27 @@ public async Task PostAsync_CallMethod_EmptyContent(Configuration.Http.RemoteSer } } - public static IEnumerable AsyncExpectContinueVersion() + public static IEnumerable ExpectContinueVersion() { var versions = new string[] {"1.0", "1.1", "2.0"}; var expectContinue = new bool?[] {true, false, null}; return - from async in AsyncBoolValues from expect in expectContinue from version in versions - select new object[] {async, expect, version}; + select new object[] {expect, version}; } [OuterLoop("Uses external server")] [Theory] - [MemberData(nameof(AsyncExpectContinueVersion))] - public async Task PostAsync_ExpectContinue_Success(bool async, bool? expectContinue, string version) + [MemberData(nameof(ExpectContinueVersion))] + public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string version) { + // Sync API supported only up to HTTP/1.1 + if (!TestAsync && version == "2.0") + { + return; + } + using (HttpClient client = CreateHttpClient()) { var req = new HttpRequestMessage(HttpMethod.Post, version == "2.0" ? Configuration.Http.Http2RemoteEchoServer : Configuration.Http.RemoteEchoServer) @@ -1892,7 +1901,7 @@ public async Task PostAsync_ExpectContinue_Success(bool async, bool? expectConti }; req.Headers.ExpectContinue = expectContinue; - using (HttpResponseMessage response = await client.SendAsync(async, req)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, req)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -1946,23 +1955,20 @@ await server.AcceptConnectionAsync(async connection => }); } - public static IEnumerable AsyncInterim1xxStatusCode() + public static IEnumerable Interim1xxStatusCode() { - foreach (var async in AsyncBoolValues) - { - yield return new object[] {async, (HttpStatusCode)100}; // 100 Continue. + yield return new object[] {(HttpStatusCode)100}; // 100 Continue. // 101 SwitchingProtocols will be treated as a final status code. - yield return new object[] {async, (HttpStatusCode)102}; // 102 Processing. - yield return new object[] {async, (HttpStatusCode)103}; // 103 EarlyHints. - yield return new object[] {async, (HttpStatusCode)150}; - yield return new object[] {async, (HttpStatusCode)180}; - yield return new object[] {async, (HttpStatusCode)199}; - } + yield return new object[] {(HttpStatusCode)102}; // 102 Processing. + yield return new object[] {(HttpStatusCode)103}; // 103 EarlyHints. + yield return new object[] {(HttpStatusCode)150}; + yield return new object[] {(HttpStatusCode)180}; + yield return new object[] {(HttpStatusCode)199}; } [ConditionalTheory] - [MemberData(nameof(AsyncInterim1xxStatusCode))] - public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(bool async, HttpStatusCode responseStatusCode) + [MemberData(nameof(Interim1xxStatusCode))] + public async Task SendAsync_1xxResponsesWithHeaders_InterimResponsesHeadersIgnored(HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -1987,7 +1993,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(async, initialMessage); + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); // Verify status code. Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -2031,8 +2037,8 @@ await server.AcceptConnectionAsync(async connection => } [ConditionalTheory] - [MemberData(nameof(AsyncInterim1xxStatusCode))] - public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(bool async, HttpStatusCode responseStatusCode) + [MemberData(nameof(Interim1xxStatusCode))] + public async Task SendAsync_Unexpected1xxResponses_DropAllInterimResponses(HttpStatusCode responseStatusCode) { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2051,8 +2057,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => initialMessage.Content = new StringContent(TestString); // No ExpectContinue header. initialMessage.Headers.ExpectContinue = false; - HttpResponseMessage response = await client.SendAsync(async, initialMessage); - + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); } @@ -2076,9 +2081,8 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalTheory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse(bool async) + [Fact] + public async Task SendAsync_MultipleExpected100Responses_ReceivesCorrectResponse() { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2096,7 +2100,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(async, initialMessage); + HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); @@ -2122,9 +2126,8 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalTheory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually(bool async) + [ConditionalFact] + public async Task SendAsync_No100ContinueReceived_RequestBodySentEventually() { #if WINHTTPHANDLER_TEST if (UseVersion >= HttpVersion20.Value) @@ -2143,7 +2146,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(async, initialMessage)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(ResponseString, await response.Content.ReadAsStringAsync()); @@ -2386,7 +2389,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); @@ -2406,7 +2409,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); @@ -2439,7 +2442,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); @@ -2473,7 +2476,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") { @@ -2500,7 +2503,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; } @@ -2538,6 +2541,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); @@ -2548,7 +2557,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( @@ -2572,7 +2581,7 @@ 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 => { @@ -2585,13 +2594,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); @@ -2610,7 +2625,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); @@ -2633,7 +2648,6 @@ await LoopbackServer.CreateServerAsync(async (server, url) => { Assert.True(false, "Invalid HTTP request version"); } - } }); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 2654cde8de9b2..e69319828a837 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -89,11 +89,6 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; - public static IEnumerable AsyncRemoteServersMemberData() => - from async in AsyncBoolValues - from remoteServer in Configuration.Http.RemoteServers - select new object[] { async, remoteServer }; - protected HttpClient CreateHttpClientForRemoteServer(Configuration.Http.RemoteServer remoteServer) { return CreateHttpClientForRemoteServer(remoteServer, CreateHttpClientHandler()); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs index 1cab9f4c84303..4ae84bc5d845c 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs @@ -946,12 +946,14 @@ public async Task Http2GetAsync_TrailingHeaders_NoData_EmptyResponseObserved() public sealed class SocketsHttpHandler_SchSendAuxRecordHttpTest : SchSendAuxRecordHttpTest { public SocketsHttpHandler_SchSendAuxRecordHttpTest(ITestOutputHelper output) : base(output) { } - } + }*/ - public sealed class SocketsHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest + public sealed class SyncHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest { - public SocketsHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { } - }*/ + protected override bool TestAsync => false; + + public SyncHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { } + } public sealed class SyncHttpHandlerTest_AutoRedirect : HttpClientHandlerTest_AutoRedirect { From 77b406f3da2c0e45a7ec2b69ef3f5f3e55692bd0 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Thu, 21 May 2020 14:44:05 +0200 Subject: [PATCH 26/60] Moar tests. --- .../HttpClientHandlerTest.Authentication.cs | 44 +- .../Net/Http/HttpClientHandlerTest.Cookies.cs | 30 +- .../System/Net/Http/HttpClientHandlerTest.cs | 6 + .../System/Net/Http/HttpProtocolTests.cs | 30 +- .../System/Net/Http/HttpRetryProtocolTests.cs | 7 +- .../System/Net/Http/IdnaProtocolTests.cs | 2 +- .../tests/System/Net/Http/PostScenarioTest.cs | 16 +- .../tests/FunctionalTests/DiagnosticsTests.cs | 19 +- .../HttpClientHandlerTest.Connect.cs | 97 + .../HttpClientHandlerTest.Headers.cs | 16 +- .../FunctionalTests/SocketsHttpHandlerTest.cs | 130 +- .../FunctionalTests/SyncHttpHandlerTest.cs | 2004 +---------------- .../System.Net.Http.Functional.Tests.csproj | 1 + 13 files changed, 251 insertions(+), 2151 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs 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 6255cbdc05cf5..f58f26ceaa110 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,43 @@ 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. + 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 }; + } + [Theory] [MemberData(nameof(Authentication_TestData))] public async Task HttpClientHandler_Authentication_Succeeds(string authenticateHeader, bool result) @@ -551,9 +588,8 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_Success() } } - [ConditionalTheory(nameof(IsDomainJoinedServerAvailable))] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success(bool async) + [ConditionalFact(nameof(IsDomainJoinedServerAvailable))] + public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success() { using (HttpClientHandler handler = CreateHttpClientHandler()) using (HttpClient client = CreateHttpClient(handler)) @@ -569,7 +605,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(async, request)) + using (HttpResponseMessage response = await client.SendAsync(TestAsync, request)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string body = await response.Content.ReadAsStringAsync(); 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 eaa7c26ab01f6..325ac0c6cd6d7 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -126,9 +126,8 @@ await LoopbackServerFactory.CreateClientAndServerAsync( }); } - [Theory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task GetAsync_AddCookieHeader_CookieHeaderSent(bool async) + [Fact] + public async Task GetAsync_AddCookieHeader_CookieHeaderSent() { await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -138,7 +137,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; requestMessage.Headers.Add("Cookie", s_customCookieHeaderValue); - await client.SendAsync(async, requestMessage); + await client.SendAsync(TestAsync, requestMessage); } }, async server => @@ -148,9 +147,8 @@ await LoopbackServerFactory.CreateClientAndServerAsync( }); } - [Theory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent(bool async) + [Fact] + public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent() { await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -162,7 +160,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( requestMessage.Headers.Add("Cookie", "B=2"); requestMessage.Headers.Add("Cookie", "C=3"); - await client.SendAsync(async, requestMessage); + await client.SendAsync(TestAsync, requestMessage); } }, async server => @@ -213,9 +211,8 @@ private string GetCookieValue(HttpRequestData request) return cookieHeaderValue; } - [Theory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent(bool async) + [Fact] + public async Task GetAsync_SetCookieContainerAndCookieHeader_BothCookiesSent() { await LoopbackServerFactory.CreateServerAsync(async (server, url) => { @@ -227,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(async, requestMessage); + Task getResponseTask = client.SendAsync(TestAsync, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); @@ -240,10 +237,9 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => } }); } - - [Theory] - [MemberData(nameof(AsyncBoolMemberData))] - public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent(bool async) + + [Fact] + public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent() { await LoopbackServerFactory.CreateServerAsync(async (server, url) => { @@ -256,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(async, requestMessage); + Task getResponseTask = client.SendAsync(TestAsync, requestMessage); Task serverTask = server.HandleRequestAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index b199f5ea6cc52..81ce1ac4d647c 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -2577,6 +2577,12 @@ public async Task SendAsync_RequestVersion20_HttpNotHttps_NoUpgradeRequest() throw new SkipTestException($"Test doesn't support {UseVersion} protocol."); } #endif + // Sync API supported only up to HTTP/1.1 + if (!TestAsync) + { + return; + } + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { using (HttpClient client = CreateHttpClient()) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs index 635cb2bed65c1..d0acc72051934 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs @@ -36,7 +36,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 +57,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 +81,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) @@ -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,10 @@ await server.AcceptConnectionAsync(async connection => [InlineData("head", "HEAD")] [InlineData("post", "POST")] [InlineData("put", "PUT")] + [InlineData("delete", "DELETE")] + [InlineData("options", "OPTIONS")] + [InlineData("trace", "TRACE")] + [InlineData("patch", "PATCH")] [InlineData("other", "other")] [InlineData("SometHING", "SometHING")] public async Task CustomMethod_SentUppercasedIfKnown(string specifiedMethod, string expectedMethod) @@ -521,7 +531,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..1aa67412f352e 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,10 @@ public SynchronizedSendContent(TaskCompletionSource sendingContent, Task c _sendingContent = sendingContent; } + protected override void SerializeToStream(Stream stream, TransportContext context, + CancellationToken cancellationToken) => + SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); + 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/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs index 1019de971acf8..47ffcf5cdaa15 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() [Fact] 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. + SpinWait.SpinUntil(() => sendTask.IsCompleted); + } 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.Connect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs new file mode 100644 index 0000000000000..549c6e9674808 --- /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(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); + }); + } + }); + } + } +} 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/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 99b0c2c2bb5b0..3c9bceece48c2 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 @@ -1092,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 @@ -1250,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 index 4ae84bc5d845c..50dc754b4a3c4 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs @@ -22,963 +22,56 @@ namespace System.Net.Http.Functional.Tests { - /*public sealed class SocketsHttpHandler_HttpClientHandler_Asynchrony_Test : HttpClientHandler_Asynchrony_Test + public sealed class SyncHttpHandler_HttpProtocolTests : HttpProtocolTests { - public SocketsHttpHandler_HttpClientHandler_Asynchrony_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task ExecutionContext_Suppressed_Success() - { - await LoopbackServerFactory.CreateClientAndServerAsync( - uri => Task.Run(() => - { - using (ExecutionContext.SuppressFlow()) - using (HttpClient client = CreateHttpClient()) - { - client.GetStringAsync(uri).GetAwaiter().GetResult(); - } - }), - async server => - { - await server.AcceptConnectionSendResponseAndCloseAsync(); - }); - } - - [OuterLoop("Relies on finalization")] - [Fact] - public async Task ExecutionContext_HttpConnectionLifetimeDoesntKeepContextAlive() - { - var clientCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - try - { - using (HttpClient client = CreateHttpClient()) - { - (Task completedWhenFinalized, Task getRequest) = MakeHttpRequestWithTcsSetOnFinalizationInAsyncLocal(client, uri); - await getRequest; - - for (int i = 0; i < 3; i++) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - - await completedWhenFinalized.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); - } - } - finally - { - clientCompleted.SetResult(true); - } - }, async server => - { - await server.AcceptConnectionAsync(async connection => - { - await connection.ReadRequestHeaderAndSendResponseAsync(); - await clientCompleted.Task; - }); - }); - } - - [MethodImpl(MethodImplOptions.NoInlining)] // avoid JIT extending lifetime of the finalizable object - private static (Task completedOnFinalized, Task getRequest) MakeHttpRequestWithTcsSetOnFinalizationInAsyncLocal(HttpClient client, Uri uri) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - // Put something in ExecutionContext, start the HTTP request, then undo the EC change. - var al = new AsyncLocal() { Value = new SetOnFinalized() { _completedWhenFinalized = tcs } }; - Task t = client.GetStringAsync(uri); - al.Value = null; - - // Return a task that will complete when the SetOnFinalized is finalized, - // as well as a task to wait on for the get request; for the get request, - // we return a continuation to avoid any test-altering issues related to - // the state machine holding onto stuff. - t = t.ContinueWith(p => p.GetAwaiter().GetResult()); - return (tcs.Task, t); - } - - private sealed class SetOnFinalized - { - internal TaskCompletionSource _completedWhenFinalized; - ~SetOnFinalized() => _completedWhenFinalized.SetResult(true); - } - } - - 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 - { - public SocketsHttpHandler_HttpProtocolTests_Dribble(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_DiagnosticsTest : DiagnosticsTest - { - public SocketsHttpHandler_DiagnosticsTest(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClient_SelectedSites_Test : HttpClient_SelectedSites_Test - { - public SocketsHttpHandler_HttpClient_SelectedSites_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientEKUTest : HttpClientEKUTest - { - public SocketsHttpHandler_HttpClientEKUTest(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_Decompression_Tests : HttpClientHandler_Decompression_Test - { - public SocketsHttpHandler_HttpClientHandler_Decompression_Tests(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test : HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test - { - public SocketsHttpHandler_HttpClientHandler_DangerousAcceptAllCertificatesValidator_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_ClientCertificates_Test : HttpClientHandler_ClientCertificates_Test - { - public SocketsHttpHandler_HttpClientHandler_ClientCertificates_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_DefaultProxyCredentials_Test : HttpClientHandler_DefaultProxyCredentials_Test - { - public SocketsHttpHandler_HttpClientHandler_DefaultProxyCredentials_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test : HttpClientHandler_Finalization_Test - { - public SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test : HttpClientHandler_Finalization_Test - { - public SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - } - - public sealed class SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test : HttpClientHandler_MaxConnectionsPerServer_Test - { - public SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test(ITestOutputHelper output) : base(output) { } - - [OuterLoop("Incurs a small delay")] - [Theory] - [InlineData(0)] - [InlineData(1)] - public async Task SmallConnectionLifetimeWithMaxConnections_PendingRequestUsesDifferentConnection(int lifetimeMilliseconds) - { - using (var handler = new SocketsHttpHandler()) - { - handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(lifetimeMilliseconds); - handler.MaxConnectionsPerServer = 1; - - using (HttpClient client = CreateHttpClient(handler)) - { - await LoopbackServer.CreateServerAsync(async (server, uri) => - { - Task request1 = client.GetStringAsync(uri); - Task request2 = client.GetStringAsync(uri); - - await server.AcceptConnectionAsync(async connection => - { - Task secondResponse = server.AcceptConnectionAsync(connection2 => - connection2.ReadRequestHeaderAndSendCustomResponseAsync(LoopbackServer.GetConnectionCloseResponse())); - - // Wait a small amount of time before sending the first response, so the connection lifetime will expire. - Debug.Assert(lifetimeMilliseconds < 100); - await Task.Delay(100); - - // Second request should not have completed yet, as we haven't completed the first yet. - Assert.False(request2.IsCompleted); - Assert.False(secondResponse.IsCompleted); - - // Send the first response and wait for the first request to complete. - await connection.ReadRequestHeaderAndSendResponseAsync(); - await request1; - - // Now the second request should complete. - await secondResponse.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); - }); - }); - } - } - } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test : HttpClientHandler_ServerCertificates_Test - { - public SocketsHttpHandler_HttpClientHandler_ServerCertificates_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_ResponseDrain_Test : HttpClientHandler_ResponseDrain_Test - { - protected override void SetResponseDrainTimeout(HttpClientHandler handler, TimeSpan time) - { - SocketsHttpHandler s = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); - Assert.NotNull(s); - s.ResponseDrainTimeout = time; - } - - public SocketsHttpHandler_HttpClientHandler_ResponseDrain_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public void MaxResponseDrainSize_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); - - handler.MaxResponseDrainSize = 0; - Assert.Equal(0, handler.MaxResponseDrainSize); - - handler.MaxResponseDrainSize = int.MaxValue; - Assert.Equal(int.MaxValue, handler.MaxResponseDrainSize); - } - } - - [Fact] - public void MaxResponseDrainSize_InvalidArgument_Throws() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); - - AssertExtensions.Throws("value", () => handler.MaxResponseDrainSize = -1); - AssertExtensions.Throws("value", () => handler.MaxResponseDrainSize = int.MinValue); - - Assert.Equal(1024 * 1024, handler.MaxResponseDrainSize); - } - } - - [Fact] - public void MaxResponseDrainSize_SetAfterUse_Throws() - { - using (var handler = new SocketsHttpHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - handler.MaxResponseDrainSize = 1; - client.GetAsync("http://" + Guid.NewGuid().ToString("N")); // ignoring failure - Assert.Equal(1, handler.MaxResponseDrainSize); - Assert.Throws(() => handler.MaxResponseDrainSize = 1); - } - } - - [Fact] - public void ResponseDrainTimeout_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); - - handler.ResponseDrainTimeout = TimeSpan.Zero; - Assert.Equal(TimeSpan.Zero, handler.ResponseDrainTimeout); - - handler.ResponseDrainTimeout = TimeSpan.FromTicks(int.MaxValue); - Assert.Equal(TimeSpan.FromTicks(int.MaxValue), handler.ResponseDrainTimeout); - } - } - - [Fact] - public void MaxResponseDraiTime_InvalidArgument_Throws() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); - - AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(-1)); - AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.MaxValue); - AssertExtensions.Throws("value", () => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(int.MaxValue)); - - Assert.Equal(TimeSpan.FromSeconds(2), handler.ResponseDrainTimeout); - } - } - - [Fact] - public void ResponseDrainTimeout_SetAfterUse_Throws() - { - using (var handler = new SocketsHttpHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - handler.ResponseDrainTimeout = TimeSpan.FromSeconds(42); - client.GetAsync("http://" + Guid.NewGuid().ToString("N")); // ignoring failure - Assert.Equal(TimeSpan.FromSeconds(42), handler.ResponseDrainTimeout); - Assert.Throws(() => handler.ResponseDrainTimeout = TimeSpan.FromSeconds(42)); - } - } - - [OuterLoop] - [Theory] - [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 3, LoopbackServer.ContentMode.ContentLength)] - [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 3, LoopbackServer.ContentMode.SingleChunk)] - [InlineData(1024 * 1024 * 2, 9_500, 1024 * 1024 * 13, LoopbackServer.ContentMode.BytePerChunk)] - public async Task GetAsyncWithMaxConnections_DisposeBeforeReadingToEnd_DrainsRequestsUnderMaxDrainSizeAndReusesConnection(int totalSize, int readSize, int maxDrainSize, LoopbackServer.ContentMode mode) - { - await LoopbackServer.CreateClientAndServerAsync( - async url => - { - var handler = new SocketsHttpHandler(); - handler.MaxResponseDrainSize = maxDrainSize; - handler.ResponseDrainTimeout = Timeout.InfiniteTimeSpan; - - // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) - handler.MaxConnectionsPerServer = 1; - - using (HttpClient client = CreateHttpClient(handler)) - { - HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - ValidateResponseHeaders(response1, totalSize, mode); - - // Read part but not all of response - Stream responseStream = await response1.Content.ReadAsStreamAsync(); - await ReadToByteCount(responseStream, readSize); - - response1.Dispose(); - - // Issue another request. We'll confirm that it comes on the same connection. - HttpResponseMessage response2 = await client.GetAsync(url); - ValidateResponseHeaders(response2, totalSize, mode); - Assert.Equal(totalSize, (await response2.Content.ReadAsStringAsync()).Length); - } - }, - async server => - { - string content = new string('a', totalSize); - string response = LoopbackServer.GetContentModeResponse(mode, content); - await server.AcceptConnectionAsync(async connection => - { - server.ListenSocket.Close(); // Shut down the listen socket so attempts at additional connections would fail on the client - await connection.ReadRequestHeaderAndSendCustomResponseAsync(response); - await connection.ReadRequestHeaderAndSendCustomResponseAsync(response); - }); - }); - } - - [OuterLoop] - [Theory] - [InlineData(100_000, 0, LoopbackServer.ContentMode.ContentLength)] - [InlineData(100_000, 0, LoopbackServer.ContentMode.SingleChunk)] - [InlineData(100_000, 0, LoopbackServer.ContentMode.BytePerChunk)] - public async Task GetAsyncWithMaxConnections_DisposeLargerThanMaxDrainSize_KillsConnection(int totalSize, int maxDrainSize, LoopbackServer.ContentMode mode) - { - await LoopbackServer.CreateClientAndServerAsync( - async url => - { - var handler = new SocketsHttpHandler(); - handler.MaxResponseDrainSize = maxDrainSize; - handler.ResponseDrainTimeout = Timeout.InfiniteTimeSpan; - - // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) - handler.MaxConnectionsPerServer = 1; - - using (HttpClient client = CreateHttpClient(handler)) - { - HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - ValidateResponseHeaders(response1, totalSize, mode); - response1.Dispose(); - - // Issue another request. We'll confirm that it comes on a new connection. - HttpResponseMessage response2 = await client.GetAsync(url); - ValidateResponseHeaders(response2, totalSize, mode); - Assert.Equal(totalSize, (await response2.Content.ReadAsStringAsync()).Length); - } - }, - async server => - { - string content = new string('a', totalSize); - await server.AcceptConnectionAsync(async connection => - { - await connection.ReadRequestHeaderAsync(); - try - { - await connection.Writer.WriteAsync(LoopbackServer.GetContentModeResponse(mode, content, connectionClose: false)); - } - catch (Exception) { } // Eat errors from client disconnect. - - await server.AcceptConnectionSendCustomResponseAndCloseAsync(LoopbackServer.GetContentModeResponse(mode, content, connectionClose: true)); - }); - }); - } - - [OuterLoop] - [Theory] - [InlineData(LoopbackServer.ContentMode.ContentLength)] - [InlineData(LoopbackServer.ContentMode.SingleChunk)] - [InlineData(LoopbackServer.ContentMode.BytePerChunk)] - public async Task GetAsyncWithMaxConnections_DrainTakesLongerThanTimeout_KillsConnection(LoopbackServer.ContentMode mode) - { - const int ContentLength = 10_000; - - await LoopbackServer.CreateClientAndServerAsync( - async url => - { - var handler = new SocketsHttpHandler(); - handler.MaxResponseDrainSize = int.MaxValue; - handler.ResponseDrainTimeout = TimeSpan.FromMilliseconds(1); - - // Set MaxConnectionsPerServer to 1. This will ensure we will wait for the previous request to drain (or fail to) - handler.MaxConnectionsPerServer = 1; - - using (HttpClient client = CreateHttpClient(handler)) - { - client.Timeout = Timeout.InfiniteTimeSpan; - - HttpResponseMessage response1 = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - ValidateResponseHeaders(response1, ContentLength, mode); - response1.Dispose(); - - // Issue another request. We'll confirm that it comes on a new connection. - HttpResponseMessage response2 = await client.GetAsync(url); - ValidateResponseHeaders(response2, ContentLength, mode); - Assert.Equal(ContentLength, (await response2.Content.ReadAsStringAsync()).Length); - } - }, - async server => - { - string content = new string('a', ContentLength); - await server.AcceptConnectionAsync(async connection => - { - string response = LoopbackServer.GetContentModeResponse(mode, content, connectionClose: false); - await connection.ReadRequestHeaderAsync(); - try - { - // Write out only part of the response - await connection.Writer.WriteAsync(response.Substring(0, response.Length / 2)); - } - catch (Exception) { } // Eat errors from client disconnect. - - response = LoopbackServer.GetContentModeResponse(mode, content, connectionClose: true); - await server.AcceptConnectionSendCustomResponseAndCloseAsync(response); - }); - }); - } - } - - public sealed class SocketsHttpHandler_PostScenarioTest : PostScenarioTest - { - public SocketsHttpHandler_PostScenarioTest(ITestOutputHelper output) : base(output) { } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task DisposeTargetStream_ThrowsObjectDisposedException(bool knownLength) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await LoopbackServerFactory.CreateClientAndServerAsync(async uri => - { - try - { - using (HttpClient client = CreateHttpClient()) - { - Task t = client.PostAsync(uri, new DisposeStreamWhileCopyingContent(knownLength)); - Assert.IsType((await Assert.ThrowsAsync(() => t)).InnerException); - } - } - finally - { - tcs.SetResult(0); - } - }, server => tcs.Task); - } - - private sealed class DisposeStreamWhileCopyingContent : HttpContent - { - private readonly bool _knownLength; - - public DisposeStreamWhileCopyingContent(bool knownLength) => _knownLength = knownLength; - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - await stream.WriteAsync(new byte[42], 0, 42); - stream.Dispose(); - } - - protected override bool TryComputeLength(out long length) - { - if (_knownLength) - { - length = 42; - return true; - } - else - { - length = 0; - return false; - } - } - } - } - - public sealed class SocketsHttpHandler_ResponseStreamTest : ResponseStreamTest - { - public SocketsHttpHandler_ResponseStreamTest(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_SslProtocols_Test : HttpClientHandler_SslProtocols_Test - { - public SocketsHttpHandler_HttpClientHandler_SslProtocols_Test(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_HttpClientHandler_Proxy_Test : HttpClientHandler_Proxy_Test - { - public SocketsHttpHandler_HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { } + public SyncHttpHandler_HttpProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - public abstract class SocketsHttpHandler_TrailingHeaders_Test : HttpClientHandlerTestBase + public sealed class SyncHttpHandler_HttpProtocolTests_Dribble : HttpProtocolTests_Dribble { - public SocketsHttpHandler_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } - - protected static byte[] DataBytes = Encoding.ASCII.GetBytes("data"); - - protected static readonly IList TrailingHeaders = new HttpHeaderData[] { - new HttpHeaderData("MyCoolTrailerHeader", "amazingtrailer"), - new HttpHeaderData("EmptyHeader", ""), - new HttpHeaderData("Accept-Encoding", "identity,gzip"), - new HttpHeaderData("Hello", "World") }; - - protected static Frame MakeDataFrame(int streamId, byte[] data, bool endStream = false) => - new DataFrame(data, (endStream ? FrameFlags.EndStream : FrameFlags.None), 0, streamId); + public SyncHttpHandler_HttpProtocolTests_Dribble(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - public class SocketsHttpHandler_Http1_TrailingHeaders_Test : SocketsHttpHandler_TrailingHeaders_Test + public sealed class SyncHttpHandler_DiagnosticsTest : DiagnosticsTest { - public SocketsHttpHandler_Http1_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task GetAsyncDefaultCompletionOption_TrailingHeaders_Available(bool includeTrailerHeader) - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClientHandler handler = CreateHttpClientHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - Task getResponseTask = client.GetAsync(url); - await TestHelper.WhenAllCompletedOrAnyFailed( - getResponseTask, - server.AcceptConnectionSendCustomResponseAndCloseAsync( - "HTTP/1.1 200 OK\r\n" + - "Connection: close\r\n" + - "Transfer-Encoding: chunked\r\n" + - (includeTrailerHeader ? "Trailer: MyCoolTrailerHeader, Hello\r\n" : "") + - "\r\n" + - "4\r\n" + - "data\r\n" + - "0\r\n" + - "MyCoolTrailerHeader: amazingtrailer\r\n" + - "Accept-encoding: identity,gzip\r\n" + - "Hello: World\r\n" + - "\r\n")); - - using (HttpResponseMessage response = await getResponseTask) - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); - - // Check the Trailer header. - if (includeTrailerHeader) - { - Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer")); - Assert.Contains("Hello", response.Headers.GetValues("Trailer")); - } - - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - Assert.Contains("identity,gzip", response.TrailingHeaders.GetValues("Accept-encoding")); - - string data = await response.Content.ReadAsStringAsync(); - Assert.Contains("data", data); - // Trailers should not be part of the content data. - Assert.DoesNotContain("MyCoolTrailerHeader", data); - Assert.DoesNotContain("amazingtrailer", data); - Assert.DoesNotContain("Hello", data); - Assert.DoesNotContain("World", data); - } - } - }); - } - - [Fact] - public async Task GetAsyncResponseHeadersReadOption_TrailingHeaders_Available() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClientHandler handler = CreateHttpClientHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - Task getResponseTask = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - await TestHelper.WhenAllCompletedOrAnyFailed( - getResponseTask, - server.AcceptConnectionSendCustomResponseAndCloseAsync( - "HTTP/1.1 200 OK\r\n" + - "Connection: close\r\n" + - "Transfer-Encoding: chunked\r\n" + - "Trailer: MyCoolTrailerHeader\r\n" + - "\r\n" + - "4\r\n" + - "data\r\n" + - "0\r\n" + - "MyCoolTrailerHeader: amazingtrailer\r\n" + - "Hello: World\r\n" + - "\r\n")); - - using (HttpResponseMessage response = await getResponseTask) - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); - Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer")); - - // Pending read on the response content. - var trailingHeaders = response.TrailingHeaders; - Assert.Empty(trailingHeaders); - - Stream stream = await response.Content.ReadAsStreamAsync(); - Byte[] data = new Byte[100]; - // Read some data, preferably whole body. - int readBytes = await stream.ReadAsync(data, 0, 4); - - // Intermediate test - haven't reached stream EOF yet. - Assert.Empty(response.TrailingHeaders); - if (readBytes == 4) - { - // If we consumed whole content, check content. - Assert.Contains("data", System.Text.Encoding.Default.GetString(data)); - } - - // Read data until EOF is reached - while (stream.Read(data, 0, data.Length) != 0) - ; - - Assert.Same(trailingHeaders, response.TrailingHeaders); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - } - } - }); - } - - [Theory] - [InlineData("Age", "1")] - [InlineData("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l")] - [InlineData("Cache-Control", "no-cache")] - [InlineData("Content-Encoding", "gzip")] - [InlineData("Content-Length", "22")] - [InlineData("Content-type", "foo/bar")] - [InlineData("Content-Range", "bytes 200-1000/67589")] - [InlineData("Date", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("Expect", "100-continue")] - [InlineData("Expires", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("Host", "foo")] - [InlineData("If-Match", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("If-None-Match", "*")] - [InlineData("If-Range", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("If-Unmodified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")] - [InlineData("Location", "/index.html")] - [InlineData("Max-Forwards", "2")] - [InlineData("Pragma", "no-cache")] - [InlineData("Range", "5/10")] - [InlineData("Retry-After", "20")] - [InlineData("Set-Cookie", "foo=bar")] - [InlineData("TE", "boo")] - [InlineData("Transfer-Encoding", "chunked")] - [InlineData("Transfer-Encoding", "gzip")] - [InlineData("Vary", "*")] - [InlineData("Warning", "300 - \"Be Warned!\"")] - public async Task GetAsync_ForbiddenTrailingHeaders_Ignores(string name, string value) - { - await LoopbackServer.CreateClientAndServerAsync(async url => - { - using (HttpClientHandler handler = CreateHttpClientHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - HttpResponseMessage response = await client.GetAsync(url); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.False(response.TrailingHeaders.TryGetValues(name, out IEnumerable values)); - Assert.Contains("Loopback", response.TrailingHeaders.GetValues("Server")); - } - }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync( - "HTTP/1.1 200 OK\r\n" + - "Connection: close\r\n" + - "Transfer-Encoding: chunked\r\n" + - $"Trailer: Set-Cookie, MyCoolTrailerHeader, {name}, Hello\r\n" + - "\r\n" + - "4\r\n" + - "data\r\n" + - "0\r\n" + - "Set-Cookie: yummy\r\n" + - "MyCoolTrailerHeader: amazingtrailer\r\n" + - $"{name}: {value}\r\n" + - "Server: Loopback\r\n" + - $"{name}: {value}\r\n" + - "\r\n")); - } - - [Fact] - public async Task GetAsync_NoTrailingHeaders_EmptyCollection() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClientHandler handler = CreateHttpClientHandler()) - using (HttpClient client = CreateHttpClient(handler)) - { - Task getResponseTask = client.GetAsync(url); - await TestHelper.WhenAllCompletedOrAnyFailed( - getResponseTask, - server.AcceptConnectionSendCustomResponseAndCloseAsync( - "HTTP/1.1 200 OK\r\n" + - "Connection: close\r\n" + - "Transfer-Encoding: chunked\r\n" + - "Trailer: MyCoolTrailerHeader\r\n" + - "\r\n" + - "4\r\n" + - "data\r\n" + - "0\r\n" + - "\r\n")); - - using (HttpResponseMessage response = await getResponseTask) - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding")); - - Assert.NotNull(response.TrailingHeaders); - Assert.Equal(0, response.TrailingHeaders.Count()); - Assert.Same(response.TrailingHeaders, response.TrailingHeaders); - } - } - }); - } + public SyncHttpHandler_DiagnosticsTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - // TODO: make generic to support HTTP/2 and HTTP/3. - public sealed class SocketsHttpHandler_Http2_TrailingHeaders_Test : SocketsHttpHandler_TrailingHeaders_Test + public sealed class SyncHttpHandler_PostScenarioTest : PostScenarioTest { - public SocketsHttpHandler_Http2_TrailingHeaders_Test(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - - // Response data. - await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes, endStream: true)); - - // Server doesn't send trailing header frame. - HttpResponseMessage response = await sendTask; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.TrailingHeaders); - Assert.Equal(0, response.TrailingHeaders.Count()); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - - // Response data, missing Trailers. - await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); - - // Additional trailing header frame. - await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:true, headers: TrailingHeaders, endStream : true); - - HttpResponseMessage response = await sendTask; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsync_TrailerHeaders_TrailingPseudoHeadersThrow() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); - // Additional trailing header frame with pseudo-headers again.. - await connection.SendResponseHeadersAsync(streamId, isTrailingHeader:false, headers: TrailingHeaders, endStream : true); - - await Assert.ThrowsAsync(() => sendTask); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address, HttpCompletionOption.ResponseHeadersRead); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - - // Response data, missing Trailers. - await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); - - HttpResponseMessage response = await sendTask; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - // Pending read on the response content. - Assert.Empty(response.TrailingHeaders); - - Stream stream = await response.Content.ReadAsStreamAsync(); - Byte[] data = new Byte[100]; - await stream.ReadAsync(data, 0, data.Length); - - // Intermediate test - haven't reached stream EOF yet. - Assert.Empty(response.TrailingHeaders); - - // Finish data stream and write out trailing headers. - await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes)); - await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders); - - // Read data until EOF is reached - while (stream.Read(data, 0, data.Length) != 0); - - Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsync_TrailerHeaders_TrailingHeaderNoBody() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - await connection.SendResponseHeadersAsync(streamId, endStream : true, isTrailingHeader:true, headers: TrailingHeaders); - - HttpResponseMessage response = await sendTask; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(TrailingHeaders.Count, response.TrailingHeaders.Count()); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public async Task Http2GetAsync_TrailingHeaders_NoData_EmptyResponseObserved() - { - using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) - using (HttpClient client = CreateHttpClient()) - { - Task sendTask = client.GetAsync(server.Address); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - - int streamId = await connection.ReadRequestHeaderAsync(); - - // Response header. - await connection.SendDefaultResponseHeadersAsync(streamId); - - // No data. - - // Response trailing headers - await connection.SendResponseHeadersAsync(streamId, isTrailingHeader: true, headers: TrailingHeaders); - - HttpResponseMessage response = await sendTask; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(Array.Empty(), await response.Content.ReadAsByteArrayAsync()); - Assert.Contains("amazingtrailer", response.TrailingHeaders.GetValues("MyCoolTrailerHeader")); - Assert.Contains("World", response.TrailingHeaders.GetValues("Hello")); - } - } + public SyncHttpHandler_PostScenarioTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - public sealed class SocketsHttpHandler_SchSendAuxRecordHttpTest : SchSendAuxRecordHttpTest - { - public SocketsHttpHandler_SchSendAuxRecordHttpTest(ITestOutputHelper output) : base(output) { } - }*/ - public sealed class SyncHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest { - protected override bool TestAsync => false; - public SyncHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } public sealed class SyncHttpHandlerTest_AutoRedirect : HttpClientHandlerTest_AutoRedirect { - protected override bool TestAsync => false; - public SyncHttpHandlerTest_AutoRedirect(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - /*public sealed class SocketsHttpHandler_DefaultCredentialsTest : DefaultCredentialsTest - { - public SocketsHttpHandler_DefaultCredentialsTest(ITestOutputHelper output) : base(output) { } - } - - public sealed class SocketsHttpHandler_IdnaProtocolTests : IdnaProtocolTests + public sealed class SyncHttpHandler_IdnaProtocolTests : IdnaProtocolTests { - public SocketsHttpHandler_IdnaProtocolTests(ITestOutputHelper output) : base(output) { } + public SyncHttpHandler_IdnaProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; protected override bool SupportsIdna => true; } - public sealed class SocketsHttpHandler_HttpRetryProtocolTests : HttpRetryProtocolTests + public sealed class SyncHttpHandler_HttpRetryProtocolTests : HttpRetryProtocolTests { - public SocketsHttpHandler_HttpRetryProtocolTests(ITestOutputHelper output) : base(output) { } + public SyncHttpHandler_HttpRetryProtocolTests(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - public sealed class SocketsHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies + /*public sealed class SocketsHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies { public SocketsHttpHandlerTest_Cookies(ITestOutputHelper output) : base(output) { } } @@ -990,1060 +83,25 @@ public SocketsHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(ou public sealed class SyncHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Http11_Cancellation_Test { - protected override bool TestAsync => false; - public SyncHttpHandler_HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { } - } - /* - public sealed class SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test : HttpClientHandler_MaxResponseHeadersLength_Test - { - public SocketsHttpHandler_HttpClientHandler_MaxResponseHeadersLength_Test(ITestOutputHelper output) : base(output) { } - } - - 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 - { - public SocketsHttpHandler_ConnectionUpgrade_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task UpgradeConnection_ReturnsReadableAndWritableStream() - { - await LoopbackServer.CreateServerAsync(async (server, url) => - { - using (HttpClient client = CreateHttpClient()) - { - // We need to use ResponseHeadersRead here, otherwise we will hang trying to buffer the response body. - Task getResponseTask = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - await server.AcceptConnectionAsync(async connection => - { - Task> serverTask = connection.ReadRequestHeaderAndSendCustomResponseAsync($"HTTP/1.1 101 Switching Protocols\r\nDate: {DateTimeOffset.UtcNow:R}\r\n\r\n"); - - await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask); - - using (Stream clientStream = await (await getResponseTask).Content.ReadAsStreamAsync()) - { - // Boolean properties returning correct values - Assert.True(clientStream.CanWrite); - Assert.True(clientStream.CanRead); - Assert.False(clientStream.CanSeek); - - // Not supported operations - Assert.Throws(() => clientStream.Length); - Assert.Throws(() => clientStream.Position); - Assert.Throws(() => clientStream.Position = 0); - Assert.Throws(() => clientStream.Seek(0, SeekOrigin.Begin)); - Assert.Throws(() => clientStream.SetLength(0)); - - // Invalid arguments - var nonWritableStream = new MemoryStream(new byte[1], false); - var disposedStream = new MemoryStream(); - disposedStream.Dispose(); - Assert.Throws(() => clientStream.CopyTo(null)); - Assert.Throws(() => clientStream.CopyTo(Stream.Null, 0)); - Assert.Throws(() => { clientStream.CopyToAsync(null, 100, default); }); - Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, 0, default); }); - Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, -1, default); }); - Assert.Throws(() => { clientStream.CopyToAsync(nonWritableStream, 100, default); }); - Assert.Throws(() => { clientStream.CopyToAsync(disposedStream, 100, default); }); - Assert.Throws(() => clientStream.Read(null, 0, 100)); - Assert.Throws(() => clientStream.Read(new byte[1], -1, 1)); - Assert.ThrowsAny(() => clientStream.Read(new byte[1], 2, 1)); - Assert.Throws(() => clientStream.Read(new byte[1], 0, -1)); - Assert.ThrowsAny(() => clientStream.Read(new byte[1], 0, 2)); - Assert.Throws(() => clientStream.BeginRead(null, 0, 100, null, null)); - Assert.Throws(() => clientStream.BeginRead(new byte[1], -1, 1, null, null)); - Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 2, 1, null, null)); - Assert.Throws(() => clientStream.BeginRead(new byte[1], 0, -1, null, null)); - Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 0, 2, null, null)); - Assert.Throws(() => clientStream.EndRead(null)); - Assert.Throws(() => { clientStream.ReadAsync(null, 0, 100, default); }); - Assert.Throws(() => { clientStream.ReadAsync(new byte[1], -1, 1, default); }); - Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 2, 1, default); }); - Assert.Throws(() => { clientStream.ReadAsync(new byte[1], 0, -1, default); }); - Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 0, 2, default); }); - - // Validate writing APIs on clientStream - - clientStream.WriteByte((byte)'!'); - clientStream.Write(new byte[] { (byte)'\r', (byte)'\n' }, 0, 2); - Assert.Equal("!", await connection.ReadLineAsync()); - - clientStream.Write(new Span(new byte[] { (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'\r', (byte)'\n' })); - Assert.Equal("hello", await connection.ReadLineAsync()); - - await clientStream.WriteAsync(new byte[] { (byte)'w', (byte)'o', (byte)'r', (byte)'l', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 7); - Assert.Equal("world", await connection.ReadLineAsync()); - - await clientStream.WriteAsync(new Memory(new byte[] { (byte)'a', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 5)); - Assert.Equal("and", await connection.ReadLineAsync()); - - await Task.Factory.FromAsync(clientStream.BeginWrite, clientStream.EndWrite, new byte[] { (byte)'b', (byte)'e', (byte)'y', (byte)'o', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 8, null); - Assert.Equal("beyond", await connection.ReadLineAsync()); - - clientStream.Flush(); - await clientStream.FlushAsync(); - - // Validate reading APIs on clientStream - await connection.Stream.WriteAsync(Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz")); - var buffer = new byte[1]; - - Assert.Equal('a', clientStream.ReadByte()); - - Assert.Equal(1, clientStream.Read(buffer, 0, 1)); - Assert.Equal((byte)'b', buffer[0]); - - Assert.Equal(1, clientStream.Read(new Span(buffer, 0, 1))); - Assert.Equal((byte)'c', buffer[0]); - - Assert.Equal(1, await clientStream.ReadAsync(buffer, 0, 1)); - Assert.Equal((byte)'d', buffer[0]); - - Assert.Equal(1, await clientStream.ReadAsync(new Memory(buffer, 0, 1))); - Assert.Equal((byte)'e', buffer[0]); - - Assert.Equal(1, await Task.Factory.FromAsync(clientStream.BeginRead, clientStream.EndRead, buffer, 0, 1, null)); - Assert.Equal((byte)'f', buffer[0]); - - var ms = new MemoryStream(); - Task copyTask = clientStream.CopyToAsync(ms); - - string bigString = string.Concat(Enumerable.Repeat("abcdefghijklmnopqrstuvwxyz", 1000)); - Task lotsOfDataSent = connection.Socket.SendAsync(Encoding.ASCII.GetBytes(bigString), SocketFlags.None); - connection.Socket.Shutdown(SocketShutdown.Send); - await copyTask; - await lotsOfDataSent; - Assert.Equal("ghijklmnopqrstuvwxyz" + bigString, Encoding.ASCII.GetString(ms.ToArray())); - } - }); - } - }); - } - } - - public sealed class SocketsHttpHandler_Connect_Test : HttpClientHandlerTestBase - { - 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 - { - public SocketsHttpHandler_HttpClientHandler_ConnectionPooling_Test(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task MultipleIterativeRequests_SameConnectionReused() - { - using (HttpClient client = CreateHttpClient()) - using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - listener.Listen(1); - var ep = (IPEndPoint)listener.LocalEndPoint; - var uri = new Uri($"http://{ep.Address}:{ep.Port}/"); - - string responseBody = - "HTTP/1.1 200 OK\r\n" + - $"Date: {DateTimeOffset.UtcNow:R}\r\n" + - "Content-Length: 0\r\n" + - "\r\n"; - - Task firstRequest = client.GetStringAsync(uri); - using (Socket server = await listener.AcceptAsync()) - using (var serverStream = new NetworkStream(server, ownsSocket: false)) - using (var serverReader = new StreamReader(serverStream)) - { - while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())); - await server.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None); - await firstRequest; - - Task secondAccept = listener.AcceptAsync(); // shouldn't complete - - Task additionalRequest = client.GetStringAsync(uri); - while (!string.IsNullOrWhiteSpace(await serverReader.ReadLineAsync())); - await server.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(responseBody)), SocketFlags.None); - await additionalRequest; - - Assert.False(secondAccept.IsCompleted, $"Second accept should never complete"); - } - } - } - - [OuterLoop("Incurs a delay")] - [Fact] - public async Task ServerDisconnectsAfterInitialRequest_SubsequentRequestUsesDifferentConnection() - { - using (HttpClient client = CreateHttpClient()) - { - await LoopbackServer.CreateServerAsync(async (server, uri) => - { - // Make multiple requests iteratively. - for (int i = 0; i < 2; i++) - { - Task request = client.GetStringAsync(uri); - await server.AcceptConnectionSendResponseAndCloseAsync(); - await request; - - if (i == 0) - { - await Task.Delay(2000); // give client time to see the closing before next connect - } - } - }); - } - } - - [Fact] - public async Task ServerSendsGarbageAfterInitialRequest_SubsequentRequestUsesDifferentConnection() - { - using (HttpClient client = CreateHttpClient()) - { - await LoopbackServer.CreateServerAsync(async (server, uri) => - { - var releaseServer = new TaskCompletionSource(); - - // Make multiple requests iteratively. - - Task serverTask1 = server.AcceptConnectionAsync(async connection => - { - await connection.Writer.WriteAsync(LoopbackServer.GetHttpResponse(connectionClose: false) + "here is a bunch of garbage"); - await releaseServer.Task; // keep connection alive on the server side - }); - await client.GetStringAsync(uri); - - Task serverTask2 = server.AcceptConnectionSendCustomResponseAndCloseAsync(LoopbackServer.GetHttpResponse(connectionClose: true)); - await new[] { client.GetStringAsync(uri), serverTask2 }.WhenAllOrAnyFailed(); - - releaseServer.SetResult(true); - await serverTask1; - }); - } - } - - [Fact] - public async Task ServerSendsConnectionClose_SubsequentRequestUsesDifferentConnection() - { - using (HttpClient client = CreateHttpClient()) - { - await LoopbackServer.CreateServerAsync(async (server, uri) => - { - string responseBody = - "HTTP/1.1 200 OK\r\n" + - $"Date: {DateTimeOffset.UtcNow:R}\r\n" + - "Content-Length: 0\r\n" + - "Connection: close\r\n" + - "\r\n"; - - // Make first request. - Task request1 = client.GetStringAsync(uri); - await server.AcceptConnectionAsync(async connection1 => - { - await connection1.ReadRequestHeaderAndSendCustomResponseAsync(responseBody); - await request1; - - // Make second request and expect it to be served from a different connection. - Task request2 = client.GetStringAsync(uri); - await server.AcceptConnectionAsync(async connection2 => - { - await connection2.ReadRequestHeaderAndSendCustomResponseAsync(responseBody); - await request2; - }); - }); - }); - } - } - - [Theory] - [InlineData("PooledConnectionLifetime")] - [InlineData("PooledConnectionIdleTimeout")] - public async Task SmallConnectionTimeout_SubsequentRequestUsesDifferentConnection(string timeoutPropertyName) - { - using (var handler = new SocketsHttpHandler()) - { - switch (timeoutPropertyName) - { - case "PooledConnectionLifetime": handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; - case "PooledConnectionIdleTimeout": handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; - default: throw new ArgumentOutOfRangeException(nameof(timeoutPropertyName)); - } - - using (HttpClient client = CreateHttpClient(handler)) - { - await LoopbackServer.CreateServerAsync(async (server, uri) => - { - // Make first request. - Task request1 = client.GetStringAsync(uri); - await server.AcceptConnectionAsync(async connection => - { - await connection.ReadRequestHeaderAndSendResponseAsync(); - await request1; - - // Wait a small amount of time before making the second request, to give the first request time to timeout. - await Task.Delay(100); - - // Make second request and expect it to be served from a different connection. - Task request2 = client.GetStringAsync(uri); - await server.AcceptConnectionAsync(async connection2 => - { - await connection2.ReadRequestHeaderAndSendResponseAsync(); - await request2; - }); - }); - }); - } - } - } - - [Theory] - [InlineData("PooledConnectionLifetime")] - [InlineData("PooledConnectionIdleTimeout")] - public async Task Http2_SmallConnectionTimeout_SubsequentRequestUsesDifferentConnection(string timeoutPropertyName) - { - await Http2LoopbackServerFactory.CreateServerAsync(async (server, url) => - { - HttpClientHandler handler = CreateHttpClientHandler(HttpVersion.Version20); - SocketsHttpHandler s = (SocketsHttpHandler)GetUnderlyingSocketsHttpHandler(handler); - switch (timeoutPropertyName) - { - case "PooledConnectionLifetime": s.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; - case "PooledConnectionIdleTimeout": s.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); break; - default: throw new ArgumentOutOfRangeException(nameof(timeoutPropertyName)); - } - - using (HttpClient client = CreateHttpClient(handler)) - { - client.DefaultRequestVersion = HttpVersion.Version20; - Task request1 = client.GetStringAsync(url); - - Http2LoopbackConnection connection = await server.EstablishConnectionAsync(); - int streamId = await connection.ReadRequestHeaderAsync(); - await connection.SendDefaultResponseAsync(streamId); - await request1; - - // Wait a small amount of time before making the second request, to give the first request time to timeout. - await Task.Delay(100); - // Grab reference to underlying socket and stream to make sure they are not disposed and closed. - (Socket socket, Stream stream) = connection.ResetNetwork(); - - // Make second request and expect it to be served from a different connection. - Task request2 = client.GetStringAsync(url); - connection = await server.EstablishConnectionAsync(); - streamId = await connection.ReadRequestHeaderAsync(); - await connection.SendDefaultResponseAsync(streamId); - await request2; - - // Close underlying socket from first connection. - socket.Close(); - } - }); - } - - [OuterLoop] - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ConnectionsPooledThenDisposed_NoUnobservedTaskExceptions(bool secure) - { - RemoteExecutor.Invoke(async (secureString, useVersionString) => - { - var releaseServer = new TaskCompletionSource(); - await LoopbackServer.CreateClientAndServerAsync(async uri => - { - using (var handler = new SocketsHttpHandler()) - using (HttpClient client = CreateHttpClient(handler, useVersionString)) - { - handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; - handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(1); - - var exceptions = new List(); - TaskScheduler.UnobservedTaskException += (s, e) => exceptions.Add(e.Exception); - - await client.GetStringAsync(uri); - await Task.Delay(10); // any value >= the lifetime - Task ignored = client.GetStringAsync(uri); // force the pool to look for the previous connection and find it's too old - await Task.Delay(100); // give some time for the connection close to fail pending reads - - GC.Collect(); - GC.WaitForPendingFinalizers(); - - // Note that there are race conditions here such that we may not catch every failure, - // and thus could have some false negatives, but there won't be any false positives. - Assert.True(exceptions.Count == 0, string.Concat(exceptions)); - - releaseServer.SetResult(true); - } - }, server => server.AcceptConnectionAsync(async connection => - { - await connection.ReadRequestHeaderAndSendResponseAsync(content: "hello world"); - await releaseServer.Task; - }), - new LoopbackServer.Options { UseSsl = bool.Parse(secureString) }); - }, secure.ToString(), UseVersion.ToString()).Dispose(); - } - - [OuterLoop] - [Fact] - public void HandlerDroppedWithoutDisposal_NotKeptAlive() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - HandlerDroppedWithoutDisposal_NotKeptAliveCore(tcs); - for (int i = 0; i < 10; i++) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - Assert.True(tcs.Task.IsCompleted); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void HandlerDroppedWithoutDisposal_NotKeptAliveCore(TaskCompletionSource setOnFinalized) - { - // This relies on knowing that in order for the connection pool to operate, it needs - // to maintain a reference to the supplied IWebProxy. As such, we provide a proxy - // that when finalized will set our event, so that we can determine the state associated - // with a handler has gone away. - IWebProxy p = new PassthroughProxyWithFinalizerCallback(() => setOnFinalized.TrySetResult(true)); - - // Make a bunch of requests and drop the associated HttpClient instances after making them, without disposal. - Task.WaitAll((from i in Enumerable.Range(0, 10) - select LoopbackServer.CreateClientAndServerAsync( - url => CreateHttpClient(new SocketsHttpHandler { Proxy = p }).GetStringAsync(url), - server => server.AcceptConnectionSendResponseAndCloseAsync())).ToArray()); - } - - private sealed class PassthroughProxyWithFinalizerCallback : IWebProxy - { - private readonly Action _callback; - - public PassthroughProxyWithFinalizerCallback(Action callback) => _callback = callback; - ~PassthroughProxyWithFinalizerCallback() => _callback(); - - public ICredentials Credentials { get; set; } - public Uri GetProxy(Uri destination) => destination; - public bool IsBypassed(Uri host) => true; - } - - [Fact] - public async Task ProxyAuth_SameConnection_Succeeds() - { - Task serverTask = LoopbackServer.CreateServerAsync(async (proxyServer, proxyUrl) => - { - string responseBody = - "HTTP/1.1 407 Proxy Auth Required\r\n" + - $"Date: {DateTimeOffset.UtcNow:R}\r\n" + - "Proxy-Authenticate: Basic\r\n" + - "Content-Length: 0\r\n" + - "\r\n"; - - using (var handler = new HttpClientHandler()) - { - handler.Proxy = new UseSpecifiedUriWebProxy(proxyUrl, new NetworkCredential("abc", "def")); - - using (HttpClient client = CreateHttpClient(handler)) - { - Task request = client.GetStringAsync($"http://notarealserver.com/"); - - await proxyServer.AcceptConnectionAsync(async connection => - { - // Get first request, no body for GET. - await connection.ReadRequestHeaderAndSendCustomResponseAsync(responseBody).ConfigureAwait(false); - // Client should send another request after being rejected with 407. - await connection.ReadRequestHeaderAndSendResponseAsync(content:"OK").ConfigureAwait(false); - }); - - string response = await request; - Assert.Equal("OK", response); - } - } - }); - await serverTask.TimeoutAfter(TestHelper.PassingTestTimeoutMilliseconds); - } - } - - public sealed class SocketsHttpHandler_PublicAPIBehavior_Test - { - [Fact] - public void AllowAutoRedirect_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.True(handler.AllowAutoRedirect); - - handler.AllowAutoRedirect = true; - Assert.True(handler.AllowAutoRedirect); - - handler.AllowAutoRedirect = false; - Assert.False(handler.AllowAutoRedirect); - } - } - - [Fact] - public void AutomaticDecompression_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(DecompressionMethods.None, handler.AutomaticDecompression); - - handler.AutomaticDecompression = DecompressionMethods.GZip; - Assert.Equal(DecompressionMethods.GZip, handler.AutomaticDecompression); - - handler.AutomaticDecompression = DecompressionMethods.Deflate; - Assert.Equal(DecompressionMethods.Deflate, handler.AutomaticDecompression); - - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - Assert.Equal(DecompressionMethods.GZip | DecompressionMethods.Deflate, handler.AutomaticDecompression); - } - } - - [Fact] - public void CookieContainer_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - CookieContainer container = handler.CookieContainer; - Assert.Same(container, handler.CookieContainer); - - var newContainer = new CookieContainer(); - handler.CookieContainer = newContainer; - Assert.Same(newContainer, handler.CookieContainer); - } - } - - [Fact] - public void Credentials_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Null(handler.Credentials); - - var newCredentials = new NetworkCredential("username", "password"); - handler.Credentials = newCredentials; - Assert.Same(newCredentials, handler.Credentials); - } - } - - [Fact] - public void DefaultProxyCredentials_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Null(handler.DefaultProxyCredentials); - - var newCredentials = new NetworkCredential("username", "password"); - handler.DefaultProxyCredentials = newCredentials; - Assert.Same(newCredentials, handler.DefaultProxyCredentials); - } - } - - [Fact] - public void MaxAutomaticRedirections_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(50, handler.MaxAutomaticRedirections); - - handler.MaxAutomaticRedirections = int.MaxValue; - Assert.Equal(int.MaxValue, handler.MaxAutomaticRedirections); - - handler.MaxAutomaticRedirections = 1; - Assert.Equal(1, handler.MaxAutomaticRedirections); - - AssertExtensions.Throws("value", () => handler.MaxAutomaticRedirections = 0); - AssertExtensions.Throws("value", () => handler.MaxAutomaticRedirections = -1); - } - } - - [Fact] - public void MaxConnectionsPerServer_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); - - handler.MaxConnectionsPerServer = int.MaxValue; - Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); - - handler.MaxConnectionsPerServer = 1; - Assert.Equal(1, handler.MaxConnectionsPerServer); - - AssertExtensions.Throws("value", () => handler.MaxConnectionsPerServer = 0); - AssertExtensions.Throws("value", () => handler.MaxConnectionsPerServer = -1); - } - } - - [Fact] - public void MaxResponseHeadersLength_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(64, handler.MaxResponseHeadersLength); - - handler.MaxResponseHeadersLength = int.MaxValue; - Assert.Equal(int.MaxValue, handler.MaxResponseHeadersLength); - - handler.MaxResponseHeadersLength = 1; - Assert.Equal(1, handler.MaxResponseHeadersLength); - - AssertExtensions.Throws("value", () => handler.MaxResponseHeadersLength = 0); - AssertExtensions.Throws("value", () => handler.MaxResponseHeadersLength = -1); - } - } - - [Fact] - public void PreAuthenticate_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.False(handler.PreAuthenticate); - - handler.PreAuthenticate = false; - Assert.False(handler.PreAuthenticate); - - handler.PreAuthenticate = true; - Assert.True(handler.PreAuthenticate); - } - } - - [Fact] - public void PooledConnectionIdleTimeout_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(TimeSpan.FromMinutes(2), handler.PooledConnectionIdleTimeout); - - handler.PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan; - Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionIdleTimeout); - - handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(0); - Assert.Equal(TimeSpan.FromSeconds(0), handler.PooledConnectionIdleTimeout); - - handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1); - Assert.Equal(TimeSpan.FromSeconds(1), handler.PooledConnectionIdleTimeout); - - AssertExtensions.Throws("value", () => handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(-2)); - } - } - - [Fact] - public void PooledConnectionLifetime_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); - - handler.PooledConnectionLifetime = Timeout.InfiniteTimeSpan; - Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); - - handler.PooledConnectionLifetime = TimeSpan.FromSeconds(0); - Assert.Equal(TimeSpan.FromSeconds(0), handler.PooledConnectionLifetime); - - handler.PooledConnectionLifetime = TimeSpan.FromSeconds(1); - Assert.Equal(TimeSpan.FromSeconds(1), handler.PooledConnectionLifetime); - - AssertExtensions.Throws("value", () => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(-2)); - } - } - - [Fact] - public void Properties_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - IDictionary props = handler.Properties; - Assert.NotNull(props); - Assert.Empty(props); - - props.Add("hello", "world"); - Assert.Equal(1, props.Count); - Assert.Equal("world", props["hello"]); - } - } - - [Fact] - public void Proxy_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.Null(handler.Proxy); - - var proxy = new WebProxy(); - handler.Proxy = proxy; - Assert.Same(proxy, handler.Proxy); - } - } - - [Fact] - public void SslOptions_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - SslClientAuthenticationOptions options = handler.SslOptions; - Assert.NotNull(options); - - Assert.True(options.AllowRenegotiation); - Assert.Null(options.ApplicationProtocols); - Assert.Equal(X509RevocationMode.NoCheck, options.CertificateRevocationCheckMode); - Assert.Null(options.ClientCertificates); - Assert.Equal(SslProtocols.None, options.EnabledSslProtocols); - Assert.Equal(EncryptionPolicy.RequireEncryption, options.EncryptionPolicy); - Assert.Null(options.LocalCertificateSelectionCallback); - Assert.Null(options.RemoteCertificateValidationCallback); - Assert.Null(options.TargetHost); - - Assert.Same(options, handler.SslOptions); - - var newOptions = new SslClientAuthenticationOptions(); - handler.SslOptions = newOptions; - Assert.Same(newOptions, handler.SslOptions); - } - } - - [Fact] - public void UseCookies_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.True(handler.UseCookies); - - handler.UseCookies = true; - Assert.True(handler.UseCookies); - - handler.UseCookies = false; - Assert.False(handler.UseCookies); - } - } - - [Fact] - public void UseProxy_GetSet_Roundtrips() - { - using (var handler = new SocketsHttpHandler()) - { - Assert.True(handler.UseProxy); - - handler.UseProxy = false; - Assert.False(handler.UseProxy); - - handler.UseProxy = true; - Assert.True(handler.UseProxy); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task AfterDisposeSendAsync_GettersUsable_SettersThrow(bool dispose) - { - using (var handler = new SocketsHttpHandler()) - { - Type expectedExceptionType; - if (dispose) - { - handler.Dispose(); - expectedExceptionType = typeof(ObjectDisposedException); - } - else - { - using (var c = new HttpMessageInvoker(handler, disposeHandler: false)) - await Assert.ThrowsAnyAsync(() => - c.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri("/shouldquicklyfail", UriKind.Relative)), default)); - expectedExceptionType = typeof(InvalidOperationException); - } - - Assert.True(handler.AllowAutoRedirect); - Assert.Equal(DecompressionMethods.None, handler.AutomaticDecompression); - Assert.NotNull(handler.CookieContainer); - Assert.Null(handler.Credentials); - Assert.Null(handler.DefaultProxyCredentials); - Assert.Equal(50, handler.MaxAutomaticRedirections); - Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); - Assert.Equal(64, handler.MaxResponseHeadersLength); - Assert.False(handler.PreAuthenticate); - Assert.Equal(TimeSpan.FromMinutes(2), handler.PooledConnectionIdleTimeout); - Assert.Equal(Timeout.InfiniteTimeSpan, handler.PooledConnectionLifetime); - Assert.NotNull(handler.Properties); - Assert.Null(handler.Proxy); - Assert.NotNull(handler.SslOptions); - Assert.True(handler.UseCookies); - Assert.True(handler.UseProxy); - - Assert.Throws(expectedExceptionType, () => handler.AllowAutoRedirect = false); - Assert.Throws(expectedExceptionType, () => handler.AutomaticDecompression = DecompressionMethods.GZip); - Assert.Throws(expectedExceptionType, () => handler.CookieContainer = new CookieContainer()); - Assert.Throws(expectedExceptionType, () => handler.Credentials = new NetworkCredential("anotheruser", "anotherpassword")); - Assert.Throws(expectedExceptionType, () => handler.DefaultProxyCredentials = new NetworkCredential("anotheruser", "anotherpassword")); - Assert.Throws(expectedExceptionType, () => handler.MaxAutomaticRedirections = 2); - Assert.Throws(expectedExceptionType, () => handler.MaxConnectionsPerServer = 2); - Assert.Throws(expectedExceptionType, () => handler.MaxResponseHeadersLength = 2); - Assert.Throws(expectedExceptionType, () => handler.PreAuthenticate = false); - Assert.Throws(expectedExceptionType, () => handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(2)); - Assert.Throws(expectedExceptionType, () => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(2)); - Assert.Throws(expectedExceptionType, () => handler.Proxy = new WebProxy()); - Assert.Throws(expectedExceptionType, () => handler.SslOptions = new SslClientAuthenticationOptions()); - Assert.Throws(expectedExceptionType, () => handler.UseCookies = false); - Assert.Throws(expectedExceptionType, () => handler.UseProxy = false); - } - } - } - - public sealed class SocketsHttpHandlerTest_LocationHeader - { - private static readonly byte[] s_redirectResponseBefore = Encoding.ASCII.GetBytes( - "HTTP/1.1 301 Moved Permanently\r\n" + - "Connection: close\r\n" + - "Transfer-Encoding: chunked\r\n" + - "Location: "); - - private static readonly byte[] s_redirectResponseAfter = Encoding.ASCII.GetBytes( - "\r\n" + - "Server: Loopback\r\n" + - "\r\n" + - "0\r\n\r\n"); - - [Theory] - // US-ASCII only - [InlineData("http://a/", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/' })] - [InlineData("http://a/asdasd", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', (byte)'a', (byte)'s', (byte)'d', (byte)'a', (byte)'s', (byte)'d' })] - // 2, 3, 4 byte UTF-8 characters - [InlineData("http://a/\u00A2", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC2, 0xA2 })] - [InlineData("http://a/\u20AC", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xE2, 0x82, 0xAC })] - [InlineData("http://a/\uD800\uDF48", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xF0, 0x90, 0x8D, 0x88 })] - // 3 Polish letters - [InlineData("http://a/\u0105\u015B\u0107", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC4, 0x85, 0xC5, 0x9B, 0xC4, 0x87 })] - // Negative cases - should be interpreted as ISO-8859-1 - // Invalid utf-8 sequence (continuation without start) - [InlineData("http://a/%C2%80", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0b10000000 })] - // Invalid utf-8 sequence (not allowed character) - [InlineData("http://a/\u00C3\u0028", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC3, 0x28 })] - // Incomplete utf-8 sequence - [InlineData("http://a/\u00C2", new byte[] { (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'a', (byte)'/', 0xC2 })] - public async Task LocationHeader_DecodesUtf8_Success(string expected, byte[] location) - { - await LoopbackServer.CreateClientAndServerAsync(async url => - { - using (HttpClientHandler handler = new HttpClientHandler()) - { - handler.AllowAutoRedirect = false; - - using (HttpClient client = new HttpClient(handler)) - { - HttpResponseMessage response = await client.GetAsync(url); - Assert.Equal(expected, response.Headers.Location.ToString()); - } - } - }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync(PreperateResponseWithRedirect(location))); - } - - private static byte[] PreperateResponseWithRedirect(byte[] location) - { - return s_redirectResponseBefore.Concat(location).Concat(s_redirectResponseAfter).ToArray(); - } - } - - public sealed class SocketsHttpHandlerTest_Http2 : HttpClientHandlerTest_Http2 - { - public SocketsHttpHandlerTest_Http2(ITestOutputHelper output) : base(output) { } - } - - [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public sealed class SocketsHttpHandlerTest_Cookies_Http2 : HttpClientHandlerTest_Cookies - { - public SocketsHttpHandlerTest_Cookies_Http2(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - } - - [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Http2 : HttpClientHandlerTest - { - public SocketsHttpHandlerTest_HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - } - - public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http11 : HttpClientHandlerTest_Headers - { - public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http11(ITestOutputHelper output) : base(output) { } - } - - [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http2 : HttpClientHandlerTest_Headers - { - public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http2(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - } - - [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] - public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http2 : HttpClientHandler_Cancellation_Test - { - public SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http2(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version20; - } - - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http3_Test : HttpClientHandler_Finalization_Test - { - public SocketsHttpHandler_HttpClientHandler_Finalization_Http3_Test(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; - } - - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandlerTest_Http3 : HttpClientHandlerTest_Http3 - { - public SocketsHttpHandlerTest_Http3(ITestOutputHelper output) : base(output) { } - } - - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandlerTest_Cookies_Http3 : HttpClientHandlerTest_Cookies - { - public SocketsHttpHandlerTest_Cookies_Http3(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; + protected override bool TestAsync => false; } - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Http3 : HttpClientHandlerTest + public sealed class SyncHttpHandler_HttpClientHandler_Authentication_Test : HttpClientHandler_Authentication_Test { - public SocketsHttpHandlerTest_HttpClientHandlerTest_Http3(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; + public SyncHttpHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http3 : HttpClientHandlerTest_Headers + public sealed class SyncHttpHandler_Connect_Test : HttpClientHandler_Connect_Test { - public SocketsHttpHandlerTest_HttpClientHandlerTest_Headers_Http3(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; + public SyncHttpHandler_Connect_Test(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3 : HttpClientHandler_Cancellation_Test + public sealed class SyncHttpHandlerTest_HttpClientHandlerTest_Headers : HttpClientHandlerTest_Headers { - public SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http3(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; + public SyncHttpHandlerTest_HttpClientHandlerTest_Headers(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - - [ConditionalClass(typeof(QuicConnection), nameof(QuicConnection.IsQuicSupported))] - public sealed class SocketsHttpHandler_HttpClientHandler_AltSvc_Test_Http3 : HttpClientHandler_AltSvc_Test - { - public SocketsHttpHandler_HttpClientHandler_AltSvc_Test_Http3(ITestOutputHelper output) : base(output) { } - protected override Version UseVersion => HttpVersion.Version30; - }*/ } 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 7d016af577bea..8a5dc43301a7d 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,7 @@ + Date: Thu, 21 May 2020 16:55:35 +0200 Subject: [PATCH 27/60] Even moar tests. --- .../Net/Http/HttpClientHandlerTestBase.cs | 11 --- .../src/System/Net/Http/EmptyContent.cs | 4 + .../System/Net/Http/FormUrlEncodedContent.cs | 14 ++++ .../tests/FunctionalTests/HttpClientTest.cs | 83 ++++++++++++------- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index e69319828a837..1011b65f30610 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -74,19 +74,8 @@ protected static LoopbackServerFactory GetFactoryForVersion(Version useVersion) } public static readonly bool[] BoolValues = new[] { true, false }; - public static readonly bool[] AsyncBoolValues = - new[] - { - true, -#if __NETCOREAPP - false -#endif - }; - - public static IEnumerable AsyncBoolMemberData() => AsyncBoolValues.Select(b => new object[] { b }); // For use by remote server tests - public static readonly IEnumerable RemoteServersMemberData = Configuration.Http.RemoteServersMemberData; protected HttpClient CreateHttpClientForRemoteServer(Configuration.Http.RemoteServer remoteServer) 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..0c45250fbefac 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,10 @@ 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/FormUrlEncodedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs index 7ca4a99a1fd12..c7ac9b8ffabfe 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs @@ -53,6 +53,20 @@ private static string Encode(string? data) return Uri.EscapeDataString(data).Replace("%20", "+"); } + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + // Only skip the original protected virtual SerializeToStream if this + // isn't a derived type that may have overridden the behavior. + if (GetType() != typeof(FormUrlEncodedContent)) + { + base.SerializeToStream(stream, context, cancellationToken); + } + else + { + SerializeToStreamCore(stream, context, cancellationToken); + } + } + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index d1d9d3e98deb2..ce4b73716150c 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -329,38 +329,6 @@ public async Task GetAsync_CustomException_Asynchronous_ThrowsException() } } - [Theory, MemberData(nameof(AsyncBoolMemberData))] - public async Task Send_NullRequest_ThrowsException(bool async) - { - using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(null)))) - { - await AssertExtensions.ThrowsAsync("request", () => client.SendAsync(async, null)); - } - } - - [Theory, MemberData(nameof(AsyncBoolMemberData))] - public async Task Send_DuplicateRequest_ThrowsException(bool async) - { - using (var client = new HttpClient(new CustomResponseHandler((r, c) => Task.FromResult(new HttpResponseMessage())))) - using (var request = new HttpRequestMessage(HttpMethod.Get, CreateFakeUri())) - { - (await client.SendAsync(async, request)).Dispose(); - await Assert.ThrowsAsync(() => client.SendAsync(async, request)); - } - } - - [Theory, MemberData(nameof(AsyncBoolMemberData))] - public async Task Send_RequestContentNotDisposed(bool async) - { - 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(async, request); - await content.ReadAsStringAsync(); // no exception - } - } - [Fact] public async Task GetStringAsync_Success() { @@ -869,5 +837,56 @@ 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; } } From 3da68cb84a257bae48155f723fa8d1e4cf4adc63 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Thu, 21 May 2020 17:23:35 +0200 Subject: [PATCH 28/60] Removed unnecessary changes. --- .../Http/HttpClientHandlerTest.Cancellation.cs | 12 ++++++------ .../Net/Http/HttpClientHandlerTest.Cookies.cs | 2 +- .../System/Net/Http/HttpClientHandlerTest.cs | 15 ++++++++------- .../System/Net/Http/HttpClientHandlerTestBase.cs | 2 -- .../src/System/Net/Http/HttpContent.cs | 2 -- .../src/System/Net/Http/MultipartContent.cs | 2 -- .../Net/Http/SocketsHttpHandler/HttpConnection.cs | 4 ---- 7 files changed, 15 insertions(+), 24 deletions(-) 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 a42a3001bc6cd..deda6b8f633ae 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -534,14 +534,14 @@ await LoopbackServerFactory.CreateClientAndServerAsync( { using (var invoker = new HttpMessageInvoker(CreateHttpClientHandler())) using (var req = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content, Version = UseVersion }) - try + try + { + using (HttpResponseMessage resp = await invoker.SendAsync(TestAsync, req, cancellationTokenSource.Token)) { - using (HttpResponseMessage resp = await invoker.SendAsync(TestAsync, req, cancellationTokenSource.Token)) - { - Assert.Equal("Hello World", await resp.Content.ReadAsStringAsync()); - } + Assert.Equal("Hello World", await resp.Content.ReadAsStringAsync()); } - catch (OperationCanceledException) { } + } + catch (OperationCanceledException) { } }, async server => { 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 325ac0c6cd6d7..861f7db438460 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cookies.cs @@ -237,7 +237,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => } }); } - + [Fact] public async Task GetAsync_SetCookieContainerAndMultipleCookieHeaders_BothCookiesSent() { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 81ce1ac4d647c..1942718c2e3f3 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1957,13 +1957,13 @@ await server.AcceptConnectionAsync(async connection => public static IEnumerable Interim1xxStatusCode() { - yield return new object[] {(HttpStatusCode)100}; // 100 Continue. - // 101 SwitchingProtocols will be treated as a final status code. - yield return new object[] {(HttpStatusCode)102}; // 102 Processing. - yield return new object[] {(HttpStatusCode)103}; // 103 EarlyHints. - yield return new object[] {(HttpStatusCode)150}; - yield return new object[] {(HttpStatusCode)180}; - yield return new object[] {(HttpStatusCode)199}; + yield return new object[] { (HttpStatusCode)100 }; // 100 Continue. + // 101 SwitchingProtocols will be treated as a final status code. + yield return new object[] { (HttpStatusCode)102 }; // 102 Processing. + yield return new object[] { (HttpStatusCode)103 }; // 103 EarlyHints. + yield return new object[] { (HttpStatusCode)150 }; + yield return new object[] { (HttpStatusCode)180 }; + yield return new object[] { (HttpStatusCode)199 }; } [ConditionalTheory] @@ -2058,6 +2058,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => // No ExpectContinue header. initialMessage.Headers.ExpectContinue = false; HttpResponseMessage response = await client.SendAsync(TestAsync, initialMessage); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); clientFinished.SetResult(true); } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 1011b65f30610..34fb2fd267e19 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -3,9 +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.Linq; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; 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 f90dc87a2af1b..467c21687b75a 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 @@ -347,8 +347,6 @@ public void CopyTo(Stream stream, TransportContext? context, CancellationToken c { if (TryGetBuffer(out ArraySegment buffer)) { - // Last chance to check for timeout/cancellation, sync Stream API doesn't have any support for it. - cancellationToken.ThrowIfCancellationRequested(); stream.Write(buffer.Array!, buffer.Offset, buffer.Count); } else 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 430aa541d82d2..96c933055042f 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 @@ -192,8 +192,6 @@ private protected void SerializeToStreamCore(Stream stream, TransportContext? co var output = new StringBuilder(); for (int contentIndex = 0; contentIndex < _nestedContent.Count; contentIndex++) { - cancellationToken.ThrowIfCancellationRequested(); - // Write divider, headers, and content. HttpContent content = _nestedContent[contentIndex]; EncodeStringToStream(stream, SerializeHeadersToString(output, contentIndex, content)); 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 2acec49423e13..ff1c43d033dac 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 @@ -779,7 +779,6 @@ private async ValueTask SendRequestContentAsync(HttpRequestMessage request, Http } else { - cancellationToken.ThrowIfCancellationRequested(); request.Content!.CopyTo(stream, _transportContext, cancellationToken); } @@ -1618,7 +1617,6 @@ private async ValueTask CopyFromBufferAsync(Stream destination, bool async, int } else { - cancellationToken.ThrowIfCancellationRequested(); destination.Write(_readBuffer, _readOffset, count); } _readOffset += count; @@ -1640,7 +1638,6 @@ private Task CopyToUntilEofAsync(Stream destination, bool async, int bufferSize, return _stream.CopyToAsync(destination, bufferSize, cancellationToken); } - cancellationToken.ThrowIfCancellationRequested(); _stream.CopyTo(destination, bufferSize); return Task.CompletedTask; } @@ -1659,7 +1656,6 @@ private async Task CopyToUntilEofWithExistingBufferedDataAsync(Stream destinatio } else { - cancellationToken.ThrowIfCancellationRequested(); _stream.CopyTo(destination, bufferSize); } } From 5fc5ac0ebe2f934764edc584bb03e96e48e993ab Mon Sep 17 00:00:00 2001 From: ManickaP Date: Tue, 26 May 2020 10:51:25 +0200 Subject: [PATCH 29/60] Fixed tests. --- .../HttpClientHandlerTest.Authentication.cs | 41 ++++-- .../HttpClientHandlerTest.Cancellation.cs | 128 +++-------------- ...entHandlerTest.MaxResponseHeadersLength.cs | 9 +- .../Net/Http/HttpClientHandlerTest.Proxy.cs | 9 +- ...ttpClientHandlerTest.ServerCertificates.cs | 17 +-- .../System/Net/Http/HttpClientHandlerTest.cs | 133 ++++++++---------- .../Net/Http/HttpClientHandlerTestBase.cs | 1 + .../System/Net/Http/HttpProtocolTests.cs | 11 +- .../System/Net/Http/HttpRetryProtocolTests.cs | 2 + .../src/System/Net/Http/HttpBaseStream.cs | 15 +- .../Http/SocketsHttpHandler/Http2Stream.cs | 13 +- .../SocketsHttpHandler/Http3RequestStream.cs | 10 ++ .../HttpClientHandlerTest.Cancellation.cs | 100 +++++++++++++ .../FunctionalTests/SyncHttpHandlerTest.cs | 28 +--- .../System.Net.Http.Functional.Tests.csproj | 1 + 15 files changed, 259 insertions(+), 259 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs 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 f58f26ceaa110..ce4d1bc9e38ff 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs @@ -56,29 +56,45 @@ 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 }; + 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 }; + "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 }; + "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 }; + 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. - yield return new object[] { "Digest realm=withoutquotes, nonce=withoutquotes", false }; + 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 }; - 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 }; + 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] @@ -639,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.Cancellation.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs index deda6b8f633ae..b8f8a3b27ced3 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -26,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) @@ -37,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 => @@ -84,7 +82,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => }); } - [ConditionalTheory] + [Theory] [MemberData(nameof(OneBoolAndCancellationMode))] public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode) { @@ -94,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()) { @@ -197,7 +193,7 @@ await ValidateClientCancellationAsync(async () => } } - [ConditionalTheory] + [Theory] [MemberData(nameof(ThreeBools))] public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync) { @@ -206,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()) { @@ -259,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; @@ -336,7 +329,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => } } - [ConditionalFact] + [Fact] public async Task MaxConnectionsPerServer_WaitingConnectionsAreCancelable() { if (LoopbackServerFactory.Version >= HttpVersion20.Value) @@ -519,16 +512,15 @@ 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) { -#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 => { @@ -612,86 +604,4 @@ from second in BoolValues from third in BoolValues select new object[] { first, second, third }; } - - 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(true); - } - }, 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/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 362c72f709111..8224d3327231d 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 => 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 eb9be0a7ecacb..29ace70da9a8b 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()) { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 1942718c2e3f3..1a13cd092159b 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 }; @@ -487,7 +485,7 @@ public static IEnumerable SecureAndNonSecure_IPBasedUri_MemberData() = 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 @@ -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) @@ -1288,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. @@ -1451,15 +1446,14 @@ 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())) @@ -1542,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) => @@ -1921,15 +1915,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 => @@ -1966,16 +1959,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"; @@ -2036,16 +2028,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"; @@ -2085,12 +2076,11 @@ await server.AcceptConnectionAsync(async connection => [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"; @@ -2127,15 +2117,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"; @@ -2173,7 +2162,7 @@ await server.AcceptConnectionAsync(async connection => }); } - [ConditionalFact] + [Fact] public async Task SendAsync_101SwitchingProtocolsResponse_Success() { // WinHttpHandler and CurlHandler will hang, waiting for additional response. @@ -2569,15 +2558,14 @@ 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) { @@ -2663,15 +2651,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 34fb2fd267e19..7dd1e56ca719a 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; diff --git a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs index d0acc72051934..4bccc61c014f3 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()) @@ -521,7 +520,9 @@ await server.AcceptConnectionAsync(async connection => [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) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs index 1aa67412f352e..044f85e04b5d7 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs @@ -127,9 +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) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs index f143e3dc6be01..13d9ff801bc0c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs @@ -97,15 +97,17 @@ public sealed override Task ReadAsync(byte[] buffer, int offset, int count, return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); } - public override void Write(byte[] buffer, int offset, int count) + public sealed override void WriteByte(byte value) + { + Write(MemoryMarshal.CreateReadOnlySpan(ref value, 1)); + } + + public sealed override void Write(byte[] buffer, int offset, int count) { ValidateBufferArgs(buffer, offset, count); Write(new ReadOnlySpan(buffer, offset, count)); } - public sealed override void WriteByte(byte value) => - Write(MemoryMarshal.CreateReadOnlySpan(ref value, 1)); - public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArgs(buffer, offset, count); @@ -126,10 +128,7 @@ protected static Task NopAsync(CancellationToken cancellationToken) => public abstract override int Read(Span buffer); public abstract override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken); + public abstract override void Write(ReadOnlySpan buffer); public abstract override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken); - - // Either Write(byte[], int, int) or Write(ReadOnlySpan) should also be overridden. If the former is overridden, - // the default implementation of the latter will call the former; if the latter is overridden, this type's override - // of the former will call the latter. If neither is overridden, stack overflows will result. } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index 71b7441a01dc9..3e8534fde6f3d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -9,6 +9,7 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -1370,17 +1371,9 @@ protected override void Dispose(bool disposing) public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) + public override void Write(ReadOnlySpan buffer) { - ValidateBufferArgs(buffer, offset, count); - Http2Stream? http2Stream = _http2Stream; - if (http2Stream == null) - { - throw new ObjectDisposedException(nameof(Http2WriteStream)); - } - - // Sync-over-async. See comment on Http2Connection.SendAsync. - http2Stream.SendDataAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken: default).AsTask().GetAwaiter().GetResult(); + WriteAsync(new ReadOnlyMemory(buffer.ToArray()), cancellationToken: default).AsTask().GetAwaiter().GetResult(); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index 394de483e20b1..562bec5dcba98 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -1235,6 +1235,11 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return _stream.ReadResponseContentAsync(_response, buffer, cancellationToken); } + public override void Write(ReadOnlySpan buffer) + { + throw new NotSupportedException(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { throw new NotSupportedException(); @@ -1271,6 +1276,11 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken throw new NotSupportedException(); } + public override void Write(ReadOnlySpan buffer) + { + throw new NotSupportedException(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { if (_stream == null) 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..2a59e6dfd144b --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs @@ -0,0 +1,100 @@ +// 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(true); + } + }, 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/SyncHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs index 50dc754b4a3c4..a9930d48a0bff 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SyncHttpHandlerTest.cs @@ -2,22 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Quic; -using System.Net.Security; -using System.Net.Sockets; -using System.Net.Test.Common; -using System.Runtime.CompilerServices; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.DotNet.RemoteExecutor; -using Xunit; using Xunit.Abstractions; namespace System.Net.Http.Functional.Tests @@ -71,15 +55,17 @@ public SyncHttpHandler_HttpRetryProtocolTests(ITestOutputHelper output) : base(o protected override bool TestAsync => false; } - /*public sealed class SocketsHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies + public sealed class SyncHttpHandlerTest_Cookies : HttpClientHandlerTest_Cookies { - public SocketsHttpHandlerTest_Cookies(ITestOutputHelper output) : base(output) { } + public SyncHttpHandlerTest_Cookies(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; } - public sealed class SocketsHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTest_Cookies_Http11 + public sealed class SyncHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTest_Cookies_Http11 { - public SocketsHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } - }*/ + public SyncHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } + protected override bool TestAsync => false; + } public sealed class SyncHttpHandler_HttpClientHandler_Cancellation_Test : HttpClientHandler_Http11_Cancellation_Test { 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 8a5dc43301a7d..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,7 @@ + From 8b8463c4770d90e5821c1bc7b8d4f0d89b081cf6 Mon Sep 17 00:00:00 2001 From: ManickaP Date: Tue, 26 May 2020 18:35:24 +0200 Subject: [PATCH 30/60] HttpStream sync write clean up. --- .../src/System/Net/Http/HttpBaseStream.cs | 16 ++++++++-------- .../Net/Http/SocketsHttpHandler/Http2Stream.cs | 6 ------ .../SocketsHttpHandler/Http3RequestStream.cs | 10 ---------- .../Http/SocketsHttpHandler/HttpContentStream.cs | 13 +++++++++---- 4 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs index 13d9ff801bc0c..f26a76648d2ab 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs @@ -97,16 +97,17 @@ public sealed override Task ReadAsync(byte[] buffer, int offset, int count, return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); } - public sealed override void WriteByte(byte value) + public override void Write(byte[] buffer, int offset, int count) { - Write(MemoryMarshal.CreateReadOnlySpan(ref value, 1)); + // This does sync-over-async, but it also should only end up being used in strange + // situations. Either a derived stream overrides this anyway, so the implementation won't be used, + // or it's being called as part of HttpContent.SerializeToStreamAsync, which means custom + // content is explicitly choosing to make a synchronous call as part of an asynchronous method. + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); } - public sealed override void Write(byte[] buffer, int offset, int count) - { - ValidateBufferArgs(buffer, offset, count); - Write(new ReadOnlySpan(buffer, offset, count)); - } + public sealed override void WriteByte(byte value) => + Write(MemoryMarshal.CreateReadOnlySpan(ref value, 1)); public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { @@ -128,7 +129,6 @@ protected static Task NopAsync(CancellationToken cancellationToken) => public abstract override int Read(Span buffer); public abstract override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken); - public abstract override void Write(ReadOnlySpan buffer); public abstract override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index 3e8534fde6f3d..6c66a674017b0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -9,7 +9,6 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -1371,11 +1370,6 @@ protected override void Dispose(bool disposing) public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) => throw new NotSupportedException(); - public override void Write(ReadOnlySpan buffer) - { - WriteAsync(new ReadOnlyMemory(buffer.ToArray()), cancellationToken: default).AsTask().GetAwaiter().GetResult(); - } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { Http2Stream? http2Stream = _http2Stream; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index 562bec5dcba98..394de483e20b1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -1235,11 +1235,6 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return _stream.ReadResponseContentAsync(_response, buffer, cancellationToken); } - public override void Write(ReadOnlySpan buffer) - { - throw new NotSupportedException(); - } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { throw new NotSupportedException(); @@ -1276,11 +1271,6 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken throw new NotSupportedException(); } - public override void Write(ReadOnlySpan buffer) - { - throw new NotSupportedException(); - } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { if (_stream == null) 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 294389040edce..17da061bb29f8 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 @@ -13,6 +13,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) @@ -30,10 +35,10 @@ protected override void Dispose(bool disposing) protected HttpConnection GetConnectionOrThrow() { return _connection ?? - // This should only ever happen if the user-code that was handed this instance disposed of - // it, which is misuse, or held onto it and tried to use it later after we've disposed of it, - // which is also misuse. - ThrowObjectDisposedException(); + // This should only ever happen if the user-code that was handed this instance disposed of + // it, which is misuse, or held onto it and tried to use it later after we've disposed of it, + // which is also misuse. + ThrowObjectDisposedException(); } private HttpConnection ThrowObjectDisposedException() => throw new ObjectDisposedException(GetType().Name); From a0f0a5d2c900433f907389bcd91d37829e89f9da Mon Sep 17 00:00:00 2001 From: ManickaP Date: Tue, 26 May 2020 18:40:10 +0200 Subject: [PATCH 31/60] Sync AuthenticateAsClient usage --- .../src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9b579e81af81b..7cb37a2cf4736 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 @@ -178,8 +178,7 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a } else { - // ToDo: [ActiveIssue("https://github.com/dotnet/runtime/issues/34638")] - sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).GetAwaiter().GetResult(); + sslStream.AuthenticateAsClient(sslOptions); } } catch (Exception e) From 0dcf44e78a005e7c8ca0548fd1f8d11b92316597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 29 May 2020 15:42:50 +0200 Subject: [PATCH 32/60] Handle cancellation and subsequent exception mapping. --- .../src/System/Net/Http/HttpContent.cs | 29 +++++++++++++++++++ .../Http/SocketsHttpHandler/ConnectHelper.cs | 17 ++++++++++- .../System.Net.Http.Unit.Tests.csproj | 2 ++ 3 files changed, 47 insertions(+), 1 deletion(-) 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 467c21687b75a..8b2a13370df27 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 @@ -426,6 +426,16 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT throw error!; } + // Register for cancellation and trear down the underlying stream in case of cancellation/timeout. + CancellationTokenRegistration cancellationRegistration = cancellationToken.Register(swr => + { + var streamWeakRef = (WeakReference)swr!; + if (streamWeakRef.TryGetTarget(out MemoryStream? stream)) + { + stream.Dispose(); + } + }, new WeakReference(tempBuffer)); + try { SerializeToStream(tempBuffer, null, cancellationToken); @@ -436,6 +446,20 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT { if (NetEventSource.IsEnabled) NetEventSource.Error(this, e); + if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) + { + // Cancellation was requested, so assume that the failure is due to + // the cancellation request. This is a bit unorthodox, as usually we'd + // prioritize a non-OperationCanceledException over a cancellation + // request to avoid losing potentially pertinent information. But given + // the cancellation design where we tear down the underlying stream upon + // a cancellation request, which can then result in a myriad of different + // exceptions (argument exceptions, object disposed exceptions, socket exceptions, + // etc.), as a middle ground we treat it as cancellation, but still propagate the + // original information as the inner exception, for diagnostic purposes. + throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); + } + if (StreamCopyExceptionNeedsWrapping(e)) { throw GetStreamCopyException(e); @@ -443,6 +467,11 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT throw; } + finally + { + // Clean up the cancellation registration. + cancellationRegistration.Dispose(); + } } public Task LoadIntoBufferAsync() => 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 7cb37a2cf4736..abfe5ec30f495 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 @@ -50,9 +50,24 @@ public static async ValueTask ConnectAsync(string host, int port, bool a socket.Connect(new DnsEndPoint(host, port)); } } - catch + catch (Exception ex) { socket.Dispose(); + + if (CancellationHelper.ShouldWrapInOperationCanceledException(ex, cancellationToken)) + { + // Cancellation was requested, so assume that the failure is due to + // the cancellation request. This is a bit unorthodox, as usually we'd + // prioritize a non-OperationCanceledException over a cancellation + // request to avoid losing potentially pertinent information. But given + // the cancellation design where we tear down the underlying stream upon + // a cancellation request, which can then result in a myriad of different + // exceptions (argument exceptions, object disposed exceptions, socket exceptions, + // etc.), as a middle ground we treat it as cancellation, but still propagate the + // original information as the inner exception, for diagnostic purposes. + throw CancellationHelper.CreateOperationCanceledException(ex, cancellationToken); + } + throw; } diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index b4c31ba80fd7b..793729cd029f9 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -222,6 +222,8 @@ Link="ProductionCode\System\Net\Http\StreamToStreamCopy.cs" /> + Date: Fri, 29 May 2020 23:06:37 +0200 Subject: [PATCH 33/60] Fixed deadlocking tests --- .../HttpClientHandlerTest.Cancellation.cs | 21 ++++++++++++++++ .../Http/SocketsHttpHandler/ConnectHelper.cs | 25 ++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) 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 76e05d2f4378a..3de084849c81e 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -433,6 +433,13 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo canReadFunc: () => true, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { + // This might be called from sync Read which has no ability to pass the actual cancellationToken. + // So rather than deadlock, take a new token from the source. + if (cancellationToken == default) + { + cancellationToken = tokenSource.Token; + } + int result = 1; if (called) { @@ -469,6 +476,13 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo positionSetFunc: _ => {}, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { + // This might be called from sync Read which has no ability to pass the actual cancellationToken. + // So rather than deadlock, take a new token from the source. + if (cancellationToken == default) + { + cancellationToken = tokenSource.Token; + } + int result = 1; if (called) { @@ -505,6 +519,13 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo positionSetFunc: _ => {}, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { + // This might be called from sync Read which has no ability to pass the actual cancellationToken. + // So rather than deadlock, take a new token from the source. + if (cancellationToken == default) + { + cancellationToken = tokenSource.Token; + } + int result = 1; if (called) { 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 abfe5ec30f495..51087aa012961 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 @@ -50,11 +50,11 @@ public static async ValueTask ConnectAsync(string host, int port, bool a socket.Connect(new DnsEndPoint(host, port)); } } - catch (Exception ex) + catch (Exception e) { socket.Dispose(); - if (CancellationHelper.ShouldWrapInOperationCanceledException(ex, cancellationToken)) + if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) { // Cancellation was requested, so assume that the failure is due to // the cancellation request. This is a bit unorthodox, as usually we'd @@ -65,7 +65,7 @@ public static async ValueTask ConnectAsync(string host, int port, bool a // exceptions (argument exceptions, object disposed exceptions, socket exceptions, // etc.), as a middle ground we treat it as cancellation, but still propagate the // original information as the inner exception, for diagnostic purposes. - throw CancellationHelper.CreateOperationCanceledException(ex, cancellationToken); + throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); } throw; @@ -193,13 +193,30 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a } else { - sslStream.AuthenticateAsClient(sslOptions); + using (cancellationToken.UnsafeRegister(s => ((Stream)s!).Dispose(), stream)) + { + sslStream.AuthenticateAsClient(sslOptions); + } } } catch (Exception e) { sslStream.Dispose(); + if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) + { + // Cancellation was requested, so assume that the failure is due to + // the cancellation request. This is a bit unorthodox, as usually we'd + // prioritize a non-OperationCanceledException over a cancellation + // request to avoid losing potentially pertinent information. But given + // the cancellation design where we tear down the underlying stream upon + // a cancellation request, which can then result in a myriad of different + // exceptions (argument exceptions, object disposed exceptions, socket exceptions, + // etc.), as a middle ground we treat it as cancellation, but still propagate the + // original information as the inner exception, for diagnostic purposes. + throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); + } + if (e is OperationCanceledException) { throw; From c8b46a6ffda436e3f32c0336ed33bb1961492d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 1 Jun 2020 12:42:20 +0200 Subject: [PATCH 34/60] Fixed merge --- .../System.Net.Http/src/System/Net/Http/HttpClientHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6fd40bc810c56..5e314ca301e46 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 @@ -240,7 +240,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, { return DiagnosticsHandler.IsEnabled() ? _diagnosticsHandler.Send(request, cancellationToken) : - _socketsHttpHandler.Send(request, cancellationToken); + _underlyingHandler.Send(request, cancellationToken); } protected internal override Task SendAsync(HttpRequestMessage request, From 4e412e994e0ea8c1b96e98eb780e2b31ef331589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 1 Jun 2020 13:29:08 +0200 Subject: [PATCH 35/60] Fixed compilaris for browser. --- .../System.Net.Http/src/System.Net.Http.csproj | 10 +++------- .../{SocketsHttpHandler => }/CancellationHelper.cs | 0 .../tests/UnitTests/System.Net.Http.Unit.Tests.csproj | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) rename src/libraries/System.Net.Http/src/System/Net/Http/{SocketsHttpHandler => }/CancellationHelper.cs (100%) 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 2340bfe30da83..9f36ea5834d03 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 @@ + @@ -115,16 +116,14 @@ Link="Common\System\Text\SimpleRegex.cs" /> - - Common\System\Net\ArrayBuffer.cs - + - @@ -702,7 +701,6 @@ - - - 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/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 793729cd029f9..176edd80460fe 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -74,6 +74,8 @@ Link="ProductionCode\System\Net\Http\HttpBaseStream.cs" /> + - Date: Tue, 2 Jun 2020 16:10:27 +0200 Subject: [PATCH 36/60] Single thread execution tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 95 ++++++++++++++++++- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index ce4b73716150c..41b5011b51815 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.DotNet.RemoteExecutor; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Test.Common; @@ -725,6 +726,82 @@ 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"; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + // 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); + } + }, + async server => + { + await server.AcceptConnectionSendResponseAndCloseAsync(content: content); + }); + } + [Fact] public void DefaultRequestVersion_InitialValueExpected() { @@ -822,13 +899,23 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella 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) @@ -838,8 +925,6 @@ protected override bool TryComputeLength(out long length) } } - - public abstract class HttpClientSendTest : HttpClientHandlerTestBase { protected HttpClientSendTest(ITestOutputHelper output) : base(output) { } From a53aee78c45e31ad5c6221c9933b1d77d53140f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 3 Jun 2020 18:13:25 +0200 Subject: [PATCH 37/60] Cancellation tests --- .../tests/FunctionalTests/HttpClientTest.cs | 94 ++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 41b5011b51815..3f0d7861c5c6e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -3,6 +3,7 @@ // 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; @@ -770,6 +771,8 @@ public async Task Send_SingleThread_Loopback_Succeeds(HttpCompletionOption compl { string content = "Test content"; + ManualResetEventSlim mres = new ManualResetEventSlim(); + await LoopbackServer.CreateClientAndServerAsync( async uri => { @@ -793,15 +796,102 @@ await LoopbackServer.CreateClientAndServerAsync( { // Should be buffered thus return completed task and execute synchronously. Stream contentStream = await response.Content.ReadAsStreamAsync(); - Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + using (StreamReader sr = new StreamReader(contentStream)) + { + Assert.Equal(content, sr.ReadToEnd()); + } } + mres.Set(); }, async server => { - await server.AcceptConnectionSendResponseAndCloseAsync(content: content); + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(content: content); + mres.Wait(); + }); + }); + } + + [Fact] + 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 => + { + cts.Cancel(); + try + { + await connection.ReadRequestHeaderAndSendResponseAsync(); + } + catch { } + }); }); } + [Fact] + 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 => + { + await connection.ReadRequestDataAsync(); + await connection.SendResponseHeadersAsync(headers: new List() { + new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) + }); + await connection.Writer.WriteLineAsync(content); + cts.Cancel(); + await Task.Delay(TimeSpan.FromSeconds(0.5)); + }); + }); + } + [Fact] public void DefaultRequestVersion_InitialValueExpected() { From 9501109a68e6998ff432174e773a46467558cc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 4 Jun 2020 12:10:24 +0200 Subject: [PATCH 38/60] Removed unnecessary changes. --- .../tests/System/Net/Http/HttpClientHandlerTest.cs | 12 ++++++------ .../src/System/Net/Http/HttpContent.cs | 2 +- .../Net/Http/SocketsHttpHandler/HttpContentStream.cs | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 1a13cd092159b..c79aefb2b6873 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1950,13 +1950,13 @@ await server.AcceptConnectionAsync(async connection => public static IEnumerable Interim1xxStatusCode() { - yield return new object[] { (HttpStatusCode)100 }; // 100 Continue. + yield return new object[] { (HttpStatusCode) 100 }; // 100 Continue. // 101 SwitchingProtocols will be treated as a final status code. - yield return new object[] { (HttpStatusCode)102 }; // 102 Processing. - yield return new object[] { (HttpStatusCode)103 }; // 103 EarlyHints. - yield return new object[] { (HttpStatusCode)150 }; - yield return new object[] { (HttpStatusCode)180 }; - yield return new object[] { (HttpStatusCode)199 }; + yield return new object[] { (HttpStatusCode) 102 }; // 102 Processing. + yield return new object[] { (HttpStatusCode) 103 }; // 103 EarlyHints. + yield return new object[] { (HttpStatusCode) 150 }; + yield return new object[] { (HttpStatusCode) 180 }; + yield return new object[] { (HttpStatusCode) 199 }; } [Theory] 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 8b2a13370df27..f205c8edefd08 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 @@ -426,7 +426,7 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT throw error!; } - // Register for cancellation and trear down the underlying stream in case of cancellation/timeout. + // Register for cancellation and tear down the underlying stream in case of cancellation/timeout. CancellationTokenRegistration cancellationRegistration = cancellationToken.Register(swr => { var streamWeakRef = (WeakReference)swr!; 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 17da061bb29f8..25c1b2fd51d43 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 @@ -35,10 +35,10 @@ protected override void Dispose(bool disposing) protected HttpConnection GetConnectionOrThrow() { return _connection ?? - // This should only ever happen if the user-code that was handed this instance disposed of - // it, which is misuse, or held onto it and tried to use it later after we've disposed of it, - // which is also misuse. - ThrowObjectDisposedException(); + // This should only ever happen if the user-code that was handed this instance disposed of + // it, which is misuse, or held onto it and tried to use it later after we've disposed of it, + // which is also misuse. + ThrowObjectDisposedException(); } private HttpConnection ThrowObjectDisposedException() => throw new ObjectDisposedException(GetType().Name); From 41212be1cc2592d0071f02ea44d2e1286d61a81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 4 Jun 2020 13:22:39 +0200 Subject: [PATCH 39/60] Missing TestAsync. --- .../tests/FunctionalTests/HttpClientHandlerTest.Connect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs index 549c6e9674808..db195df96b773 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Connect.cs @@ -22,7 +22,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => 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); + Task responseTask = client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead); await server.AcceptConnectionAsync(async connection => { @@ -80,7 +80,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => 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); + Task responseTask = client.SendAsync(TestAsync, request, HttpCompletionOption.ResponseHeadersRead); await server.AcceptConnectionAsync(async connection => { Task> serverTask = connection.ReadRequestHeaderAndSendResponseAsync(HttpStatusCode.Forbidden, content: "error"); From b6f86f96e3aeb92330d1e9a264cd2967d61bb269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 4 Jun 2020 13:55:01 +0200 Subject: [PATCH 40/60] Tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 3f0d7861c5c6e..45e186279e18e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -837,6 +837,7 @@ await LoopbackServer.CreateClientAndServerAsync( }); TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.Contains("HttpContent.CopyTo(", ex.InnerException.StackTrace); Assert.IsNotType(ex.InnerException); }, async server => @@ -875,6 +876,7 @@ await LoopbackServer.CreateClientAndServerAsync( }); TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.Contains("HttpContent.LoadIntoBuffer(", ex.StackTrace); Assert.IsNotType(ex.InnerException); }, async server => @@ -882,12 +884,12 @@ await LoopbackServer.CreateClientAndServerAsync( await server.AcceptConnectionAsync(async connection => { await connection.ReadRequestDataAsync(); - await connection.SendResponseHeadersAsync(headers: new List() { + await connection.SendResponseAsync(headers: new List() { new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) }); - await connection.Writer.WriteLineAsync(content); - cts.Cancel(); await Task.Delay(TimeSpan.FromSeconds(0.5)); + cts.Cancel(); + await connection.Writer.WriteLineAsync(content); }); }); } From 52c56014238728b4111cfe462eeb99498e0f7d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Thu, 4 Jun 2020 17:32:18 +0200 Subject: [PATCH 41/60] Timeout tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 139 +++++++++++++++--- 1 file changed, 116 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 45e186279e18e..7d4918fa7d26f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -776,39 +776,47 @@ public async Task Send_SingleThread_Loopback_Succeeds(HttpCompletionOption compl await LoopbackServer.CreateClientAndServerAsync( async uri => { - // To prevent deadlock - await Task.Yield(); + try + { + // To prevent deadlock + await Task.Yield(); - int currentThreadId = Thread.CurrentThread.ManagedThreadId; + int currentThreadId = Thread.CurrentThread.ManagedThreadId; - using HttpClient httpClient = CreateHttpClient(); + 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); + 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)) + // ToDo: use synchronous ReadAsStream once it's been implemented. + if (completionOption == HttpCompletionOption.ResponseContentRead) { - Assert.Equal(content, sr.ReadToEnd()); + // 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()); + } } } - mres.Set(); + 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(); }); }); @@ -824,6 +832,7 @@ await LoopbackServer.CreateClientAndServerAsync( { var sendTask = Task.Run(() => { using HttpClient httpClient = CreateHttpClient(); + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { Content = new CustomContent(new Action(stream => { @@ -837,23 +846,69 @@ await LoopbackServer.CreateClientAndServerAsync( }); TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); - Assert.Contains("HttpContent.CopyTo(", ex.InnerException.StackTrace); Assert.IsNotType(ex.InnerException); + Assert.Contains("HttpContent.CopyTo(", ex.ToString()); }, async server => { await server.AcceptConnectionAsync(async connection => { + await connection.ReadRequestHeaderAsync(); cts.Cancel(); try { - await connection.ReadRequestHeaderAndSendResponseAsync(); + await connection.ReadRequestBodyAsync(); } catch { } }); }); } + [Fact] + public async Task Send_TimeoutRequestContent_Throws() + { + ManualResetEventSlim mres = new ManualResetEventSlim(); + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + try + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(0.5); + + 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)); + } + })) + }); + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); + Assert.Contains("HttpContent.CopyTo(", ex.ToString()); + } + finally + { + mres.Set(); + } + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAsync(); + mres.Wait(); + }); + }); + } + [Fact] public async Task Send_CancelledResponseContent_Throws() { @@ -876,8 +931,8 @@ await LoopbackServer.CreateClientAndServerAsync( }); TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); - Assert.Contains("HttpContent.LoadIntoBuffer(", ex.StackTrace); Assert.IsNotType(ex.InnerException); + Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); }, async server => { @@ -894,6 +949,44 @@ await server.AcceptConnectionAsync(async connection => }); } + [Fact] + public async Task Send_TimeoutResponseContent_Throws() + { + string content = "Test content"; + + await LoopbackServer.CreateClientAndServerAsync( + async uri => + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(0.5); + + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { + Content = new CustomContent(stream => + { + stream.Write(Encoding.UTF8.GetBytes(content)); + }) + }); + }); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); + Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); + }, + async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestDataAsync(); + await connection.SendResponseAsync(headers: new List() { + new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) + }); + await Task.Delay(TimeSpan.FromSeconds(1)); + await connection.Writer.WriteLineAsync(content); + }); + }); + } + [Fact] public void DefaultRequestVersion_InitialValueExpected() { From 0f90f0c90bcfce7f8ce49106116fc6b1b482175e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 5 Jun 2020 10:36:09 +0200 Subject: [PATCH 42/60] Clean up after #37441 discussion --- .../HttpClientHandlerTest.Cancellation.cs | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) 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 3de084849c81e..869071e69392e 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -433,13 +433,6 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo canReadFunc: () => true, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { - // This might be called from sync Read which has no ability to pass the actual cancellationToken. - // So rather than deadlock, take a new token from the source. - if (cancellationToken == default) - { - cancellationToken = tokenSource.Token; - } - int result = 1; if (called) { @@ -476,13 +469,6 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo positionSetFunc: _ => {}, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { - // This might be called from sync Read which has no ability to pass the actual cancellationToken. - // So rather than deadlock, take a new token from the source. - if (cancellationToken == default) - { - cancellationToken = tokenSource.Token; - } - int result = 1; if (called) { @@ -519,13 +505,6 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo positionSetFunc: _ => {}, readAsyncFunc: async (buffer, offset, count, cancellationToken) => { - // This might be called from sync Read which has no ability to pass the actual cancellationToken. - // So rather than deadlock, take a new token from the source. - if (cancellationToken == default) - { - cancellationToken = tokenSource.Token; - } - int result = 1; if (called) { @@ -555,7 +534,11 @@ public static IEnumerable PostAsync_Cancel_CancellationTokenPassedToCo [MemberData(nameof(PostAsync_Cancel_CancellationTokenPassedToContent_MemberData))] public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent content, CancellationTokenSource cancellationTokenSource) { - if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) + if (IsWinHttpHandler) + { + return; + } + if (!TestAsync) { return; } From 453aa6bd40d973be66b435c257837867fdad4afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 5 Jun 2020 11:28:45 +0200 Subject: [PATCH 43/60] Response content timeout test. --- .../src/System/Net/Http/HttpContent.cs | 8 ++--- .../tests/FunctionalTests/HttpClientTest.cs | 30 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) 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 f205c8edefd08..0015dd88b3880 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 @@ -429,12 +429,12 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT // Register for cancellation and tear down the underlying stream in case of cancellation/timeout. CancellationTokenRegistration cancellationRegistration = cancellationToken.Register(swr => { - var streamWeakRef = (WeakReference)swr!; - if (streamWeakRef.TryGetTarget(out MemoryStream? stream)) + var weakThisRef = (WeakReference)swr!; + if (weakThisRef.TryGetTarget(out HttpContent? strongThisRef)) { - stream.Dispose(); + strongThisRef.Dispose(); } - }, new WeakReference(tempBuffer)); + }, new WeakReference(this)); try { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 7d4918fa7d26f..78624f84a9255 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -954,24 +954,28 @@ 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 + { + var sendTask = Task.Run(() => { + using HttpClient httpClient = CreateHttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(1); - HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri) { - Content = new CustomContent(stream => - { - stream.Write(Encoding.UTF8.GetBytes(content)); - }) + HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri)); }); - }); - TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); - Assert.IsType(ex.InnerException); - Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); + Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); + } + finally + { + mres.Set(); + } }, async server => { @@ -981,7 +985,7 @@ await server.AcceptConnectionAsync(async connection => await connection.SendResponseAsync(headers: new List() { new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) }); - await Task.Delay(TimeSpan.FromSeconds(1)); + mres.Wait(); await connection.Writer.WriteLineAsync(content); }); }); From 157431cda4fef6eb7535d9f4767818bc44389664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Fri, 5 Jun 2020 14:04:25 +0200 Subject: [PATCH 44/60] Timeout tests too slow to check for exact call stack line. --- .../System.Net.Http/tests/FunctionalTests/HttpClientTest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 78624f84a9255..6f5a7abd07be0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -892,7 +892,6 @@ await LoopbackServer.CreateClientAndServerAsync( TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); Assert.IsType(ex.InnerException); - Assert.Contains("HttpContent.CopyTo(", ex.ToString()); } finally { @@ -963,14 +962,13 @@ await LoopbackServer.CreateClientAndServerAsync( { var sendTask = Task.Run(() => { using HttpClient httpClient = CreateHttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(1); + httpClient.Timeout = TimeSpan.FromSeconds(0.5); HttpResponseMessage response = httpClient.Send(new HttpRequestMessage(HttpMethod.Get, uri)); }); TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); Assert.IsType(ex.InnerException); - Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); } finally { From d9de995731d2159aa1d291b45cfac7d111ba0a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 8 Jun 2020 11:43:21 +0200 Subject: [PATCH 45/60] Review comments. --- .../System/Net/Http/HttpClientHandlerTest.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index c79aefb2b6873..120fa86be132a 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1867,31 +1867,29 @@ public async Task PostAsync_CallMethod_EmptyContent(Configuration.Http.RemoteSer public static IEnumerable ExpectContinueVersion() { - var versions = new string[] {"1.0", "1.1", "2.0"}; - var expectContinue = new bool?[] {true, false, null}; return - from expect in expectContinue - from version in versions + 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] [MemberData(nameof(ExpectContinueVersion))] - public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string version) + public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, Version version) { // Sync API supported only up to HTTP/1.1 - if (!TestAsync && version == "2.0") + 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; @@ -1902,7 +1900,7 @@ public async Task PostAsync_ExpectContinue_Success(bool? expectContinue, string 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()); } From f20d300baad5ec244060e6a4e108f43edf983c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 8 Jun 2020 15:43:46 +0200 Subject: [PATCH 46/60] Review comments. --- .../tests/System/Net/Http/HttpClientHandlerTestBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs index 7dd1e56ca719a..e59fd00ed5268 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTestBase.cs @@ -147,10 +147,11 @@ public static Task SendAsync(this HttpClient client, bool a // 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."); - return Task.FromResult(null); + throw new Exception("Shouldn't be reachable"); #endif } } + public static Task SendAsync(this HttpMessageInvoker invoker, bool async, HttpRequestMessage request, CancellationToken cancellationToken = default) { if (async) @@ -167,7 +168,7 @@ public static Task SendAsync(this HttpMessageInvoker invoke // 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."); - return Task.FromResult(null); + throw new Exception("Shouldn't be reachable"); #endif } } From 0f8bbd8e984711fc0c81d3fc502b00bc6d994085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Mon, 8 Jun 2020 15:47:22 +0200 Subject: [PATCH 47/60] Resx review comments. --- src/libraries/System.Net.Http/src/Resources/Strings.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 0f332dd1166ee..90ab7e8f40980 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -556,9 +556,9 @@ Stream aborted by peer ({0}). - The synchronous API is not supported by '{0}'. If you're using custom '{1}' and wish to use synchronous HTTP APIs, you must override '{2}' virtual method. + 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 API is not supported by '{0}' for HTTP/2 or higher. Either use an async implementation or downgrade you request version to HTTP/1.1 or lower. + 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. From 43ed804dbe210594fb936f5617e39a222a469fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 9 Jun 2020 11:21:30 +0200 Subject: [PATCH 48/60] Last review comments. --- .../src/System/Net/Http/DiagnosticsHandler.cs | 8 +-- .../src/System/Net/Http/HttpClient.cs | 14 ++-- .../Http/SocketsHttpHandler/ConnectHelper.cs | 69 ++++++++++--------- .../tests/FunctionalTests/DiagnosticsTests.cs | 2 +- 4 files changed, 46 insertions(+), 47 deletions(-) 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 41e99127aaf2a..a79efd9b1817b 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 @@ -114,15 +114,15 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : InjectHeaders(currentActivity, request); } + Task? responseTask = null; HttpResponseMessage? response = null; TaskStatus taskStatus = TaskStatus.Faulted; try { if (async) { - Task? responseTask = base.SendAsync(request, cancellationToken); + responseTask = base.SendAsync(request, cancellationToken); response = await responseTask.ConfigureAwait(false); - taskStatus = responseTask.Status; } else { @@ -158,7 +158,7 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : // 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, - taskStatus)); + responseTask?.Status ?? taskStatus)); } // Try to write System.Net.Http.Response event (deprecated) if (s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated)) @@ -169,7 +169,7 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : response, loggingRequestId, timestamp, - taskStatus)); + responseTask?.Status ?? taskStatus)); } } } 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 85a183dc7f55d..36a138d917110 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 @@ -542,17 +542,11 @@ private async ValueTask FinishSendAsync(ValueTask ConnectAsync(string host, int port, bool a // (For async, we need to do more gymnastics with SocketAsyncEventArgs.) if (!async) { - cancellationToken.ThrowIfCancellationRequested(); - Socket? 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)) - { - // Cancellation was requested, so assume that the failure is due to - // the cancellation request. This is a bit unorthodox, as usually we'd - // prioritize a non-OperationCanceledException over a cancellation - // request to avoid losing potentially pertinent information. But given - // the cancellation design where we tear down the underlying stream upon - // a cancellation request, which can then result in a myriad of different - // exceptions (argument exceptions, object disposed exceptions, socket exceptions, - // etc.), as a middle ground we treat it as cancellation, but still propagate the - // original information as the inner exception, for diagnostic purposes. - throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); - } - - throw; - } - - return new NetworkStream(socket, ownsSocket: true); + return Connect(host, port, cancellationToken); } // Rather than creating a new Socket and calling ConnectAsync on it, we use the static @@ -118,6 +87,42 @@ public static async ValueTask ConnectAsync(string host, int port, bool a } } + private static Stream Connect(string host, int port, CancellationToken cancellationToken) + { + 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)) + { + // Cancellation was requested, so assume that the failure is due to + // the cancellation request. This is a bit unorthodox, as usually we'd + // prioritize a non-OperationCanceledException over a cancellation + // request to avoid losing potentially pertinent information. But given + // the cancellation design where we tear down the underlying stream upon + // a cancellation request, which can then result in a myriad of different + // exceptions (argument exceptions, object disposed exceptions, socket exceptions, + // etc.), as a middle ground we treat it as cancellation, but still propagate the + // original information as the inner exception, for diagnostic purposes. + 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 { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs index 47ffcf5cdaa15..fb9b31085289e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs @@ -801,7 +801,7 @@ public void SendAsync_ExpectedDiagnosticSynchronousExceptionActivityLogging() { // 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. - SpinWait.SpinUntil(() => sendTask.IsCompleted); + ((IAsyncResult)sendTask).AsyncWaitHandle.WaitOne(); } Assert.True(sendTask.IsFaulted); Assert.IsType(sendTask.Exception.InnerException); From 04d567fe5fac9f6cad0577cd2e7cc83d3a080e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 9 Jun 2020 20:25:39 +0200 Subject: [PATCH 49/60] Fail proofing tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 6f5a7abd07be0..dff5ce4954563 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -902,8 +902,12 @@ await LoopbackServer.CreateClientAndServerAsync( { await server.AcceptConnectionAsync(async connection => { - await connection.ReadRequestHeaderAsync(); - mres.Wait(); + try + { + await connection.ReadRequestHeaderAsync(); + mres.Wait(); + } + catch { } }); }); } @@ -943,7 +947,11 @@ await server.AcceptConnectionAsync(async connection => }); await Task.Delay(TimeSpan.FromSeconds(0.5)); cts.Cancel(); - await connection.Writer.WriteLineAsync(content); + try + { + await connection.Writer.WriteLineAsync(content); + } + catch { } }); }); } @@ -979,12 +987,16 @@ await LoopbackServer.CreateClientAndServerAsync( { await server.AcceptConnectionAsync(async connection => { - await connection.ReadRequestDataAsync(); - await connection.SendResponseAsync(headers: new List() { - new HttpHeaderData("Content-Length", (content.Length * 2).ToString()) - }); - mres.Wait(); - await connection.Writer.WriteLineAsync(content); + 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 { } }); }); } From 53c24a917f24db05a92e655b21dc660686bb6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 16:08:16 +0200 Subject: [PATCH 50/60] Removed some unnecessary newlines. --- .../Common/tests/System/Net/Http/ByteAtATimeContent.cs | 3 +-- .../Common/tests/System/Net/Http/HttpRetryProtocolTests.cs | 3 +-- .../src/System/Net/Http/DiagnosticsHandler.cs | 3 +-- .../Net/Http/SocketsHttpHandler/HttpMessageHandlerStage.cs | 6 ++---- .../FunctionalTests/HttpClientHandlerTest.Cancellation.cs | 3 +-- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs index 7c32624c77830..4fad8e55c30e4 100644 --- a/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs +++ b/src/libraries/Common/tests/System/Net/Http/ByteAtATimeContent.cs @@ -26,8 +26,7 @@ public ByteAtATimeContent(int length, Task waitToSend, TaskCompletionSource + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); #endif diff --git a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs index 044f85e04b5d7..dda79c80b0562 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpRetryProtocolTests.cs @@ -128,8 +128,7 @@ public SynchronizedSendContent(TaskCompletionSource sendingContent, Task c } #if NETCOREAPP - protected override void SerializeToStream(Stream stream, TransportContext context, - CancellationToken cancellationToken) => + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); #endif 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 a79efd9b1817b..93938a3b4b37d 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 @@ -41,8 +41,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, return sendTask.GetAwaiter().GetResult(); } - protected internal override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) => + protected internal override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => SendAsyncCore(request, async: true, cancellationToken).AsTask(); private async ValueTask SendAsyncCore(HttpRequestMessage request, 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 index d8bd8a9c58546..126962427eb67 100644 --- 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 @@ -14,11 +14,9 @@ protected internal sealed override HttpResponseMessage Send(HttpRequestMessage r return sendTask.GetAwaiter().GetResult(); } - protected internal sealed override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) => + 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); + internal abstract ValueTask SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs index 913037d6738f2..38d9bd759fad5 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Cancellation.cs @@ -84,8 +84,7 @@ private sealed class SetTcsContent : StreamContent public SetTcsContent(Stream stream, TaskCompletionSource tcs) : base(stream) => _tcs = tcs; - protected override void SerializeToStream(Stream stream, TransportContext context, - CancellationToken cancellationToken) => + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context).GetAwaiter().GetResult(); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) From 1bc2f15604edc701b2475b563b14996a37afa7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 16:15:04 +0200 Subject: [PATCH 51/60] Explanation why the test is skipped. --- .../tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs | 2 ++ 1 file changed, 2 insertions(+) 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 869071e69392e..ac77cd7e258a2 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -538,6 +538,8 @@ 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; From 864b1588a26fbacf7d37756d60c5f50adf5e1aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 16:18:23 +0200 Subject: [PATCH 52/60] ConnectHelper.ConnectAsync split even further. --- .../Net/Http/SocketsHttpHandler/ConnectHelper.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 ccaea1f79807b..1a4ee4a702541 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,15 +34,13 @@ public CertificateCallbackMapper(Func ConnectAsync(string host, int port, bool async, CancellationToken cancellationToken) + public static ValueTask ConnectAsync(string host, int port, bool async, CancellationToken cancellationToken) { - // For synchronous connections, we can just create a socket and make the connection. - // (For async, we need to do more gymnastics with SocketAsyncEventArgs.) - if (!async) - { - return Connect(host, port, cancellationToken); - } + return async ? ConnectAsync(host, port, cancellationToken) : new ValueTask(Connect(host, port, cancellationToken)); + } + public 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 // to cancel it if needed. @@ -89,6 +87,7 @@ public static async ValueTask ConnectAsync(string host, int port, bool a 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 From 77669b8f81dd75d5de9a078884bb6d0cafdf5aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 16:19:55 +0200 Subject: [PATCH 53/60] Renamed resource. --- src/libraries/System.Net.Http/src/Resources/Strings.resx | 2 +- .../System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 90ab7e8f40980..3ddc08d1c397b 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -558,7 +558,7 @@ 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/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index 30c8bf5901ca8..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 @@ -339,7 +339,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, { if (request.Version.Major >= 2) { - throw new NotSupportedException(SR.Format(SR.net_http_sync_not_supported, GetType())); + throw new NotSupportedException(SR.Format(SR.net_http_http2_sync_not_supported, GetType())); } CheckDisposed(); From c624dd8187bfe361b9d4a83e88990acff4ca7774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 17:25:13 +0200 Subject: [PATCH 54/60] Removed check for missing SerializeToStream override from HttpContent. --- .../src/System/Net/Http/ByteArrayContent.cs | 16 +--------------- .../src/System/Net/Http/EmptyContent.cs | 3 +-- .../src/System/Net/Http/FormUrlEncodedContent.cs | 14 -------------- .../src/System/Net/Http/MultipartContent.cs | 14 -------------- .../System/Net/Http/MultipartFormDataContent.cs | 15 --------------- .../src/System/Net/Http/StreamContent.cs | 15 --------------- .../src/System/Net/Http/StringContent.cs | 15 --------------- 7 files changed, 2 insertions(+), 90 deletions(-) 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 cb450c3dbddb8..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 @@ -47,21 +47,7 @@ public ByteArrayContent(byte[] content, int offset, int count) _count = count; } - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(ByteArrayContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - - private protected void SerializeToStreamCore(Stream stream, TransportContext? context, CancellationToken cancellationToken) => + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) => stream.Write(_content, _offset, _count); protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => 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 0c45250fbefac..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,8 +17,7 @@ protected internal override bool TryComputeLength(out long length) return true; } - protected override void SerializeToStream(Stream stream, TransportContext? context, - CancellationToken cancellationToken) + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) { } protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs index c7ac9b8ffabfe..7ca4a99a1fd12 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs @@ -53,20 +53,6 @@ private static string Encode(string? data) return Uri.EscapeDataString(data).Replace("%20", "+"); } - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(FormUrlEncodedContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. 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 96c933055042f..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 @@ -167,20 +167,6 @@ Collections.IEnumerator Collections.IEnumerable.GetEnumerator() // 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) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(MultipartContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - - private protected void SerializeToStreamCore(Stream stream, TransportContext? context, CancellationToken cancellationToken) { Debug.Assert(stream != null); try diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs index 97270d70252a5..8b579c7821715 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs @@ -85,21 +85,6 @@ private void AddInternal(HttpContent content, string name, string? fileName) base.Add(content); } - protected override void SerializeToStream(Stream stream, TransportContext? context, - CancellationToken cancellationToken) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(MultipartFormDataContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. 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 47cc43767b7fe..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 @@ -56,21 +56,6 @@ private void InitializeContent(Stream content, int bufferSize) } protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(StreamContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - - private void SerializeToStreamCore(Stream stream, TransportContext? context, - CancellationToken cancellationToken) { Debug.Assert(stream != null); PrepareContent(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs index fa599c2448d4e..d19d8f2e2f126 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs @@ -54,21 +54,6 @@ private static byte[] GetContentByteArray(string content, Encoding? encoding) return encoding.GetBytes(content); } - protected override void SerializeToStream(Stream stream, TransportContext? context, - CancellationToken cancellationToken) - { - // Only skip the original protected virtual SerializeToStream if this - // isn't a derived type that may have overridden the behavior. - if (GetType() != typeof(StringContent)) - { - base.SerializeToStream(stream, context, cancellationToken); - } - else - { - SerializeToStreamCore(stream, context, cancellationToken); - } - } - protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => // Only skip the original protected virtual SerializeToStreamAsync if this // isn't a derived type that may have overridden the behavior. From ed6eb2a918686534119e95f0518ca636e7aa9bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 17:32:22 +0200 Subject: [PATCH 55/60] DiagnosticHandler and task status. --- .../src/System/Net/Http/DiagnosticsHandler.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) 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 93938a3b4b37d..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 @@ -113,30 +113,26 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : InjectHeaders(currentActivity, request); } - Task? responseTask = null; HttpResponseMessage? response = null; - TaskStatus taskStatus = TaskStatus.Faulted; + TaskStatus taskStatus = TaskStatus.RanToCompletion; try { - if (async) - { - responseTask = base.SendAsync(request, cancellationToken); - response = await responseTask.ConfigureAwait(false); - } - else - { - response = base.Send(request, cancellationToken); - taskStatus = TaskStatus.RanToCompletion; - } + 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 @@ -157,7 +153,7 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : // 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)); + taskStatus)); } // Try to write System.Net.Http.Response event (deprecated) if (s_diagnosticListener.IsEnabled(DiagnosticsHandlerLoggingStrings.ResponseWriteNameDeprecated)) @@ -168,7 +164,7 @@ await base.SendAsync(request, cancellationToken).ConfigureAwait(false) : response, loggingRequestId, timestamp, - responseTask?.Status ?? taskStatus)); + taskStatus)); } } } From 851c8b6ad266ecca975e27385043bd83c33cd38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 17:57:22 +0200 Subject: [PATCH 56/60] The last of the comments --- .../src/System/Net/Http/HttpClient.cs | 4 +--- .../src/System/Net/Http/HttpContent.cs | 21 ++++------------- .../Http/SocketsHttpHandler/ConnectHelper.cs | 23 ------------------- 3 files changed, 5 insertions(+), 43 deletions(-) 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 36a138d917110..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 @@ -494,7 +494,6 @@ public Task SendAsync(HttpRequestMessage request, HttpCompl private ValueTask SendAsyncCore(HttpRequestMessage request, HttpCompletionOption completionOption, bool async, CancellationToken cancellationToken) { - if (request == null) { throw new ArgumentNullException(nameof(request)); @@ -507,8 +506,7 @@ private ValueTask SendAsyncCore(HttpRequestMessage request, // PrepareRequestMessage will resolve the request address against the base address. // Combines given cancellationToken with the global one and the timeout. - CancellationTokenSource cts = - PrepareCancellationTokenSource(cancellationToken, out bool disposeCts, out long timeoutTime); + CancellationTokenSource cts = PrepareCancellationTokenSource(cancellationToken, out bool disposeCts, out long timeoutTime); // Initiate the send. ValueTask responseTask; 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 0015dd88b3880..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 @@ -427,14 +427,10 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT } // Register for cancellation and tear down the underlying stream in case of cancellation/timeout. - CancellationTokenRegistration cancellationRegistration = cancellationToken.Register(swr => - { - var weakThisRef = (WeakReference)swr!; - if (weakThisRef.TryGetTarget(out HttpContent? strongThisRef)) - { - strongThisRef.Dispose(); - } - }, new WeakReference(this)); + // 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 { @@ -448,15 +444,6 @@ internal void LoadIntoBuffer(long maxBufferSize, CancellationToken cancellationT if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) { - // Cancellation was requested, so assume that the failure is due to - // the cancellation request. This is a bit unorthodox, as usually we'd - // prioritize a non-OperationCanceledException over a cancellation - // request to avoid losing potentially pertinent information. But given - // the cancellation design where we tear down the underlying stream upon - // a cancellation request, which can then result in a myriad of different - // exceptions (argument exceptions, object disposed exceptions, socket exceptions, - // etc.), as a middle ground we treat it as cancellation, but still propagate the - // original information as the inner exception, for diagnostic purposes. throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); } 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 1a4ee4a702541..50b84ccf9ae91 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 @@ -104,15 +104,6 @@ private static Stream Connect(string host, int port, CancellationToken cancellat if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) { - // Cancellation was requested, so assume that the failure is due to - // the cancellation request. This is a bit unorthodox, as usually we'd - // prioritize a non-OperationCanceledException over a cancellation - // request to avoid losing potentially pertinent information. But given - // the cancellation design where we tear down the underlying stream upon - // a cancellation request, which can then result in a myriad of different - // exceptions (argument exceptions, object disposed exceptions, socket exceptions, - // etc.), as a middle ground we treat it as cancellation, but still propagate the - // original information as the inner exception, for diagnostic purposes. throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); } @@ -212,20 +203,6 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a { sslStream.Dispose(); - if (CancellationHelper.ShouldWrapInOperationCanceledException(e, cancellationToken)) - { - // Cancellation was requested, so assume that the failure is due to - // the cancellation request. This is a bit unorthodox, as usually we'd - // prioritize a non-OperationCanceledException over a cancellation - // request to avoid losing potentially pertinent information. But given - // the cancellation design where we tear down the underlying stream upon - // a cancellation request, which can then result in a myriad of different - // exceptions (argument exceptions, object disposed exceptions, socket exceptions, - // etc.), as a middle ground we treat it as cancellation, but still propagate the - // original information as the inner exception, for diagnostic purposes. - throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); - } - if (e is OperationCanceledException) { throw; From 2eb6c2d47efa759e47f954b59abf5bbba487c6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Wed, 10 Jun 2020 18:04:07 +0200 Subject: [PATCH 57/60] Fixed test hanging on 3s timeout. --- .../Common/tests/System/Net/Http/HttpProtocolTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs index 4bccc61c014f3..a7eaa9c77d51c 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpProtocolTests.cs @@ -91,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}); From 7e5c47d69072a49140ed9ea4116244c5fb348d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 23 Jun 2020 13:29:24 +0200 Subject: [PATCH 58/60] Fixed method visibility --- .../src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 50b84ccf9ae91..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 @@ -39,7 +39,7 @@ public static ValueTask ConnectAsync(string host, int port, bool async, return async ? ConnectAsync(host, port, cancellationToken) : new ValueTask(Connect(host, port, cancellationToken)); } - public static async ValueTask ConnectAsync(string host, int port, CancellationToken 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 From 96ecccecae8b1a21a77054ae59e61e6663fdec25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 23 Jun 2020 13:33:05 +0200 Subject: [PATCH 59/60] Fixed hanging tests. --- .../tests/FunctionalTests/HttpClientTest.cs | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index fffbe9c11327a..55271b6570d4a 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -872,12 +872,12 @@ public async Task Send_TimeoutRequestContent_Throws() await LoopbackServer.CreateClientAndServerAsync( async uri => { - try - { - var sendTask = Task.Run(() => { - using HttpClient httpClient = CreateHttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(0.5); + 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 => { @@ -888,15 +888,15 @@ await LoopbackServer.CreateClientAndServerAsync( } })) }); - }); + } + finally + { + mres.Set(); + } + }); - TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); - Assert.IsType(ex.InnerException); - } - finally - { - mres.Set(); - } + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); }, async server => { @@ -966,22 +966,21 @@ public async Task Send_TimeoutResponseContent_Throws() await LoopbackServer.CreateClientAndServerAsync( async uri => { - try - { - var sendTask = Task.Run(() => { - using HttpClient httpClient = CreateHttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(0.5); - + 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); - } - finally - { - mres.Set(); - } + TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.IsType(ex.InnerException); }, async server => { From 7844fbf39812bd591dc10dc0fdfbd5fc1137d507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= Date: Tue, 23 Jun 2020 16:06:40 +0200 Subject: [PATCH 60/60] Proofing test against slow infra. --- .../tests/FunctionalTests/HttpClientTest.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 55271b6570d4a..2f50789b34f58 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -823,6 +823,7 @@ await server.AcceptConnectionAsync(async connection => } [Fact] + [OuterLoop] public async Task Send_CancelledRequestContent_Throws() { CancellationTokenSource cts = new CancellationTokenSource(); @@ -847,16 +848,15 @@ await LoopbackServer.CreateClientAndServerAsync( TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); Assert.IsNotType(ex.InnerException); - Assert.Contains("HttpContent.CopyTo(", ex.ToString()); }, async server => { await server.AcceptConnectionAsync(async connection => { - await connection.ReadRequestHeaderAsync(); - cts.Cancel(); try { + await connection.ReadRequestHeaderAsync(); + cts.Cancel(); await connection.ReadRequestBodyAsync(); } catch { } @@ -865,6 +865,7 @@ await server.AcceptConnectionAsync(async connection => } [Fact] + [OuterLoop] public async Task Send_TimeoutRequestContent_Throws() { ManualResetEventSlim mres = new ManualResetEventSlim(); @@ -913,6 +914,7 @@ await server.AcceptConnectionAsync(async connection => } [Fact] + [OuterLoop] public async Task Send_CancelledResponseContent_Throws() { string content = "Test content"; @@ -935,20 +937,19 @@ await LoopbackServer.CreateClientAndServerAsync( TaskCanceledException ex = await Assert.ThrowsAsync(() => sendTask); Assert.IsNotType(ex.InnerException); - Assert.Contains("HttpContent.LoadIntoBuffer(", ex.ToString()); }, async server => { await server.AcceptConnectionAsync(async connection => { - 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(); 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 { } @@ -957,6 +958,7 @@ await server.AcceptConnectionAsync(async connection => } [Fact] + [OuterLoop] public async Task Send_TimeoutResponseContent_Throws() { string content = "Test content";