Skip to content

Commit 3251f00

Browse files
authored
WinHttp: always read HTTP/2 streams to the end (#62870)
By it's default behavior, WinHttp stops reading the stream when Content-Length is specified, this prevents us to read the remaining trailers. Opt-in into WINHTTP_OPTION_REQUIRE_STREAM_END, so WinHttpHandler reads HTTP2 streams to the end regardless of Content-Length.
1 parent 8cdf339 commit 3251f00

File tree

4 files changed

+71
-12
lines changed

4 files changed

+71
-12
lines changed

src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ internal static partial class WinHttp
168168

169169
public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152;
170170
public const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159;
171+
public const uint WINHTTP_OPTION_REQUIRE_STREAM_END = 160;
171172

172173
public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE
173174
{

src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ private void SetSessionHandleOptions(SafeWinHttpHandle sessionHandle)
11201120
SetSessionHandleTimeoutOptions(sessionHandle);
11211121
SetDisableHttp2StreamQueue(sessionHandle);
11221122
SetTcpKeepalive(sessionHandle);
1123+
SetRequireStreamEnd(sessionHandle);
11231124
}
11241125

11251126
private unsafe void SetTcpKeepalive(SafeWinHttpHandle sessionHandle)
@@ -1145,6 +1146,27 @@ private unsafe void SetTcpKeepalive(SafeWinHttpHandle sessionHandle)
11451146
}
11461147
}
11471148

1149+
private void SetRequireStreamEnd(SafeWinHttpHandle sessionHandle)
1150+
{
1151+
if (WinHttpTrailersHelper.OsSupportsTrailers)
1152+
{
1153+
// Setting WINHTTP_OPTION_REQUIRE_STREAM_END to TRUE is needed for WinHttp to read trailing headers
1154+
// in case the response has Content-Lenght defined.
1155+
// According to the WinHttp team, the feature-detection logic in WinHttpTrailersHelper.OsSupportsTrailers
1156+
// should also indicate the support of WINHTTP_OPTION_REQUIRE_STREAM_END.
1157+
// WINHTTP_OPTION_REQUIRE_STREAM_END doesn't have effect on HTTP 1.1 requests, therefore it's safe to set it on
1158+
// the session handle so it is inhereted by all request handles.
1159+
uint optionData = 1;
1160+
if (!Interop.WinHttp.WinHttpSetOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_REQUIRE_STREAM_END, ref optionData))
1161+
{
1162+
if (NetEventSource.Log.IsEnabled())
1163+
{
1164+
NetEventSource.Info(this, "Failed to enable WINHTTP_OPTION_REQUIRE_STREAM_END error code: " + Marshal.GetLastWin32Error());
1165+
}
1166+
}
1167+
}
1168+
}
1169+
11481170
private void SetSessionHandleConnectionOptions(SafeWinHttpHandle sessionHandle)
11491171
{
11501172
uint optionData = (uint)_maxConnectionsPerServer;

src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/TrailingHeadersTest.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection()
6767
}
6868
}
6969

70-
[ConditionalFact(nameof(TestsEnabled))]
71-
public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted()
70+
[InlineData(false)]
71+
[InlineData(true)]
72+
[ConditionalTheory(nameof(TestsEnabled))]
73+
public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted(bool responseHasContentLength)
7274
{
7375
using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
7476
using (HttpClient client = CreateHttpClient())
@@ -80,7 +82,14 @@ public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted()
8082
int streamId = await connection.ReadRequestHeaderAsync();
8183

8284
// Response header.
83-
await connection.SendDefaultResponseHeadersAsync(streamId);
85+
if (responseHasContentLength)
86+
{
87+
await connection.SendResponseHeadersAsync(streamId, endStream: false, headers: new[] { new HttpHeaderData("Content-Length", DataBytes.Length.ToString()) });
88+
}
89+
else
90+
{
91+
await connection.SendDefaultResponseHeadersAsync(streamId);
92+
}
8493

8594
// Response data, missing Trailers.
8695
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));
@@ -98,8 +107,10 @@ public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted()
98107
}
99108
}
100109

101-
[ConditionalFact(nameof(TestsEnabled))]
102-
public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available()
110+
[InlineData(false)]
111+
[InlineData(true)]
112+
[ConditionalTheory(nameof(TestsEnabled))]
113+
public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available(bool responseHasContentLength)
103114
{
104115
using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
105116
using (HttpClient client = CreateHttpClient())
@@ -111,7 +122,14 @@ public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Availab
111122
int streamId = await connection.ReadRequestHeaderAsync();
112123

113124
// Response header.
114-
await connection.SendDefaultResponseHeadersAsync(streamId);
125+
if (responseHasContentLength)
126+
{
127+
await connection.SendResponseHeadersAsync(streamId, endStream: false, headers: new[] { new HttpHeaderData("Content-Length", DataBytes.Length.ToString()) });
128+
}
129+
else
130+
{
131+
await connection.SendDefaultResponseHeadersAsync(streamId);
132+
}
115133

116134
// Response data, missing Trailers.
117135
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));

src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -837,8 +837,10 @@ public async Task Http2GetAsync_NoTrailingHeaders_EmptyCollection()
837837
}
838838
}
839839

840-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
841-
public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted()
840+
[InlineData(false)]
841+
[InlineData(true)]
842+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
843+
public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted(bool responseHasContentLength)
842844
{
843845
using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
844846
using (HttpClient client = CreateHttpClient())
@@ -850,7 +852,14 @@ public async Task Http2GetAsync_MissingTrailer_TrailingHeadersAccepted()
850852
int streamId = await connection.ReadRequestHeaderAsync();
851853

852854
// Response header.
853-
await connection.SendDefaultResponseHeadersAsync(streamId);
855+
if (responseHasContentLength)
856+
{
857+
await connection.SendResponseHeadersAsync(streamId, endStream: false, headers: new[] { new HttpHeaderData("Content-Length", DataBytes.Length.ToString()) });
858+
}
859+
else
860+
{
861+
await connection.SendDefaultResponseHeadersAsync(streamId);
862+
}
854863

855864
// Response data, missing Trailers.
856865
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));
@@ -888,8 +897,10 @@ public async Task Http2GetAsync_TrailerHeaders_TrailingPseudoHeadersThrow()
888897
}
889898
}
890899

891-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
892-
public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available()
900+
[InlineData(false)]
901+
[InlineData(true)]
902+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
903+
public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Available(bool responseHasContentLength)
893904
{
894905
using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
895906
using (HttpClient client = CreateHttpClient())
@@ -901,7 +912,14 @@ public async Task Http2GetAsyncResponseHeadersReadOption_TrailingHeaders_Availab
901912
int streamId = await connection.ReadRequestHeaderAsync();
902913

903914
// Response header.
904-
await connection.SendDefaultResponseHeadersAsync(streamId);
915+
if (responseHasContentLength)
916+
{
917+
await connection.SendResponseHeadersAsync(streamId, endStream: false, headers: new[] { new HttpHeaderData("Content-Length", DataBytes.Length.ToString()) });
918+
}
919+
else
920+
{
921+
await connection.SendDefaultResponseHeadersAsync(streamId);
922+
}
905923

906924
// Response data, missing Trailers.
907925
await connection.WriteFrameAsync(MakeDataFrame(streamId, DataBytes));

0 commit comments

Comments
 (0)