Feedback

Robuste HTTP-Calls in .NET 8 ohne eigene Retry-Logik

Sprache: C#

.NET 8+ bringt eine integrierte Resilience-Pipeline für HttpClient mit, die typische Fehlerfälle bei HTTP-Aufrufen sauber abfedert – ganz ohne selbstgeschriebene Retry-Schleifen oder externe Libraries. Besonders relevant sind dabei temporäre Fehler, bei denen ein erneuter Versuch sinnvoll ist: [b]429 – Too Many Requests[/b] [u](Server lehnt Anfragen wegen Rate Limiting ab)[/u] [b]503 – Service Unavailable[/b] [u](Dienst ist kurzzeitig nicht erreichbar oder überlastet)[/u] Genau für solche Situationen stellt [b]AddStandardResilienceHandler[/b] sinnvolle Defaults bereit: Retries mit Backoff, Timeouts pro Versuch, ein globales Request-Timeout sowie ein Circuit Breaker, der bei anhaltenden Fehlern weitere Requests kurzzeitig blockiert. Das Ergebnis sind robuste HTTP-Calls, die kurzzeitige API-Probleme selbstständig überstehen, externe Abhängigkeiten nicht unnötig weiter belasten und den eigenen Service vor Kettenreaktionen schützen. Ideal für Web-APIs, Worker Services und alle Anwendungen, die regelmäßig mit externen HTTP-Services sprechen.
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<WeatherClient>(client =>
{
    client.BaseAddress = new Uri("https://example.com/");
    client.Timeout = Timeout.InfiniteTimeSpan;
})
.AddStandardResilienceHandler(options =>
{
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(20);
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);

    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromMilliseconds(200);
    options.Retry.BackoffType = Microsoft.Extensions.Http.Resilience.HttpRetryBackoffType.Exponential;
    options.Retry.UseJitter = true;
    options.Retry.ShouldHandle = args =>
        args.Outcome.Result is { } r && (
            r.StatusCode == HttpStatusCode.TooManyRequests ||
            r.StatusCode == HttpStatusCode.RequestTimeout ||
            (int)r.StatusCode >= 500);

    options.CircuitBreaker.MinimumThroughput = 20;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
    options.CircuitBreaker.FailureRatio = 0.5;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
});

var app = builder.Build();

app.MapGet("/weather/{city}", async (string city, WeatherClient client, CancellationToken ct) =>
{
    var forecast = await client.GetForecastAsync(city, ct);
    return Results.Ok(forecast);
});

app.Run();

public sealed class WeatherClient(HttpClient http)
{
    public async Task<string> GetForecastAsync(string city, CancellationToken ct)
    {
        using var res = await http.GetAsync($"api/forecast?city={Uri.EscapeDataString(city)}", ct);
        res.EnsureSuccessStatusCode();
        return await res.Content.ReadAsStringAsync(ct);
    }
}
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<WeatherClient>(client =>
{
    client.BaseAddress = new Uri("https://example.com/");
    client.Timeout = Timeout.InfiniteTimeSpan;
})
.AddStandardResilienceHandler(options =>
{
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(20);
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);

    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromMilliseconds(200);
    options.Retry.BackoffType = Microsoft.Extensions.Http.Resilience.HttpRetryBackoffType.Exponential;
    options.Retry.UseJitter = true;
    options.Retry.ShouldHandle = args =>
        args.Outcome.Result is { } r && (
            r.StatusCode == HttpStatusCode.TooManyRequests ||
            r.StatusCode == HttpStatusCode.RequestTimeout ||
            (int)r.StatusCode >= 500);

    options.CircuitBreaker.MinimumThroughput = 20;
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
    options.CircuitBreaker.FailureRatio = 0.5;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
});

var app = builder.Build();

app.MapGet("/weather/{city}", async (string city, WeatherClient client, CancellationToken ct) =>
{
    var forecast = await client.GetForecastAsync(city, ct);
    return Results.Ok(forecast);
});

app.Run();

public sealed class WeatherClient(HttpClient http)
{
    public async Task<string> GetForecastAsync(string city, CancellationToken ct)
    {
        using var res = await http.GetAsync($"api/forecast?city={Uri.EscapeDataString(city)}", ct);
        res.EnsureSuccessStatusCode();
        return await res.Content.ReadAsStringAsync(ct);
    }
}

1 Kommentar

  1. Wenn du [b]nicht .NET 8+[/b] nutzt, kannst du das gleiche Prinzip in .NET 6/7 (oder älter) sehr ähnlich abbilden – typischerweise mit Polly als Policy-Handler am HttpClient. Damit bekommst du wieder Retries (mit Backoff), Timeout pro Versuch und optional Circuit Breaker – nur eben nicht über AddStandardResilienceHandler, sondern über Policies.

    [code]using Polly;
    using Polly.Extensions.Http;
    using System.Net;

    builder.Services.AddHttpClient(client =>
    {
    client.BaseAddress = new Uri(„https://example.com/“);
    client.Timeout = Timeout.InfiniteTimeSpan; // Timeout kommt über Policy
    })
    .AddPolicyHandler(HttpPolicyExtensions
    .HandleTransientHttpError()
    .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
    .WaitAndRetryAsync(3, retry => TimeSpan.FromMilliseconds(200) * Math.Pow(2, retry)))
    .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromSeconds(5)))
    .AddPolicyHandler(HttpPolicyExtensions
    .HandleTransientHttpError()
    .CircuitBreakerAsync(handledEventsAllowedBeforeBreaking: 5, durationOfBreak: TimeSpan.FromSeconds(15)));
    [/code]

    Damit verhält sich dein Client [b]konzeptionell wie in .NET 8[/b]: kurze API-Aussetzer werden abgefedert, 429/5xx werden sinnvoll behandelt, und bei anhaltenden Fehlern schützt der Circuit Breaker deinen Service vor „Dauerschleifen“.