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
- I've used here a
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 thePostConfigure
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();
...
}