File: FallbackToHttpMessageHandler.cs
Web Access
Project: ..\..\..\src\Containers\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj (Microsoft.NET.Build.Containers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using Microsoft.Extensions.Logging;
using Microsoft.NET.Build.Containers.Resources;
 
namespace Microsoft.NET.Build.Containers;
 
/// <summary>
/// A delegating handler that falls back from https to http for a specific hostname.
/// </summary>
internal sealed partial class FallbackToHttpMessageHandler : DelegatingHandler
{
    private readonly string _registryName;
    private readonly string _host;
    private readonly int _port;
    private readonly ILogger _logger;
    private bool _fallbackToHttp;
 
    public FallbackToHttpMessageHandler(string registryName, string host, int port, HttpMessageHandler innerHandler, ILogger logger)
        : base(innerHandler)
    {
        _registryName = registryName;
        _host = host;
        _port = port;
        _logger = logger;
    }
 
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri is null)
        {
            throw new ArgumentException(Resource.GetString(nameof(Strings.NoRequestUriSpecified)), nameof(request));
        }
 
        bool canFallback = request.RequestUri.Host == _host && request.RequestUri.Port == _port && request.RequestUri.Scheme == "https";
        do
        {
            try
            {
                if (canFallback && _fallbackToHttp)
                {
                    FallbackToHttp(_registryName, request);
                    canFallback = false;
                }
 
                return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            }
            catch (HttpRequestException re) when (canFallback && ShouldAttemptFallbackToHttp(re))
            {
                string uri = request.RequestUri.ToString();
                try
                {
                    // Try falling back.
                    _logger.LogTrace("Attempt to fall back to http for {uri}.", uri);
                    FallbackToHttp(_registryName, request);
                    HttpResponseMessage response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
 
                    // Fall back was successful. Use http for all new requests.
                    _logger.LogTrace("Fall back to http for {uri} was successful.", uri);
                    _fallbackToHttp = true;
 
                    return response;
                }
                catch (Exception ex)
                {
                    _logger.LogInformation(ex, "Fall back to http for {uri} failed with message \"{message}\".", uri, ex.Message);
                }
 
                // Falling back didn't work, throw original exception.
                throw;
            }
        } while (true);
    }
 
    internal static bool ShouldAttemptFallbackToHttp(HttpRequestException exception)
    {
        return exception.HttpRequestError == HttpRequestError.SecureConnectionError;
    }
 
    private static bool RegistryNameContainsPort(string registryName)
    {
        // use `container` scheme which does not have a default port.
        return new Uri($"container://{registryName}").Port != -1;
    }
 
    private static void FallbackToHttp(string registryName, HttpRequestMessage request)
    {
        var uriBuilder = new UriBuilder(request.RequestUri!);
        uriBuilder.Scheme = "http";
        if (RegistryNameContainsPort(registryName) == false)
        {
            // registeryName does not contains port number, so reset the port number to -1, otherwise it will be https default port 443
            uriBuilder.Port = -1;
        }
 
        request.RequestUri = uriBuilder.Uri;
    }
}