Hey folks, this is a short but crucial blog post for anyone writing custom middleware for ASP.NET. In this post, we’ll see how we can correctly add headers to an HTTP response and avoid the dreaded System.InvalidOperationException error.

Let’s get into it!

Why Does This Happen?

We may have seen the System.InvalidOperationException when building a custom middleware.

System.InvalidOperationException: Headers are read-only, response has already started.

This exception occurs when our middleware takes the following form.

public class OopsMiddlware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        await next(context);
        context.Response.Headers["Nope"] = "crap!";
    }
}

Note that we call the next delegate and then try to alter the HTTP response. The pattern can lead to issues, especially if the next delegate has already started writing the HTTP response and its headers to the client.

Normally, middleware processes the request, and either terminates the HTTP request by returning the response or calls the RequestDelegate next parameter. When possible, call next as the last line of your middleware, as it is the best option in most cases.

public class SuccessMiddlware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        context.Response.Headers["Yep"] = "success!";
        await next(context);
    }
}

Why would someone want to call next before working on the response? Well, we may want to call next first because we want to inspect the current state of our HTTP response and add conditional headers. If we find ourselves in that situation, what do we do?

Adding Headers After Calling Next

When the middleware we’re building requires knowledge from the previous middlewares, then we need to use the OnStarting method found on HttpResponse. We can see it in use below.

public class AddHeadersMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        context.Response.OnStarting(async state =>
        {
            if (state is HttpContext httpContext)
            {
                var request = httpContext.Request;
                var response = httpContext.Response;
                
                // Modify the response
                response.Headers.Add("yep", "success");
            }
        }, context);
    }
}

Now we can safely modify the header collection before the ASP.NET pipeline writes the response over the wire. The OnStarting method uses an internal stack to keep track of all delegates and executes them by popping each delegate from the top. The stack collection means the delegates will execute in the reverse order that we register them, the last registration is executed first. Here is the registration code found in the HttpProtocol class under the Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace.

public void OnStarting(Func<object, Task> callback, object state)
{
    if (HasResponseStarted)
    {
        ThrowResponseAlreadyStartedException(nameof(OnStarting));
    }

    if (_onStarting == null)
    {
        _onStarting = new Stack<KeyValuePair<Func<object, Task>, object>>();
    }
    _onStarting.Push(new KeyValuePair<Func<object, Task>, object>(callback, state));
}

Here is an example of two middlewares using the OnStarting approach to enhancing the headers on a response.

app.Use(async (context, next) =>
{
    context.Response.OnStarting(async o => {
        if (o is HttpContext ctx) {
            ctx.Response.Headers["OnStarting-One"] = "1";
        }
    }, context);
    await next();
});

app.Use(async (context, next) =>
{
    context.Response.OnStarting(async o => {
        if (o is HttpContext ctx) {
            ctx.Response.Headers["OnStarting-Two"] = "2";
        }
    }, context);
    await next();
});

After executing these two middleware, we can see the results in the browser’s dev tools.

headers in chrome dev tools

The OnStarting method is a great way to handle exceptions, and that’s exactly what the ExceptionHandlerMiddleware in ASP.NET usages it for. Here is the implmenentation to clear cache headers, so that our exception responses aren’t cached by the client.

private static Task ClearCacheHeaders(object state)
{
    var headers = ((HttpResponse)state).Headers;
    headers[HeaderNames.CacheControl] = "no-cache";
    headers[HeaderNames.Pragma] = "no-cache";
    headers[HeaderNames.Expires] = "-1";
    headers.Remove(HeaderNames.ETag);
    return Task.CompletedTask;
}

Conclusion

Depending on our middleware, we might need to add headers to the HTTP response arbitrarily, or we may need to understand the current state of our response and act accordingly. Using the OnStarting method on the HttpResponse gives us one final opportunity to inspect the HTTP response and alter it. Remember, registration of our middleware matters, as the OnStarting method is pushed onto a stack collection. Registering our middleware in the correct location is vital to our application’s behavior. In general, we should only use the OnStarting method when other approaches are not adequate.