.Net Core 3.1 WebApi调用stream.CopyToAsync后UseExceptionHandler失效问题
问题根因
核心原因
UseExceptionHandler 生效的前提是HTTP响应尚未启动(即 context.Response.HasStarted == false),此时异常处理中间件可以修改响应状态码、重写响应内容。你执行 await stream.CopyToAsync(originalBody) 时,已经将响应数据写入原始输出流,触发了响应头的发送,HasStarted 变为 true,后续抛出的异常无法再被异常处理中间件捕获处理。
次要触发原因
你在 finally 块中主动调用了 originalBody.Close(),提前关闭了框架托管的响应流,即便异常处理中间件要写入响应也没有可用的流载体。
修复方案
- 调整操作顺序:先执行所有可能抛出异常的逻辑(包括日志写入),确认无异常后再将内存流内容拷贝到原始响应流,避免未处理异常发生在响应启动之后。
- 移除手动关闭响应流的代码:原始响应流由ASP.NET Core框架生命周期管理,无需手动关闭,否则会破坏管道后续操作。
- 优化异常抛出写法:
catch块中直接throw ex会重置异常调用栈,改为throw;保留原始栈信息便于排查问题。
修改后代码示例
Invoke方法
public async Task Invoke(HttpContext context) { Stream originalBody = context.Response.Body; try { using var memStream = new MemoryStream(); context.Response.Body = memStream; await _next(context).ConfigureAwait(false); // 先处理日志等可能抛异常的逻辑,最后再拷贝流到响应 await LogResponse(context, originalBody, memStream).ConfigureAwait(false); } catch (Exception ex) { // 异常抛出前先把响应流改回原始流,保证异常处理中间件可以写入内容 context.Response.Body = originalBody; throw; // 保留原始异常栈 } finally { // 只需要重置响应流引用,不要手动关闭原始流 context.Response.Body = originalBody; } }
LogResponse方法
private async Task LogResponse(HttpContext context, Stream originalBody, MemoryStream stream) { stream.Seek(0, SeekOrigin.Begin); string body = await new StreamReader(stream).ReadToEndAsync().ConfigureAwait(false); stream.Seek(0, SeekOrigin.Begin); // 先执行日志写入等可能抛异常的操作 var resp = new ApiLogItemRequest() { AppName = "AppName", CreatedBy = "AppName", DateTime = DateTime.Now, RawBodyJson = body, TraceIdentifier = context.TraceIdentifier }; await _logHelper.LogRawHttpBodyItem(resp); // 所有逻辑执行成功后,最后再拷贝流到原始响应 await stream.CopyToAsync(originalBody).ConfigureAwait(false); }
可选优化
如果允许日志写入失败不影响接口正常返回,可以给日志操作单独套一层try/catch,避免日志异常导致接口报错:
try { await _logHelper.LogRawHttpBodyItem(resp); } catch (Exception logEx) { // 自行处理日志异常,比如本地打控制台日志,不抛出到上层 Console.WriteLine($"日志写入失败:{logEx.Message}"); }
内容的提问来源于stack exchange,提问作者Hope
相关产品推荐
相关产品推荐

