Reputation: 894
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
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