-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Polly and HttpClientFactory
TL;DR HttpClient factory in ASPNET Core 2.1 provides a way to pre-configure instances of HttpClient
which apply Polly policies to every outgoing call (among other benefits).
Sidenote: If you experience diamond dependency conflicts using Polly v7 with HttpClientFactory, follow the resolution here.
From ASPNET Core 2.1, Polly integrates with IHttpClientFactory. HttpClient factory is a factory that simplifies the management and usage of HttpClient
in four ways. It:
-
allows you to name and configure logical
HttpClient
s. For instance, you may configure a client that is pre-configured to access the github API; -
manages the lifetime of
HttpClientMessageHandler
s to avoid some of the pitfalls associated with managingHttpClient
yourself (the disposing-it-too-often-can-cause-socket-exhaustion but also only-using-a-singleton-can-miss-DNS-updates aspects); -
provides configurable logging (via
ILogger
) for all requests and responses performed by clients created with the factory; -
provides a simple API for adding middleware to outgoing calls, be that for logging, authorisation, service discovery, or resilience with Polly.
The Microsoft early announcement speaks more to these topics, and Steve Gordon's quartet of blog posts (1; 2; 3; 4) are also an excellent read for deeper background and some great worked examples. UPDATE: The official documentation is also now out.
Have your project grab the ASPNET Core 2.1 packages from nuget. You'll typically need the AspNetCore metapackage, and the extension package Microsoft.Extensions.Http.Polly
.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.0" />
</ItemGroup>
</Project>
Note: later versions of these packages may be available when you read this.
In your standard Startup.ConfigureServices(...)
method, start by configuring a named client as below:
public void ConfigureServices(IServiceCollection services)
{
// Configure a client named as "GitHub", with various default properties.
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
});
// ...
}
(We've used magic strings for clarity, but of course you can obtain those from config or declare them once as consts.)
We'll focus on configuring this with Polly policies, but there are many more options for configuring the named HttpClient
which you can read about from the official doco, or Steve Gordon or Scott Hanselman. To keep the examples in this post shorter, we've used named clients, but the documentation and blogs above also cover how to use typed clients, which offer the advantages of strong-typing and allowing you to build call overloads focused on your specific needs.
To apply Polly policies, you simply extend the above example with some fluent configuration:
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}));
This example creates a policy which will handle typical transient faults, retrying the underlying http request up to 3 times if necessary. The policy will apply a delay of 1 second before the first retry; 5 seconds before a second retry; and 10 seconds before the third.
The overload .AddTransientHttpErrorPolicy(...)
is one of a number of options, which we'll look at after covering the basics.
For completeness, here's an example of consuming the configured HttpClient
. For a named client (as the above example), take an IHttpClientFactory
by dependency injection at the usage site. Then use that factory to obtain an HttpClient
configured to the specification you defined in Startup
:
public class MyController : Controller
{
private readonly IHttpClientFactory _httpClientFactory;
public MyController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public Task<IActionResult> SomeAction()
{
// Get an HttpClient configured to the specification you defined in StartUp.
var client = _httpClientFactory.CreateClient("GitHub");
return Ok(await client.GetStringAsync("/someapi"));
}
}
Again, Steve Gordon's and Scott Hanselman's blogs give richer examples, including if you prefer typed clients.
The policy or policies configured on your HttpClient
are applied to outbound calls by Polly-based DelegatingHandler
s.
This means the policies will be applied to all outgoing calls through that configured HttpClient
.
If you've tried in the past to hand-craft retries outside calls to HttpClient.SendAsync(...)
which pass in an HttpRequestMessage
, you may have discovered that the HttpRequestMessage
passed in cannot be reused once sent (doing so raises an InvalidOperationException
). The DelegatingHandler
approach avoids this problem.
A DelegatingHandler
is simply middleware for an outbound http call: see Steve Gordon's third blog post for a great introduction to how delegating handlers work.
Let's look at the example from Step 2 above again:
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}));
This uses a new convenience method, .AddTransientHttpErrorPolicy(...)
. This configures a policy to handle errors typical of Http calls:
- Network failures (
System.Net.Http.HttpRequestException
) - HTTP 5XX status codes (server errors)
- HTTP 408 status code (request timeout)
Using .AddTransientHttpErrorPolicy(...)
pre-configures what the policy handles. The builder => builder
clause then specifies how the policy will handle those faults.
In the builder => builder
clause you can choose any reactive policy from Polly's offerings: a retry strategy (as in the above example), circuit-breaker or fallback policy.
The choice in .AddTransientHttpErrorPolicy(...)
to handle HttpRequestException
, HTTP 5xx, HTTP 408 is a convenience option, but not mandatory. If that error filter doesn't suit your needs - which you should think through - you can extend the definition of errors to handle, or build an entirely bespoke Polly policy.
Overloads are also available taking any IAsyncPolicy<HttpResponseMessage>
, so you can define and apply any kind of policy: you specify both the what to handle and how to handle.
This example demonstrates .AddPolicyHandler(...)
to add a policy where we coded our own specification of faults to handle:
var retryPolicy = Policy.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(response => MyCustomResponsePredicate(response))
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}));
services.AddHttpClient(/* etc */)
.AddPolicyHandler(retryPolicy);
As well as Polly's reactive policies (such as retry and circuit-breaker), these overloads mean you can also use proactive policies such as timeout:
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
services.AddHttpClient(/* etc */)
.AddPolicyHandler(timeoutPolicy);
All calls through HttpClient
return an HttpResponseMessage
, so the policies configured must be of type IAsyncPolicy<HttpResponseMessage>
. Non-generic policies IAsyncPolicy
can also be converted to IAsyncPolicy<HttpResponseMessage>
with a simple convenience method:
var timeoutPolicy = Policy.TimeoutAsync(10);
services.AddHttpClient(/* etc */)
.AddPolicyHandler(timeoutPolicy.AsAsyncPolicy<HttpResponseMessage>());
The definition of errors handled by .AddTransientHttpErrorPolicy(...)
is also available from a Polly extension package, Polly.Extensions.Http (github; nuget).
Using this allows you to take the base specification of errors to handle (HttpRequestException
, HTTP 5xx, HTTP 408) and extend it. For example, the policy configured below would handle status code 429 additionally:
using Polly.Extensions.Http; // After installing the nuget package: Polly.Extensions.Http
// ..
var policy = HttpPolicyExtensions
.HandleTransientHttpError() // HttpRequestException, 5XX and 408
.OrResult(response => (int)response.StatusCode == 429) // RetryAfter
.WaitAndRetryAsync(/* etc */);
All overloads for configuring policies can also be chained to apply multiple policies:
services.AddHttpClient(/* etc */)
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}))
.AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30)
));
When you configure multiple policies (as in the above example), the policies are applied to each call from outer (first-configured) to inner (last-configured) order.
In the above example, the call will:
- first be placed through the (outer) retry policy, which will in turn:
- place the call through the (inner) circuit-breaker, which in turn:
- makes the underlying http call.
The sequencing of policies in this example was chosen because the circuit-breaker may change state in one of those periods (1, 5 or 10 seconds) when the retry policy is waiting between tries. The circuit-breaker is configured 'inside' the retry, so that the circuit state is tested again as part of the action of making a retry.
The above example applies two policies (retry and circuit-breaker), but any number is possible. A common useful combination might be to apply a retry, a circuit-breaker, and a timeout-per-try (see below).
For those familiar with Polly's PolicyWrap
, configuring multiple policies with the pattern shown above is entirely equivalent to using a PolicyWrap
. All the usage recommendations in the PolicyWrap
wiki apply.
Likewise, if you combine PolicyHttpMessageHandler with other DelegatingHandlers, consider whether the policy handlers should be 'inside' or 'outside' the other delegating handlers in the middleware pipeline you construct. The sequence in which DelegatingHandlers are applied corresponds to the sequence you configure them in after the .AddHttpClient(/* etc */)
call.
Overloads of .AddPolicyHandler(...)
exist allowing you to select policies dynamically based on the request.
One use case for this is to apply different policy behaviour for endpoints which are not idempotent. POST operations typically are not idempotent. PUT operations should be idempotent, but may not be for a given API (there is no substitute for knowing the behaviour of the API you are calling). So, you might want to define a strategy which retries for GET requests but not for other http verbs:
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();
services.AddHttpClient(/* etc */)
// Select a policy based on the request: retry for Get requests, noOp for other http verbs.
.AddPolicyHandler(request => request.Method == HttpMethod.Get ? retryPolicy : noOpPolicy);
The above example uses NoOp policy for http verbs other than GET. NoOp policy simply executes the underlying call 'as is', without any additional policy behaviour.
When using the .AddPolicyHandler(policySelector: request => ...)
(and similar) overloads on HttpClientClientFactory with stateful policies such as circuit-breaker and bulkhead, you must make sure that the policySelector
does not manufacture a new instance per request, but instead selects a single instance of the circuit-breaker or bulkhead. This is so that the single instance can be statefully reused across requests. Do not code:
// BAD CODE (do not use)
services.AddHttpClient(/* etc */)
.AddPolicyHandler(request => /* some func sometimes returning */
HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...)) // This will manufacture a new circuit-breaker per request.
Instead:
var circuitBreaker = HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...);
services.AddHttpClient(/* etc */)
.AddPolicyHandler(request => /* some func sometimes returning */ circuitBreaker )
Polly also provides PolicyRegistry as a central store for policies you might reuse in multiple places in your application. Overloads of .AddPolicyHandler(...)
exist allowing you to select a policy from the registry.
The following example registers a PolicyRegistry
with the IServiceCollection
, adds some policies to the registry, and then defines two logical clients using different policies from the registry.
var registry = services.AddPolicyRegistry();
registry.Add("defaultretrystrategy",
HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(/* etc */));
registry.Add("defaultcircuitbreaker",
HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(/* etc */));
services.AddHttpClient(/* etc */)
.AddPolicyHandlerFromRegistry("defaultretrystrategy");
services.AddHttpClient(/* etc */)
.AddPolicyHandlerFromRegistry("defaultretrystrategy")
.AddPolicyHandlerFromRegistry("defaultcircuitbreaker");
More complex use cases for PolicyRegistry
include dynamically updating the policies in your registry from an external source, to facilitate dynamic reconfiguration of policies during running.
You may want to configure a policy which makes use of other services registered for Dependency Injection. A typical example would be to configure a policy whose policy hooks access an ILogger<T>
configured with DI.
An .AddPolicyHandler(...)
overload exists allowing you to configure a policy which can resolve services from IServiceProvider
when the policy is created.
Because the typical .NET Core logging pattern prefers generic ILogger<T>
, this approach plays best with typed clients.
services.AddHttpClient<MyServiceHttpClient>(/* etc */)
.AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
},
onRetry: (outcome, timespan, retryAttempt, context) =>
{
services.GetService<ILogger<MyServiceHttpClient>>()
.LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
}
));
HttpClient
already has a Timeout
property, but how does this apply when a retry policy is in use? And where does Polly's TimeoutPolicy
fit?
-
HttpClient.Timeout
will apply as an overall timeout to each entire call throughHttpClient
, including all tries and waits between retries. - To apply a timeout-per-try, configure a
RetryPolicy
before a PollyTimeoutPolicy
.
In this case, you may want the retry policy to retry if any individual try timed out. To do this, make the retry policy handle the TimeoutRejectedException
which Polly's timeout policy throws.
This example uses the Polly.Extensions.Http package described earlier, to extend the convenience error set (HttpRequestException
, HTTP 5XX, and HTTP 408) with extra handling:
using Polly.Extensions.Http;
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>() // thrown by Polly's TimeoutPolicy if the inner call times out
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
});
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10); // Timeout for an individual try
serviceCollection.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.Timeout = TimeSpan.FromSeconds(60); // Overall timeout across all tries
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(timeoutPolicy); // We place the timeoutPolicy inside the retryPolicy, to make it time out each try.
If you have configured the retry and timeout policy in the other order (configuring timeoutPolicy before, thus outside, the retryPolicy), that TimeoutPolicy will instead act as an overall timeout for the whole operation (just as HttpClient.Timeout
does), not as a timeout-per-try. This is a natural consequence of the way multiple policies act as nested steps in a middleware pipeline.
Policy instances applied to a named HttpClient
configuration are shared across all calls through that HttpClient
configuration.
For the stateful policy circuit breaker, this means that all calls through a named HttpClient
configured with a circuit-breaker will share that same circuit state.
This usually plays well with HttpClient
s configured via HttpClient factory, because those HttpClient
s typically define a common BaseAddress
, meaning all calls are to some endpoint on that same BaseAddress
. In that case, we might expect that if one endpoint on BaseAddress
is unavailable, others will be too. The scoping then plays well: if calls to one endpoint through that HttpClient
configuration break the circuit, the circuit will also be broken for others.
If, however, this 'shared' scoping of the circuit-breaker is not appropriate for your scenario, define separate named HttpClient
instances and configure each with a separate circuit-breaker policy instance.
The same consideration applies if you use Polly's other stateful policy, Bulkhead
. With a Bulkhead policy applied to a named HttpClient
configuration, the Bulkhead capacity will be shared across all calls placed through that HttpClient
.
Polly CachePolicy
can be used in a DelegatingHandler configured via IHttpClientFactory. Polly is generic (not tied to Http requests), so at time of writing, the Polly CachePolicy determines the cache key to use from the Polly.Context
. This can be set on an HttpRequestMessage request
immediately prior to placing the call through HttpClient
, by using an extension method: (add using Polly;
to access the extension method)
request.SetPolicyExecutionContext(new Polly.Context("CacheKeyToUseWithThisRequest"));
Using CachePolicy with HttpClientFactory thus also requires that you use overloads on HttpClient
which take an HttpRequestMessage
as an input parameter.
Some additional considerations flow from the fact that caching with Polly CachePolicy in a DelegatingHandler
caches at the HttpResponseMessage
level.
If the HttpResponseMessage
is the end content you wish to re-use (perhaps to re-serve in whole or in part), then caching at the HttpResponseMessage
level may be a good fit.
In cases such as calling to a webservice to obtain some serialized data which will then be deserialized to some local types in your app, HttpResponseMessage
may not be the optimal granularity for caching.
In these cases, caching at the HttpResponseMessage
level implies subsequent cache hits repeat the stream-read and deserialize-content operations, which is unnecessary from a performance perspective.
It may be more appropriate to cache at a level higher-up - for example, cache the results of stream-reading and deserializing to the local types in your app.
-
The
HttpResponseMessage
can containHttpContent
which behaves like a forward-only stream - you can only read it once. This can mean that when CachePolicy retrieves it from cache the second time, the stream cannot be re-read unless you also reinitialise the stream pointer. -
Consider de-personalisation and timestamping. Personal information (if any) and timestamps from a cached result may not be appropriate to re-supply to later requesters.
-
Exercise care to only cache 200 OK responses. Consider using code such as
response.EnsureSuccessStatusCode();
to ensure that only successful responses pass to the cache policy. Or you can use a customITtlStrategy
as described here.
An execution-scoped instance of the class Polly.Context
travels with every execution through a Polly policy. The role of this class is to provide context and to allow the exchange of information between the pre-execution, mid-execution, and post-execution phases.
For executions through HttpClient
s configured with Polly via HttpClientFactory, you can use the extension method HttpRequestMessage.SetPolicyExecutionContext(context)
, prior to execution, to set the Polly.Context
that will be used with the Http call. Context
has dictionary-semantics, allowing you to pass any arbitrary data.
var context = new Polly.Context();
context["MyCustomData"] = foo;
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.SetPolicyExecutionContext(context);
var response = await client.SendAsync(request, cancellationToken);
Polly passes that Context
instance as an input parameter to any delegate hooks such as onRetry
configured on the policy. For example, the HttpClient
may have been pre-configured with a policy:
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
},
onRetryAsync: async (outcome, timespan, retryCount, context) => {
/* Do something with context["MyCustomData"] */
// ...
});
Delegate hooks may also set information on Context:
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
},
onRetryAsync: async (outcome, timespan, retryCount, context) => {
context["RetriesInvoked"] = retryCount;
// ...
});
And this information can be read from the context after execution:
var response = await client.SendAsync(request, cancellationToken);
var context = response.RequestMessage?.GetPolicyExecutionContext(); // (if not already held in a local variable)
if (context?.TryGetValue("RetriesInvoked", out int? retriesNeeded) ?? false)
{
// Do something with int? retriesNeeded
}
Note that the context from HttpRequestMessage.GetPolicyExecutionContext()
is only available post-execution if you used HttpRequestMessage.SetPolicyExecutionContext(Context)
to set a context prior to execution.
Going forward, Polly vNext and a future ASPNET Core release intend to integrate policy logging with HttpClient factory, so that policies applied to clients configured via HttpClient factory will log - with no extra user configuration needed - to the same loggers used by the HttpClient
instance.
- Home
- Polly RoadMap
- Contributing
- Transient fault handling and proactive resilience engineering
- Supported targets
- Retry
- Circuit Breaker
- Advanced Circuit Breaker
- Timeout
- Bulkhead
- Cache
- Rate-Limit
- Fallback
- PolicyWrap
- NoOp
- PolicyRegistry
- Polly and HttpClientFactory
- Asynchronous action execution
- Handling InnerExceptions and AggregateExceptions
- Statefulness of policies
- Keys and Context Data
- Non generic and generic policies
- Polly and interfaces
- Some policy patterns
- Debugging with Polly in Visual Studio
- Unit-testing with Polly
- Polly concept and architecture
- Polly v6 breaking changes
- Polly v7 breaking changes
- DISCUSSION PROPOSAL- Polly eventing and metrics architecture