1. ASP.NET Core中间件错误处理机制解析
在ASP.NET Core应用中,中间件管道是处理HTTP请求的核心机制。当请求进入管道后,会依次经过各个中间件组件,任何一个环节都可能抛出异常。开发环境默认使用DeveloperExceptionPage中间件,它会显示详细的错误堆栈、查询参数、Cookie和请求头信息。
生产环境则需要更安全的处理方式。UseExceptionHandler中间件是推荐方案,它能够:
- 捕获未处理的异常
- 记录错误日志
- 重定向到错误页面
- 防止敏感信息泄露
典型配置示例:
csharp复制if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
警告:永远不要向客户端暴露堆栈跟踪等敏感信息,这会导致严重的安全漏洞。生产环境应该只显示友好的错误提示。
1.1 开发环境与生产环境的差异处理
开发环境需要详细错误信息辅助调试,而生产环境需要友好提示。这种差异通过环境变量来区分:
csharp复制// 开发环境配置
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
// 生产环境配置
app.UseExceptionHandler("/Home/Error");
app.UseHsts(); // 强制HTTPS
}
开发人员异常页包含以下关键信息:
- 异常堆栈跟踪
- 引发异常的源代码行
- 当前请求的详细信息
- 查询字符串和Cookie内容
2. 高级异常处理策略
2.1 自定义异常处理中间件
创建自定义异常处理中间件可以更灵活地控制错误响应:
csharp复制public class CustomExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CustomExceptionMiddleware> _logger;
public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred.");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
// 根据不同异常类型设置状态码
context.Response.StatusCode = exception switch
{
ArgumentException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
_ => StatusCodes.Status500InternalServerError
};
return context.Response.WriteAsync(new {
StatusCode = context.Response.StatusCode,
Message = "An error occurred. Please try again later.",
Detailed = context.Response.StatusCode == 500 ? null : exception.Message
}.ToJson());
}
}
2.2 异常过滤器与中间件的协同
MVC过滤器与中间件可以协同工作:
| 处理方式 | 作用范围 | 执行时机 | 适用场景 |
|---|---|---|---|
| 异常中间件 | 全局 | 管道最外层 | 未捕获的异常 |
| 异常过滤器 | 控制器/动作 | MVC管道内 | 业务逻辑异常 |
| IExceptionHandler | 全局 | 中间件之后 | 集中式异常处理 |
推荐组合方案:
- 使用中间件捕获基础设施层异常
- 使用过滤器处理业务逻辑异常
- 实现IExceptionHandler接口统一格式化错误响应
3. 结构化日志记录实践
3.1 Serilog集成配置
结构化日志能更好地支持日志分析系统:
csharp复制builder.Host.UseSerilog((ctx, lc) => lc
.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}")
.WriteTo.File("logs/webapi-.log",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.Enrich.FromLogContext()
.MinimumLevel.Information()
.Filter.ByExcluding(c => c.Properties.Any(p => p.Value.ToString().Contains("healthcheck")))
);
3.2 上下文增强与关键字段
在中间件中丰富日志上下文:
csharp复制public async Task InvokeAsync(HttpContext context)
{
using (LogContext.PushProperty("TraceId", context.TraceIdentifier))
using (LogContext.PushProperty("RequestPath", context.Request.Path))
{
try
{
await _next(context);
}
catch (Exception ex)
{
var logContext = new {
User = context.User.Identity?.Name,
IP = context.Connection.RemoteIpAddress,
Headers = context.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString())
};
_logger.LogError(ex, "Request failed with {@LogContext}", logContext);
throw;
}
}
}
关键日志字段建议:
- TraceId:关联整个请求链
- UserId:定位用户相关错误
- RequestDuration:记录请求耗时
- StatusCode:HTTP响应状态
- ExceptionType:异常分类
4. 状态码页面的高级处理
4.1 动态状态码处理
ASP.NET Core提供了多种状态码处理方式:
csharp复制// 基本文本响应
app.UseStatusCodePages();
// 格式化字符串
app.UseStatusCodePages("text/plain", "Status code: {0}");
// Lambda自定义处理
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "application/json";
await context.HttpContext.Response.WriteAsync(
JsonSerializer.Serialize(new {
Status = context.HttpContext.Response.StatusCode,
Path = context.HttpContext.Request.Path,
TraceId = context.HttpContext.TraceIdentifier
}));
});
4.2 重定向与重新执行
两种特殊的状态码处理方式对比:
| 特性 | UseStatusCodePagesWithRedirects | UseStatusCodePagesWithReExecute |
|---|---|---|
| 工作方式 | 302重定向到错误页 | 保持原URL执行错误处理器 |
| 状态码 | 最终返回200 | 保留原始状态码 |
| 请求上下文 | 新请求丢失原信息 | 保留原始请求信息 |
| SEO影响 | 较大(URL改变) | 较小 |
| 典型场景 | 传统Web应用 | API和SPA应用 |
配置示例:
csharp复制// 重定向方式
app.UseStatusCodePagesWithRedirects("/error/{0}");
// 重新执行方式
app.UseStatusCodePagesWithReExecute("/error-handler", "?code={0}");
5. 问题详情(ProblemDetails)规范实现
5.1 RFC 7807标准实现
ProblemDetails是HTTP API错误响应的标准格式:
csharp复制builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions.Add("instance", ctx.HttpContext.Request.Path);
ctx.ProblemDetails.Extensions.Add("timestamp", DateTime.UtcNow);
};
});
// 中间件启用
app.UseExceptionHandler();
app.UseStatusCodePages();
标准字段说明:
- type:错误类型URI
- title:人类可读的错误摘要
- status:HTTP状态码
- detail:详细错误说明
- instance:具体请求标识
5.2 自定义问题详情
扩展标准ProblemDetails:
csharp复制public class CustomProblemDetails : ProblemDetails
{
public string Code { get; set; }
public string DocUrl { get; set; }
public IDictionary<string, object> Metadata { get; } = new Dictionary<string, object>();
}
// 自定义写入器
public class CustomProblemDetailsWriter : IProblemDetailsWriter
{
public bool CanWrite(ProblemDetailsContext context)
=> context.HttpContext.Request.Path.StartsWithSegments("/api");
public ValueTask WriteAsync(ProblemDetailsContext context)
{
var customProblem = new CustomProblemDetails
{
Title = context.ProblemDetails.Title,
Status = context.ProblemDetails.Status,
Code = context.HttpContext.Features.Get<ExceptionFeature>()?.Error.GetType().Name,
DocUrl = $"https://docs.example.com/errors#{context.ProblemDetails.Status}"
};
return new ValueTask(context.HttpContext.Response.WriteAsJsonAsync(customProblem));
}
}
6. 性能优化与错误抑制
6.1 错误处理性能考量
错误处理中的性能陷阱:
- 过度日志记录导致IO瓶颈
- 异常对象构造开销
- 堆栈跟踪收集成本
- 同步日志调用阻塞线程
优化建议:
csharp复制// 使用异步日志
logger.LogErrorAsync(ex, "Error occurred");
// 避免在热路径中抛出异常
if (invalidCondition)
{
// 而不是 throw new Exception
return Results.Problem(...);
}
// 限制堆栈跟踪深度
new Exception().StackTrace.Split('\n').Take(10)
6.2 特定异常抑制策略
某些异常可能需要特殊处理:
csharp复制app.Use(async (context, next) =>
{
try
{
await next();
}
catch (Exception ex) when (ex is OperationCanceledException ||
ex is TaskCanceledException)
{
// 客户端取消请求是正常情况
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
}
catch (BadHttpRequestException ex) when (ex.StatusCode == 413)
{
// 文件过大特殊处理
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
await context.Response.WriteAsync("File size exceeds limit");
}
});
7. 实战:构建企业级错误处理管道
完整的企业级配置示例:
csharp复制var builder = WebApplication.CreateBuilder(args);
// 添加问题详情服务
builder.Services.AddProblemDetails(options =>
{
options.IncludeExceptionDetails = (ctx, ex) => ctx.RequestServices
.GetRequiredService<IHostEnvironment>().IsDevelopment();
});
// 配置Serilog
builder.Host.UseSerilog((ctx, cfg) => /* 日志配置 */);
var app = builder.Build();
// 开发环境特殊处理
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler(app =>
{
app.Run(async context =>
{
var exceptionHandler = context.Features.Get<IExceptionHandlerPathFeature>();
var problemService = context.RequestServices.GetService<IProblemDetailsService>();
if (problemService != null && exceptionHandler?.Error != null)
{
await problemService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails = { Detail = exceptionHandler.Error.Message }
});
}
});
});
app.UseHsts();
}
// 状态码处理
app.UseStatusCodePagesWithReExecute("/error", "?status={0}");
// 健康检查特殊处理
app.MapHealthChecks("/health").WithMetadata(new SuppressProblemDetailsMetadata());
// 其他中间件
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
// 终结点
app.MapControllers();
app.MapRazorPages();
// 全局异常处理器
app.UseExceptionHandler();
app.Run();
关键设计原则:
- 开发与生产环境分离
- 错误信息分级控制
- 日志上下文完整记录
- 符合行业标准规范
- 性能影响最小化
8. 常见问题排查指南
8.1 错误处理失效场景
常见问题及解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 未捕获异常 | 中间件顺序错误 | 确保UseExceptionHandler在最外层 |
| 日志丢失 | 异步上下文问题 | 使用LogContext.PushProperty |
| 重复记录 | 多级捕获 | 统一异常处理入口 |
| 性能下降 | 同步IO操作 | 改用异步日志记录 |
| 信息泄露 | 生产环境配置错误 | 严格环境检测 |
8.2 高级调试技巧
使用DiagnosticListener进行深度诊断:
csharp复制// 订阅诊断事件
var listener = new DiagnosticListener("Microsoft.AspNetCore");
var subscription = listener.Subscribe(new ErrorDiagnosticObserver());
public class ErrorDiagnosticObserver : IObserver<KeyValuePair<string, object>>
{
public void OnNext(KeyValuePair<string, object> value)
{
if (value.Key == "Microsoft.AspNetCore.Diagnostics.UnhandledException")
{
var exception = (Exception)value.Value.GetType()
.GetProperty("exception")?.GetValue(value.Value);
var httpContext = (HttpContext)value.Value.GetType()
.GetProperty("httpContext")?.GetValue(value.Value);
// 自定义处理逻辑
}
}
public void OnCompleted() { }
public void OnError(Exception error) { }
}
在复杂分布式系统中,还需要考虑:
- 分布式追踪ID传递
- 错误分类与聚合
- 告警阈值设置
- 错误率监控看板
