Compile-time safety for
.NETHttpClient,IHttpClientFactory, typed clients, handlers, retries, timeouts, and outbound HTTP resilience.
HttpClient.Resilience.Analyzers is a Roslyn analyzer package for catching fragile outbound HTTP code before it reaches production.
It focuses on the bugs that compile cleanly, pass code review, and then show up as socket exhaustion, stale DNS, duplicated writes, broken retries, handler scope leaks, undisposed responses, or brittle service-to-service calls.
Package name: HttpClient.Resilience.Analyzers
Repository name: HttpClient.Resilience.Analyzers
Primary keyword: .NET HttpClient analyzer
Secondary keywords: IHttpClientFactory analyzer, HttpClient resilience analyzer, Polly analyzer, Microsoft.Extensions.Http.Resilience analyzer, typed HttpClient analyzer, socket exhaustion analyzer
Suggested NuGet description:
Roslyn analyzers for .NET HttpClient, IHttpClientFactory, and Microsoft.Extensions.Http.Resilience. Catches socket exhaustion risks, DNS-stale clients, typed-client lifetime bugs, unsafe retries, handler scope leaks, response disposal mistakes, and fragile outbound HTTP patterns at compile time.
Suggested NuGet tags:
httpclient;ihttpclientfactory;resilience;polly;dotnet;csharp;roslyn;analyzer;analyser;aspnetcore;static-analysis;socket-exhaustion;typed-client;retry;resilience-pipeline
Most .NET teams use HttpClient, but many production failures come from subtle patterns that are hard to spot manually:
- Creating and disposing
HttpClientper request. - Holding factory-created clients for too long.
- Injecting typed clients into singleton services.
- Retrying unsafe methods such as
POST,PUT,PATCH, orDELETEwithout an explicit idempotency strategy. - Capturing request-scoped data inside
DelegatingHandlerinstances. - Using
ResponseHeadersReadwithout disposing theHttpResponseMessage. - Creating high-concurrency outbound HTTP fan-out without connection limits or bounded concurrency.
This analyzer should not be a generic style package. It should be a high-signal production-safety analyzer for outbound HTTP.
The production-safety analyzer for
.NEToutbound HTTP.
HttpClient.Resilience.Analyzers catches dangerous HttpClient and IHttpClientFactory patterns at compile time, including socket exhaustion risks, DNS staleness, typed-client lifetime bugs, unsafe retries, handler scope leaks, and response lifetime mistakes.
- A Roslyn analyzer package for
.NETservices and libraries. - Focused on outbound HTTP correctness and resilience.
- Opinionated, but conservative by default.
- Built for teams using
IHttpClientFactory, typed clients, named clients, Polly, orMicrosoft.Extensions.Http.Resilience.
- Not a formatting analyzer.
- Not a style analyzer.
- Not a generic performance analyzer.
- Not a replacement for runtime resilience policies.
- Not a package that fires 100 noisy warnings on day one.
Start with 10 rules. Make these excellent before adding more.
| Rule ID | Title | Default severity | Code fix | Notes |
|---|---|---|---|---|
HCR001 |
Do not create and dispose HttpClient per request |
Warning | Partial | Detect obvious new HttpClient() inside methods, loops, controllers, handlers, workers, and request paths. |
HCR002 |
Long-lived manual HttpClient should configure PooledConnectionLifetime |
Warning | Yes | Applies to static/singleton clients using default handler construction. |
HCR003 |
Do not cache IHttpClientFactory.CreateClient() results long-term |
Warning | Guide | Detect assignment of factory-created clients into singleton fields or static fields. |
HCR004 |
Do not inject typed HttpClient clients into singleton services |
Warning | Guide | Reuse lifetime graph logic from DI analyzer work. |
HCR005 |
Do not separately register a typed client already registered by AddHttpClient<T>() |
Warning | Yes | Avoids accidental duplicate or conflicting registrations. |
HCR020 |
DelegatingHandler should not capture scoped request data |
Warning | Guide | Detect IHttpContextAccessor, HttpContext, scoped services, user/session/request data in handlers. |
HCR040 |
Do not stack multiple standard resilience handlers | Warning | Yes | Detect duplicate AddStandardResilienceHandler() / resilience handler stacking. |
HCR041 |
Unsafe HTTP methods should not be retried unless explicitly configured | Warning | Yes | Hero rule. Warn on standard retry pipelines used with unsafe methods unless disabled or explicitly justified. |
HCR060 |
Dispose HttpResponseMessage when using ResponseHeadersRead |
Warning | Yes | Detect missing using, await using, try/finally, or ownership transfer. |
HCR080 |
High-concurrency HTTP fan-out should use bounded concurrency or connection limits | Suggestion | Guide | Heuristic. Keep low severity until proven. |
The standard HTTP resilience pipeline can retry requests. Retrying unsafe HTTP methods such as POST, PUT, PATCH, or DELETE may duplicate side effects unless the operation is explicitly idempotent.
HCR041: Standard resilience retries unsafe HTTP methods. Disable retries for POST/PUT/PATCH/DELETE unless the operation is idempotent.
services.AddHttpClient<PaymentsClient>()
.AddStandardResilienceHandler();public sealed class PaymentsClient(HttpClient httpClient)
{
public Task<HttpResponseMessage> CreatePaymentAsync(CreatePaymentRequest request, CancellationToken cancellationToken)
{
return httpClient.PostAsJsonAsync("/payments", request, cancellationToken);
}
}services.AddHttpClient<PaymentsClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.DisableForUnsafeHttpMethods();
});Suppress this rule only when at least one of the following is true:
- The endpoint is idempotent by design.
- The request uses idempotency keys.
- The retry predicate excludes unsafe methods.
- The API contract explicitly guarantees safe retries for the operation.
Suggested suppression comment:
#pragma warning disable HCR041 // Endpoint uses idempotency keys and is safe to retry.
services.AddHttpClient<PaymentsClient>()
.AddStandardResilienceHandler();
#pragma warning restore HCR041- Prefer fewer, stronger diagnostics. A warning should feel like a production incident avoided.
- Only warn when the analyzer has enough evidence. Use
InfoorSuggestionfor heuristics. - Every rule needs docs. Each rule page should explain the bug, the fix, and safe suppression cases.
- Every code fix must be boring and safe. Do not rewrite architecture automatically.
- Make brownfield adoption painless. Provide
.editorconfigprofiles for different strictness levels.
HttpClient.Resilience.Analyzers/
README.md
LICENSE
Directory.Build.props
Directory.Packages.props
global.json
src/
HttpClient.Resilience.Analyzers/
Diagnostics/
DiagnosticIds.cs
DiagnosticCategories.cs
DiagnosticDescriptors.cs
Analyzers/
Lifetime/
HCR001_NewHttpClientInRequestPathAnalyzer.cs
HCR002_LongLivedHttpClientWithoutPooledConnectionLifetimeAnalyzer.cs
HCR003_CachedFactoryClientAnalyzer.cs
TypedClients/
HCR004_TypedClientInjectedIntoSingletonAnalyzer.cs
HCR005_DuplicateTypedClientRegistrationAnalyzer.cs
Handlers/
HCR020_DelegatingHandlerCapturesScopedDataAnalyzer.cs
Resilience/
HCR040_StackedResilienceHandlersAnalyzer.cs
HCR041_UnsafeMethodRetryAnalyzer.cs
ResponseLifetime/
HCR060_ResponseHeadersReadDisposalAnalyzer.cs
Concurrency/
HCR080_UnboundedHttpFanOutAnalyzer.cs
CodeFixes/
HCR002_AddPooledConnectionLifetimeCodeFixProvider.cs
HCR005_RemoveDuplicateTypedClientRegistrationCodeFixProvider.cs
HCR041_DisableUnsafeMethodRetriesCodeFixProvider.cs
HCR060_DisposeResponseCodeFixProvider.cs
KnownSymbols/
HttpClientSymbols.cs
DependencyInjectionSymbols.cs
ResilienceSymbols.cs
PollySymbols.cs
Models/
HttpClientRegistrationModel.cs
ServiceRegistrationModel.cs
OutboundHttpCallModel.cs
Flow/
HttpClientSourceTracker.cs
ResponseDisposalTracker.cs
HttpClientResilienceAnalyzersResources.resx
HttpClient.Resilience.Analyzers.Package/
HttpClient.Resilience.Analyzers.Package.csproj
tests/
HttpClient.Resilience.Analyzers.Tests/
Lifetime/
TypedClients/
Handlers/
Resilience/
ResponseLifetime/
Concurrency/
TestInfrastructure/
samples/
HttpClient.Resilience.Showcase/
docs/
rules/
HCR001.md
HCR002.md
HCR003.md
HCR004.md
HCR005.md
HCR020.md
HCR040.md
HCR041.md
HCR060.md
HCR080.md
adoption.md
configuration.md
false-positive-policy.md
profiles/
default.editorconfig
strict-ci.editorconfig
brownfield-adoption.editorconfig
library-author.editorconfig
Use HCR as the prefix:
HCR = HttpClient Resilience
Reserved ranges:
| Range | Area |
|---|---|
HCR001 - HCR019 |
HttpClient lifetime and factory usage |
HCR020 - HCR039 |
Handlers, DI scopes, and request data |
HCR040 - HCR059 |
Resilience, retries, timeouts, Polly, Microsoft.Extensions.Http.Resilience |
HCR060 - HCR079 |
Request/response correctness |
HCR080 - HCR099 |
High-load, concurrency, and fan-out |
HCR100+ |
Future rules |
- Create analyzer project targeting
netstandard2.0. - Create test project using
Microsoft.CodeAnalysis.CSharp.Analyzer.Testing. - Add shared test helpers for verifying diagnostics and code fixes.
- Add
DiagnosticIds, categories, and descriptor helpers. - Add NuGet packaging metadata.
- Add CI build running tests and package validation.
- Implement
HCR001for obvious method-localnew HttpClient()usage. - Implement
HCR002for static/singletonHttpClientwithoutSocketsHttpHandler.PooledConnectionLifetime. - Implement
HCR003for factory-created clients stored in static fields or singleton services. - Add docs and examples for all three rules.
- Build a lightweight
IServiceCollectionregistration model. - Track
AddSingleton,AddScoped,AddTransient,AddHttpClient, and typed client registrations. - Implement
HCR004for typed clients injected into singletons. - Implement
HCR005for duplicate typed client registration. - Add docs, tests, and safe suppression guidance.
- Track
AddStandardResilienceHandlerand custom resilience handler registrations. - Implement
HCR040for stacked standard resilience handlers. - Implement
HCR041for unsafe HTTP methods with retry-enabled clients. - Add code fix for
DisableForUnsafeHttpMethods(). - Make
HCR041the hero demo in the README.
- Implement
HCR060forHttpCompletionOption.ResponseHeadersReadwithout response disposal. - Detect
using var,using (...),try/finally, and ownership transfer patterns. - Add code fix for simple local variable cases.
- Add rule documentation pages.
- Add
brownfield-adoption.editorconfig. - Add sample project with intentionally bad examples.
- Add README badges.
- Publish
0.1.0-preview.1. - Create launch blog post.
Detect:
using var client = new HttpClient();using (var client = new HttpClient())
{
// ...
}foreach (var item in items)
{
var client = new HttpClient();
await client.GetAsync(item.Url);
}Start conservative:
- Warn when
new HttpClient()appears inside a method body and the containing type looks like a controller, endpoint, worker, handler, service, or repository. - Increase confidence if the client is disposed in the same method.
- Increase confidence if it appears inside a loop.
- Do not warn for obvious tests unless test-analysis mode is enabled.
Detect:
private static readonly HttpClient Client = new();private static readonly HttpClient Client = new HttpClient();Suggest:
private static readonly HttpClient Client = new(
new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});Avoid warning when:
- A
SocketsHttpHandleris supplied withPooledConnectionLifetime. - The client is clearly created by
IHttpClientFactory. - The code is in generated files.
Detect:
public sealed class MySingleton
{
private readonly HttpClient _client;
public MySingleton(IHttpClientFactory factory)
{
_client = factory.CreateClient("github");
}
}Warn when the containing service is known singleton or when assigned to static fields.
Suggested remediation:
public sealed class MySingleton
{
private readonly IHttpClientFactory _factory;
public MySingleton(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/repos");
var client = _factory.CreateClient("github");
using var response = await client.SendAsync(request, cancellationToken);
}
}Detect:
services.AddHttpClient<PaymentsClient>();
services.AddSingleton<PaymentJob>();
public sealed class PaymentJob(PaymentsClient paymentsClient)
{
}Suggested remediation options:
- Make the consuming service scoped/transient if appropriate.
- Inject
IHttpClientFactoryinstead. - Use a scoped service boundary.
- Use a long-lived manually configured client with
SocketsHttpHandlerwhere appropriate.
Do not provide an automatic code fix unless the lifetime change is obvious and safe.
Detect:
services.AddHttpClient<PaymentsClient>();
services.AddTransient<PaymentsClient>();Fix:
services.AddHttpClient<PaymentsClient>();Detect:
public sealed class UserHeaderHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var userId = httpContextAccessor.HttpContext?.User.FindFirst("sub")?.Value;
request.Headers.Add("X-User-Id", userId);
return base.SendAsync(request, cancellationToken);
}
}Start with high-confidence dependencies:
IHttpContextAccessorHttpContextClaimsPrincipalISession- Known scoped services from the service registration graph
Suggested remediation:
- Pass request data through
HttpRequestMessage.Options. - Use a custom scope-aware wrapper outside the cached handler pipeline.
- Keep handlers stateless where possible.
Detect:
services.AddHttpClient<GitHubClient>()
.AddStandardResilienceHandler()
.AddStandardResilienceHandler();Fix:
services.AddHttpClient<GitHubClient>()
.AddStandardResilienceHandler();Also consider duplicate named pipeline registrations in later versions.
Detect clients where:
AddStandardResilienceHandler()is used.- Retry configuration has not disabled unsafe HTTP methods.
- The typed client or named client sends unsafe methods.
Unsafe methods:
POST
PUT
PATCH
DELETE
CONNECT
Potentially safe methods:
GET
HEAD
OPTIONS
TRACE
Do not warn when:
DisableForUnsafeHttpMethods()is used.- Retry predicates explicitly exclude unsafe methods.
- The method is unknown and no outbound calls are visible.
Detect:
var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);Fix:
using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);Avoid warning when:
- Response is returned to caller.
- Response is assigned to an owning wrapper.
- Response is disposed in a
finallyblock. - Response is used in a
usingstatement/declaration.
Detect obvious cases:
await Task.WhenAll(items.Select(item => client.GetAsync(item.Url, cancellationToken)));Suggested remediation:
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = 8,
CancellationToken = cancellationToken
}, async (item, ct) =>
{
using var response = await client.GetAsync(item.Url, ct);
});Keep as Suggestion initially because this rule is heuristic.
[*.cs]
dotnet_diagnostic.HCR001.severity = warning
dotnet_diagnostic.HCR002.severity = warning
dotnet_diagnostic.HCR003.severity = warning
dotnet_diagnostic.HCR004.severity = warning
dotnet_diagnostic.HCR005.severity = warning
dotnet_diagnostic.HCR020.severity = warning
dotnet_diagnostic.HCR040.severity = warning
dotnet_diagnostic.HCR041.severity = warning
dotnet_diagnostic.HCR060.severity = warning
dotnet_diagnostic.HCR080.severity = suggestion[*.cs]
dotnet_diagnostic.HCR001.severity = suggestion
dotnet_diagnostic.HCR002.severity = suggestion
dotnet_diagnostic.HCR003.severity = suggestion
dotnet_diagnostic.HCR004.severity = suggestion
dotnet_diagnostic.HCR005.severity = suggestion
dotnet_diagnostic.HCR020.severity = suggestion
dotnet_diagnostic.HCR040.severity = warning
dotnet_diagnostic.HCR041.severity = warning
dotnet_diagnostic.HCR060.severity = warning
dotnet_diagnostic.HCR080.severity = silent[*.cs]
dotnet_diagnostic.HCR001.severity = error
dotnet_diagnostic.HCR002.severity = error
dotnet_diagnostic.HCR003.severity = error
dotnet_diagnostic.HCR004.severity = error
dotnet_diagnostic.HCR005.severity = error
dotnet_diagnostic.HCR020.severity = error
dotnet_diagnostic.HCR040.severity = error
dotnet_diagnostic.HCR041.severity = error
dotnet_diagnostic.HCR060.severity = error
dotnet_diagnostic.HCR080.severity = warning# HttpClient.Resilience.Analyzers
Compile-time safety for `.NET` `HttpClient`, `IHttpClientFactory`, typed clients, retries, handlers, and outbound HTTP resilience.
## What it catches
- `HttpClient` created and disposed per request.
- Static/manual `HttpClient` instances without connection lifetime configuration.
- Factory-created clients cached in singleton services.
- Typed clients injected into singleton services.
- Duplicate typed client registrations.
- `DelegatingHandler` implementations that capture scoped request data.
- Stacked resilience handlers.
- Unsafe HTTP methods retried without explicit idempotency configuration.
- `ResponseHeadersRead` responses that are not disposed.
- Obvious unbounded outbound HTTP fan-out.
## Install
```bash
dotnet add package HttpClient.Resilience.Analyzersservices.AddHttpClient<PaymentsClient>()
.AddStandardResilienceHandler();HCR041 warns because the standard resilience pipeline may retry unsafe HTTP methods such as POST, PUT, PATCH, and DELETE unless configured otherwise.
services.AddHttpClient<PaymentsClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.DisableForUnsafeHttpMethods();
});
## Test strategy
Use analyzer tests for each rule covering:
- Happy path: no diagnostic.
- One obvious diagnostic.
- Multiple diagnostics in one file.
- Code fix output, where applicable.
- Top-level statements.
- Minimal API registrations.
- Traditional `Startup` registrations.
- Extension method registrations.
- Generic typed clients.
- Named clients.
- False-positive cases.
Example test naming convention:
```text
HCR041_WhenStandardResilienceHandlerRetriesPost_ReportsDiagnostic
HCR041_WhenUnsafeMethodsAreDisabled_DoesNotReport
HCR041_WhenOnlyGetIsUsed_DoesNotReport
HCR041_CodeFix_AddsDisableForUnsafeHttpMethods
Implement this first:
HCR060 - Dispose HttpResponseMessage when using ResponseHeadersRead
Why start here?
- It is local to a method.
- It does not require full DI graph analysis.
- It has a clean code fix.
- It proves the analyzer/code-fix/test infrastructure quickly.
Then implement:
HCR001 - Do not create and dispose HttpClient per request
HCR041 - Unsafe HTTP methods should not be retried unless explicitly configured
This gives the project three strong demos:
- Lifetime safety.
- Response lifetime safety.
- Resilience/retry safety.
- Package builds locally.
- Analyzer tests pass.
- Code-fix tests pass.
- README includes install instructions.
- README includes before/after examples.
- Each diagnostic has a docs page.
- Each diagnostic has suppression guidance.
- NuGet package contains analyzer DLL in the correct analyzer path.
- Package icon, license, repository URL, and tags are configured.
- CI runs build, test, pack.
- Sample project demonstrates every MVP rule.
- Publish
0.1.0-preview.1.
Do not build these until the MVP rules are stable:
| Rule ID | Idea |
|---|---|
HCR061 |
Do not ignore unsuccessful HTTP responses. |
HCR062 |
Prefer per-request headers over mutating shared DefaultRequestHeaders. |
HCR063 |
Avoid sync-over-async around outbound HTTP. |
HCR064 |
Use cancellation-aware HTTP APIs where available. |
HCR081 |
Streaming responses should pass cancellation and dispose streams. |
HCR082 |
Avoid per-request creation of resilience pipelines. |
HCR083 |
Warn when typed clients have no BaseAddress and use relative URLs. |
HCR084 |
Warn when named client names are stringly duplicated. |
HttpClientguidelines for.NETIHttpClientFactoryusage and troubleshootingMicrosoft.Extensions.Http.ResiliencedocumentationPollyretry and resilience documentation- NuGet analyzer packaging conventions
- Roslyn analyzer testing documentation
Title:
Implement HCR060: Dispose HttpResponseMessage when using ResponseHeadersRead
Body:
## Goal
Detect calls to `HttpClient.SendAsync` using `HttpCompletionOption.ResponseHeadersRead` where the returned `HttpResponseMessage` is not disposed.
## Bad
```csharp
var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);using var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);- Reports diagnostic for a local response variable that is not disposed.
- Does not report when response is used in a
using vardeclaration. - Does not report when response is used in a
usingstatement. - Does not report when response is returned to the caller.
- Provides a code fix for simple local variable declarations.
- Adds docs page at
docs/rules/HCR060.md.
## Final product north star
This package should become the thing a senior `.NET` engineer installs and says:
> “Yep. This catches the exact kind of `HttpClient` bugs that cost us incidents.”
Keep it practical. Keep it high-signal. Make every warning earn its place.