diff --git a/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs b/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs index e7e1256c4cb471..c8f49628123c5d 100644 --- a/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs +++ b/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs @@ -68,6 +68,7 @@ internal partial class WinHttp public const uint WINHTTP_QUERY_STATUS_TEXT = 20; public const uint WINHTTP_QUERY_RAW_HEADERS = 21; public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22; + public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000; public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29; public const uint WINHTTP_QUERY_SET_COOKIE = 43; public const uint WINHTTP_QUERY_CUSTOM = 65535; @@ -164,6 +165,7 @@ internal partial class WinHttp public const uint WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE = 123; public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152; + public const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159; public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE { diff --git a/src/libraries/Common/src/System/Net/SecurityProtocol.cs b/src/libraries/Common/src/System/Net/SecurityProtocol.cs index f6de449ef3b8d5..a8ff9b3cbc6984 100644 --- a/src/libraries/Common/src/System/Net/SecurityProtocol.cs +++ b/src/libraries/Common/src/System/Net/SecurityProtocol.cs @@ -8,7 +8,7 @@ namespace System.Net internal static class SecurityProtocol { public const SslProtocols DefaultSecurityProtocols = -#if !NETSTANDARD2_0 && !NETFRAMEWORK +#if !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETFRAMEWORK SslProtocols.Tls13 | #endif SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 4e02843b71d517..a53359893f0616 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -132,7 +132,7 @@ public static bool IsNonZeroLowerBoundArraySupported // OSX - SecureTransport doesn't expose alpn APIs. TODO https://github.com/dotnet/runtime/issues/27727 public static bool IsOpenSslSupported => IsLinux || IsFreeBSD || Isillumos || IsSolaris; - public static bool SupportsAlpn => (IsWindows && !IsWindows7) || + public static bool SupportsAlpn => (IsWindows && !IsWindows7 && !IsNetFramework) || (IsOpenSslSupported && (OpenSslVersion.Major >= 1 && (OpenSslVersion.Minor >= 1 || OpenSslVersion.Build >= 2))); diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj b/src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj index 8e62c79985b8c0..bb8420ef2f22d9 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj @@ -1,7 +1,7 @@ true - netstandard2.0-windows;netstandard2.0;net461-windows + netstandard2.0-windows;netstandard2.0;netstandard2.1-windows;netstandard2.1;net461-windows true enable @@ -91,6 +91,7 @@ + diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs index 1f9c88a019a91a..9f324ada27a0f8 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs @@ -29,7 +29,7 @@ public static HttpResponseMessage CreateResponseMessage( // Create a single buffer to use for all subsequent WinHttpQueryHeaders string interop calls. // This buffer is the length needed for WINHTTP_QUERY_RAW_HEADERS_CRLF, which includes the status line // and all headers separated by CRLF, so it should be large enough for any individual status line or header queries. - int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF); + int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, isTrailingHeaders: false); char[] buffer = ArrayPool.Shared.Rent(bufferLength); try { @@ -58,7 +58,7 @@ public static HttpResponseMessage CreateResponseMessage( string.Empty; // Create response stream and wrap it in a StreamContent object. - var responseStream = new WinHttpResponseStream(requestHandle, state); + var responseStream = new WinHttpResponseStream(requestHandle, state, response); state.RequestHandle = null; // ownership successfully transfered to WinHttpResponseStram. Stream decompressedStream = responseStream; @@ -223,19 +223,26 @@ private static unsafe int GetResponseHeader(SafeWinHttpHandle requestHandle, uin /// /// Returns the size of the char array buffer. /// - private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel) + public static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, bool isTrailingHeaders) { char* buffer = null; int bufferLength = 0; uint index = 0; + uint infoLevel = Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF; + if (isTrailingHeaders) + { + infoLevel |= Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS; + } + if (!QueryHeaders(requestHandle, infoLevel, buffer, ref bufferLength, ref index)) { int lastError = Marshal.GetLastWin32Error(); - Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND); + Debug.Assert(isTrailingHeaders || lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND); - if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER) + if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER && + (!isTrailingHeaders || lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND)) { throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders)); } @@ -306,10 +313,7 @@ private static void ParseResponseHeaders( reader.ReadLine(); // Parse the array of headers and split them between Content headers and Response headers. - string headerName; - string headerValue; - - while (reader.ReadHeader(out headerName, out headerValue)) + while (reader.ReadHeader(out string headerName, out string headerValue)) { if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue)) { @@ -331,6 +335,27 @@ private static void ParseResponseHeaders( } } + public static void ParseResponseTrailers( + SafeWinHttpHandle requestHandle, + HttpResponseMessage response, + char[] buffer) + { + HttpHeaders responseTrailers = WinHttpTrailersHelper.GetResponseTrailers(response); + + int bufferLength = GetResponseHeader( + requestHandle, + Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS, + buffer); + + var reader = new WinHttpResponseHeaderReader(buffer, 0, bufferLength); + + // Parse the array of headers and split them between Content headers and Response headers. + while (reader.ReadHeader(out string headerName, out string headerValue)) + { + responseTrailers.TryAddWithoutValidation(headerName, headerValue); + } + } + private static bool IsResponseHttp2(SafeWinHttpHandle requestHandle) { uint data = 0; diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs index 812a56e7232cff..2922291faa01f8 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs @@ -16,11 +16,14 @@ internal sealed class WinHttpResponseStream : Stream { private volatile bool _disposed; private readonly WinHttpRequestState _state; + private readonly HttpResponseMessage _responseMessage; private SafeWinHttpHandle _requestHandle; + private bool _readTrailingHeaders; - internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state) + internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state, HttpResponseMessage responseMessage) { _state = state; + _responseMessage = responseMessage; _requestHandle = requestHandle; } @@ -126,6 +129,7 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati int bytesAvailable = await _state.LifecycleAwaitable; if (bytesAvailable == 0) { + ReadResponseTrailers(); break; } Debug.Assert(bytesAvailable > 0); @@ -142,12 +146,17 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati int bytesRead = await _state.LifecycleAwaitable; if (bytesRead == 0) { + ReadResponseTrailers(); break; } Debug.Assert(bytesRead > 0); // Write that data out to the output stream +#if NETSTANDARD2_1 + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); +#else await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); +#endif } } finally @@ -240,7 +249,14 @@ private async Task ReadAsyncCore(byte[] buffer, int offset, int count, Canc } } - return await _state.LifecycleAwaitable; + int bytesRead = await _state.LifecycleAwaitable; + + if (bytesRead == 0) + { + ReadResponseTrailers(); + } + + return bytesRead; } finally { @@ -249,6 +265,35 @@ private async Task ReadAsyncCore(byte[] buffer, int offset, int count, Canc } } + private void ReadResponseTrailers() + { + // Only load response trailers if: + // 1. WINHTTP_QUERY_FLAG_TRAILERS is supported by the OS + // 2. HTTP/2 or later (WINHTTP_QUERY_FLAG_TRAILERS does not work with HTTP/1.1) + // 3. Response trailers not already loaded + if (!WinHttpTrailersHelper.OsSupportsTrailers || _responseMessage.Version < WinHttpHandler.HttpVersion20 || _readTrailingHeaders) + { + return; + } + + _readTrailingHeaders = true; + + var bufferLength = WinHttpResponseParser.GetResponseHeaderCharBufferLength(_requestHandle, isTrailingHeaders: true); + + if (bufferLength != 0) + { + char[] trailersBuffer = ArrayPool.Shared.Rent(bufferLength); + try + { + WinHttpResponseParser.ParseResponseTrailers(_requestHandle, _responseMessage, trailersBuffer); + } + finally + { + ArrayPool.Shared.Return(trailersBuffer); + } + } + } + public override int Read(byte[] buffer, int offset, int count) { return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpTrailersHelper.cs b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpTrailersHelper.cs new file mode 100644 index 00000000000000..7be5a4c5a240d0 --- /dev/null +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpTrailersHelper.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle; + +namespace System.Net.Http +{ + internal static class WinHttpTrailersHelper + { + // UNITTEST is true when building against WinHttpHandler.Unit.Tests, which includes the source file. +#if !NETSTANDARD2_1 && !UNITTEST + // Trailer property name was chosen to be descriptive and be unlikely to collide with a user set property. + // Apps and libraries will use this key so it shouldn't change. + private const string RequestMessagePropertyName = "__ResponseTrailers"; + private class HttpResponseTrailers : HttpHeaders + { + } +#endif + private static Lazy s_trailersSupported = new Lazy(GetTrailersSupported); + public static bool OsSupportsTrailers => s_trailersSupported.Value; + + public static HttpHeaders GetResponseTrailers(HttpResponseMessage response) + { +#if NETSTANDARD2_1 || UNITTEST + return response.TrailingHeaders; +#else + HttpResponseTrailers responseTrailers = new HttpResponseTrailers(); + response.RequestMessage.Properties[RequestMessagePropertyName] = responseTrailers; + return responseTrailers; +#endif + } + + // There is no way to verify if WINHTTP_QUERY_FLAG_TRAILERS is supported by the OS without creating a request. + // Instead, the WinHTTP team recommended to check if WINHTTP_OPTION_STREAM_ERROR_CODE is recognized by the OS. + // Both features were introduced in Manganese and are planned to be backported to older Windows versions together. + private static bool GetTrailersSupported() + { + using SafeWinHttpHandle sessionHandle = Interop.WinHttp.WinHttpOpen( + IntPtr.Zero, + Interop.WinHttp.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + Interop.WinHttp.WINHTTP_NO_PROXY_NAME, + Interop.WinHttp.WINHTTP_NO_PROXY_BYPASS, + (int)Interop.WinHttp.WINHTTP_FLAG_ASYNC); + + if (sessionHandle.IsInvalid) return false; + uint buffer = 0; + uint bufferSize = sizeof(uint); + if (Interop.WinHttp.WinHttpQueryOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_STREAM_ERROR_CODE, ref buffer, ref bufferSize)) + { + Debug.Fail("Querying WINHTTP_OPTION_STREAM_ERROR_CODE on a session handle should never succeed."); + return false; + } + + int lastError = Marshal.GetLastWin32Error(); + + // New Windows builds are expected to fail with ERROR_WINHTTP_INCORRECT_HANDLE_TYPE, + // when querying WINHTTP_OPTION_STREAM_ERROR_CODE on a session handle. + return lastError != Interop.WinHttp.ERROR_INVALID_PARAMETER; + } + } +} diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/System.Net.Http.WinHttpHandler.Functional.Tests.csproj b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/System.Net.Http.WinHttpHandler.Functional.Tests.csproj index 58aa20055a8fc3..d27cac268cecd6 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/System.Net.Http.WinHttpHandler.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/System.Net.Http.WinHttpHandler.Functional.Tests.csproj @@ -14,11 +14,11 @@ - + + Link="Common\System\IO\DelegateStream.cs" /> + Link="Common\System\Net\Http\ResponseStreamTest.cs" /> + diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/TrailingHeadersTest.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/TrailingHeadersTest.cs new file mode 100644 index 00000000000000..cef52465524dcf --- /dev/null +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/TrailingHeadersTest.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http.Functional.Tests; +using System.Net.Http.Headers; +using System.Net.Test.Common; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.WinHttpHandlerFunctional.Tests +{ + public class TrailingHeadersTest : HttpClientHandlerTestBase + { + public TrailingHeadersTest(ITestOutputHelper output) : base(output) + { } + + // Build number suggested by the WinHttp team. + // It can be reduced after the backport of WINHTTP_QUERY_FLAG_TRAILERS is finished, + // and the patches are rolled out to CI machines. + public static bool OsSupportsWinHttpTrailingHeaders => Environment.OSVersion.Version >= new Version(10, 0, 19622, 0); + + public static bool TestsEnabled => OsSupportsWinHttpTrailingHeaders && PlatformDetection.SupportsAlpn; + + protected override Version UseVersion => new Version(2, 0); + + 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); + + [ConditionalFact(nameof(TestsEnabled))] + 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); + + var trailingHeaders = GetTrailingHeaders(response); + Assert.NotNull(trailingHeaders); + Assert.Equal(0, trailingHeaders.Count()); + } + } + + [ConditionalFact(nameof(TestsEnabled))] + 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); + + var trailingHeaders = GetTrailingHeaders(response); + Assert.Equal(TrailingHeaders.Count, trailingHeaders.Count()); + Assert.Contains("amazingtrailer", trailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", trailingHeaders.GetValues("Hello")); + } + } + + [ConditionalFact(nameof(TestsEnabled))] + 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. + var trailingHeaders = GetTrailingHeaders(response); + Assert.True(trailingHeaders == null || trailingHeaders.Count() == 0); + + Stream stream = await response.Content.ReadAsStreamAsync(TestAsync); + Byte[] data = new Byte[100]; + await stream.ReadAsync(data, 0, data.Length); + + // Intermediate test - haven't reached stream EOF yet. + trailingHeaders = GetTrailingHeaders(response); + Assert.True(trailingHeaders == null || trailingHeaders.Count() == 0); + + // 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) ; + + trailingHeaders = GetTrailingHeaders(response); + Assert.Equal(TrailingHeaders.Count, trailingHeaders.Count()); + Assert.Contains("amazingtrailer", trailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", trailingHeaders.GetValues("Hello")); + + // Read when already zero. Trailers shouldn't be changed. + stream.Read(data, 0, data.Length); + + trailingHeaders = GetTrailingHeaders(response); + Assert.Equal(TrailingHeaders.Count, trailingHeaders.Count()); + } + } + + [ConditionalFact(nameof(TestsEnabled))] + 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); + + var trailingHeaders = GetTrailingHeaders(response); + Assert.Equal(TrailingHeaders.Count, trailingHeaders.Count()); + Assert.Contains("amazingtrailer", trailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", trailingHeaders.GetValues("Hello")); + } + } + + [ConditionalFact(nameof(TestsEnabled))] + 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()); + + var trailingHeaders = GetTrailingHeaders(response); + Assert.Contains("amazingtrailer", trailingHeaders.GetValues("MyCoolTrailerHeader")); + Assert.Contains("World", trailingHeaders.GetValues("Hello")); + } + } + + private HttpHeaders GetTrailingHeaders(HttpResponseMessage responseMessage) + { +#if !NET48 + return responseMessage.TrailingHeaders; +#else +#pragma warning disable CS0618 // Type or member is obsolete + responseMessage.RequestMessage.Properties.TryGetValue("__ResponseTrailers", out object trailers); +#pragma warning restore CS0618 // Type or member is obsolete + return (HttpHeaders)trailers; +#endif + } + } +} diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs index 596ce8212979b0..e26f1976418d6a 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs @@ -422,6 +422,12 @@ public static bool WinHttpQueryOption( ref uint buffer, ref uint bufferSize) { + if (option == WINHTTP_OPTION_STREAM_ERROR_CODE) + { + TestControl.LastWin32Error = (int)ERROR_INVALID_PARAMETER; + return false; + } + return true; } diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/System.Net.Http.WinHttpHandler.Unit.Tests.csproj b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/System.Net.Http.WinHttpHandler.Unit.Tests.csproj index 32e7c9c9e44aa6..52b02b402fb79f 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/System.Net.Http.WinHttpHandler.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/System.Net.Http.WinHttpHandler.Unit.Tests.csproj @@ -1,9 +1,10 @@ - + $(NoWarn);0436 true ../../src/Resources/Strings.resx $(NetCoreAppCurrent)-windows + UNITTEST annotations @@ -84,6 +85,8 @@ Link="ProductionCode\WinHttpResponseStream.cs" /> +