pampua84
pampua84

Reputation: 894

Fallback.AsyncFallbackEngine Exception

I have an application in .NET Framework 4.8 (WCF) that makes http calls, also using Polly for retries and fallback management, but sometimes a System.NullReferenceException is raised, but I can't figure out where be the problem. This is the code:

private static async Task<bool> CallApi<TRequest>(TRequest request, string requestUri, Func<string, StringContent, Task<HttpResponseMessage>> func)
{
    var jsonRequest = JsonConvert.SerializeObject(request);

    var fallBackPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
        .FallbackAsync(new HttpResponseMessage(HttpStatusCode.SeeOther)
        {
            Content = new StringContent($"Exception has occurred in migration call. RequestUri: {requestUri}")
        }, 
        result => 
        {
            LogEventService.Logger.Error(result.Exception, "An unhandled exception occurred while retrying calling");
            return Task.CompletedTask;
        });

    var waitAndRetryPolicy = Policy.HandleResult<HttpResponseMessage>(res => res.StatusCode == HttpStatusCode.InternalServerError).
        WaitAndRetryAsync(2, retryAttempts => TimeSpan.FromMilliseconds(500));

    var response = await fallBackPolicy
        .WrapAsync(waitAndRetryPolicy)
        .ExecuteAsync(async () =>
        {
            using (var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"))
            {
                content.Headers.Add("X-Correlation-ID", HttpContext.Current.Session[RequestId].ToString());

                return await func(requestUri, content);
            }
        });
    
    if(response.IsSuccessStatusCode)
        return true;

    await LogMessage(LogLevel.Error, response, requestUri);
    
    return false;
}

and this is the StackTrace

System.NullReferenceException: Object reference not set to an instance of an object.
at UserMigrationService.<>c__DisplayClass22_0'1.<b__3>d.MoveNext() in ...\Services\UserMigrationService.cs:line 474
--- End of stack trace from previous location where exception was thrown ---

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Polly.Retry.AsyncRetryEngine.d__0'1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Polly.AsyncPolicy'1.d__13.MoveNext()
--- End of stack trace from previous location where exception was thrown ---

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Polly.Wrap.AsyncPolicyWrapEngine.<>c__DisplayClass0_0'1.<b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---

at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() > at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Polly.Fallback.AsyncFallbackEngine.d__0'1.MoveNext()

Can you help me find where the wrong part is? How can I better understand why this exception is thrown?
Thank you all

Upvotes: 0

Views: 442

Answers (1)

Peter Csala
Peter Csala

Reputation: 22829

As it turned out the NRE is thrown at this line:

ontent.Headers.Add("X-Correlation-ID", HttpContext.Current.Session[RequestId].ToString());

Which indicates that HttpContext.Current might be null. The ExecuteAsync receives a delegate which might not be run on same thread as the rest of the code of your CallApi method.

That's why the HttpContext will not flow into the delegate.

The fix is quite easy: you have to capture the RequestId inside the CallApi, not inside the ExecuteAsync delegate:

var correlationId = HttpContext.Current.Session[RequestId].ToString();
var response = await fallBackPolicy
        .WrapAsync(waitAndRetryPolicy)
        .ExecuteAsync(async () =>
        {
            using (var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"))
            {
                content.Headers.Add("X-Correlation-ID", correlationId);
                return await func(requestUri, content);
            }
        });

I would also recommend to use Policy.Wrap (Reference) instead of calling the WrapAsync method on one of the policies. The following two lines are equivalent:

fallBackPolicy.WrapAsync(waitAndRetryPolicy)
Policy.WrapAsync(fallbackPolicy, waitAndRetryPolicy)

So, your code could be rewritten like this:

var correlationId = HttpContext.Current.Session[RequestId].ToString();
var strategy = Policy.WrapAsync(fallbackPolicy, waitAndRetryPolicy);
var response = await strategy
        .ExecuteAsync(async (ct) =>
        {
            using (var content = new StringContent(jsonRequest, Encoding.UTF8, MediaTypeNames.Application.Json))
            {
                content.Headers.Add("X-Correlation-ID", correlationId);
                //TODO: pass the cancellationToken to the func
                return await func(requestUri, content);
            }
        }, CancellationToken.None);

I've used an other overload of ExecuteAsync where you are receiving a CancellationToken. This can be extremely useful whenever you consider to use a TimeoutPolicy as well. In that case you should pass that token to the func function.

Upvotes: 1

Related Questions