Hellang.Middleware.ProblemDetails error mapping - c#

I'm learning about using problem details middleware found here
I have the setup working all fine but I got curious why it's mapping validation errors differently than the default status code.
To explain better, in the sample repo provided by the owner try the following:
call https://localhost:54547/mvc/modelstate
response "status":422
In the project's Program.cs, comment out the MVC override AddProblemDetailsConventions (line 46) and call again
response "status":400
400 is the default status code for validation errors automatically inserted when you add the ApiController attribute to your controller.
In a previous discussion with the owner here, it was recommended to call AddProblemDetailsConventions
if you want to have 100% consistent error responses from your API (produced by the middleware).
I understand the middleware is to control the "format" of response error message to follow RFC7870, but why is it changing the error code for this example case? is 422 more specific/better practice than 400?
I tried to look for more details, but couldn't find any. like what other mappings are changed, or if there's a way to configure the middleware mapping for default validation error (since in our project we already have test suit asserting on 400 for validation scenarios).

From that same conversation with the author you cited, he does mention a way to override the default status response code in this post.
Regarding the 422 status code; it's an opinion of mine that syntactically correct, but semantically invalid requests should return back a different status code than 400 Bad Request
He also mentions that not everyone may choose to follow that convention, so he provides a way to override the default:
Some people don't like it (often because it's part of the WebDAV RFC and not an "official" HTTP RFC (but this will soon change, with the inclusion of 422 in HTTPbis' upcoming HTTP Semantics RFC, which obsoletes RFC 7231), so I've added an option to change it:
And provides a link to the source code value of ProblemDetailsOptions.ValidationProblemStatusCode.
You can pass in the options value to the configuration like this to change the default back to a 400 status code:
services.AddProblemDetails(options =>
{
options.ValidationProblemStatusCode = 400;
});
Or if you prefer to use the private configuration method like in the sample library:
private void ConfigureProblemDetails(ProblemDetailsOptions options)
{
options.ValidationProblemStatusCode = 400;
// the rest of the code setup he used in the example
}
As far as the other mappings that were changed, I don't see much in the source code that is configured by default apart from setting the status code to 500 if there is no status code present.

Related

Blazor WASM - Controller not found when making a PostAsJsonAsync Request

I am building a WASM app for the first time, and have been following tutorials.
The Solution I have is composed of 3 projects created by the wizard (Client, Server and Shared).
I am having trouble when making the following request from the index page:
var msg = await Http.PostAsJsonAsync<u001_000_001>("api/u001_000_001", userRec);
If (msg.IsSuccessStatusCode) ClearUserScr();
In the Server project, I have a Controllers folder with a controller named u001-000-001Controller (although the class name in the file is u001_000_001Controller). The relevant lines of code from the controller class are as follows:
[ApiController]
[Route("api/[controller]")]
public class u001_000_001Controller : ControllerBase
{
[HttpPost]
public async Task<u001_000_001> Post([FromBody] u001_000_001 create)
{
EntityEntry<u001_000_001> user = await db.u001_000_001.AddAsync(create);
await db.SaveChangesAsync();
return user.Entity;
}
}
The HttpClient is registered using the builder.HostEnvironment.baseAddress as the Uri in the Client Program.cs file.
The Shared folder contains the handler called u001-000-001 (class name u001_000_001).
I have tried all the different combinations I can think of in terms of changing the names in the actual call, and nothing works. I keep getting the same "not found - HTTP 400' error.
I would sincerely appreciate help from experienced eyes to see if there is a simple mistake I'm making or if there's something more serious I'm missing. Many thanks in advance for your time.
Many hours of research later, the error was found to be in the fields being fed initially into the handler, rather than anything happening with the actual HttpClient request or the controller.
Although I found that the Http/1.1 400 Bad request error could be generated by a range of issues, I would highly recommend reviewing the structure of the data being input as a first step, since this was overlooked in my case.
Clarification of Issue and Solution:
I have a process for creating new user logins, and the goal of the HttpClient PostAsJsonAsync request was to send new account details to the database. However one of the fields in the user record is a PIN number, and as this is not chosen by the new user in the first registration step, it was left as null in the code.
Keeping it null was fine for the code, but the Controller expects data to be input for all fields, and will not accept PostAsJsonAsync calls where any of the fields are left null.
The solution in my case was to set a temporary value, and then make the PostAsJsonAsync request with all of the fields filled and sent through the request.
I wish to thank the professionals who commented with potential solutions, as they helped to improve my code.

How do I check if a controller and / or action exists as a valid endpoint from a middleware?

I need to figure out from a middleware if a route or context points to a valid endpoint in my API. I want to do this in order to send a valid json-formatted error response, instead of the default empty error message that the API sends.
An alternative solution that figures out that the endpoint resulted in nothing is fine too. My first thought was to use a middleware, but perhaps sending an error with a fall-back controller works too?
I would like to give an answer to my own question, as I have found a way to manually check if a route exists. This was something I did not think of at the time, as I did not realise you could get information about your API through a dependency.
The way I have done this now is to make use of the IActionDescriptorCollectionProvider provider. This will allow me to receive all current routes in the API. Using this, I created the following middleware:
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value;
var routes = _actionDescriptorCollectionProvider.ActionDescriptors.Items.Select(ad => $"/{ad.AttributeRouteInfo.Template}").ToList();
if (!routes.Any(route => path.Equals(route, StringComparison.InvariantCultureIgnoreCase))) {
context = await context.HandleRequest(HttpStatusCode.NotFound, "RouteNotFound", "De server heeft geen geldige actie voor de gegeven route.");
return;
}
await _next(context);
}
This fully allows me to respond with a custom error (this is using HandleRequest(), which is an extension of my own), and handle the rest in the frontend.
I found another way to solve this to use pre-initialised documentation by the API. I'm not sure what to call it, but adding the following code to your csproj file creates an XML which gives the same benefits:
<NoWarn>$(NoWarn);1591</NoWarn>
<DocumentationFile>Files\Documentation\$(Configuration)_$(AssemblyName)_doc.xml</DocumentationFile>
This means that the XML has be parsed of course.
I am still looking for different solutions, perhaps better ones if there are problems with this one.

