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 callAddStandardResilienceHandler()when the typed-client implementation class visibly calls an unsafeHttpClientmethod anywhere in the compilation, including resolved namespace-aware matching for qualified or unqualified typed-client registration names, the implementation type inAddHttpClient<TService, TImplementation>(), and registrations split through a visibleIHttpClientBuilderlocal. - Named-client
IServiceCollection-shaped registrations such asAddHttpClient("payments")orAddHttpClient(ClientNames.Payments)when visible code in the compilation callsCreateClient("payments")or the same compile-time constant and then sends an unsafe HTTP method, including registrations split through a visibleIHttpClientBuilderlocal. Send(...)andSendAsync(...)calls when the request is visibly created with an unsafeHttpMethod, including directHttpRequestMessageconstruction, object initializers, simple local request variables that have not been reassigned before the send call, null-forgiving request and method expressions, and customHttpMethodinstances created from string literals or compile-time string constants. Unsafe methods arePOST,PUT,PATCH,DELETE, andCONNECT.
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:
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.