HTTPClients in ASP.NET Core - DelegatingHandlers vs PrimaryMessageHandlers

Sven Hennessen

In modern ASP.NET applications—especially with the advent of minimal APIs in .NET 9, configuring HTTPClients for external calls is both powerful and flexible. One of the key strengths is the ability to create named HTTPClients and extend their behavior using message handlers. In this post, we'll explore two types of message handlers:

  • PrimaryMessageHandlers: Provide control over the underlying transport (e.g., enabling TLS1.3, customizing SSL certificate validation).
  • DelegatingHandlers: Allow you to modify or inspect HTTP requests and responses at the application level (e.g., setting HTTP version, logging, custom headers).

Below, we’ll see how each can be used in minimal API ASP.NET services along with code examples.


Named HTTPClients in Minimal API Services

ASP.NET’s dependency injection makes it simple to register named HTTPClients. You can configure different clients with different behaviors, and later retrieve them using the IHttpClientFactory.

Example Registration:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("MyClient");
// Add other named clients

var app = builder.Build();

app.MapGet("/", async (IHttpClientFactory clientFactory) =>
{
    var client = clientFactory.CreateClient("MyClient");
    var response = await client.GetAsync("https://example.com");
    return await response.Content.ReadAsStringAsync();
});

app.Run();

PrimaryMessageHandlers: Transport-level Customization

The PrimaryMessageHandler is the underlying handler that deals with network transport. By configuring it, you can control low-level details like TLS protocols and SSL certificate validation. This is especially useful when you need to enforce specific security requirements.

Example: Configuring TLS1.3 and Custom SSL Certificate Validation

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("MyPrimaryClient")
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        SslOptions = new SslClientAuthenticationOptions
        {
            // Enforce TLS1.3 for secure communication
            EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13,
            RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
            {
                // Custom logic to validate SSL certificate can be added here
                Console.WriteLine("Primary handler: Validating SSL certificate.");
                return true; // Or implement proper validation logic
            }
        }
    });
// Configure other named clients

var app = builder.Build();

app.MapGet("/primary", async (IHttpClientFactory clientFactory) =>
{
    var client = clientFactory.CreateClient("MyPrimaryClient");
    var response = await client.GetAsync("https://example.com");
    return await response.Content.ReadAsStringAsync();
});

app.Run();

DelegatingHandlers: Application-level Customization

DelegatingHandlers sit in the HTTPClient pipeline and allow you to modify the request or response. They are ideal for tasks such as logging, adding custom headers, or enforcing application-level protocols like HTTP/2.

Example: Custom Delegating Handler to Enforce HTTP/2

public class CustomDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Enforce HTTP/2 for the outgoing request
        request.Version = HttpVersion.Version20;
        Console.WriteLine("CustomDelegatingHandler: Request is set to use HTTP/2.");
        return await base.SendAsync(request, cancellationToken);
    }
}

Lesson Learned

Trying to use the lean modular approach of DelegatingHandlers to apply transport-level configuration does not work. Modify the InnerHandler of a DelegatingHandler results an exception:

System.InvalidOperationException: The 'InnerHandler' property must be null. 'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached.

This is due to the fact that modifying the InnerHandler would break the handler pipeline. Wrapping each DelegatingHandler's request in a new HttpMessageInvoker will workaround the exception, but still break the chain.

TL;DR: Don't use DelegatingHandlers for transport-level.

Registering the Handler with a Named HTTPClient

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<CustomDelegatingHandler>();

builder.Services.AddHttpClient("MyDelegatingClient")
    .AddHttpMessageHandler<CustomDelegatingHandler>();

var app = builder.Build();

app.MapGet("/delegating", async (IHttpClientFactory clientFactory) =>
{
    var client = clientFactory.CreateClient("MyDelegatingClient");
    var response = await client.GetAsync("https://example.com");
    return await response.Content.ReadAsStringAsync();
});

app.Run();

Conclusion

By leveraging both DelegatingHandlers and PrimaryMessageHandlers, you can finely control HTTP communication in your ASP.NET Core minimal API services:

  • Named HTTP Clients provide a modular way of configuring HTTP clients for different targets.
  • PrimaryMessageHandlers provide a gateway to transport-level customizations (e.g., enforcing TLS1.3, managing SSL certificate validations).
  • DelegatingHandlers allow manipulation of application-level behaviors (e.g., enforcing HTTP/2, adding headers).

This layered approach enables robust, secure, and flexible HTTP interactions tailored to the specific needs of your application. Happy coding!

Never miss an article

No spam. Only relevant news about and from us. Unsubscribe anytime.