How to fail ASP.NET Web API deserialization when types do not match

I am having a problem with silently failing deserialization in ASP.NET Web API (version 5.1.2). I would like the deserialization to raise an error instead but I am unable to find a configuration for it.
My specific (simplified) case is this. A client application (AngularJS) sends a HTTP POST request to the ASP.NET Web API backend. As a payload there are a bunch of strings:
["ABC100", "ABC200", "ABC300"]
However, the server is expecting a list of integers:
List<int> Ids { get; set; }
What ends up happening is that the deserialization fails, the Ids list will be empty and there are no errors.
Ids: []
Of course the mismatch needs to be fixed as well, but it seems obvious to me that the POST request should fail in this case. How can I make it the default?
One solution to this problem seems to be checking the ModelState.IsValid property right at the start of the controller method:
[HttpPost]
[Route("Stuff/Ids/")]
public void PostStuff(List<int> Ids)
{
if(!ModelState.IsValid)
throw new Exception("ModelState is not valid.");
// Carry on...
}
The ModelState.IsValid is indeed false in the case described by my question.
The check can be made global by creating an action filter out of it. Instructions for this can be found for example in this article: Model Validation in ASP.NET Web API

ASP.NET MVC 6 handling errors based on HTTP status code

I want to display different error messages for each status code e.g:
400 Bad Request
403 Forbidden
500 Internal Server Error
404 Not Found
401 Unauthorized
How can I achieve this in the new ASP.NET MVC 6 applications? Can I do this using the built in UseErrorHandler method?
application.UseErrorHandler("/error");
Also, I noticed that even with the above handler, entering a non-existent URL e.g. /this-page-does-not-exist, causes an ugly 404 Not Found error page from IIS. How can this also be handled?
In MVC 5 we have had to use the system.web customerrors section for ASP.NET and the system.webServer httpErrors section in the web.config file but it was difficult to work with an unwieldy, with lots of very strange behaviour. Does MVC 6 make this a lot simpler?
You could use the StatusCodePagesMiddleware for this. Following is an example:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
app.UseStatusCodePagesWithReExecute("/StatusCodes/StatusCode{0}");
app.UseMvcWithDefaultRoute();
Controller which handles the status code requests:
public class StatusCodesController : Controller
{
public IActionResult StatusCode404()
{
return View(viewName: "NotFound"); // you have a view called NotFound.cshtml
}
... more actions here to handle other status codes
}
Some Notes:
Check other extension methods like UseStatusCodePagesWithRedirects
and UseStatusCodePages for other capabilities.
I tried having StatusCode as a query string in my example, but looks like this
middleware doesn't handle query strings, but you can take a look at
this code and fix this issue.
How can I achieve this in the new ASP.NET MVC 6 applications? Can I do this using the built in UseErrorHandler method?
Quick answer: Not in an elegant fashion.
Explanation/Alternative: To start lets first look at what the UseErrorHandler method is actually doing: https://github.com/aspnet/Diagnostics/blob/6dbbe831c493e6e7259de81f83a04d1654170137/src/Microsoft.AspNet.Diagnostics/ErrorHandlerExtensions.cs#L25 which adds the following middleware: https://github.com/aspnet/Diagnostics/blob/6dbbe831c493e6e7259de81f83a04d1654170137/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs Note lines 29-78 (the invoke method)
The invoke method is executed whenever a request comes in (controlled by the location of your application.UseErrorHandler("...") in your Startup.cs). So the UseErrorHandler is a glorified way of adding a custom middleware: middleware = component that can act on an http request.
Now with that background, if we wanted to add our own error middleware that differentiated requests. We could do this by adding a similar middleware that's like the default ErrorHandlerMiddleware by modifying these lines: https://github.com/aspnet/Diagnostics/blob/6dbbe831c493e6e7259de81f83a04d1654170137/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs#L48-L51 With that approach we could control the redirect path based on the status code.
In MVC 5 we have had to use the system.web customerrors section for ASP.NET and the system.webServer httpErrors section in the web.config file but it was difficult to work with an unwieldy, with lots of very strange behaviour. Does MVC 6 make this a lot simpler?
Answer: It sure does :). Just like the above answer the fix lies in adding middleware. There's a shortcut to adding simple middleware via the IApplicationBuilder in your Startup.cs; at the end of your Configure method you can add the following:
app.Run(async (context) =>
{
await context.Response.WriteAsync("Could not handle the request.");
// Nothing else will run after this middleware.
});
This will work because it means that you reached the end of your http pipeline without the request being handled (since it's at the end of your Configure method in Startup.cs). If you want to add this middleware (in the quick fashion) with the option to execute middleware after you, here's how:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Could not handle the request.");
// This ensures that any other middelware added after you runs.
await next();
});
Hope this helps!

How do you throw HttpResponseException in ASP 5 (vnext)

I'm writing an api controller in ASP 5. I want to return a bad request code exception if the parameters passed to the service are incorrect. In the current version of webapi I would do:
throw new HttpResponseException(HttpStatusCode.BadRequest);
However HttpResponseException is part of System.Web, which has been removed from ASP 5 and thus I cannot instantiate it anymore.
What is the proper way to do this in vNext?
To answer your question, you can use the WebApiCompatibilityShim which ports HttpResponseException (and many other features) forward into MVC 6. (thanks to DWright for the link to that article.)
It seems like the MVC-6 way to do it is to return an IActionResponse from your method and then call HttpBadRequest() which is a method on the base Microsoft.AspNet.Mvc.Controller class that returns an BadRequestResult, which I believe is the best way to get a 400 status into the response.
The Controller class has methods for other response codes as well - including the HttpNotFound() method which causes a 404 to be returned.
See also: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api

Categories