清理 ASP.NET Core 的自动 400 响应
Sanitize ASP.NET Core's automatic 400 responses
当操作收到错误输入时,运行时的 automatic 400 response 功能会生成一个 ProblemDetails
,其中包含错误消息 (errors.$[0]
),如下所示:
"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
我不想泄露实现细节。
如何排除 CompanyName.Foo.Bar
?
(我正在使用 ASP.NET Core 5,带有 API 控制器,而不是 MVC。)
想出了解决办法。可能有更好/更简单/更好的方法。
在Startup.ConfigureServices()
中:
services.Configure<ApiBehaviorOptions>(o => {
o.InvalidModelStateResponseFactory = actionContext => {
var problemsDetailsFactory = actionContext.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
var modelState = new ModelStateDictionary();
foreach (var key in actionContext.ModelState.Keys) {
var value = actionContext.ModelState[key];
foreach (var error in value.Errors) {
var errorMessage = Regex.Replace(error.ErrorMessage, @"^(The JSON value could not be converted)( to .*)(\. Path:.*)$", "");
modelState.AddModelError(key, errorMessage);
}
}
var problemDetails = problemsDetailsFactory.CreateValidationProblemDetails(actionContext.HttpContext, modelState, StatusCodes.Status400BadRequest);
return new BadRequestObjectResult(problemDetails);
};
});
消毒:
"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
收件人:
"The JSON value could not be converted. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
让我与您分享我们在模型绑定错误的情况下自定义响应的实现。
首先,让我们定义一个接口,其中包含一个可以传递给 InvalidModelStateResponseFactory
:
的方法
public interface IModelBindingErrorHandler
{
IActionResult HandleInvalidModelState(ActionContext context);
}
让我们继续定义两个模型。一个用于记录,另一个用于响应:
public class InvalidInputModel
{
public string FieldName { get; init; }
public string[] Errors { get; init; }
public override string ToString() => $"{FieldName}: {string.Join("; ", Errors)}";
}
public class GlobalErrorModel
{
public string ErrorMessage { get; init; }
public string ErrorTracingId { get; init; }
}
如您所见,它们都足够通用,也可以用于其他错误处理程序。
现在让我们实现IModelBindingErrorHandler
接口:
public class ModelBindingErrorHandler : IModelBindingErrorHandler
{
private ILogger<ModelBindingErrorHandler> logger;
public ModelBindingErrorHandler(ILogger<ModelBindingErrorHandler> logger)
=> this.logger = logger;
public IActionResult HandleInvalidModelState(ActionContext context)
{
var modelErrors = context.ModelState
.Where(stateEntry => stateEntry.Value.Errors.Any())
.Select(stateEntry => new InvalidInputModel
{
FieldName = stateEntry.Key,
Errors = stateEntry.Value.Errors.Select(error => error.ErrorMessage).ToArray()
});
var traceId = Guid.NewGuid();
logger.LogError("Invalid input model has been captured. ModelState: {modelErrors}, TraceId: {traceId}", modelErrors, traceId);
return new BadRequestObjectResult(new GlobalErrorModel
{
ErrorMessage = "Sorry, the request contains invalid data. Please revise.",
ErrorTracingId = traceId.ToString()
});
}
}
- 所以,在这里我们基本上收集了所有有价值的信息 (
Errors
) 并且我们正在记录它们
- 我们使用
traceId
将日志条目与响应连接起来
- 为了简单起见,我在这里使用
Guid.NewGuid()
而不是 correlationId
为了便于使用此实现,这里有两种自注册扩展方法:
public static class ModelBindingErrorHandlerRegister
{
public static IServiceCollection AddModelBinderErrorHandler(this IServiceCollection services)
{
return AddModelBinderErrorHandler<ModelBindingErrorHandler>(services);
}
public static IServiceCollection AddModelBinderErrorHandler<TImpl>(this IServiceCollection services)
where TImpl : class, IModelBindingErrorHandler
{
services.AddSingleton<IModelBindingErrorHandler, TImpl>();
var serviceProvider = services.BuildServiceProvider();
var handler = serviceProvider.GetService<IModelBindingErrorHandler>();
services.Configure((ApiBehaviorOptions options) =>
options.InvalidModelStateResponseFactory = handler.HandleInvalidModelState);
return services;
}
}
- 第一种方法注册上面的实现
- 第二种方法允许在需要时注册一个自定义的
InvalidModelStateResponseFactory
赋值也可以在 PostConfigure
中完成
有了这些,我们可以用一行注册一个自定义模型活页夹处理程序:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddModelBinderErrorHandler();
...
}
当操作收到错误输入时,运行时的 automatic 400 response 功能会生成一个 ProblemDetails
,其中包含错误消息 (errors.$[0]
),如下所示:
"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
我不想泄露实现细节。
如何排除 CompanyName.Foo.Bar
?
(我正在使用 ASP.NET Core 5,带有 API 控制器,而不是 MVC。)
想出了解决办法。可能有更好/更简单/更好的方法。
在Startup.ConfigureServices()
中:
services.Configure<ApiBehaviorOptions>(o => {
o.InvalidModelStateResponseFactory = actionContext => {
var problemsDetailsFactory = actionContext.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
var modelState = new ModelStateDictionary();
foreach (var key in actionContext.ModelState.Keys) {
var value = actionContext.ModelState[key];
foreach (var error in value.Errors) {
var errorMessage = Regex.Replace(error.ErrorMessage, @"^(The JSON value could not be converted)( to .*)(\. Path:.*)$", "");
modelState.AddModelError(key, errorMessage);
}
}
var problemDetails = problemsDetailsFactory.CreateValidationProblemDetails(actionContext.HttpContext, modelState, StatusCodes.Status400BadRequest);
return new BadRequestObjectResult(problemDetails);
};
});
消毒:
"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
收件人:
"The JSON value could not be converted. Path: $ | LineNumber: 0 | BytePositionInLine: 3."
让我与您分享我们在模型绑定错误的情况下自定义响应的实现。
首先,让我们定义一个接口,其中包含一个可以传递给 InvalidModelStateResponseFactory
:
public interface IModelBindingErrorHandler
{
IActionResult HandleInvalidModelState(ActionContext context);
}
让我们继续定义两个模型。一个用于记录,另一个用于响应:
public class InvalidInputModel
{
public string FieldName { get; init; }
public string[] Errors { get; init; }
public override string ToString() => $"{FieldName}: {string.Join("; ", Errors)}";
}
public class GlobalErrorModel
{
public string ErrorMessage { get; init; }
public string ErrorTracingId { get; init; }
}
如您所见,它们都足够通用,也可以用于其他错误处理程序。
现在让我们实现IModelBindingErrorHandler
接口:
public class ModelBindingErrorHandler : IModelBindingErrorHandler
{
private ILogger<ModelBindingErrorHandler> logger;
public ModelBindingErrorHandler(ILogger<ModelBindingErrorHandler> logger)
=> this.logger = logger;
public IActionResult HandleInvalidModelState(ActionContext context)
{
var modelErrors = context.ModelState
.Where(stateEntry => stateEntry.Value.Errors.Any())
.Select(stateEntry => new InvalidInputModel
{
FieldName = stateEntry.Key,
Errors = stateEntry.Value.Errors.Select(error => error.ErrorMessage).ToArray()
});
var traceId = Guid.NewGuid();
logger.LogError("Invalid input model has been captured. ModelState: {modelErrors}, TraceId: {traceId}", modelErrors, traceId);
return new BadRequestObjectResult(new GlobalErrorModel
{
ErrorMessage = "Sorry, the request contains invalid data. Please revise.",
ErrorTracingId = traceId.ToString()
});
}
}
- 所以,在这里我们基本上收集了所有有价值的信息 (
Errors
) 并且我们正在记录它们 - 我们使用
traceId
将日志条目与响应连接起来- 为了简单起见,我在这里使用
Guid.NewGuid()
而不是 correlationId
- 为了简单起见,我在这里使用
为了便于使用此实现,这里有两种自注册扩展方法:
public static class ModelBindingErrorHandlerRegister
{
public static IServiceCollection AddModelBinderErrorHandler(this IServiceCollection services)
{
return AddModelBinderErrorHandler<ModelBindingErrorHandler>(services);
}
public static IServiceCollection AddModelBinderErrorHandler<TImpl>(this IServiceCollection services)
where TImpl : class, IModelBindingErrorHandler
{
services.AddSingleton<IModelBindingErrorHandler, TImpl>();
var serviceProvider = services.BuildServiceProvider();
var handler = serviceProvider.GetService<IModelBindingErrorHandler>();
services.Configure((ApiBehaviorOptions options) =>
options.InvalidModelStateResponseFactory = handler.HandleInvalidModelState);
return services;
}
}
- 第一种方法注册上面的实现
- 第二种方法允许在需要时注册一个自定义的
InvalidModelStateResponseFactory
赋值也可以在PostConfigure
中完成
有了这些,我们可以用一行注册一个自定义模型活页夹处理程序:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddModelBinderErrorHandler();
...
}