/// When an Alt-Svc authority fails due to 421 Misdirected Request, it is placed in the blocklist to be ignored
/// for <see cref="AltSvcBlocklistTimeoutInMilliseconds"/> milliseconds. Initialized on first use.
/// </summary>
- private volatile HashSet<HttpAuthority>? _altSvcBlocklist;
+ private volatile Dictionary<HttpAuthority, Exception?>? _altSvcBlocklist;
private CancellationTokenSource? _altSvcBlocklistTimerCancellation;
private volatile bool _altSvcEnabled = true;
// 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.
[DoesNotReturn]
- private static void ThrowGetVersionException(HttpRequestMessage request, int desiredVersion)
+ private static void ThrowGetVersionException(HttpRequestMessage request, int desiredVersion, Exception? inner = null)
{
Debug.Assert(desiredVersion == 2 || desiredVersion == 3);
- throw 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), inner);
}
private bool CheckExpirationOnGet(HttpConnectionBase connection)
if (NetEventSource.Log.IsEnabled()) Trace($"QUIC connection failed: {e}");
// Disables HTTP/3 until server announces it can handle it via Alt-Svc.
- BlocklistAuthority(authority);
+ BlocklistAuthority(authority, e);
throw;
}
return null;
}
- if (IsAltSvcBlocked(authority))
+ Exception? reasonException;
+ if (IsAltSvcBlocked(authority, out reasonException))
{
- ThrowGetVersionException(request, 3);
+ ThrowGetVersionException(request, 3, reasonException);
}
long queueStartingTimestamp = HttpTelemetry.Log.IsEnabled() ? Stopwatch.GetTimestamp() : 0;
if (nextAuthority == null && value != null && value.AlpnProtocolName == "h3")
{
var authority = new HttpAuthority(value.Host ?? _originAuthority!.IdnHost, value.Port);
-
- if (IsAltSvcBlocked(authority))
+ if (IsAltSvcBlocked(authority, out _))
{
// Skip authorities in our blocklist.
continue;
/// <summary>
/// Checks whether the given <paramref name="authority"/> is on the currext Alt-Svc blocklist.
+ /// If it is, then it places the cause in the <paramref name="reasonException"/>
/// </summary>
/// <seealso cref="BlocklistAuthority" />
- private bool IsAltSvcBlocked(HttpAuthority authority)
+ private bool IsAltSvcBlocked(HttpAuthority authority, out Exception? reasonException)
{
if (_altSvcBlocklist != null)
{
lock (_altSvcBlocklist)
{
- return _altSvcBlocklist.Contains(authority);
+ return _altSvcBlocklist.TryGetValue(authority, out reasonException);
}
}
+ reasonException = null;
return false;
}
+
/// <summary>
/// Blocklists an authority and resets the current authority back to origin.
/// If the number of blocklisted authorities exceeds <see cref="MaxAltSvcIgnoreListSize"/>,
/// For now, the spec states alternate authorities should be able to handle ALL requests, so this
/// is treated as an exceptional error by immediately blocklisting the authority.
/// </remarks>
- internal void BlocklistAuthority(HttpAuthority badAuthority)
+ internal void BlocklistAuthority(HttpAuthority badAuthority, Exception? exception = null)
{
Debug.Assert(badAuthority != null);
- HashSet<HttpAuthority>? altSvcBlocklist = _altSvcBlocklist;
+ Dictionary<HttpAuthority, Exception?>? altSvcBlocklist = _altSvcBlocklist;
if (altSvcBlocklist == null)
{
altSvcBlocklist = _altSvcBlocklist;
if (altSvcBlocklist == null)
{
- altSvcBlocklist = new HashSet<HttpAuthority>();
+ altSvcBlocklist = new Dictionary<HttpAuthority, Exception?>();
_altSvcBlocklistTimerCancellation = new CancellationTokenSource();
_altSvcBlocklist = altSvcBlocklist;
}
lock (altSvcBlocklist)
{
- added = altSvcBlocklist.Add(badAuthority);
+ added = altSvcBlocklist.TryAdd(badAuthority, exception);
if (added && altSvcBlocklist.Count >= MaxAltSvcIgnoreListSize && _altSvcEnabled)
{
};
HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10)));
+
Assert.IsType<AuthenticationException>(ex.InnerException);
clientDone.Release();
await new[] { clientTask, serverTask }.WhenAllOrAnyFailed(200_000);
}
+ [Fact]
+ public async Task Alpn_NonH3_FailureEstablishConnection()
+ {
+ var options = new Http3Options() { Alpn = "h3-29" }; // anything other than "h3"
+ using Http3LoopbackServer server = CreateHttp3LoopbackServer(options);
+
+ using HttpClient client = CreateHttpClient();
+ using HttpRequestMessage request = new()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = server.Address,
+ Version = HttpVersion30,
+ VersionPolicy = HttpVersionPolicy.RequestVersionExact
+ };
+ using HttpRequestMessage request2 = new()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = server.Address,
+ Version = HttpVersion30,
+ VersionPolicy = HttpVersionPolicy.RequestVersionExact
+ };
+ HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10)));
+
+ // second request should throw the same exception as inner as the first one
+ HttpRequestException ex2 = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request2).WaitAsync(TimeSpan.FromSeconds(10)));
+
+ Assert.Equal(ex, ex2.InnerException);
+ }
+
+
private SslApplicationProtocol ExtractMsQuicNegotiatedAlpn(Http3LoopbackConnection loopbackConnection)
{
FieldInfo quicConnectionField = loopbackConnection.GetType().GetField("_connection", BindingFlags.Instance | BindingFlags.NonPublic);