You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 19:29:54