关于 http-client-resilience
http-client-resilience 是面向 ASP.NET Core 开发者的 HTTP 通信可靠性模式技能,介绍如何结合 IHttpClientFactory 与 Polly 处理重试、熔断和超时等问题。
主要功能
- 01配置命名及类型化 HttpClient
- 02设置重试、熔断和超时策略
- 03构建自定义 Polly 管道
- 04记录请求、响应与标头
- 05集成外部服务健康检查
适用场景
- 01提高外部 API 调用的容错性
- 02为关键 HTTP 服务设置专属策略
- 03监控外部服务的可用状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill http-client-resilience安装后在 Claude Code 中输入/http-client-resilience确认已加载。
详细介绍
原理
对外部服务的 HTTP 调用本质上并不可靠。在分布式系统中,网络问题、服务中断和暂时性故障都很常见。若没有恰当的弹性模式,应用将遭遇级联故障。使用 IHttpClientFactory 和 Polly 实现这些模式,可为 HTTP 通信提供生产级可靠性。
模式
模式 1:使用弹性的命名 HttpClient
配置命名客户端,并采用全面的弹性策略,包括重试、熔断和超时。
// Program.cs - Configuration
builder.Services.AddHttpClient("PaymentApi", client =>
{
client.BaseAddress = new Uri("https://api.payment-provider.com/v1/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("X-API-Key", builder.Configuration["PaymentApi:Key"]!);
})
.AddStandardResilienceHandler(options =>
{
// Retry configuration
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
// Circuit breaker configuration
options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(1);
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 10;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
// Timeout configuration
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(10);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});
// Typed client for type-safe usage
public interface IPaymentClient
{
Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default);
Task<RefundResult> ProcessRefundAsync(string transactionId, CancellationToken ct = default);
}
public class PaymentClient : IPaymentClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<PaymentClient> _logger;
public PaymentClient(IHttpClientFactory httpClientFactory, ILogger<PaymentClient> logger)
{
_httpClient = httpClientFactory.CreateClient("PaymentApi");
_logger = logger;
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken ct = default)
{
var response = await _httpClient.PostAsJsonAsync("payments", request, ct);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Payment API rate limit hit");
throw new PaymentRateLimitException("Payment provider is experiencing high load");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>(ct)
?? throw new PaymentException("Invalid response from payment provider");
}
public async Task<RefundResult> ProcessRefundAsync(string transactionId, CancellationToken ct = default)
{
var response = await _httpClient.PostAsync($"payments/{transactionId}/refund", null, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RefundResult>(ct)
?? throw new PaymentException("Invalid response");
}
}
模式 2:使用 Polly 自定义弹性管道
对于高级场景,可构建自定义 Polly 管道,针对不同类型的故障进行专门处理。
// Custom resilience pipeline configuration
builder.Services.AddResiliencePipeline("critical-api", builder =>
{
// Add retry with specific handling
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = args => args.Outcome switch
{
{ Result: { StatusCode: HttpStatusCode.TooManyRequests } } => PredicateResult.True(),
{ Result: { StatusCode: HttpStatusCode.ServiceUnavailable } } => PredicateResult.True(),
{ Result: { StatusCode: HttpStatusCode.GatewayTimeout } } => PredicateResult.True(),
{ Exception: HttpRequestException } => PredicateResult.True(),
{ Exception: TimeoutRejectedException } => PredicateResult.True(),
_ => PredicateResult.False()
},
OnRetry = args =>
{
Console.WriteLine($"Retry {args.AttemptNumber} for {args.Outcome.Result?.RequestMessage?.RequestUri}");
return ValueTask.CompletedTask;
}
});
// Add circuit breaker
builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
SamplingDuration = TimeSpan.FromMinutes(2),
FailureRatio = 0.6,
MinimumThroughput = 20,
BreakDuration = TimeSpan.FromMinutes(2),
ShouldHandle = args => args.Outcome.Result?.IsSuccessStatusCode is false
? PredicateResult.True()
: PredicateResult.False(),
OnOpened = args =>
{
Console.WriteLine($"Circuit opened! {args.FailureRatio * 100}% failure rate");
return ValueTask.CompletedTask;
},
OnClosed = args =>
{
Console.WriteLine("Circuit closed - service recovered");
return ValueTask.CompletedTask;
}
});
// Add timeout per attempt
builder.AddTimeout(TimeSpan.FromSeconds(15));
});
// Usage with typed client
public class InventoryClient
{
private readonly HttpClient _httpClient;
private readonly ResiliencePipeline<HttpResponseMessage> _pipeline;
public InventoryClient(
IHttpClientFactory factory,
ResiliencePipelineProvider<HttpResponseMessage> pipelineProvider)
{
_httpClient = factory.CreateClient("InventoryApi");
_pipeline = pipelineProvider.GetPipeline("critical-api");
}
public async Task<StockLevel> GetStockAsync(string sku, CancellationToken ct = default)
{
var response = await _pipeline.ExecuteAsync(
async token => await _httpClient.GetAsync($"stock/{sku}", token),
ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StockLevel>(ct)
?? throw new InvalidOperationException("Invalid response");
}
}
模式 3:集成 Razor Pages
在 Razor Pages 中正确集成 HTTP 客户端,并妥善处理释放和错误。
// Typed client registration
builder.Services.AddHttpClient<IGeoLocationService, GeoLocationService>(client =>
{
client.BaseAddress = new Uri("https://api.geolocation.com/");
client.Timeout = TimeSpan.FromSeconds(10);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(60);
});
// Service implementation
public interface IGeoLocationService
{
Task<LocationInfo?> GetLocationAsync(string ipAddress, CancellationToken ct = default);
}
public class GeoLocationService : IGeoLocationService
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeoLocationService> _logger;
public GeoLocationService(HttpClient httpClient, ILogger<GeoLocationService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<LocationInfo?> GetLocationAsync(string ipAddress, CancellationToken ct = default)
{
try
{
var response = await _httpClient.GetAsync($"json/{ipAddress}", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LocationInfo>(ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to get location for IP {Ip}", ipAddress);
return null; // Graceful degradation
}
}
}
// PageModel usage
public class AnalyticsModel : PageModel
{
private readonly IGeoLocationService _geoService;
private readonly ILogger<AnalyticsModel> _logger;
[BindProperty]
public LocationInfo? Location { get; set; }
public string? ErrorMessage { get; set; }
public async Task OnGetAsync()
{
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "8.8.8.8";
try
{
Location = await _geoService.GetLocationAsync(ipAddress);
if (Location is null)
{
ErrorMessage = "Unable to determine location";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in analytics page");
ErrorMessage = "Service temporarily unavailable";
}
}
}
模式 4:请求/响应日志记录与标头
实现恰当的日志记录和自定义标头,以支持可观测性和身份验证。
// Delegating handler for request/response logging
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid().ToString("N")[..8];
request.Headers.Add("X-Request-ID", requestId);
_logger.LogInformation(
"[{RequestId}] HTTP {Method} {Uri}",
requestId,
request.Method,
request.RequestUri);
var stopwatch = Stopwatch.StartNew();
try
{
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"[{RequestId}] HTTP {StatusCode} in {ElapsedMs}ms",
requestId,
(int)response.StatusCode,
stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"[{RequestId}] HTTP request failed after {ElapsedMs}ms",
requestId,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
// Authentication handler
public class ApiKeyHandler : DelegatingHandler
{
private readonly string _apiKey;
public ApiKeyHandler(string apiKey)
{
_apiKey = apiKey;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
return base.SendAsync(request, cancellationToken);
}
}
// Registration with handlers
builder.Services.AddTransient<LoggingHandler>();
builder.Services.AddHttpClient("SecureApi", (sp, client) =>
{
client.BaseAddress = new Uri("https://api.secure-service.com/");
})
.AddHttpMessageHandler<LoggingHandler>()
.AddHttpMessageHandler(sp => new ApiKeyHandler(
sp.GetRequiredService<IConfiguration>()["ApiKeys:SecureService"]!));
模式 5:集成运行状况检查
集成 HTTP 客户端运行状况检查,以监控服务。
// Custom health check for external services
public class ExternalApiHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
public ExternalApiHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var client = _httpClientFactory.CreateClient("PaymentApi");
var response = await client.GetAsync("health", cancellationToken);
if (response.IsSuccessStatusCode)
{
return HealthCheckResult.Healthy("Payment API is accessible");
}
return HealthCheckResult.Degraded(
$"Payment API returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Payment API is unreachable", ex);
}
}
}
// Registration in Program.cs
builder.Services.AddHealthChecks()
.AddCheck<ExternalApiHealthCheck>("payment-api");
// Or use built-in URI health check
builder.Services.AddHealthChecks()
.AddUrlGroup(
new Uri("https://api.service.com/health"),
name: "external-service",
failureStatus: HealthStatus.Degraded,
tags: new[] { "external" });
反模式
// ❌ BAD: Using HttpClient directly with 'new'
var client = new HttpClient(); // Socket exhaustion!
var response = await client.GetAsync("https://api.example.com/data");
// ✅ GOOD: Use IHttpClientFactory
public class GoodService
{
private readonly IHttpClientFactory _factory;
public GoodService(IHttpClientFactory factory) => _factory = factory;
public async Task GetDataAsync()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("https://api.example.com/data");
}
}
// ❌ BAD: Static/shared HttpClient instance
public class BadService
{
private static readonly HttpClient _client = new(); // DNS changes not respected!
public async Task GetDataAsync()
{
var response = await _client.GetAsync("...");
}
}
// ❌ BAD: No timeout handling
public async Task<string> FetchDataAsync()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("https://slow-api.com/data"); // Hangs forever!
return await response.Content.ReadAsStringAsync();
}
// ✅ GOOD: Set timeout and handle cancellation
public async Task<string?> FetchDataAsync(CancellationToken ct)
{
var client = _factory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
try
{
var response = await client.GetAsync("https://slow-api.com/data", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(ct);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError("Request timed out");
return null;
}
}
// ❌ BAD: Swallowing HTTP errors without context
try
{
var response = await client.GetAsync("/api/data");
return await response.Content.ReadFromJsonAsync<Data>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Request failed"); // No context about the failure
return null;
}
// ✅ GOOD: Specific error handling with context
try
{
var response = await client.GetAsync("/api/data", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
{
_logger.LogWarning("Data not found for ID {Id}", id);
return null;
}
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
_logger.LogWarning("Rate limited by external API");
throw new RateLimitException("Please try again later");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Data>(ct);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
_logger.LogError(ex, "External service unavailable");
throw new ServiceUnavailableException("Service temporarily unavailable");
}
// ❌ BAD: Not disposing HttpResponseMessage
var response = await client.GetAsync("/api/data");
return await response.Content.ReadAsStringAsync();
// ✅ GOOD: Proper disposal
using var response = await client.GetAsync("/api/data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
// ❌ BAD: Blocking in async context
public string GetData()
{
var client = _factory.CreateClient();
var response = client.GetAsync("/api/data").Result; // Deadlock risk!
return response.Content.ReadAsStringAsync().Result;
}
// ✅ GOOD: Async all the way
public async Task<string> GetDataAsync(CancellationToken ct)
{
var client = _factory.CreateClient();
using var response = await client.GetAsync("/api/data", ct);
return await response.Content.ReadAsStringAsync(ct);
}
参考资料
评论
支持 Markdown