Skip to content

Commit

Permalink
Remove two async state machines for typical HTTP/1.1 request path (#5…
Browse files Browse the repository at this point in the history
…8092)

* Remove two async state machines for typical HTTP/1.1 request path

* Remove unused doRequestAuth parameter for HTTP/2 and HTTP/3

* Inline HTTP/1.x handling into SendWithRetryAsync

* Inline HTTP/2 as well

* Add back assert
  • Loading branch information
stephentoub authored Aug 27, 2021
1 parent 40575af commit 999e412
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ private static async ValueTask<bool> TrySetDigestAuthToken(HttpRequestMessage re
private static ValueTask<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
{
return isProxyAuth ?
pool.SendWithRetryAsync(request, async, doRequestAuth, cancellationToken) :
pool.SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken) :
pool.SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,12 @@ private object SyncObj
// we will remove it from the list of available connections, if it is present there.
// If not, then it must be unavailable at the moment; we will detect this and ensure it is not added back to the available pool.

private static HttpRequestException GetVersionException(HttpRequestMessage request, int desiredVersion)
[DoesNotReturn]
private static void ThrowGetVersionException(HttpRequestMessage request, int desiredVersion)
{
Debug.Assert(desiredVersion == 2 || desiredVersion == 3);

return new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion));
throw new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion));
}

private bool CheckExpirationOnGet(HttpConnectionBase connection)
Expand Down Expand Up @@ -887,144 +888,118 @@ private async ValueTask<Http3Connection> GetHttp3ConnectionAsync(HttpRequestMess
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private async ValueTask<HttpResponseMessage?> TrySendUsingHttp3Async(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
private async ValueTask<HttpResponseMessage?> TrySendUsingHttp3Async(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
if (_http3Enabled && (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)))
// Loop in case we get a 421 and need to send the request to a different authority.
while (true)
{
// Loop in case we get a 421 and need to send the request to a different authority.
while (true)
{
HttpAuthority? authority = _http3Authority;

// If H3 is explicitly requested, assume prenegotiated H3.
if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
authority = authority ?? _originAuthority;
}

if (authority == null)
{
break;
}

if (IsAltSvcBlocked(authority))
{
throw GetVersionException(request, 3);
}

Http3Connection connection = await GetHttp3ConnectionAsync(request, authority, cancellationToken).ConfigureAwait(false);
HttpResponseMessage response = await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false);

// If an Alt-Svc authority returns 421, it means it can't actually handle the request.
// An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug.
// In this case, we blocklist the authority and retry the request at the origin.
if (response.StatusCode == HttpStatusCode.MisdirectedRequest && connection.Authority != _originAuthority)
{
response.Dispose();
BlocklistAuthority(connection.Authority);
continue;
}
HttpAuthority? authority = _http3Authority;

return response;
// If H3 is explicitly requested, assume prenegotiated H3.
if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
authority ??= _originAuthority;
}
}

return null;
}

// Returns null if HTTP2 cannot be used.
private async ValueTask<HttpResponseMessage?> TrySendUsingHttp2Async(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
// Send using HTTP/2 if we can.
if (_http2Enabled && (request.Version.Major >= 2 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)) &&
// If the connection is not secured and downgrade is possible, prefer HTTP/1.1.
(request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower || IsSecure))
{
Http2Connection? connection = await GetHttp2ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);
if (connection is null)
if (authority == null)
{
Debug.Assert(!_http2Enabled);
return null;
}

return await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false);
}

return null;
}

private async ValueTask<HttpResponseMessage> SendUsingHttp11Async(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
HttpConnection connection = await GetHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);

// In case we are doing Windows (i.e. connection-based) auth, we need to ensure that we hold on to this specific connection while auth is underway.
connection.Acquire();
try
{
return await SendWithNtConnectionAuthAsync(connection, request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
}
finally
{
connection.Release();
}
}
if (IsAltSvcBlocked(authority))
{
ThrowGetVersionException(request, 3);
}

