Original issue description added by @SteveSandersonMS
Currently if a backend service takes a while to handle a request (e.g., > 5 sec), the default HTTP resilience policy will retry.
This is fine for idempotent requests (e.g., GET), but dangerous/harmful for other request types (POST, PUT). While building eShopSupport I often encountered cases where it would create duplicate messages/tickets just because either:
[1] I was debugging, and hence causing things to be slow
[2] Or, some lower-level service was still starting up and loading an AI model or similar, causing it to be temporarily delayed
It's fine for developers to configure a retry policy for POST/PUT/etc themselves, but I don't think the default should be to retry those and was very surprised when finding that it is.
Background and motivation
Microsoft.Extensions.Http.Resilience package provides a delegating handler called StandardResilienceHandler that applies various resilience strategies (retry, timeout, rate limiting) to HttpClient. For example, it automatically makes retries of an HTTP request if it fails with a transient error. You can register StandardResilienceHandler for HttpClient as following:
services.AddHttpClient("test").AddStandardResilienceHandler();
or you can use an overload that accepts options configuring resilience strategies:
services.AddHttpClient("test").AddStandardResilienceHandler(options =>
{
// You can configure resilience options here.
});
Each resilience strategy of StandardResilienceHandler has a predicate deciding whether to apply the strategy or not based on the outcome of an HTTP request, i.e. whether the outcome is a transient error or not. Retry strategy by default is configured to make retries for all HTTP methods, i.e. it will make retries for both idempotent and non-idempotent HTTP methods. That brings the issue described above at the beginning of this comment. While the current default behavior might be undesired for some services, we don't want to change it because:
- In general, we cannot make assumptions about idempotency of APIs based on HTTP methods being used to access those APIs.
- Such a change will be breaking and would lead to unpleasant results for existing services that already use
StandardResilienceHandler.
As a compromise solution to the issue we would like to introduce a new API allowing to easily disable the default behavior and don't do retries for non-idempotent HTTP methods.
API Proposal
namespace Microsoft.Extensions.Http.Resilience;
/// <summary>
/// Extensions for HttpRetryStrategyOptionsExtensions.
/// </summary>
public static class HttpRetryStrategyOptionsExtensions
{
/// <summary>
/// Disables retries for POST, PATCH, PUT, DELETE, and CONNECT.
/// </summary>
/// <param name="option">Retry strategy options.</param>
public static void DisableForUnsafeHttpMethods(this HttpRetryStrategyOptions option);
/// <summary>
/// Disables retries for the given list of HTTP methods.
/// </summary>
/// <param name="options">Retry strategy options.</param>
/// <param name="methods">List of HTTP methods.</param>
public static void DisableFor(this HttpRetryStrategyOptions options, params HttpMethod[] methods);
}
According to RFC safe HTTP methods are those whose semantics is read-only. POST, PATCH, PUT, DELETE, and CONNECT are unsafe. While for PUT and DELETE doing retries might be safe, we propose an API method DisableForUnsafeHttpMethods disabling retries for all unsafe HTTP methods, i.e. HTTP methods that could be harmful for the server. Additionally, to have some flexibility we want to introduce a method DisableFor allowing users to specify which HTTP methods their service considers as non-retriable.
API Usage
DisableForUnsafeHttpMethods:
services.AddHttpClient("test").AddStandardResilienceHandler(options =>
{
options.Retry.DisableForUnsafeHttpMethods();
});
DisableFor:
services.AddHttpClient("test").AddStandardResilienceHandler(options =>
{
options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch);
});
Alternative Designs
Extend existing AddStandardResilienceHandler methods with an argument accepting a list of HTTP methods for which users don't want to do retries.
API:
namespace Microsoft.Extensions.DependencyInjection;
public static partial class ResilienceHttpClientBuilderExtensions
{
// Registers StandardResilienceHanlder in the given IHttpClientBuilder and configures its retry strategy to not do retries for the given list of HTTP methods.
public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, params HttpMethod[] nonIdempotentHttpMethods);
// Like the first method + accepts a delegate to configure resilience options.
public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, Action<HttpStandardResilienceOptions> configure, params HttpMethod[] nonIdempotentHttpMethods);
// Like the first method + accepts a configuration section to configure resilience options.
public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, IConfigurationSection section, params HttpMethod[] nonIdempotentHttpMethods);
}
Usage:
services.AddHttpClient("test").AddStandardResilienceHandler(HttpMethod.Post, HttpMethod.Patch);
The downside of this approach is that:
- We have to duplicate each overload of
AddStandardResilienceHandler method with an argument accepting a list of non-idempotent HTTP methods.
- A list of non-idempotent HTTP methods affects only
Retry strategy, but we pass it to a method registering a resilience handler also combining other resilience strategies, e.g. timeout, rate limiting, circuit breaker.
Risks
Not aware of any risks.
Original issue description added by @SteveSandersonMS
Currently if a backend service takes a while to handle a request (e.g., > 5 sec), the default HTTP resilience policy will retry.
This is fine for idempotent requests (e.g., GET), but dangerous/harmful for other request types (POST, PUT). While building eShopSupport I often encountered cases where it would create duplicate messages/tickets just because either:
[1] I was debugging, and hence causing things to be slow
[2] Or, some lower-level service was still starting up and loading an AI model or similar, causing it to be temporarily delayed
It's fine for developers to configure a retry policy for POST/PUT/etc themselves, but I don't think the default should be to retry those and was very surprised when finding that it is.
API Proposal added by @iliar-turdushev
Background and motivation
Microsoft.Extensions.Http.Resiliencepackage provides a delegating handler calledStandardResilienceHandlerthat applies various resilience strategies (retry, timeout, rate limiting) to HttpClient. For example, it automatically makes retries of an HTTP request if it fails with a transient error. You can registerStandardResilienceHandlerfor HttpClient as following:or you can use an overload that accepts options configuring resilience strategies:
Each resilience strategy of
StandardResilienceHandlerhas a predicate deciding whether to apply the strategy or not based on the outcome of an HTTP request, i.e. whether the outcome is a transient error or not.Retrystrategy by default is configured to make retries for all HTTP methods, i.e. it will make retries for both idempotent and non-idempotent HTTP methods. That brings the issue described above at the beginning of this comment. While the current default behavior might be undesired for some services, we don't want to change it because:StandardResilienceHandler.As a compromise solution to the issue we would like to introduce a new API allowing to easily disable the default behavior and don't do retries for non-idempotent HTTP methods.
API Proposal
According to RFC safe HTTP methods are those whose semantics is read-only. POST, PATCH, PUT, DELETE, and CONNECT are unsafe. While for PUT and DELETE doing retries might be safe, we propose an API method
DisableForUnsafeHttpMethodsdisabling retries for all unsafe HTTP methods, i.e. HTTP methods that could be harmful for the server. Additionally, to have some flexibility we want to introduce a methodDisableForallowing users to specify which HTTP methods their service considers as non-retriable.API Usage
DisableForUnsafeHttpMethods:DisableFor:Alternative Designs
Extend existing
AddStandardResilienceHandlermethods with an argument accepting a list of HTTP methods for which users don't want to do retries.API:
Usage:
The downside of this approach is that:
AddStandardResilienceHandlermethod with an argument accepting a list of non-idempotent HTTP methods.Retrystrategy, but we pass it to a method registering a resilience handler also combining other resilience strategies, e.g. timeout, rate limiting, circuit breaker.Risks
Not aware of any risks.