Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit a273be6

Browse files
authored
Fix fragment handling in HttpClient (#27360)
* Fix fragment handling in HttpClient SocketsHttpHandler isn't sending fragments, nor is it properly inheriting the fragment from the original request URI into the redirect location URI when the original URI had one and the redirect URI did not, even though RFC 7231 says it must. This commit fixes that for SocketsHttpHandler. WinHttpHandler also isn't handling this inheritance according to the RFC. It appears that the logic for WinHttpHandler would actually need to be changed in WINHTTP itself, or else WinHttpHandler would need to be changed to do the redirects itself. Neither CurlHandler or NetFxHandler send fragments at all. This commit also fixes the test to correctly compare the expected and actual Uris... apparently Uri equality doesn't factor in fragments, so they're first converted to strings. It also updates the test to also validate that the server received the URI with the fragment included. * Disable the fragments test on WinHttpHandler Apparently it sometimes doesn't do the "right thing" even in other cases.
1 parent 3de3cd7 commit a273be6

File tree

3 files changed

+61
-11
lines changed

3 files changed

+61
-11
lines changed

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
278278
await WriteAsciiStringAsync(request.RequestUri.IdnHost).ConfigureAwait(false);
279279
}
280280

281-
await WriteStringAsync(request.RequestUri.PathAndQuery).ConfigureAwait(false);
281+
await WriteStringAsync(request.RequestUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped)).ConfigureAwait(false);
282282

283283
// Fall back to 1.1 for all versions other than 1.0
284284
Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,30 @@ private Uri GetUriForRedirect(Uri requestUri, HttpResponseMessage response)
8181
return null;
8282
}
8383

84+
// Ensure the redirect location is an absolute URI.
8485
if (!location.IsAbsoluteUri)
8586
{
8687
location = new Uri(requestUri, location);
8788
}
8889

90+
// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
91+
// fragment should inherit the fragment from the original URI.
92+
string requestFragment = requestUri.Fragment;
93+
if (!string.IsNullOrEmpty(requestFragment))
94+
{
95+
string redirectFragment = location.Fragment;
96+
if (string.IsNullOrEmpty(redirectFragment))
97+
{
98+
location = new UriBuilder(location) { Fragment = requestFragment }.Uri;
99+
}
100+
}
101+
89102
// Disallow automatic redirection from secure to non-secure schemes
90103
if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme) && !HttpUtilities.IsSupportedSecureScheme(location.Scheme))
91104
{
92105
if (NetEventSource.IsEnabled)
93106
{
94-
NetEventSource.Info(this, $"Insecure https to http redirect from {requestUri} to {location} blocked.");
107+
NetEventSource.Info(this, $"Insecure https to http redirect from '{requestUri}' to '{location}' blocked.");
95108
}
96109

97110
return null;

src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -912,40 +912,77 @@ await TestHelper.WhenAllCompletedOrAnyFailed(
912912
}
913913

914914
[OuterLoop] // TODO: Issue #11345
915-
[ConditionalTheory(nameof(IsNotWindows7))] // Skip test on Win7 since WinHTTP has bugs w/ fragments.
915+
[Theory]
916916
[InlineData("#origFragment", "", "#origFragment", false)]
917917
[InlineData("#origFragment", "", "#origFragment", true)]
918918
[InlineData("", "#redirFragment", "#redirFragment", false)]
919919
[InlineData("", "#redirFragment", "#redirFragment", true)]
920920
[InlineData("#origFragment", "#redirFragment", "#redirFragment", false)]
921921
[InlineData("#origFragment", "#redirFragment", "#redirFragment", true)]
922-
[ActiveIssue(27217)]
923922
public async Task GetAsync_AllowAutoRedirectTrue_RetainsOriginalFragmentIfAppropriate(
924923
string origFragment, string redirFragment, string expectedFragment, bool useRelativeRedirect)
925924
{
925+
if (IsCurlHandler)
926+
{
927+
// Starting with libcurl 7.20, "fragment part of URLs are no longer sent to the server".
928+
// So CurlHandler doesn't send fragments.
929+
return;
930+
}
931+
932+
if (IsNetfxHandler)
933+
{
934+
// Similarly, netfx doesn't send fragments at all.
935+
return;
936+
}
937+
938+
if (IsWinHttpHandler)
939+
{
940+
// According to https://tools.ietf.org/html/rfc7231#section-7.1.2,
941+
// "If the Location value provided in a 3xx (Redirection) response does
942+
// not have a fragment component, a user agent MUST process the
943+
// redirection as if the value inherits the fragment component of the
944+
// URI reference used to generate the request target(i.e., the
945+
// redirection inherits the original reference's fragment, if any)."
946+
// WINHTTP is not doing this, and thus neither is WinHttpHandler.
947+
// It also sometimes doesn't include the fragments for redirects
948+
// even in other cases.
949+
return;
950+
}
951+
926952
HttpClientHandler handler = CreateHttpClientHandler();
927953
handler.AllowAutoRedirect = true;
928954
using (var client = new HttpClient(handler))
929955
{
930956
await LoopbackServer.CreateServerAsync(async (origServer, origUrl) =>
931957
{
932-
origUrl = new Uri(origUrl.ToString() + origFragment);
933-
Uri redirectUrl = useRelativeRedirect ?
934-
new Uri(origUrl.PathAndQuery + redirFragment, UriKind.Relative) :
935-
new Uri(origUrl.ToString() + redirFragment);
936-
Uri expectedUrl = new Uri(origUrl.ToString() + expectedFragment);
958+
origUrl = new UriBuilder(origUrl) { Fragment = origFragment }.Uri;
959+
Uri redirectUrl = new UriBuilder(origUrl) { Fragment = redirFragment }.Uri;
960+
if (useRelativeRedirect)
961+
{
962+
redirectUrl = new Uri(redirectUrl.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.SafeUnescaped), UriKind.Relative);
963+
}
964+
Uri expectedUrl = new UriBuilder(origUrl) { Fragment = expectedFragment }.Uri;
937965

966+
// Make and receive the first request that'll be redirected.
938967
Task<HttpResponseMessage> getResponse = client.GetAsync(origUrl);
939968
Task firstRequest = origServer.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Found, $"Location: {redirectUrl}\r\n");
940969
Assert.Equal(firstRequest, await Task.WhenAny(firstRequest, getResponse));
941970

942-
Task secondRequest = origServer.AcceptConnectionSendResponseAndCloseAsync();
971+
// Receive the second request.
972+
Task<List<string>> secondRequest = origServer.AcceptConnectionSendResponseAndCloseAsync();
943973
await TestHelper.WhenAllCompletedOrAnyFailed(secondRequest, getResponse);
944974

975+
// Make sure the server received the second request for the right Uri.
976+
Assert.NotEmpty(secondRequest.Result);
977+
string[] statusLineParts = secondRequest.Result[0].Split(' ');
978+
Assert.Equal(3, statusLineParts.Length);
979+
Assert.Equal(expectedUrl.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.SafeUnescaped), statusLineParts[1]);
980+
981+
// Make sure the request message was updated with the correct redirected location.
945982
using (HttpResponseMessage response = await getResponse)
946983
{
947984
Assert.Equal(200, (int)response.StatusCode);
948-
Assert.Equal(expectedUrl, response.RequestMessage.RequestUri);
985+
Assert.Equal(expectedUrl.ToString(), response.RequestMessage.RequestUri.ToString());
949986
}
950987
});
951988
}

0 commit comments

Comments
 (0)