常见错误处理示例
直接读取原始响应流
// 错误写法! var originalStream = context.Response.Body; await _next(context); // 此时数据可能已写入原始流 if (condition) { // 这里已经无法修改响应,因为部分数据可能已发送给客户端 await originalStream.WriteAsync(...); }
正确写法
默认情况下,响应体是一个只写一次的流,一旦开始写入,就不能直接修改。如果在中间件中直接读取或修改响应体,可能会导致数据丢失或异常。因此,常见的做法是用一个临时流来捕获后续中间件的输出,处理完后再决定如何修改响应内容,再写回原始流。在修改响应内容时,需要确保在修改响应内容时,重置响应流,并设置正确的Content-Type,同时处理可能已经部分写入的响应内容。例如,在写入自定义JSON之前,可能需要清除之前的响应内容,并设置正确的状态码和头部信息。总结来说,用户的中间件需要调整结构,正确捕获异常,并在处理管道中的适当位置执行,同时确保能够获取到终结点元数据,以便判断是否属于特定的控制器和Action。此外,需要处理响应流的重置和内容的正确写入,避免冲突或错误。
// 关键代码 及 解释说明 public async Task InvokeAsync(HttpContext context) { // 阶段1:准备分拣台 var originalBodyStream = context.Response.Body; // 记录原始传送带位置 using var memoryStream = new MemoryStream(); // 准备临时分拣台 context.Response.Body = memoryStream; // 暂时重定向到分拣台 try { // 阶段2:传递处理 await _next(context); // 让控制器等后续流程处理包裹 // 阶段3:质量检查 memoryStream.Seek(0, SeekOrigin.Begin); // 将分拣台倒带到起点 if (ShouldHandle(context)) { // 特殊处理流程 context.Response.Body = originalBodyStream; // 恢复原始传送带 await CreateCustomResponse(memoryStream, context); // 定制新包装 } else { // 常规放行 memoryStream.Seek(0, SeekOrigin.Begin); // 再次倒带 await memoryStream.CopyToAsync(originalBodyStream); // 放回原始传送带 } } finally { context.Response.Body = originalBodyStream; // 确保传送带归位 } }
为什么需要使用一个临时流?
防止数据丢失
如果直接操作原始流,一旦开始读取,框架可能已经部分发送数据给客户端。
灵活决策
需要先看到完整处理结果(如状态码),再决定如何修改响应。
避免管道污染
临时流相当于沙箱环境,所有操作不会直接影响最终输出,直到确认需要修改。