清理 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();
    ...
}