HCR043¶
Unsafe HTTP methods should not be retried by custom resilience pipelines.
Why¶
AddResilienceHandler is the common way to build a named Polly pipeline on an HttpClient. AddRetry(new HttpRetryStrategyOptions()) still retries POST, PUT, PATCH, DELETE, and CONNECT unless DisableForUnsafeHttpMethods() or a safe-method-only ShouldHandle predicate is configured. Retrying those methods can duplicate side effects—charges, orders, mutations—unless the operation is explicitly idempotent.
HCR041 covers AddStandardResilienceHandler(). This rule covers the custom pipeline shape that HCR041 does not see.
Bad¶
services.AddHttpClient<PaymentsClient>()
.AddResilienceHandler("payments", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions());
});
public sealed class PaymentsClient(HttpClient httpClient)
{
public Task<HttpResponseMessage> CreateAsync(CancellationToken cancellationToken)
{
return httpClient.PostAsync("/payments", null, cancellationToken);
}
}
Better¶
services.AddHttpClient<PaymentsClient>()
.AddResilienceHandler("payments", builder =>
{
var retryOptions = new HttpRetryStrategyOptions();
retryOptions.DisableForUnsafeHttpMethods();
builder.AddRetry(retryOptions);
});
services.AddHttpClient<PaymentsClient>()
.AddResilienceHandler("payments", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
ShouldHandle = args =>
args.Outcome.Result?.RequestMessage?.Method == HttpMethod.Get
});
});
A timeout-only or circuit-breaker-only pipeline does not retry, so this rule stays quiet when AddRetry is absent.
Current Detection¶
The implementation reports:
- Framework
AddResilienceHandlerregistrations (Microsoft.Extensions.DependencyInjection, with global-namespace stubs supported for lightweight source tests) whose configure callback is an inline lambda, anonymous method, or local function in the same method and contains a frameworkAddRetry(Pollyor global namespace) whose receiver is the pipeline builder parameter or a fluent chain from it (AddTimeout(...).AddRetry(...)). - Typed-client
IServiceCollection-shaped registrations, includingAddHttpClient<TService, TImplementation>()and registrations split through a visible unreassignedIHttpClientBuilderlocal, when the implementation class visibly sends an unsafe HTTP method. - Named-client registrations such as
AddHttpClient("payments")orAddHttpClient(ClientNames.Payments)when visible code callsCreateClientwith the same literal or compile-time constant and then sends an unsafe method. Send/SendAsyncrequest shapes with literal or constant unsafeHttpMethodvalues, includingCONNECT. Unsafe methods arePOST,PUT,PATCH,DELETE, andCONNECT.
The diagnostic is placed on the AddRetry name token. It uses the same compilation-wide unsafe-call index as HCR041 and HCR042, built once per compilation and only after a framework AddResilienceHandler with a visible builder-bound AddRetry is paired with a typed or named client.
It does not report when:
- the callback has no builder-bound
AddRetry(timeout, circuit breaker, or rate limiter only); DisableForUnsafeHttpMethods()is invoked on the retry options in the callback (Microsoft.Extensions.Http.Resilienceor global);- a visible Polly.Retry
ShouldHandleassignment is composed exclusively from safeHttpMethodchecks; MaxRetryAttemptsis a literal0on the retry options passed to thatAddRetry;AddRetryis invoked on a different pipeline builder than the callback parameter;- the configure argument is a method group on another type;
- lookalike custom
AddResilienceHandler/AddRetry/DisableForUnsafeHttpMethodsAPIs are used; - no visible unsafe typed-client or named-client call exists.
It does not infer retries from static ResiliencePipelineBuilder instances that are not wired through AddResilienceHandler, or from configuration-driven / mutable method names.
Code Fix¶
When AddRetry is passed new HttpRetryStrategyOptions { ... }, the code fix introduces a local, calls DisableForUnsafeHttpMethods(), and passes that local to AddRetry. Expression lambdas are converted to blocks. The fix is withheld when the argument is not an object creation of HttpRetryStrategyOptions (including generic RetryStrategyOptions<T> and existing locals), so an existing retry policy is never overwritten.
Suppression¶
Suppress only when the endpoint is idempotent by contract, the request uses idempotency keys, or retry predicates exclude unsafe methods in a way the analyzer cannot model.