private async ValueTask<HttpResponseMessage> DetermineVersionAndSendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
HttpResponseMessage? response;
Http3Connection connection = await GetHttp3ConnectionAsync(request, authority, cancellationToken).ConfigureAwait(false);
HttpResponseMessage response = await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false);

if (IsHttp3Supported())
{
response = await TrySendUsingHttp3Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
if (response is not null)
// If an Alt-Svc authority returns 421, it means it can't actually handle the request.
// An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug.
// In this case, we blocklist the authority and retry the request at the origin.
if (response.StatusCode == HttpStatusCode.MisdirectedRequest && connection.Authority != _originAuthority)
{
return response;
response.Dispose();
BlocklistAuthority(connection.Authority);
continue;
}
}

// We cannot use HTTP/3. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
throw GetVersionException(request, 3);
}

response = await TrySendUsingHttp2Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
if (response is not null)
{
return response;
}

// We cannot use HTTP/2. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
throw GetVersionException(request, 2);
}

return await SendUsingHttp11Async(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
}

private async ValueTask<HttpResponseMessage> SendAndProcessAltSvcAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
/// <summary>Check for the Alt-Svc header, to upgrade to HTTP/3.</summary>
private void ProcessAltSvc(HttpResponseMessage response)
{
HttpResponseMessage response = await DetermineVersionAndSendAsync(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);

// Check for the Alt-Svc header, to upgrade to HTTP/3.
if (_altSvcEnabled && response.Headers.TryGetValues(KnownHeaders.AltSvc.Descriptor, out IEnumerable<string>? altSvcHeaderValues))
{
HandleAltSvc(altSvcHeaderValues, response.Headers.Age);
}

return response;
}

public async ValueTask<HttpResponseMessage> SendWithRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
// Loop on connection failures (or other problems like version downgrade) and retry if possible.
int retryCount = 0;
while (true)
{
// Loop on connection failures (or other problems like version downgrade) and retry if possible.
try
{
return await SendAndProcessAltSvcAsync(request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
HttpResponseMessage? response = null;

// Use HTTP/3 if possible.
if (IsHttp3Supported() && // guard to enable trimming HTTP/3 support
_http3Enabled &&
(request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)))
{
response = await TrySendUsingHttp3Async(request, async, cancellationToken).ConfigureAwait(false);
}

if (response is null)
{
// We could not use HTTP/3. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
ThrowGetVersionException(request, 3);
}

// Use HTTP/2 if possible.
if (_http2Enabled &&
(request.Version.Major >= 2 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)) &&
(request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower || IsSecure)) // prefer HTTP/1.1 if connection is not secured and downgrade is possible
{
Http2Connection? connection = await GetHttp2ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);
Debug.Assert(connection is not null || !_http2Enabled);
if (connection is not null)
{
response = await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false);
}
}

if (response is null)
{
// We could not use HTTP/2. Do not continue if downgrade is not allowed.
if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
{
ThrowGetVersionException(request, 2);
}

// Use HTTP/1.x.
HttpConnection connection = await GetHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);
connection.Acquire(); // In case we are doing Windows (i.e. connection-based) auth, we need to ensure that we hold on to this specific connection while auth is underway.
try
{
response = await SendWithNtConnectionAuthAsync(connection, request, async, doRequestAuth, cancellationToken).ConfigureAwait(false);
}
finally
{
connection.Release();
}
}
}

ProcessAltSvc(response);
return response;
}
catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnConnectionFailure)
{
Expand Down Expand Up @@ -1332,7 +1307,7 @@ public ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage
return AuthenticationHelper.SendWithProxyAuthAsync(request, _proxyUri!, async, ProxyCredentials, doRequestAuth, this, cancellationToken);
}

return SendWithRetryAsync(request, async, doRequestAuth, cancellationToken);
return SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken);
}

public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
Expand Down

0 comments on commit 999e412

Please sign in to comment.