AspNetCore 7/8自定义异常处理器结合ProblemDetails时XML失效排查
现象描述
将API升级到AspNetCore 7/8后,使用默认的app.UseExceptionHandler();时,Accept: application/json和Accept: application/xml格式的异常响应都能正常返回符合RFC7807的ProblemDetails内容。
测试请求示例:
curl https://localhost:7089/Test -H "Accept: application/json"
返回:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1", "title": "An error occurred while processing your request.", "status": 500, "traceId": "00-7165c3ae88dba50c1c055a15f2bcf893-79d5c49179a9b30c-00" }
curl https://localhost:7089/Test -H "Accept: application/xml"
返回:
<problem xmlns="urn:ietf:rfc:7807"> <status>500</status> <title>An error occurred while processing your request.</title> <type>https://tools.ietf.org/html/rfc9110#section-15.6.1</type> <traceId>00-d8cf6db0704770b8badf2376da7c4f4c-4d6ead3e06d28c25-00</traceId> </problem>
但按照官方文档示例添加自定义异常处理器后:
app.UseExceptionHandler(x => x.Run(async context => { if (context.Response.HasStarted) return; var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error; if (exception == null) throw new Exception("ExceptionHandler without exception!"); var problemDetailsService = context.RequestServices.GetService<IProblemDetailsService>(); await problemDetailsService.WriteAsync(new ProblemDetailsContext { HttpContext = context, ProblemDetails = { Title = exception.Message, } }); }));
JSON格式响应正常,但XML格式返回空内容:
curl https://localhost:7089/Test -H "Accept: application/json"
返回:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1", "title": "message from exception", "status": 500 }
curl https://localhost:7089/Test -H "Accept: application/xml"
无返回内容,日志中出现异常:
Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.dll
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware: Error: An exception was thrown attempting to execute the error handler.System.InvalidOperationException: Unable to find a registered
IProblemDetailsWriterthat can write to the given context.at Microsoft.AspNetCore.Http.ProblemDetailsService.WriteAsync(ProblemDetailsContext context)
原因分析
默认的ExceptionHandlerMiddleware内部会借助MVC的格式化系统(包括通过AddXmlSerializerFormatters()注册的XML格式化器)来处理响应序列化,但自定义异常处理器直接调用IProblemDetailsService.WriteAsync()时,DI容器中默认只注册了JSON格式的IProblemDetailsWriter实现,XML对应的Writer并没有被自动注册——AddXmlSerializerFormatters()仅配置了MVC的XML格式化器,并未关联到IProblemDetailsService的写入器体系。
解决方案
方案1:注册内置的XmlProblemDetailsWriter(推荐)
AspNetCore内置了XmlProblemDetailsWriter,只需手动将其注册到DI容器即可:
using Microsoft.AspNetCore.Mvc.Formatters.Xml; // 需要添加此命名空间 var builder = WebApplication.CreateBuilder(args); // 添加Controllers并启用XML格式化器 builder.Services.AddControllers().AddXmlSerializerFormatters(); // 注册XML格式的ProblemDetails写入器 builder.Services.AddTransient<IProblemDetailsWriter, XmlProblemDetailsWriter>(); builder.Services.AddProblemDetails(); // 后续代码不变...
方案2:利用MVC的ObjectResult处理格式化
如果不想额外注册Writer,可以在自定义异常处理器中直接使用MVC的格式化系统,通过ObjectResultExecutor来处理ProblemDetails的序列化:
app.UseExceptionHandler(x => x.Run(async context => { if (context.Response.HasStarted) return; var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error; if (exception == null) throw new InvalidOperationException("ExceptionHandler without exception!"); // 构建自定义ProblemDetails var problemDetails = new ProblemDetails { Title = exception.Message, Status = StatusCodes.Status500InternalServerError, Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1", Extensions = { ["traceId"] = context.TraceIdentifier } }; // 获取MVC的ObjectResult执行器,复用已配置的格式化器 var executor = context.RequestServices.GetRequiredService<ObjectResultExecutor>(); var result = new ObjectResult(problemDetails) { StatusCode = problemDetails.Status }; await executor.ExecuteAsync(new ActionContext(context, context.GetRouteData(), new ControllerActionDescriptor()), result); }));
验证结果
修改后重新发送XML格式请求,会返回符合RFC7807规范的XML响应:
<problem xmlns="urn:ietf:rfc:7807"> <status>500</status> <title>This is a test exception</title> <type>https://tools.ietf.org/html/rfc9110#section-15.6.1</type> <traceId>00-xxxxxx-xxxxxx-00</traceId> </problem>
内容的提问来源于stack exchange,提问作者adrianm

