0

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

3
  • 1
    Could you please tell us which line is the 474th? Commented Jul 16, 2021 at 6:48
  • 1
    Without knowing which line is throwing the exception, my guess is that HttpContext.Current is null sometimes. Commented Jul 16, 2021 at 6:53
  • The line 474th is: content.Headers.Add("X-Correlation-ID", HttpContext.Current.Session[RequestId].ToString()); How can HttpContext be null in an HTTP context? Commented Jul 16, 2021 at 9:10

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

You were very clear, thanks for the explanation.
I made the change, but I still haven't deployed to the production environment. I update you
@pampua84 I don't want to look like impatient, but after 2 weeks I think it is a vaild request from me to ask some update from you :)
Hello sorry thanks for your interest, however, I confirm that the problem is solved, even for this, I had checked the solution. Thanks again
@pampua84 Great news :D If my proposed solution had used then could you please mark it as the answer to close this topic. If not then please leave a post where you explain how did you solve the problem. Thanks! Have a great day

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.