useexceptionhandler必须置于userouting之后、useendpoints之前;自定义中间件需try包裹await next(context);记录请求体需调用enablebuffering并重置position;异常日志需主动注入traceidentifier等上下文。

Middleware中捕获未处理异常的正确位置
ASP.NET Core 的 UseExceptionHandler 必须放在 UseRouting 之后、UseEndpoints(或 UseAuthorization)之前,否则它根本收不到路由匹配后的异常。很多项目把 UseExceptionHandler 放在最开头,结果 404 或中间件自身抛出的异常(比如 NullReferenceException 在 UseAuthentication 里)全被漏掉。
典型可靠顺序:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseExceptionHandler("/error"); // ✅ 此处生效
app.UseEndpoints(...);
注意:/error 是一个实际存在的终结点(Controller Action),不是路径占位符;如果没配对应 endpoint,会 fallback 到默认白页,日志也记不到。
自定义异常处理中间件要手动写 try-catch 吗
不用。直接继承 IMiddleware 并在 InvokeAsync 里包裹 next() 即可,但必须确保 try 包住整个 await next(context) 调用 —— 否则下游抛出的异常不会进你的 catch。
常见错误写法:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// ❌ 错:只包了部分逻辑,没包 next()
await LogRequest(context);
await next(context); // 异常从这里飞出去,catch 捕不到
await LogResponse(context);
}
正确写法:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context); // ✅ 全部包裹
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
throw; // 若需继续向上抛(如触发 UseExceptionHandler),保留这句;否则删掉
}
}
-
HandleExceptionAsync里应设置context.Response.StatusCode和context.Response.ContentType - 别在 catch 里直接
WriteAsync响应体后就 return —— 如果后续还有中间件依赖响应头,可能已发送过 headers,会抛InvalidOperationException: Headers are read-only
记录日志时 HttpContext.Request.Body 读不了怎么办
因为 Request.Body 默认是不可重放流(non-replayable stream),第一次读完就 EOF 了。想记录请求体又不影响后续 Controller 绑定,必须提前开启缓冲:
- 在
Startup.ConfigureServices中加:services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip);(非必需,但避免 JSON 注释导致反序列化失败) - 在中间件开头加:
context.Request.EnableBuffering(); - 读完后记得
context.Request.Body.Position = 0;,否则 MVC Model Binding 会从流末尾开始读,拿不到数据
示例片段:
context.Request.EnableBuffering(); using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); string body = await reader.ReadToEndAsync(); context.Request.Body.Position = 0; // ⚠️ 关键一步
注意:启用 buffering 会把整个 Body 加载进内存,大文件上传场景慎用;生产环境建议只对 POST/PUT 且 Content-Type 包含 json 或 form 的请求做此操作。
全局异常日志里看不到 Controller 内部变量值
Middleware 层拿到的 Exception 对象本身不带局部变量快照,这是 .NET 运行时限制。想关联上下文信息(如当前用户 ID、请求 ID、入参摘要),得靠主动注入:
- 用
HttpContext.TraceIdentifier作为日志 correlation ID,在所有日志里带上 - 在 Controller Action 开头手动打点:
_logger.LogInformation("User {UserId} calling {Action} with {@Input}", userId, actionName, input); - 借助
ILogger.BeginScope()统一注入请求级上下文:using var scope = logger.BeginScope(new Dictionary<string object> { ["UserId"] = userId, ["TraceId"] = context.TraceIdentifier });</string>
别指望中间件自动提取 [FromBody] 参数对象的属性值 —— 那个对象还没被反序列化出来,或者已被 GC 掉。真正能稳定取到的,只有原始请求体字符串(经缓冲后)、查询参数、Header 和 Route Data。










