Why is IHttpContextAccessor.HttpContext sometimes null? - c#

Using .Net Core 2.2, I'm using HttpClient to call an API, along with an Http Message Handler. The message handler injects IHttpContextAccessor, but that object sometimes contains an HttpContext property that is null - I can't understand why that would ever be null.
Here's the service registration in startup:
services.AddTransient<HttpClientTokenHandler>();
services.AddHttpClient("client", c =>
{
c.BaseAddress = new Uri(options.ApiUrl.TrimEnd('/') + '/');
}).AddHttpMessageHandler<HttpClientTokenHandler>();
services.AddHttpContextAccessor();
And the token handler implementation where HttpContext is sometimes null.
public HttpClientTokenHandler(IHttpContextAccessor context)
{
_context = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var access_token = await _context.HttpContext.GetTokenAsync(Constants.TokenTypes.AccessToken);
if (!string.IsNullOrEmpty(access_token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
}
var response = await base.SendAsync(request, cancellationToken);
return response;
}
What could cause _context.HttpContext to be null, when it's simply being injected?

Related

Polly - 'Cannot access a closed Stream'

I am upgrading a Xamarin app to MAUI and thought of decoupling things a bit. Before i had a datastore which handled all requests to an API, now i have a service for each section of the app from which requests go to a HttpManager, problem is when the policy retries, it works for the first time but on the second retry it fails with the message "Cannot access a closed Stream". Searched a bit but couldn't find a fix.
I call the service from the viewModel.
LoginViewModel.cs
readonly IAuthService _authService;
public LoginViewModel(IAuthService authService)
{
_authService = authService;
}
[RelayCommand]
private async Task Login()
{
...
var loginResponse = await _authService.Login(
new LoginDTO(QRSettings.StaffCode, Password, QRSettings.Token));
...
}
In the service i set send the data to the HttpManager and process the response
AuthService.cs
private readonly IHttpManager _httpManager;
public AuthService(IHttpManager manager)
{
_httpManager = manager;
}
public async Task<ServiceResponse<string>> Login(LoginDTO model)
{
var json = JsonConvert.SerializeObject(model);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpManager.PostAsync<string>("Auth/Login", content);
...
}
And in here i send the request.
HttpManager.cs
readonly IConnectivity _connectivity;
readonly AsyncPolicyWrap _retryPolicy = Policy
.Handle<TimeoutRejectedException>()
.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1), (exception, timespan, retryAttempt, context) =>
{
App.AppViewModel.RetryTextVisible = true;
App.AppViewModel.RetryText = $"Attempt number {retryAttempt}...";
})
.WrapAsync(Policy.TimeoutAsync(11, TimeoutStrategy.Pessimistic));
HttpClient HttpClient;
public HttpManager(IConnectivity connectivity)
{
_connectivity = connectivity;
HttpClient = new HttpClient();
}
public async Task<ServiceResponse<T>> PostAsync<T>(string endpoint, HttpContent content, bool shouldRetry = true)
{
...
// Post request
var response = await Post($""http://10.0.2.2:5122/{endpoint}", content, shouldRetry);
...
}
async Task<HttpResponseMessage> Post(string url, HttpContent content, bool shouldRetry)
{
if (shouldRetry)
{
// This is where the error occurs, in the PostAsync
var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
await HttpClient.PostAsync(url, content, token), CancellationToken.None);
...
}
...
}
And this is the MauiProgram if it matters
...
private static MauiAppBuilder RegisterServices(this MauiAppBuilder builder)
{
...
builder.Services.AddSingleton<IHttpManager, HttpManager>();
builder.Services.AddSingleton<IAuthService, AuthService>();
return builder;
}
Can't figure out what the issue is...
I tried various try/catches, tried finding a solution online but no luck.
On the second retry it always gives that error
Disclaimer: In the comments section I've suggested to rewind the underlying stream. That suggestion was wrong, let me correct myself.
TL;DR: You can't reuse a HttpContent object you need to re-create it.
In order to be able to perform a retry attempt with a POST verb you need to recreate the HttpContent payload for each attempt.
There are several ways to fix your code:
Pass the serialized string as parameter
async Task<HttpResponseMessage> Post(string url, string content, bool shouldRetry)
{
if (shouldRetry)
{
var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
await HttpClient.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"), token), CancellationToken.None);
...
}
...
}
Pass the to-be-serialized object as parameter
async Task<HttpResponseMessage> Post(string url, object content, bool shouldRetry)
{
if (shouldRetry)
{
var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
await HttpClient.PostAsync(url, JsonContent.Create(content), token), CancellationToken.None);
...
}
...
}
Here we are taking advantage of the JsonContent type which was introduced in .NET 5
Pass the to-be-serialized object as parameter #2
async Task<HttpResponseMessage> Post(string url, object content, bool shouldRetry)
{
if (shouldRetry)
{
var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
await HttpClient.PostAsJsonAsync(url, content, token), CancellationToken.None);
...
}
...
}
Here we are taking advantage of an extension method called PostAsJsonAsync
It was introduced under the HttpClientExtensions
But nowadays it resides inside the HttpClientJsonExtensions

