Search code examples
c#.net-coredotnet-httpclientpollyretry-logic

Polly log all requests with URL, Headers, Content and Response


I have a project that calls many rest APIs from other projects, and I have some problems to identify not only errors from those APIs but also correct responses but the information is not correct on the other system. I did this part, but it only log the retry and I need to log the success also.

services.AddHttpClient<IClient, Client>("AuthClient", x =>
    {
        x.BaseAddress = new Uri(urlAn);
    }).AddPolicyHandler((services, request) => 
    HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(
    new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    },
    onRetry: (outcome, timespan, retryAttempt, context) =>
    {
        services.GetService<ILogger>()
            .LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
    }));

Solution

  • The onRetry method is executed only if there was an error, which is handled by the policy. TheHandleTransientHttpError triggers the policy

    • either when there was a HttpRequestException
    • or when the response code is either 408 or 5xxx.

    To inject a logic which should be executed in every situation you need to make use of a custom DelegatingHandler. This extension point let's you inject custom code into the HttpClient's pipeline (1).

    Here is a naive implementation of a LoggerHandler:

    class LoggerHandler: DelegatingHandler
    {
        private readonly ILogger<LoggerHandler> _logger;
    
        public LoggerHandler(ILogger<LoggerHandler> logger)
        {
            _logger = logger;
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var response = await base.SendAsync(request, cancellationToken);
                _logger.LogInformation(response.StatusCode.ToString());
                return response;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Request has failed after several retries");
                throw;
            }
        }
    }
    
    • As you can see we have injected the logger into the handler
      • In case of flawless downstream request we log some fact on Information level
      • In case of faulty downstream request we log the exception on Error level

    Now, let's wire up all the things:

    var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(
        new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        });
    
    services.AddHttpClient<IClient, Client>("AuthClient", x => { x.BaseAddress = new Uri(urlAn); })
        .AddPolicyHandler(retryPolicy)
        .AddHttpMessageHandler<LoggerHandler>();
    
    

    Please bear in mind the registration order matters.

    • Please check this SO topic for further details.

    There are several tiny things that could be improved as well:

    • You don't have to specify the name for the HttpClient, because you are using Typed-Client.
      • services.AddHttpClient<IClient, Client>(x => ...)
    • I highly recommend to use better naming than IClient and Client. Image a situation that you need to add one more client to your app. How would name that? AuthClient might be a better name:
      • services.AddHttpClient<IAuthClient, AuthClient>(x => ...)
    • I would also encourage you to use jitter to add randomness for your retry sleep duration. If all of the clients try to perform the retries against the overloaded server then it won't help the downstream.
      • Try to distribute the retries with jitter.
    • I also suggest to read this article about Retry, Timeout and DelegatingHandler.