How to modify response headers in ASP.NET Core middleware

I had a problem setting response headers in ASP.NET Core that I wanted to share some findings on. This might be clear to a lot of you but hopefully, this will help someone in the same situation as me. Here goes!

In a small internal API, I wanted to include some additional response headers on all requests made to the API. ASP.NET Core middleware is a great way of including code like this and something that I've blogged about multiple times already (like this and this). The middleware for adding the response headers looks similar to this:

internal class TimingMiddleware
{
    private readonly RequestDelegate _next;

    public TimingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        await _next(context);
        
        context.Response.Headers["X-Took"] = $"{stopwatch.ElapsedMilliseconds} ms";
    }
}

All the middleware does it to create a new Stopwatch and add the elapsed ms as a custom response header named X-Took. I know that there are alternative and better ways of measuring performance on an ASP.NET Core API but that's not really the focus here so play along.

The middleware class is added in the Program.cs file:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

app.UseMiddleware<TimingMiddleware>(); // ⬅️ this is where the magic happens

// ...

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

When running the project, I was expecting an X-Took header but all I got was this exception:

After some googling, I figured out that this exception is caused by the fact that I'm trying to add headers after invoking the next (the next piece of middleware in the pipeline). ASP.NET Core doesn't allow headers to be added after generating the response body.

Would your users appreciate fewer errors?

➡️ Reduce errors by 90% with elmah.io error logging and uptime monitoring ⬅️

One solution is to migrate my code to Action Filters which I use for other projects. But luckily there's a hook in the pipeline named OnStarting that I can use for this purpose. With a bit of refactoring, the code looks like this:

internal class TimingMiddleware
{
    private readonly RequestDelegate _next;

    public TimingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        context.Response.OnStarting(() =>
        {
            context.Response.Headers["X-Took"] = $"{stopwatch.ElapsedMilliseconds} ms";
            return Task.CompletedTask;
        });

        await _next(context);
    }
}

Adding the response header is moved inside the OnStarting action. This action is called just before response headers are sent to the client, which makes it a great place to include custom headers like this.