ASP.NET Core cookbook · Intermediate

Call external APIs with typed HttpClients and resilience

Outbound calls exhaust sockets, have no timeouts, and fail on transient errors.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Register a typed client with AddHttpClient (pooled handlers, DNS refresh), configure its base address, and add the standard resilience pipeline (retries with jitter, circuit breaker, timeouts).

Solution

using System.Net.Http.Json;
using Microsoft.Extensions.Http.Resilience;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<ShippingClient>(client =>
    {
        client.BaseAddress = new Uri(builder.Configuration["Shipping:BaseUrl"] ?? "https://shipping.example.com/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    })
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(8);
    });
var app = builder.Build();
app.MapGet("/api/shipping/{country}", async (string country, ShippingClient shipping, CancellationToken ct) =>
    await shipping.GetQuoteAsync(country, ct) is { } quote ? Results.Ok(quote) : Results.StatusCode(503));
app.Run();

public record Quote(string Country, int PriceCents);

public class ShippingClient(HttpClient http, ILogger<ShippingClient> logger)
{
    public async Task<Quote?> GetQuoteAsync(string country, CancellationToken ct)
    {
        try
        {
            return await http.GetFromJsonAsync<Quote>($"quotes/{Uri.EscapeDataString(country)}", ct);
        }
        catch (HttpRequestException ex)
        {
            logger.LogWarning(ex, "Shipping quote failed for {Country}", country);
            return null;
        }
    }
}

Pitfalls

  • new HttpClient() per request, exhausting sockets; or one static instance that never sees DNS changes.
  • Retrying POSTs that aren't idempotent; disable retries for unsafe methods or send idempotency keys.
  • A total timeout shorter than retries need, or no total timeout at all.

Variations

  • Build custom pipelines with AddResilienceHandler("name", pipeline => ...) (Polly v8).
  • Add a hedging handler for latency-sensitive reads across replicas.

Further reading: HTTP resilience

Reference: https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience

More ASP.NET Core recipes

Was this page helpful?