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.
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.