Home > OS >  Sanitize ASP.NET Core's automatic 400 responses
Sanitize ASP.NET Core's automatic 400 responses

Time:10-23

When an action receives bad inputs, the runtime's automatic 400 response feature generates a ProblemDetails which contains an error message (errors.$[0]) like this:

"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

I don't want to leak implementation details.

How can I exclude CompanyName.Foo.Bar?

(I'm using ASP.NET Core 5, with API controllers, not MVC.)

CodePudding user response:

Figured out a solution. There could be a better / easier / perfer way.

In 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:.*)$", "$1$3");
        modelState.AddModelError(key, errorMessage);
      }
    }

    var problemDetails = problemsDetailsFactory.CreateValidationProblemDetails(actionContext.HttpContext, modelState, StatusCodes.Status400BadRequest);
    return new BadRequestObjectResult(problemDetails);
  };
});

That sanitises:

"The JSON value could not be converted to CompanyName.Foo.Bar. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

To:

"The JSON value could not be converted. Path: $ | LineNumber: 0 | BytePositionInLine: 3."

CodePudding user response:

Let me share with you our implementation of a custom response in case of Model Binding error.

First, let's define an interface which contains a method that can be passed to the InvalidModelStateResponseFactory:

public interface IModelBindingErrorHandler
{
    IActionResult HandleInvalidModelState(ActionContext context);
}

Let's continue by defining two models. One for logging and another for response:

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; }
}

As you can see, both of them are generic enough to be used in other error handlers as well.

Now let's implement the IModelBindingErrorHandler interface:

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()
        });
    }
}
  • So, here we basically collect all valuable information (Errors) and we are logging them
  • We connect the log entry with the response by using a traceId
    • I've used here a Guid.NewGuid() instead of a correlationId for the sake of simplicity

In order to make the usage of this implementation easy here are two extension methods for self-registration:

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;
    }
}
  • The first method registers the above implementation
  • The second method allows to register a custom one if needed
  • The InvalidModelStateResponseFactory assignment can be done inside the PostConfigure as well

With these in our hand we can register a custom model binder handler with a single line:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddModelBinderErrorHandler();
    ...
}

  • Related