Skip to content

HCR041

Unsafe HTTP methods should not be retried unless explicitly configured.

Why

The standard resilience handler can retry requests. Retrying POST, PUT, PATCH, DELETE, or CONNECT can duplicate side effects unless the endpoint is explicitly idempotent or retries exclude unsafe methods.

Bad

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

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

Better

services.AddHttpClient<PaymentsClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.DisableForUnsafeHttpMethods();
    });
services.AddHttpClient<PaymentsClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.ShouldHandle = args =>
            args.Outcome.Result?.RequestMessage?.Method == HttpMethod.Get;
    });

Current Detection

The implementation reports:

  • Typed-client IServiceCollection-shaped registrations that call AddStandardResilienceHandler() 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.

Typed-client call scanning validates that the unsafe method call is made on a simple HttpClient receiver, an explicitly qualified System.Net.Http.HttpClient receiver, or a this.-qualified HttpClient field/property, so qualified custom HttpClient types and lookalike PostAsync(...) methods on custom collaborators are skipped. Named-client call scanning validates the CreateClient(...) receiver as a simple IHttpClientFactory, explicitly qualified System.Net.Http.IHttpClientFactory, or this.-qualified factory field/property before following unsafe calls on the returned local until that local is reassigned. Named-client names and custom HttpMethod string names are matched when they are string literals or compile-time string constants; mutable variables and configuration values are intentionally not followed. The registration chain or visible unreassigned builder local initializer must visibly originate from IServiceCollection, including resolved or visibly declared minimal-hosting Services properties typed as IServiceCollection, so lookalike custom AddHttpClient(...).AddStandardResilienceHandler() APIs and unresolved builder Services chains are skipped. When Roslyn resolves the handler extension, it must be declared in Microsoft.Extensions.DependencyInjection (global-namespace declarations remain supported for lightweight source tests), so custom lookalike extensions are skipped. It does not report when the handler configuration calls the framework DisableForUnsafeHttpMethods() extension from Microsoft.Extensions.Http.Resilience (with global declarations supported for lightweight source tests) or when a visible Polly ShouldHandle property assignment uses an expression lambda or single-return block lambda composed exclusively from safe, symbol-resolved System.Net.Http.HttpMethod equality checks, safe-member .Equals(...) calls, or object.Equals(...) calls containing a safe method, including fully qualified member access and parenthesized or null-forgiving safe expressions; every || branch must be safe, while a safe equality on either side of && constrains the result. Inequality checks, unrestricted disjunctions, resolved custom lookalike guard methods, ShouldHandle properties, and HttpMethod types do not suppress the diagnostic.

Code Fix

For simple .AddStandardResilienceHandler() calls, the code fix adds:

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

The fix is withheld when the handler already has an options or predicate argument, so an existing retry policy is never overwritten; review and compose that policy manually.

Suppression

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

References