I'm using typed clients with IHttpClientFactory. Like this:
// Startup.cs
services.AddHttpClient<MyHttpClient>()
// MyHttpClient.cs
public class MyHttpClient
{
public MyHttpClient(HttpClient client)
{
Client = client;
}
public HttpClient Client { get; }
}
// MyService.cs
public class MyService {
public MyService(MyHttpClient httpClient) {}
public async Task SendRequestAsync(string uri, string accessToken) {
_httpClient.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
await _httpClient.Client.GetAsync(uri);
}
}
I'm unsure how this works. Will the request headers be set only for this request, or for every subsequent request that is made using this instance of httpClient. How can I set header on a per request basis?
You can use an DelegatingHandler to add an header to each request the HttpClient will make.
public class HeaderHandler: DelegatingHandler
{
public HeaderHandler()
{
}
public HeaderHandler(DelegatingHandler innerHandler): base(innerHandler)
{
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("CUSTOM-HEADER","CUSTOM HEADER VALUE");
return await base.SendAsync(request, cancellationToken);
}
}
}
You register the hanlder using :
service.AddTransient<HeaderHandler>()
.AddHttpClient<MyHttpClient>()
.AddHttpMessageHandler<HeaderHandler>();
Related
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);
}
}
Given the well known dilemma's and issues of using HttpClient - namely socket exhaustion and not respecting DNS updates, its considered best practice to use IHttpClientFactory and let the container decide when and how to utilise http pool connections efficiency. Which is all good, but now I cannot instantiate a custom DelegatingHandler with custom data on each request.
Sample below on how I did it before using the factory method:
public class HttpClientInterceptor : DelegatingHandler
{
private readonly int _id;
public HttpClientInterceptor(int id)
{
_id = id;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// associate the id with this request
Database.InsertEnquiry(_id, request);
return await base.SendAsync(request, cancellationToken);
}
}
For every time I instantiate a HttpClient a Id can be passed along:
public void DoEnquiry()
{
// Insert a new enquiry hypothetically
int id = Database.InsertNewEnquiry();
using (var http = new HttpClient(new HttpClientInterceptor(id)))
{
// and do some operations on the http client
// which will be logged in the database associated with id
http.GetStringAsync("http://url.com");
}
}
But now I cannot instantiate the HttpClient nor the Handlers.
public void DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
// and now??
http.GetStringAsync("http://url.com");
}
How would I be able to achieve similar using the factory?
I cannot instantiate a custom DelegatingHandler with custom data on each request.
This is correct. But you can use a custom DelegatingHandler that is reusable (and stateless), and pass the data as part of the request. This is what HttpRequestMessage.Properties is for. When doing these kinds of "custom context" operations, I prefer to define my context "property" as an extension method:
public static class HttpRequestExtensions
{
public static HttpRequestMessage WithId(this HttpRequestMessage request, int id)
{
request.Properties[IdKey] = id;
return request;
}
public static int? TryGetId(this HttpRequestMessage request)
{
if (request.Properties.TryGetValue(IdKey, out var value))
return value as int?;
return null;
}
private static readonly string IdKey = Guid.NewGuid().ToString("N");
}
Then you can use it as such in a (reusable) DelegatingHandler:
public class HttpClientInterceptor : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var id = request.TryGetId();
if (id == null)
throw new InvalidOperationException("This request must have an id set.");
// associate the id with this request
Database.InsertEnquiry(id.Value, request);
return await base.SendAsync(request, cancellationToken);
}
}
The disadvantage to this approach is that each callsite then has to specify the id. This means you can't use the built-in convenience methods like GetStringAsync; if you do, the exception above will be thrown. Instead, your code will have to use the lower-level SendAsync method:
var request = new HttpRequestMessage(HttpMethod.Get, "http://url.com");
request.SetId(id);
var response = await client.SendAsync(request);
The calling boilerplate is rather ugly. You can wrap this up into your own GetStringAsync convenience method; something like this should work:
public static class HttpClientExtensions
{
public static async Task<string> GetStringAsync(int id, string url)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.SetId(id);
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
and now your call sites end up looking cleaner again:
public async Task DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
var result = await http.GetStringAsync(id, "http://url.com");
}
In my aspnet core project I am using HttpClient, I have generic interface and class as well, but I can not register them in startup.
My interface looks like:
public interface IHttpClientWrapper<T> where T : class
{
Task<T> GetAsync(string url, string authType, string token, CancellationToken cancellationToken);
}
My class look like:
public class HttpClientWrapper<T> : IHttpClientWrapper<T> where T : class
{
private readonly HttpClient _client;
public HttpClientWrapper(HttpClient client)
{
_client = client;
}
public async Task<T> GetAsync(string url, string authType, string token,
CancellationToken cancellationToken)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authType, token);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
using (var response = await _client.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken))
{
var stream = await response.Content.ReadAsStreamAsync();
response.EnsureSuccessStatusCode();
return stream.ReadAndDeserializeFromJson<T>();
}
}
}
What I would like to achieve:
services.AddHttpClient<IHttpClientWrapper<T>, HttpClientWrapper<T>>()
.ConfigurePrimaryHttpMessageHandler(handler =>
new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip
});
What I tried and it was failed:
services.AddTransient(typeof(IHttpClientWrapper<>),typeof(HttpClientWrapper<>)).ConfigureOptions(
new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip
});
In short I need to somehow register AutomaticDecompression = System.Net.DecompressionMethods.GZip it can be with Transient if possible
I'm not sure why you registering IHttpClientWrapper. If it's transient, then you can you can just create it when you need it.
If you have a good reason to use DI for it then you could make the class non-generic and make the method generic:
public interface IHttpClientWrapper
{
Task<T> GetAsync<T>(string url, string authType, string token, CancellationToken cancellationToken);
}
I am using Refit (5.1.67) as my HttpClient wrapper, in a .NET Core 3.1 app using IHttpClientFactory.
The API I am calling is secured using a client credentials token.
I am registering the client with this:
services.AddRefitClient<ISomeApiClient>().ConfigureHttpClient(c =>
c.BaseAddress = new Uri(Configuration["BaseUrlFromConfig"]));
The client has methods that look like this:
public interface ISomeApiClient
{
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems([Header("X-User-Id")] string userId, [Header("Authorization")] string accessToken);
[Get("/api/somewhere-else")]
Task<MyResponseObject> GetItem([Header("X-User-Id")] string userId, [Header("Authorization")] string accessToken, int id);
}
What I want to avoid is having to explicitly pass accessToken and userId every time I call an endpoint (like above). Ideally, I want to have my client look like this:
public interface ISomeApiClient
{
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems();
[Get("/api/somewhere")]
Task<IEnumerable<MyResponseObject>> GetItems(int id);
}
It feels like I need some sort of request middleware for outgoing requests, where I can add these two headers. If they were static I would just decorate the whole interface but because these are runtime values that will not work.
I cannot find any help on this one in the docs, and would appreciate any pointers.
Refit docs now explain how to do this
https://github.com/reactiveui/refit#reducing-header-boilerplate-with-delegatinghandlers-authorization-headers-worked-example
Add a header handler:
class AuthHeaderHandler : DelegatingHandler
{
private readonly IAuthTokenStore authTokenStore;
public AuthHeaderHandler(IAuthTokenStore authTokenStore)
{
this.authTokenStore = authTokenStore;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await authTokenStore.GetToken();
//potentially refresh token here if it has expired etc.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
then register in Startup.cs when registering the client:
services.AddTransient<AuthHeaderHandler>();
services.AddRefitClient<ISomeThirdPartyApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
You can use DI to inject your client where do you need it. We use it like this:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly ISomeApiClient_client;
public ValuesController(ISomeApiClient client)
{
_client = client;
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "Your Oauth token");
}
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
Instead of the usual response of Status : 400 and body message of "Error" : "invalid_client" when the token has expired, are there any methods of changing the status code and body to display something else?
Currently, I've managed to do something with headers as following :
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
AuthenticationTicket ticket;
if (_refreshTokens.TryRemove(context.Token, out ticket))
{
if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc.Value.LocalDateTime < DateTime.Now)
{
context.Response.Headers.Add("Expired", new string[] { "Yes" });
}
context.SetTicket(ticket);
}
}
Any help anyone?
Thanks.
You can implement a custom ASP.NET WebApi DelegatingHandler (if you want the validation to happen for all the requests) or ActionFilter (if you want the validation to happen for specific requests/per endpoint) to check whether the token is still valid and interrupt the request to return a more meaningful response. See the links for details.
I've implemented a simple one for your reference:
public class CustomTokenCheckMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (HasMyTokenExpired())
{
return new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.Unauthorized,
ReasonPhrase = "",
Content = new StringContent("Test") // See HttpContent for more https://msdn.microsoft.com/en-us/library/system.net.http.httpcontent(v=vs.118).aspx
};
}
return await base.SendAsync(request, cancellationToken);
}
public bool HasMyTokenExpired()
{
//Your custom logic here
return true;
}
}
Then you need to register it in the WebApiConfig file like this:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
/*
All other config goes here
*/
//This line registers the handler
config.MessageHandlers.Add(new CustomTokenCheckMessageHandler());
}
}