Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WinHttpHandler: Read HTTP/2 trailing headers #46602

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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;
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Common/src/System/Net/SecurityProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IncludeDllSafeSearchPathAttribute>true</IncludeDllSafeSearchPathAttribute>
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;net461-windows</TargetFrameworks>
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;netstandard2.1-windows;netstandard2.1;net461-windows</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF, isTrailingHeaders: false);
char[] buffer = ArrayPool<char>.Shared.Rent(bufferLength);
try
{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -93,7 +93,7 @@ public static HttpResponseMessage CreateResponseMessage(
response.RequestMessage = request;

// Parse raw response headers and place them into response message.
ParseResponseHeaders(requestHandle, response, buffer, stripEncodingHeaders);
ParseResponseHeaders(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF, response, buffer, stripEncodingHeaders, isTrailers: false);

if (response.RequestMessage.Method != HttpMethod.Head)
{
Expand Down Expand Up @@ -223,7 +223,7 @@ private static unsafe int GetResponseHeader(SafeWinHttpHandle requestHandle, uin
/// <summary>
/// Returns the size of the char array buffer.
/// </summary>
private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel)
public static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel, bool isTrailingHeaders)
{
char* buffer = null;
int bufferLength = 0;
Expand All @@ -233,11 +233,21 @@ private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle re
{
int lastError = Marshal.GetLastWin32Error();

Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);
if (!isTrailingHeaders)
{
Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);

if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER)
if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER)
{
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
}
}
else
{
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
if (!(lastError == Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER || lastError == Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND))
Copy link
Member

@antonfirsov antonfirsov Jan 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ERROR_WINHTTP_HEADER_NOT_FOUND here to fail silently when WINHTTP_QUERY_FLAG_TRAILERS is unsupported?

Copy link
Member Author

@JamesNK JamesNK Jan 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is to handle HTTP responses that don't have any trailers. A response will always have headers, but trailers are optional.

I think it is better to have an OS version check to see whether trailers are supported. That would determine whether the call is made to begin with.

How are Windows version checks does in this library? (or other runtime libraries)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this library these checks are unprecedental, setting unsupported options usually leads to WinHttpException. I've seen checks on Environment.OSVersion in other BCL libs though .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if WinHttp decides to backport the feature to earlier OS versions? I would feel more comfortable with established pattern of relying on WinHttpException. Is that possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current design, this feature is implicit (filling the collection whenever possible), so throwing is not an option.

However I think that checking the error code after the first WinHttpQueryHeaders shall be a sufficient (and actually better) solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to consider here --- we should be very careful about having an API that, depending on OS version:

a) works as expected.
b) appears to work, but has an empty collection with zero notice to user.

This will be very fragile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scalablecory I tried to address this in my proposal in #44778 (comment). I suggest to discuss the API under the issue, comments and ideas are more than welcome there.

Copy link
Member Author

@JamesNK JamesNK Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed OS support detection for trailers with WinHttp team and this is their suggestion:

How about this for detection.

You try to query WINHTTP_OPTION_STREAM_ERROR_CODE from you session handle.

If it fails with ERROR_INVALID_PARAMETER, there is no support for trailers.

Otherwise, it should fail ERROR_WINHTTP_INCORRECT_HANDLE_TYPE, as the option cannot be queried from session handles.

They prefered doing that over hardcoding an OS version number because WinHttp trailer support could be backported to more Windows versions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You try to query WINHTTP_OPTION_STREAM_ERROR_CODE from you session handle.

I guess these are distinct features which have been introduced in the same release right? What if they get misaligned because trailing headers get backported to an earlier version?

Copy link
Member Author

@JamesNK JamesNK Jan 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume so. This is the WinHttp team's suggestion. They recommended against checking OS version.

{
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
}
}
}

Expand Down Expand Up @@ -286,47 +296,69 @@ private static string GetReasonPhrase(HttpStatusCode statusCode, char[] buffer,
new string(buffer, 0, bufferLength);
}

