Asp.Net WebApi模板项目内联中间件失效及响应头异常问题
新建Asp.Net WebApi项目测试,按照官方文档添加内联中间件后,应用能启动,但通过Swagger调用接口时抛出异常:System.InvalidOperationException: Headers are read-only, response has already started。调试发现中间件和接口代码均已执行,调整中间件位置后问题依旧,部分场景下Swagger页面甚至无法显示。后续使用Asp.Net Core Empty模板测试也出现相同异常,且该问题在.Net6版本之前不存在。
WebApi项目Program代码
public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // 注册服务 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // 配置请求管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.Use(async (context, next) => { // 尝试向响应写入内容 await context.Response.WriteAsync("Hello from middleware."); await next.Invoke(); // 请求后的日志或其他不写入响应的操作 }); app.MapControllers(); app.Run(); }
更新:Empty模板测试情况
使用Asp.Net Core Empty模板测试也得到相同结果,异常由调用next前的await context.Response.WriteAsync触发,始终抛出System.InvalidOperationException: Headers are read-only, response has already started。Empty模板代码如下:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Use(async (context, next) => { // 尝试向响应写入内容 await context.Response.WriteAsync("Hello from middleware."); await next.Invoke(); // 请求后的日志或其他不写入响应的操作 }); app.MapGet("/", () => "Hello World!"); app.Run();
在.Net6及以后的WebApplication简化模型中,一旦通过Response.WriteAsync向响应写入内容,响应的状态码和头部会立即发送至客户端,此时Response.HasStarted属性变为true,响应进入已启动状态,头部被标记为只读。
当下游中间件(比如控制器端点、MapGet的委托)执行时,会尝试修改响应头部或再次写入响应内容,但此时响应头部已锁定,因此抛出该异常。而.Net6之前的Startup模型中,请求管道的响应处理逻辑不同,不会在写入内容后立即锁定头部,所以不会触发此问题。
根据不同需求,可采用以下几种正确实现方式:
场景1:仅在下游未生成响应时输出内容
如果中间件仅需在下游没有输出响应的情况下写入内容,需先检查响应是否已启动:
app.Use(async (context, next) => { // 先执行下游中间件 await next.Invoke(); // 仅当下游未启动响应时,才写入自定义内容 if (!context.Response.HasStarted) { await context.Response.WriteAsync("Hello from middleware."); } });
场景2:在下游响应内容前添加自定义内容
若要在下游输出的内容前添加前缀,需通过内存流捕获下游响应,再合并内容输出:
using System.IO; app.Use(async (context, next) => { // 保存原始响应流,替换为内存流用于捕获下游输出 var originalStream = context.Response.Body; using var memoryStream = new MemoryStream(); context.Response.Body = memoryStream; try { // 执行下游中间件 await next.Invoke(); // 重置内存流位置,读取下游输出内容 memoryStream.Seek(0, SeekOrigin.Begin); var downstreamContent = await new StreamReader(memoryStream).ReadToEndAsync(); // 恢复原始响应流,写入合并后的内容 context.Response.Body = originalStream; await context.Response.WriteAsync($"Hello from middleware.{Environment.NewLine}{downstreamContent}"); } finally { // 确保恢复原始响应流 context.Response.Body = originalStream; } });
场景3:仅执行无响应修改的操作
如果中间件仅用于日志、权限验证等不修改响应的逻辑,直接调整执行顺序,避免提前写入响应:
app.Use(async (context, next) => { // 请求前逻辑:如日志记录、参数验证等 Console.WriteLine($"请求路径:{context.Request.Path}"); await next.Invoke(); // 请求后逻辑:如响应状态码日志等 Console.WriteLine($"响应状态码:{context.Response.StatusCode}"); });
内容的提问来源于stack exchange,提问作者Daniele Armanasco

