Model Binding is the process of binding parameters of the route handler to request parameters (from route segments, query string parameters, cookies, request headers or request body) or services in the DI container.
The automatically added EndpointMiddleware
, which is the last middleware in the request pipeline, performs model binding, then invokes the handler for the requested route.
If an error occurs during model binding, then a Problem Details response is NOT returned by the EndpointMiddleware
. I have verified the following behaviour:
If a model binding error occurred but NOT due to an issue with contents of the HTTP request, then, in any ASP.NET Core environment, the EndpointMiddleware
would return a 500 status code.
An example is when a service that was meant to be resolved from the DI container and passed in as an argument to the handler could not be resolved. No exception would be throw, only 500
would be set as the status code of HttpContext.Request
If the error occurred due to an issue with contents of the request, e.g. a required field or value is missing or a request parameter has an invalid/malformed value or a value of an incorrect data type, then
-
In
Development
environment (and possibly in any non-Production
environment), aBadHttpRequestException
is thrown by the middleware. This has:-
StatusCode
property set to400
-
a
Message
property that is informative such as:Failed to read parameter "CreateProductArgs createProductArgs" from the request body as JSON.
-
InnerException
property which, if there was problem deserializing the JSON request body, would beSystem.Text.Json.JsonException
.Again, this has an informative
Message
property:
JSON deserialization for type 'problemdetailstestapi.CreateProductArgs' was missing required properties, including the following: price
-
-
In
Production
Environment, theEndpointMiddleware
sets status code400
in the outgoing response with a blank response body.However we can enable throwing of
BadHttpRequestException
inProduction
environment by adding the following line inProgram.cs
beforebuilder.Build
()
is called (from this issue in dotnet/aspnetcpore repo) to get our hands on this information.Now, even in
Production
, if a model binding error occurred due to an issue with contents of the request, then aBadHttpRequestException
would be thrown byEndpointMiddleware
. This is useful as the exception can be quite informative:
builder.Services.Configure<RouteHandlerOptions>( options => { options.ThrowOnBadRequest = true; } );
Clearly, the messages above in BadHttpRequestException
are useful but cannot be sent back to the client as doing so would reveal internal execution and implementation details.
I also do not want to parse them to extract information as for the same exception, the structure of the error message can be different in different sitautions. Also, exception messages may change in the future.
However, the messages are very useful for logging, at Information
level or above, and that alone is a good reason to turn on throwing of BadHttpRequestException
in Production
environment (i.e. to catch and log the exception).
I have verified that an error does not gets logged from within EndpointMiddleware
when there is a model binding exception binding some request to handelr parameters at level Information
or above. So we do need to catch and log this exception ourselves.
Based on the above behaviour, I believe we can create and return informative Problem Details responses in the event of a model binding error, as follows:
Enable throwing
HttpBadRequestException
inProduction
environment.Add a middleware just before
RoutingMiddleware
. This would catch catch aBadHttpRequestException
.-
If a
BadHttpRequestException
exception is caught in the middleware that was thrown by the RoutingMiddleware rather than by an endpoint filter or by the invoked router handler:- If
InnerException
isSystem.Text.Json.JsonException
then, since by default only a single request body is invalid in the specific sense that either a provided value is of an incorrect type, or a required value is missing. Report this with a ProblemDetails response. - Otherwise the issue with some other part of the request. Examples include the following: a requried route segment is missing or is of an incorrect value or the JSON body is missing. Report this with a ProblemDetails response.
- If
-
If a
BadHttpRequestException
was NOT caught in the middleware that was thrown by the EndpointMiddleware, i.e. no exception was caught, or an exception was caught but it was notBadHtpRequestException
or aBadHttpRequestException
was caught but it hadn’t been thrown by the EndpointMiddleware (i.e. had ben thrown by an endpoint filter or from somewhere in the invoked route handler), then we do not have the information to create a meaningful ProblemDetails response.So we just let it - the reponse or the exception - propagate up the request pipeline.
The trouble is, for all of this logic, that is not difficult to implement but is a a bit convoluted, all I get is the ability to distinguish between two classes of errors in the request that between each other cover pretty much anything that could go wrong with the request (headers, route segments, query string, request body, cookies).
I implemented a middleware to do this (see below) and it was not pretty, and it didn't include the check for the stack trace to see if the BadHttpRequestException
had been thrown by the EndpointMiddleware or not
CONCLUSION: So I don’t see what value distinguishing between these two classes of errors would add over just sending back the 400. SO I WILL NOT IMPLEMENT THIS SOLUTION.
The middleware was as follows:
using System.Text;
using System.Text.Json;
namespace problemdetailsmiddleware.Middleware;
public class ProblemDetailsForBadRequestMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ProblemDetailsForBadRequestMiddleware> _logger;
public ProblemDetailsForBadRequestMiddleware(RequestDelegate next, ILogger<ProblemDetailsForBadRequestMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (BadHttpRequestException ex)
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync(ex.StackTrace ?? "");
return;
context.Response.StatusCode = ex.StatusCode;
_logger.LogError(ex, "BadRequestException occurred while processing HTTP request");
if (ex.InnerException is JsonException)
{
//this would only happen if the
// var validationProblem = TypedResults.ValidationProblem(new Dictionary<string, string[]> {
// {"request body json", new string[] {@"An error occurred when parsing the provided request body. One of three things is likely to be wrong:
// 1. The provided request body is not well-formed JSON
// 2. A required key is missing in the request body JSON
// 3. A value of an incorrect type is provided for a key in the request body JSON"}}
// });
var problem = TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
type: "http://example.com/problems/invalid-request-body-json",
title: "\"An error occurred while parsing request body JSON\","
detail: "Request body was provided but an error occurred when parsing it. One of three things is likely to be wrong: 1. The provided request body is not well-formed JSON 2. A required property is missing in the request body JSON 3. A value of an incorrect or incompatible type was provided for a property in the request body JSON"
);
await problem.ExecuteAsync(context);
}
else
{
var problem = TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
type: "http://example.com/problems/missing-body-or-invalid-request-parameter-values",
title: "\"Request body is missing or a request parameter value is missing or invalid\","
detail: "Either the request body is required but missing, or the value of a request parameter - in request headers, query string, route segments or cookies - is either missing ( in case of a required parameter) or invalid (e.g. of an incorrect type). Check your request against the OpenAPI description of the operation."
);
await problem.ExecuteAsync(context);
}
}
}
}
public static class ProblemDetailsForBadRequestMiddlewareExtensions
{
public static IApplicationBuilder UseProblemDetailsForBadRequest(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ProblemDetailsForBadRequestMiddleware>();
}
}
Distinguishing between whether the BadHtpRequestException
was thrown from EndpointMiddleware of from further down the request processing pipeline:
If every frame in the stack (each is a new line) begins with at Microsoft.AspNetCore.Http.RequestDelegateFactory
until potentially a --- End of stack trace from previous location ---
line is ensountered, then the exception was thrown from EndpointMiddleware (I believe RequestDelegateFactory
create a RequestDelegate
out of every middleware in the pipeline that is to be invoked). For example,
at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.InvalidJsonRequestBody(HttpContext httpContext, String parameterTypeName, String parameterName, Exception exception, Boolean shouldThrow)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<HandleRequestBodyAndCompileRequestDelegateForJson>g__TryReadBodyAsync|102_0(HttpContext httpContext, Type bodyType, String parameterTypeName, String parameterName, Boolean allowEmptyRequestBody, Boolean throwOnBadRequest, JsonTypeInfo jsonTypeInfo)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForJson>b__2>d.MoveNext()
--- End of stack trace from previous location ---
at problemdetailsmiddleware.Middleware.ProblemDetailsForBadRequestMiddleware.InvokeAsync(HttpContext context) in C:\MyWork\problemdetails\problemdetailsmiddleware\Middleware\ProblemDetailsForBadRequestMiddleware.cs:line 20
If there are other frames, theses would come from an endpoint filter or from somewhere in the invoked route handler. In this case, not all frames, until the line --- End of stack trace from previous location ---
is encountered, would start with at Microsoft.AspNetCore.Http.RequestDelegateFactory
, as in this example from a Release build of a minimal API (for some reason the Release build also contained a .pdb
, hence the topmost frame even tells youthat the error occurred at line 80):
at Program.<>c.<<Main>$>b__0_3(IList`1 products, LinkGenerator linkGen, CreateProductArgs createProductArgs) in C:\MyWork\problemdetails\problemdetailsmiddleware\Program.cs:line 80
at lambda_method4(Closure, EndpointFilterInvocationContext)
at FluentValidation.AspNetCore.Http.FluentValidationEndpointFilter.InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<ExecuteValueTaskOfObject>g__ExecuteAwaited|129_0(ValueTask`1 valueTask, HttpContext httpContext, JsonTypeInfo`1 jsonTypeInfo)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForJson>b__2>d.MoveNext()
--- End of stack trace from previous location ---
at problemdetailsmiddleware.Middleware.ProblemDetailsForBadRequestMiddleware.InvokeAsync(HttpContext context) in C:\MyWork\problemdetails\problemdetailsmiddleware\Middleware\ProblemDetailsForBadRequestMiddleware.cs:line 20
Top comments (1)
Model Binding in ASP.NET Core maps HTTP request data to handler parameters or services. Errors during this process can trigger exceptions, which vary by environment (Development vs. Production). While custom middleware can catch and log these errors, it often adds complexity without significant benefits over a simple 400 response.
EchoAPI can streamline this process by allowing you to simulate various request scenarios during development. This helps you test error handling efficiently before going to Production. Check out EchoAPI echoapi.com/ to simplify API testing.