ASP.NET
Core HttpClient Dependency Injection
▌Introduction
Creating
a new instance of HttpClient every time is thought as an bad idea, cus it will create
a new TCP/IP connection and wait for a period time even if you dispose HttpClient
instance. (Reference: YOU'RE
USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE)
After
ASP.NET Core 2.1, we can use IHttpClientFactory to manage(register/create) the HttpClient instance(s) and
manage the pooling and lifetime of underlying HttpMessageHandler instances to avoid common DNS problems (Such as DNS changes).
·
Basic Usage
·
Named Clients
·
Typed Clients
The
following picture from Microsoft
Document shows how Typed Client
works, but the concept of the above ways are the same.
▌Environment
▋ASP.NET Core 3.0.100
▌Implement
▋Basic usage
Register
the IHttpClientFactory to service container in Startup: ConfigureServices,
▋Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// …skip
services.AddHttpClient();
}
Then we
can use it on our other API Controller or injected service, for example,
▋API Controller
[Route("api/[controller]")]
[ApiController]
public class MyController : ControllerBase
{
private readonly IHttpClientFactory httpClientFactory = null;
public MyController(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
[HttpGet("Test")]
public async Task<IActionResult> Test()
{
var httpClient = this.httpClientFactory.CreateClient();
var result = await httpClient.GetAsync("https://karatejb.blogspot.com/");
return this.Ok();
}
}
▋Named Clients
For
different configuration of HttpClient, we can use Named Clients to distinguish
them.
For
example,
▋Startup.cs :
ConfigureServices
services.AddHttpClient("AuthHttpClient",
config =>
{
config.Timeout = TimeSpan.FromMinutes(5);
config.BaseAddress = new Uri("https://MyAuthServer/");
config.DefaultRequestHeaders.Add("Accept", "application/json");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
services.AddHttpClient("NormalHttpClient",
config =>
{
config.Timeout = TimeSpan.FromMinutes(1);
config.BaseAddress = new Uri("https://karatejb.blogspot.com");
config.DefaultRequestHeaders.Add("Accept", "text/xml");
config.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Bearer XXXXXX");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(2));
Notice
the default HttpMessageHandler‘s lifetime is 2 minutes and can be reused from pool, and
after 2 mins the created HttpClientHandler (which inherits HttpMessageHandler) instance will no longer be used by new HttpClient
instance and will be waiting for GC. We can update the lifetime by SetHandlerLifetime method as the above sample codes.
After
injecting Named Clients, we can create the new specified HttpClient by passing
name to IHttpClientFactory.CreateClient,
[Route("api/[controller]")]
[ApiController]
public class MyController : ControllerBase
{
private readonly IHttpClientFactory httpClientFactory = null;
public MyController(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
[HttpGet("Test")]
public async Task<IActionResult> Test()
{
var httpClient = this.httpClientFactory.CreateClient("NormalHttpClient");
var result = await httpClient.GetAsync("2019/07/aspnet-core-identity-server-4-ldap.html");
return this.Ok();
}
}
Which
will results sending a request as below,
▋Typed Clients
The
Typed-Client way provides injecting the HttpClient as Transient-lifetime into
services.
▋Startup.cs :
ConfigureServices
For
example, we can inject the Typed Client to a custom service: IdentityClient,
services.AddHttpClient<IIdentityClient, IdentityClient>( config => config.BaseAddress= new Uri("https://localhost:6001/")).SetHandlerLifetime(TimeSpan.FromMinutes(2)) // HttpMessageHandler default lifetime = 2 min
PS. However the IdentityClient
instance will be Transient as well!
|
Then get
the HttpClient instance like this,
▋IdentityClient.cs
public class IdentityClient : IIdentityClient
{
private readonly HttpClient httpClient = null;
public IdentityClient(HttpClient httpClient)
{
this.httpClient = httpClient;
}
private async Task<HttpResponseMessage> SendMyRequest()
{
var response = await this.httpClient.GetAsync("https://localhost/xxxxx");
return response;
}
}
▋Outgoing request middleware
We can
use and chain delegating handlers to perform some works BEFORE and AFTER the
outgoing request.
Take
sending a refreshing-token request for example, we can check
1. If the target host is allowed BEFORE sending request
2. If the request url is secure with Https BEFORE sending
request
And if we
want to allow one-time refresh token (that means after first-time refreshing
token, we will not send a new Refresh Token to do the second-time refreshing
token). We can withdraw the new refresh token AFTER receiving the response.
Create
the following delegating handlers to meet the above requirements by inheriting System.Net.Http.DelegatingHandler.
▋AuthHostCheckRequestHandler:
Check request’s target host
public class AuthHostCheckRequestHandler : DelegatingHandler
{
private readonly string[] ALLOWED_DOMAINS = new string[] { "localhost", "my_auth_server" };
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!ALLOWED_DOMAINS.Contains(request.RequestUri.Host))
{
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
var jObject = JObject.Parse("{\"error\":\"The target domain " + request.RequestUri.Host + " is not allowed!\"}");
response.Content = new StringContent(jObject.ToString(), Encoding.UTF8, "application/json");
return response;
};
return await base.SendAsync(request, cancellationToken);
}
}
▋SecureRequestHandler:
Https url required
public class SecureRequestHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!request.RequestUri.ToString().StartsWith("https"))
{
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
var jObject = JObject.Parse("{\"error\":\"You cannot send request to insecure endpoints!\"}");
response.Content = new StringContent(jObject.ToString(), Encoding.UTF8, "application/json");
return response;
};
return await base.SendAsync(request, cancellationToken);
}
}
▋OneTimeRefreshTokenResponseHandler:
Withdraw new refresh token
public class OneTimeRefreshTokenResponseHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
var jsonStr = await response.Content.ReadAsStringAsync();
JObject jObject = JObject.Parse(jsonStr);
jObject.Remove("refresh_token");
response.Content = new StringContent(jObject.ToString(), Encoding.UTF8, "application/json");
}
return response;
}
}
▋Startup: ConfigureServices
We have
to register the delegating handlers and add them to HttpClient by HttpClientBuilderExtensions: AddHttpMessageHandler method as following,
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<AuthHostCheckRequestHandler>();
services.AddTransient<SecureRequestHandler>();
services.AddTransient<OneTimeRefreshTokenResponseHandler>();
services.AddHttpClient("RefreshTokenHttpClient",
config =>
{
config.Timeout = TimeSpan.FromMinutes(5);
config.BaseAddress = new Uri("https://my_auth_server/");
config.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<AuthHostCheckRequestHandler>()
.AddHttpMessageHandler<SecureRequestHandler>()
.AddHttpMessageHandler<OneTimeRefreshTokenResponseHandler>();
}
Notice that the delegating handlers will be triggered by the
order of AddHttpMessageHandler.
The order will be AuthHostCheckRequestHandler -> SecureRequestHandler
-> OneTimeRefreshTokenResponseHandler.
|
Lets take
a look at the final response for every scenario.
3. Successfully refreshing token and the new refresh token is
withdrawed (intercepted by OneTimeRefreshTokenResponseHandler)
BTW, if not using this delegating handler, the original response will be
BTW, if not using this delegating handler, the original response will be
▋Polly: dynamic policies
Polly is
a .NET resilience and transient-fault-handling library that allows developers
to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead
Isolation, and Fallback in a fluent and thread-safe manner. (From Github)
You have
to install Microsoft.Extensions.Http.Polly to use the extension methods later in the sample codes.
▋Polly Timeout
Policy sample
Here is an example for setting a timeout policy to HttpClient (in Startup: ConfigureServices),
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
services.AddHttpClient("MyHttpClient").AddPolicyHandler(timeout);
Or set
conditional policies for different Http methods,
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(60));
services.AddHttpClient("MyHttpClient")
.AddPolicyHandler(request =>
request.Method == HttpMethod.Get ? timeout : longTimeout);
▋Polly Retry
Policy sample
A retry policy
for failing request (try up to 3 times).
services.AddHttpClient("MyHttpClient")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3));
A retry policy
for certain Response’s status code (e.q. 500 or 400) and do something before
re-sending the request:
var retryPolicy = Policy.HandleResult<HttpResponseMessage>(
r => r.StatusCode.Equals(HttpStatusCode.BadRequest) || r.StatusCode.Equals(HttpStatusCode.InternalServerError)).RetryAsync(3, (exception, retryCount) =>
{
// Logic to be executed before sending retry request
Debug.WriteLine("Do something...");
});
services.AddHttpClient("MyHttpClient")
////.AddPolicyHandler(request => request.Method == HttpMethod.Post ? retryPolicy : null)
.AddPolicyHandler(retryPolicy);
Other ways:
var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.BadRequest)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
services.AddHttpClient("MyHttpClient").AddPolicyHandler(retryPolicy);
var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError().OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.BadRequest)
.RetryAsync(2, (exception, retryCount) =>
{
// Logic to be executed before sending retry request
Debug.WriteLine("Update data ...");
});
services.AddHttpClient("MyHttpClient").AddPolicyHandler(retryPolicy);
The above ways are
appending the retry policy on the HttpClientFactory level.
If you want
HttpClient to use different retry policy on the fly, you can execute the retry
policy when sending a request and even cancel a retry-request:
var httpClient = this.httpClientFactory.CreateClient("MyHttpClient");
CancellationTokenSource cts = new CancellationTokenSource();
var retryPolicy = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.UnprocessableEntity)
.RetryAsync(3, async (exception, retryCount) =>
{
if (someUnexpectedCondition)
{
// Cancel
the retry request
cts.Cancel();
}
else
{
// Do
something before sending the retry request
}
});
HttpResponseMessage response = null;
var result = await retryPolicy.ExecuteAsync(
async (token) =>
{
if (!token.IsCancellationRequested)
{
response = await httpClient.PostAsync(url, new StringContent(myContent, Encoding.UTF8, type));
}
return response;
}, cts.Token);
return result;
▋Polly Mixed
Policy sample
services.AddHttpClient ("MyHttpClient")
.AddTransientHttpErrorPolicy (p => p.RetryAsync (3))
.AddTransientHttpErrorPolicy (p => p.CircuitBreakerAsync (5, TimeSpan.FromSeconds (30)));
The
second policy is a circuit breaker policy. Further requests are blocked for 30
seconds if five failed attempts occur sequentially. Circuit breaker policies
are stateful. All calls through this client share the same circuit state.
▋Some notes
▋Self-signed SSL
cert error
We may
encounter a scenario that HttpClient will not allow accessing a self-signed
cert even if it is in our trusted store in Linux or macOS.
We can
bypass it by the following setting, (ONLY use it on non-production
environment!)
services.AddHttpClient<IIdentityClient, IdentityClient>()
.ConfigurePrimaryHttpMessageHandler(h =>
{
var handler = new HttpClientHandler();
if (this.env.IsDevelopment())
{
//Allow untrusted Https connection
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
return handler;
});
▌Reference
沒有留言:
張貼留言