DelegatingHandler Magic: HTTP 호출을 위한 파이프라인 구축

작성자

카테고리:

← 피드로
DEV Community · Nick · 2026-09-10 개발(SW)
Cover image for DelegatingHandler Magic: Build a Pipeline for Your HTTP Calls

Nick

Nick

Posted on Sep 10 AI-assisted

What’s up, .NET devs! 👋

Today I want to show you one of the most underrated features in HttpClient — the DelegatingHandler pipeline. It’s like middleware for your HTTP calls!

What’s a DelegatingHandler?

Think of DelegatingHandler as a chain of interceptors. Every HTTP request passes through each handler, and every response comes back through the same chain (in reverse).

🔗 Fun Fact: The handler pipeline is modeled after the “Chain of Responsibility” design pattern from the Gang of Four book!

Request:  Client → Handler1 → Handler2 → Handler3 → Network
Response: Client ← Handler1 ← Handler2 ← Handler3 ← Network

Enter fullscreen mode Exit fullscreen mode

Your First Handler: 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 stopwatch = Stopwatch.StartNew();

        _logger.LogInformation("→ {Method} {Uri}", request.Method, request.RequestUri);

        var response = await base.SendAsync(request, cancellationToken);

        stopwatch.Stop();

        _logger.LogInformation("← {StatusCode} in {ElapsedMs}ms",
            (int)response.StatusCode, stopwatch.ElapsedMilliseconds);

        return response;
    }
}

Enter fullscreen mode Exit fullscreen mode

Register It

services.AddTransient<LoggingHandler>();

services.AddHttpClient<IMyApiClient, MyApiClient>()
    .AddHttpMessageHandler<LoggingHandler>();

Enter fullscreen mode Exit fullscreen mode

Correlation ID Propagation

In microservices, trace requests across services:

public class CorrelationIdHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CorrelationIdHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var correlationId = _httpContextAccessor.HttpContext?
            .Request.Headers["X-Correlation-ID"].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        request.Headers.Add("X-Correlation-ID", correlationId);
        return base.SendAsync(request, cancellationToken);
    }
}

Enter fullscreen mode Exit fullscreen mode

Auth Token Injection

Never manually add auth headers again:

public class AuthTokenHandler : DelegatingHandler
{
    private readonly ITokenService _tokenService;

    public AuthTokenHandler(ITokenService tokenService)
    {
        _tokenService = tokenService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await _tokenService.GetAccessTokenAsync(cancellationToken);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

Enter fullscreen mode Exit fullscreen mode

Metrics Collection

public class MetricsHandler : DelegatingHandler
{
    private readonly IMeterFactory _meterFactory;
    private readonly Histogram<double> _requestDuration;

    public MetricsHandler(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HttpClient.Metrics");
        _requestDuration = meter.CreateHistogram<double>("http_request_duration_ms");
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        var tags = new TagList
        {
            { "method", request.Method.Method },
            { "host", request.RequestUri?.Host ?? "unknown" }
        };

        try
        {
            var response = await base.SendAsync(request, cancellationToken);
            tags.Add("status_code", ((int)response.StatusCode).ToString());
            return response;
        }
        finally
        {
            stopwatch.Stop();
            _requestDuration.Record(stopwatch.Elapsed.TotalMilliseconds, tags);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Chaining Multiple Handlers

services.AddTransient<LoggingHandler>();
services.AddTransient<CorrelationIdHandler>();
services.AddTransient<AuthTokenHandler>();
services.AddTransient<MetricsHandler>();

services.AddHttpClient<IMyApiClient, MyApiClient>()
    .AddHttpMessageHandler<CorrelationIdHandler>()  // First in, last out
    .AddHttpMessageHandler<AuthTokenHandler>()
    .AddHttpMessageHandler<LoggingHandler>()
    .AddHttpMessageHandler<MetricsHandler>();       // Last in, first out

Enter fullscreen mode Exit fullscreen mode

🎯 Pro Tip: Order Matters!

Handlers execute in the order you add them for requests, and reverse order for responses.

  • Logging should be first (sees raw request) or last (sees final request)
  • Auth should be after correlation ID
  • Retry policies (Polly) should wrap most other handlers

Wrapping Up

DelegatingHandler is your Swiss Army knife for HTTP concerns. Instead of polluting your business logic with logging, auth, and metrics — put it in handlers!

Happy pipelining! 🚀

원문에서 계속 ↗