Skip to content

Fix default HTTP resilience not to retry non-idempotent requests (POST, PUT, etc.) #5248

Description

@SteveSandersonMS

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.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.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions