在Endpoint中间件后添加响应报头遇异常,如何解决?
我正在编写自定义中间件,需要将处理结果(比如请求耗时指标)作为响应报头附加到请求中,且无需修改现有业务代码。编写的中间件代码如下:
public class MyCustomMiddleware { private readonly RequestDelegate _next; public MyCustomMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(Microsoft.AspNetCore.Http.HttpContext context, IConfiguration configuration) { debug.print("I'm before the next piece of middleware"); await _next(context); Context.Response.Headers.Add("x-special-header-4", "super secret response header"); } }
运行后抛出异常:
Exception thrown: 'System.InvalidOperationException' in Microsoft.AspNetCore.Server.Kestrel.Core.dll
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.System.Collections.Generic.IDictionary<System.String,Microsoft.Extensions.Primitives.StringValues>.Add(String key, StringValues value)
使用ActionResult或请求路由返回404时都会触发该异常。当前应用启动的中间件配置顺序如下:
app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseMiddleware<MyCustomMiddleware>(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
核心问题是:后续中间件(端点路由、控制器)开始向客户端发送响应内容后,响应头会被锁定,此时无法再修改。以下两种方案可解决该问题:
1. 调整中间件注册顺序
将自定义中间件移至UseRouting之前,确保它在路由匹配、控制器执行前执行,且在响应启动前完成头的添加。修改后的配置顺序:
app.UseHttpsRedirection(); // 自定义中间件移至此处 app.UseMiddleware<MyCustomMiddleware>(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
限制:若中间件需要依赖路由、认证后的上下文数据,此方式不适用。
2. 使用OnStarting回调监听响应启动事件
ASP.NET Core提供HttpContext.Response.OnStarting方法,会在响应即将启动、响应头未锁定时触发回调。修改中间件的InvokeAsync方法:
public async Task InvokeAsync(HttpContext context, IConfiguration configuration) { var stopwatch = Stopwatch.StartNew(); // 注册响应启动前的回调,此时可安全修改响应头 context.Response.OnStarting(() => { stopwatch.Stop(); // 添加耗时指标头 context.Response.Headers.TryAdd("x-request-duration-ms", stopwatch.ElapsedMilliseconds.ToString()); context.Response.Headers.TryAdd("x-special-header-4", "super secret response header"); return Task.CompletedTask; }); await _next(context); }
这是最优方案:无需调整中间件顺序,能确保在响应头锁定前完成修改,完全适配测量请求耗时并添加响应头、不修改业务代码的需求。
额外提示
- 避免在
await _next(context)后直接修改响应头,此时响应可能已开始发送。 - 使用
TryAdd替代Add,可避免因重复键抛出异常。
内容的提问来源于stack exchange,提问作者user3012708