private static void ParseResponseHeaders(
private class HttpResponseTrailers : HttpHeaders
{
}

public static void ParseResponseHeaders(
SafeWinHttpHandle requestHandle,
uint infoLevel,
HttpResponseMessage response,
char[] buffer,
bool stripEncodingHeaders)
bool stripEncodingHeaders,
bool isTrailers)
{
HttpResponseHeaders responseHeaders = response.Headers;
HttpContentHeaders contentHeaders = response.Content.Headers;
#if NETSTANDARD2_1
HttpResponseHeaders responseTrailers = response.TrailingHeaders;
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
#else
HttpResponseTrailers responseTrailers = new HttpResponseTrailers();
response.RequestMessage.Properties["__ResponseTrailers"] = responseTrailers;
#endif

int bufferLength = GetResponseHeader(
requestHandle,
Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF,
infoLevel,
buffer);

var reader = new WinHttpResponseHeaderReader(buffer, 0, bufferLength);

// Skip the first line which contains status code, etc. information that we already parsed.
reader.ReadLine();
if (!isTrailers)
{
// Skip the first line which contains status code, etc. information that we already parsed.
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))
{
if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue))
if (!isTrailers)
{
if (stripEncodingHeaders)
if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue))
{
// Remove Content-Length and Content-Encoding headers if we are
// decompressing the response stream in the handler (due to
// WINHTTP not supporting it in a particular downlevel platform).
// This matches the behavior of WINHTTP when it does decompression itself.
if (string.Equals(HttpKnownHeaderNames.ContentLength, headerName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(HttpKnownHeaderNames.ContentEncoding, headerName, StringComparison.OrdinalIgnoreCase))
if (stripEncodingHeaders)
{
continue;
// Remove Content-Length and Content-Encoding headers if we are
// decompressing the response stream in the handler (due to
// WINHTTP not supporting it in a particular downlevel platform).
// This matches the behavior of WINHTTP when it does decompression itself.
if (string.Equals(HttpKnownHeaderNames.ContentLength, headerName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(HttpKnownHeaderNames.ContentEncoding, headerName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
}

contentHeaders.TryAddWithoutValidation(headerName, headerValue);
contentHeaders.TryAddWithoutValidation(headerName, headerValue);
}
}
else
{
responseTrailers.TryAddWithoutValidation(headerName, headerValue);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any observable advantages from using this overload of Stream.WriteAsync explicitly? I would expect that the performance of (most) underlying implementations is identical. Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an error when not calling it on netstandard2.1:

error CA1835: Change the 'WriteAsync' method call to use the 'Stream.WriteAsync(ReadOnlyMemory, CancellationToken)' overload

#else
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
#endif
}
}
finally
Expand Down Expand Up @@ -240,7 +249,14 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
}
}

return await _state.LifecycleAwaitable;
int bytesRead = await _state.LifecycleAwaitable;

if (bytesRead == 0)
{
ReadResponseTrailers();
}

return bytesRead;
}
finally
{
Expand All @@ -249,6 +265,37 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
}
}

private void ReadResponseTrailers()
{
// Only load response trailers if:
// 1. HTTP/2 or later
// 2. Response trailers not already loaded
if (_readTrailingHeaders || _responseMessage.Version < WinHttpHandler.HttpVersion20)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OS version check here?

{
return;
}

_readTrailingHeaders = true;

var bufferLength = WinHttpResponseParser.GetResponseHeaderCharBufferLength(
_requestHandle,
Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS,
isTrailingHeaders: true);

if (bufferLength != 0)
{
char[] trailersBuffer = ArrayPool<char>.Shared.Rent(bufferLength);
try
{
WinHttpResponseParser.ParseResponseHeaders(_requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS, _responseMessage, trailersBuffer, stripEncodingHeaders: false, isTrailers: true);
}
finally
{
ArrayPool<char>.Shared.Return(trailersBuffer);
}
}
}

public override int Read(byte[] buffer, int offset, int count)
{
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<Compile Include="BaseCertificateTest.cs" />
<Compile Include="ServerCertificateTest.cs" />
<Compile Include="WinHttpHandlerTest.cs" />
<Compile Include="XunitTestAssemblyAtrributes.cs" />
<Compile Include="XunitTestAssemblyAtrributes.cs" />
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
<Compile Include="$(CommonPath)\System\Net\Http\HttpHandlerDefaults.cs"
Link="Common\System\Net\Http\HttpHandlerDefaults.cs" />
<Compile Include="$(CommonTestPath)System\IO\DelegateStream.cs"
Link="Common\System\IO\DelegateStream.cs" />
Link="Common\System\IO\DelegateStream.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs"
Link="Common\System\Net\Configuration.Certificates.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Security.cs"
Expand Down Expand Up @@ -126,7 +126,7 @@
<Compile Include="$(CommonTestPath)System\Net\Http\RepeatedFlushContent.cs"
Link="Common\System\Net\Http\RepeatedFlushContent.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\ResponseStreamTest.cs"
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\SchSendAuxRecordHttpTest.cs"
Link="Common\System\Net\Http\SchSendAuxRecordHttpTest.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\SyncBlockingContent.cs"
Expand All @@ -141,6 +141,7 @@
<Compile Include="WinHttpClientHandler.cs" />
<Compile Include="PlatformHandlerTest.cs" />
<Compile Include="ClientCertificateTest.cs" />
<Compile Include="TrailingHeadersTest.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
Expand Down
Loading