Skip to content

HCR042

Unsafe HTTP methods should not be hedged.

Why

AddStandardHedgingHandler() can send the same request to multiple endpoints at once. Hedging POST, PUT, PATCH, DELETE, or CONNECT can duplicate side effects concurrently—duplicate charges, duplicate orders—unless the operation is explicitly idempotent. This is stricter than retry: the copies overlap in flight instead of waiting for a failure.

Bad

services.AddHttpClient<PaymentsClient>()
    .AddStandardHedgingHandler();

public sealed class PaymentsClient(HttpClient httpClient)
{
    public Task<HttpResponseMessage> CreateAsync(CancellationToken cancellationToken)
    {
        return httpClient.PostAsync("/payments", null, cancellationToken);
    }
}

Better

Prefer standard resilience without unsafe-method retries when the client sends non-idempotent methods:

services.AddHttpClient<PaymentsClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.DisableForUnsafeHttpMethods();
    });

Or keep hedging only for a read-only client, and give unsafe methods their own named or typed client.

services.AddHttpClient<CatalogClient>()
    .AddStandardHedgingHandler();

services.AddHttpClient<PaymentsClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.DisableForUnsafeHttpMethods();
    });

A hedging ShouldHandle predicate that allows only safe methods also keeps this rule quiet:

services.AddHttpClient<MixedClient>()
    .AddStandardHedgingHandler(options =>
    {
        options.Hedging.ShouldHandle = args =>
            args.Outcome.Result?.RequestMessage?.Method == HttpMethod.Get;
    });

Current Detection

The implementation reports:

  • Typed-client IServiceCollection-shaped registrations that call AddStandardHedgingHandler() when the typed-client implementation class visibly calls an unsafe HttpClient method anywhere in the compilation, including resolved namespace-aware matching for qualified or unqualified typed-client registration names, the implementation type in AddHttpClient<TService, TImplementation>(), and registrations split through a visible IHttpClientBuilder local.
  • Named-client IServiceCollection-shaped registrations such as AddHttpClient("payments") or AddHttpClient(ClientNames.Payments) when visible code in the compilation calls CreateClient("payments") or the same compile-time constant and then sends an unsafe HTTP method, including registrations split through a visible IHttpClientBuilder local.
  • Send(...) and SendAsync(...) calls when the request is visibly created with an unsafe HttpMethod, including direct HttpRequestMessage construction, object initializers, simple local request variables that have not been reassigned before the send call, null-forgiving request and method expressions, and custom HttpMethod instances created from string literals or compile-time string constants. Unsafe methods are POST, PUT, PATCH, DELETE, and CONNECT.

It uses the same compilation-wide unsafe-call index as HCR041, built once per compilation and only after a framework hedging (or retry) handler is paired with a typed or named client. Custom lookalike AddStandardHedgingHandler extensions, unresolved builder Services chains, and non-IServiceCollection receivers are skipped. DisableForUnsafeHttpMethods() on retry options does not suppress this diagnostic, because it does not disable hedging. A visible Hedging.ShouldHandle assignment suppresses only when it compares the request method to a safe, symbol-resolved HttpMethod constant (GET / HEAD / OPTIONS / TRACE). Constant-vs-constant checks such as HttpMethod.Get == HttpMethod.Get, lookalike HttpMethod types, and unbound ShouldHandle / HttpMethod symbols do not suppress.

Code Fix

For simple .AddStandardHedgingHandler() calls, the code fix replaces hedging with:

.AddStandardResilienceHandler(options => options.Retry.DisableForUnsafeHttpMethods())

The title states the behavior change explicitly. The fix is withheld when the hedging call already has an options argument, so an existing hedging policy is never overwritten.

Suppression

Suppress only when the endpoint is idempotent by contract, the request uses idempotency keys, or hedging predicates exclude unsafe methods in a way the analyzer cannot model.

References