Blazor WASM and Refit token

I am using refit in my blazor wasm application, I want to set the token in AuthorizationHeaderValueGetter, the api to which I connect is not written in .net. but I have registered refit in the program.cs
builder.Services.AddRefitClient<IApi>(settings).ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("Address");
})
do I have to create a DelegatingHandler for this?
public class AuthHeaderHandler : DelegatingHandler
{
private readonly ILocalStorageService _localStorageService;
public AuthHeaderHandler(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var auth = request.Headers.Authorization;
if (auth != null)
{
if (await _localStorageService.ContainKeyAsync("Token"))
{
string token = await _localStorageService.GetItemAsync<string>("Token");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}

Authentication state provider not working inside custom message handler

Custom message handler
It is expected to be logged out if the token cannot be renewed.
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
await SetAuthorizationHeader(request);
var response = await base.SendAsync(request, cancellationToken);
if (IsRefreshRequired(response))
{
var tokenRefreshed = await _authService.TryRefreshTokenAsync();
if (tokenRefreshed)
{
await SetAuthorizationHeader(request);
return await base.SendAsync(request, cancellationToken);
}
await _authService.LogoutAsync();
_navigationManager.NavigateTo(Routes.App.Login);
}
return response;
}
private async Task SetAuthorizationHeader(HttpRequestMessage request)
{
var accessToken = await _localStorage.GetItemAsync<string>(Auth.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue(Auth.Scheme, accessToken);
}
}
Authentication service
public async Task LogoutAsync()
{
await RemoveTokensAsync();
AuthStateProvider.NotifyAuthenticationStateChanged();
}
Custom authentication state provider
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var accessToken = await _localStorage.GetItemAsync<string>(Auth.AccessToken);
if (string.IsNullOrWhiteSpace(accessToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var claims = TokenParser.ParseClaims(accessToken);
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, Auth.AuthType));
return new AuthenticationState(user);
}
public void NotifyAuthenticationStateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
After calling LogoutAsync method, it successfully redirects me to the Login page, but it does not update the header:
If I will update the page or I will call LogoutAsync not from handler but from razor component all will work:
Also it does not depend on http client I am using.
Based on the comments. Thanks #enet and #HenkHolterman.
Root cause: The StateHasChanged() logic does not propagate 'out and up'. The logout is caused by a call on an inner component, the LoginDisplay component is not involved in the default updating.
Solution: UI rerender. There is no single way how to do it.
In my case I changed
_navigationManager.NavigateTo(Routes.App.Login); to
_navigationManager.NavigateTo(Routes.App.Logout);
As result logout page triggers changing authentication state logic on component level and then redirects user to the login page.
Other way to do it -- define an event handler in message handler which should be triggered from within the LogoutAsync, after the call to NotifyAuthenticationStateChanged. The subscriber to this event should be the MainLayout component, which re-renders.

Redirect outside of the Controllers context in ASP.NET Core

I do not know if this is actually possible, but I think it' s worth a try to find out.
There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.
When you have to call an API you could do it directly from within the controller using the HttpClient like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: get some access token to use in api call
var accessToken = await HttpContext.GetTokenAsync("access_token");
//Example: do an API call direcly using a static HttpClient wrapt in a service
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.Client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var rederectUrl = "/account/login?returnUrl="+Request.Path;
return Redirect(rederectUrl);
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
return null;
}
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.
Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.
Lets say I create a Middleware for the HttpClient like this:
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var context = _contextAccessor.HttpContext;
var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
context.Response.Redirect(rederectUrl); //not working
}
//When 403 user probably does not have authorization to use endpoint
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
}
return response;
}
}
We can just do the request like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?
Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:
Setup
We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.
authentication
When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.
authorization
When we get a 403 we want to show the user that he has no access or something similar like that.
Solution
To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.
Create custom Exceptions
public class ApiAuthenticationException : Exception
{
public ApiAuthenticationException()
{
}
public ApiAuthenticationException(string message) : base(message)
{
}
}
public class ApiAuthorizationException : Exception
{
public ApiAuthorizationException()
{
}
public ApiAuthorizationException(string message) : base(message)
{
}
}
Throw exceptions
Create a wrapper or use the HttpClient middleware to manage the exception throwing.
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new ApiAuthenticationException();
}
//When 403 user probably does not have authorization to use endpoint -> show error page
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ApiAuthorizationException();
}
return response;
}
}
Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.
You could do it like this:
public class ResourceGatewayClient : IApiClient
{
private static HttpClient _client;
public HttpClient Client => _client;
public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
{
if (_client == null)
{
_client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
//configurate default base address
_client.BaseAddress = "https://gateway.domain.com/api";
}
}
}
And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ResourceGatewayClient>();
Now you can use the dependency injection in any controller you would like.
Handle the Exceptions
Create something like this middleware (with thanks to this answer):
public class ApiErrorMiddleWare
{
private readonly RequestDelegate next;
public ApiErrorMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception is ApiAuthenticationException)
{
context.Response.Redirect("/account/login");
}
if (exception is ApiAuthorizationException)
{
//handle not authorized
}
}
Register your middleware
Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.
This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.
I would like to hear some feedback on this solution or an alternative approach.

How can I safely throw exceptions from within a DelegatingHandler

I have a delegating handler that throws specific exceptions based on fault messages received from the server.
This allows the client to retry (using polly, but as you'll see the mechanism doesn't matter)
After two failed attempts the third attempt simply hangs. No data hits the server, and nothing further happens. This seems to be because the HttpClient has a max of two connections, which suggests these connections are not being closed properly.
Is there something I can do to close the connection or should I change the design so that the delegating handlers do not throw exceptions?
See simple example below to reproduce
private class Test : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
throw new Exception("afesaf");
}
}
[HttpGet]
[Route("B")]
public async Task B()
{
var handler = new HttpClientHandler();
var pipeline = HttpClientFactory.CreatePipeline(handler, new[] { new Test() });
var http = new HttpClient(pipeline);
http.BaseAddress = new Uri("http://google.com");
var count = 0;
while (count++ < 5)
try
{
Log.Info("A");
var request = new HttpRequestMessage(HttpMethod.Get, "");
await http.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false);
}
catch
{
}
}
Dispose of the response object.
I've gone with the following in the delegating handler as I have multiple points where the exception could be thrown in the real code
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
try
{
response = await base.SendAsync(request, cancellationToken);
throw new Exception("afesaf");
}
catch
{
response?.Dispose();
throw
}
}

Categories