Custom error code pages with message - asp.net-core

I am trying to create a custom error code page that displays a message I pass to it in my .NET Core MVC 1.1 application. I setup custom error code pages support in the Startup.cs class file and then created a simple view in a controller that does public IActionResult Example1 => NotFound("Some custom error message"). I expected this message to be pushed to the controller however this is not the case. Calling NotFound() without any parameters hits the error controller but as soon as I pass a message through, the controller is never used and a simple text message is displayed.
I could have sworn I used to do this in the past with classic .NET MVC but it has been awhile.
How can I have custom error code pages that display the proper error. I also need the ability in a controller to return the standard text or JSON response during the error for cases when I expect a JSON response (API actions and such). I am assuming there is a way to do this with a attribute but I have yet to find a way to do either of these tasks.

What you could do is something similar to how the StatusCodePages middleware works. That middleware allows a pipeline re-execution model, to allow handling status code errors through the normal MVC pipeline. So when you return a non-successful status code from MVC, the middleware detects that and then re-executes the whole pipeline for a status code error route. That way, you are able to fully design status code errors. But as Chris Pratt already mentioned, those status codes are typically limited to just their code. There is not really a way to add additional details to it.
But what we could do is create our own error handling implementation on top of that re-execution model. For that, we create a CustomErrorResponseMiddleware which basically checks for CustomErrorResponseException exceptions and then re-executes the middleware pipeline for our error handler.
// Custom exceptions that can be thrown within the middleware
public class CustomErrorResponseException : Exception
{
public int StatusCode { get; set; }
public CustomErrorResponseException(string message, int statusCode)
: base(message)
{
StatusCode = statusCode;
}
}
public class NotFoundResponseException : CustomErrorResponseException
{
public NotFoundResponseException(string message)
: base(message, 404)
{ }
}
// Custom context feature, to store information from the exception
public interface ICustomErrorResponseFeature
{
int StatusCode { get; set; }
string StatusMessage { get; set; }
}
public class CustomErrorResponseFeature : ICustomErrorResponseFeature
{
public int StatusCode { get; set; }
public string StatusMessage { get; set; }
}
// Middleware implementation
public class CustomErrorResponseMiddleware
{
private readonly RequestDelegate _next;
private readonly string _requestPath;
public CustomErrorResponseMiddleware(RequestDelegate next, string requestPath)
{
_next = next;
_requestPath = requestPath;
}
public async Task Invoke(HttpContext context)
{
try
{
// run the pipeline normally
await _next(context);
}
catch (CustomErrorResponseException ex)
{
// store error information to be retrieved in the custom handler
context.Features.Set<ICustomErrorResponseFeature>(new CustomErrorResponseFeature
{
StatusCode = ex.StatusCode,
StatusMessage = ex.Message,
});
// backup original request data
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
// set new request data for re-execution
context.Request.Path = _requestPath;
context.Request.QueryString = QueryString.Empty;
try
{
// re-execute middleware pipeline
await _next(context);
}
finally
{
// restore original request data
context.Request.Path = originalPath;
context.Request.QueryString = originalQueryString;
}
}
}
}
Now, all we need to do is hook that up. So we add the middleware within our Startup.Configure, somewhere near the beginning:
app.UseMiddleware<CustomErrorResponseMiddleware>("/custom-error-response");
The /custom-error-response is the route that we are re-executing when a custom response is being requested. This can be a normal MVC controller action:
[Route("/custom-error-response")]
public IActionResult CustomErrorResponse()
{
var customErrorResponseFeature = HttpContext.Features.Get<ICustomErrorResponseFeature>();
var view = View(customErrorResponseFeature);
view.StatusCode = customErrorResponseFeature.StatusCode;
return view;
}
Since this uses MVC, this also needs a view:
#model ICustomErrorResponseFeature
#{
ViewData["Title"] = "Error";
}
<p>There was an error with your request:</p>
<p>#Model.StatusMessage</p>
And that’s basically all. Now, we can just throw our custom error response exceptions from our MVC actions to trigger this:
// generate a 404
throw new NotFoundResponseException("This item could not be found");
// or completely custom
throw new CustomErrorResponseException("This did not work", 400);
Of course, we could also expand this further, but that should be the basic idea.
If you are already using the StatusCodePages middleware, you might think whether all this custom re-execution is really necessary, when you already have exactly that in the StatusCodePages middleware. And well, it is not. We can also just expand on that directly.
For that, we will just add the context features, which we can set at any point during the normal execution. Then, we just return a status code, and let the StatusCodePages middleware run. Inside its handler, we can then look for our feature and use the information there to expand the status code error page:
// Custom context feature
public interface IStatusCodePagesInfoFeature
{
string StatusMessage { get; set; }
}
public class StatusCodePagesInfoFeature : IStatusCodePagesInfoFeature
{
public string StatusMessage { get; set; }
}
// registration of the StatusCodePages middleware inside Startup.Configure
app.UseStatusCodePagesWithReExecute("/error/{0}");
// and the MVC action for that URL
[Route("/error/{code}")]
public IActionResult StatusCode(int code)
{
var statusCodePagesInfoFeature = HttpContext.Features.Get<IStatusCodePagesInfoFeature>();
return View(model: statusCodePagesInfoFeature?.StatusMessage);
}
Inside of the normal controller actions, we can set that feature before returning a status code:
HttpContext.Features.Set<IStatusCodePagesInfoFeature>(new StatusCodePagesInfoFeature
{
StatusMessage = "This item could not be found"
});
return NotFound();
It is too bad you cannot intercept NotFound, Unauthorized, etc. responses in a middleware class.
Okay, option three! You can totally intercept those responses, just not inside of middleware, since these are MVC results and will not leave the MVC pipeline. So you have to intercept them within the MVC filter pipeline. But we could absolutely run a filter, for example a result filter, that modifies the result.
The problem is that we still need a way to pass the information on. We could use a context feature again, but we can also use the MVC object results. So the idea is that we can just do the following in the MVC actions:
return NotFound("The item was not found");
So usually, that string would be the plain text response. But before the result is being executed and the response is being generated, we can run a result filter to modify this and return a view result instead.
public class StatusCodeResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// retrieve a typed controller, so we can reuse its data
if (context.Controller is Controller controller)
{
// intercept the NotFoundObjectResult
if (context.Result is NotFoundObjectResult notFoundResult)
{
// set the model, or other view data
controller.ViewData.Model = notFoundResult.Value;
// replace the result by a view result
context.Result = new ViewResult()
{
StatusCode = 404,
ViewName = "Views/Errors/NotFound.cshtml",
ViewData = controller.ViewData,
TempData = controller.TempData,
};
}
// intercept other results here…
}
await next();
}
}
All you need is a view at Views/Errors/NotFound.cshtml now and everything will magically work once you have the filter registered.
You can either register the filter by adding a [TypeFilter(typeof(StatusCodeResultFilter))] attribute to the controller or individual actions, or you can register it globally.

What you want is not possible. When you do something like return NotFound with a message, that message will be included in the response body only if it's left unmolested. When you do something like enable status code pages, the NotFound is simply caught by the middleware, and the request will simply be handed off to your error handling action to ultimately obtain the response. Importantly, that means your original NotFoundResult along with any custom message has been round-filed.

Related

Core 7 - Api error handling, model state validation + UseExceptionhandler

I am currently working on implementing some Apis using swagger/swashbuckle in net core 7 and implementing some error handling, I've gone down the route of using an exception handler. With separate endpoints from dev/prod.
E.g. Startup.cs
if (env.IsDevelopment())
{
...details ommited
app.UseExceptionHandler("/dev-error");
}
else
{
...details ommited
app.UseExceptionHandler("/error");
}
ErrorController.cs
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : Controller
{
private ILogger _logger;
public ErrorController(ILogger logger)
{
_logger = logger;
}
[Route("dev-error")]
public IAttempt DevError()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
return Attempt.Fail(exception);
}
[Route("error")]
public IAttempt Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
_logger.Log(LogLevel.Error, exception, exception.Message);
switch (exception)
{
case UnauthorizedAccessException:
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return Attempt.Fail("Unauthorised");
default:
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
return Attempt.Fail("Generic Error");
}
}
}
The idea is that all responses are of IAttempt, so that the FE user can check if its succeeded etc. and whether to handle the result or exception in a user friendly way.
This has been working great up until now when I've been implementing Api's that require the model to be validated. I wanted to amend the IAttempt class to provide modelstate feedback, however I have tried many approaches and cant seem to get modelstate validation flow through the exception handler.
I wanted to implement a custom ValidationException that contains the errors which is then handled in these controllers. But when an exception is thrown in either an IActionFilter or when overriding the InvalidModelStateResponseFactory the exception isn't caught by the exception handler.
Is there a work around? Am I missing something?
Alternatively I could define a InvalidModelStateResponseFactory that returns a similar model(IAttempt), but it would be nice for Failed requests to be handled in one place.
Cheers in advance
I think you can make the InvalidModelStateResponseFactory redirect to the ErrorController, sending the required data to create your response
According to your description, I suggest you could consider using the customer action filter to achieve your requirement.
Inside the custom action filter, we could get the model state's results, then you could throw the exception inside it.
More details, you could refer to below codes:
1.Create the custom action filter:
public class CustomValidationActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (!context.ModelState.IsValid)
{
var errorList = context.ModelState.Values
.SelectMany(m => m.Errors)
.Select(m => m.ErrorMessage)
.ToList();
throw new Exception();
}
}
public void OnActionExecuting(ActionExecutingContext context) { }
}
2.Inside the program.cs
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new CustomValidationActionFilter());
});
Then if it thrown the exception, it will go to the controller's error action method, since you set the global exception handler.
I was unnecessarily over complicating things so I have dropped what I attempted to do as in theory responses should be handled accordingly to their response status code rather then the object thats passed in.

OnAuthorizationAsync not being called when creating custom AuthorizeFilter that inherits from AuthorizeFilter

I've created a custom authorize filter which looks like this:
public class BearerTokenAuthorizeFilter : AuthorizeFilter
{
public override async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
await base.OnAuthorizationAsync(context);
if (context.Result is ChallengeResult)
{
// Then return a problem detail
ObjectResult result = new ObjectResult(new ProblemDetails
{
Type = ProblemDetailsTypes.Unauthorized,
Title = ReasonPhrases.GetReasonPhrase(StatusCodes.Status401Unauthorized),
Status = StatusCodes.Status401Unauthorized,
Detail = ProblemDetailsDescriptions.Unauthorized
});
result.ContentTypes.Add(new MediaTypeHeaderValue(new Microsoft.Extensions.Primitives.StringSegment("application/problem+json")));
context.Result = result;
await context.HttpContext.ChallengeAsync();
}
else if (context.Result is ForbidResult)
{
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
await context.HttpContext.ForbidAsync();
}
}
}
I am registering this filter like this:
services.AddMvcCore(options =>
{
options.Filters.Add<BearerTokenAuthorizeFilter>();
});
I have set the default authentication to be 'Bearer':
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
I have added Authorize attribute on the controller. Whenever I send an unauthorized request to the endpoint my custom filter is never called and I have no idea why? My goal is to return problem details if the request is unauthorized to provide a little bit more information to the consumer than just the status code. Why is my filter not being called?
Try implement IAuthorizationFilter or IAsyncAuthorizationFilter instead of AuthorizeFilter. It work for me. Also I noticed that GetFilter(..) method returns AuthorizeFilter instance directly in AuthorizationApplicationModelProvider when filter class implements AuthorizeFilter. But when filter implements IAuthorizationFilter or IAsyncAuthorizationFilter this method being not called I think that is issue in ASP NET
I have ended up implementing my own IControllerModelConvention class which looks like this:
public class BearerTokenAuthorizeConvention : IControllerModelConvention
{
private AuthorizationPolicy _policy;
public BearerTokenAuthorizeConvention(AuthorizationPolicy policy)
{
_policy = policy;
}
public void Apply(ControllerModel controller)
{
if (controller.Filters.OfType<BearerTokenAuthorizeFilter>().FirstOrDefault() == null)
{
//default policy only used when there is no authorize filter in the controller
controller.Filters.Add(new BearerTokenAuthorizeFilter(_policy));
}
}
}
This will be executed once per controller. I then registered this convention like this:
// Configure application filters and conventions
services.Configure<MvcOptions>(options =>
{
AuthorizationPolicy defaultPolicy = new AuthorizationOptions().DefaultPolicy;
options.Conventions.Add(new BearerTokenAuthorizeConvention(defaultPolicy));
});
At this point every controller I have will be tagged with this custom filter which will call base implementation of AuthorizeFilter. The reason why I wanted to derive from AuthorizeFilter was because I wanted to call the default implementation of Authorize and then handle failed response on my own. I thought I could accomplish this very functionality and somehow still be able to only use Authorize attribute. This doesn't seem to be possible. Unless it is an I'm missing something?

CustomActionFilter not getting called for POST/PUT endpoint in web api [duplicate]

I am using .NET Core 2.2 with Web API. I have created one class, i.e., as below:
public class NotificationRequestModel
{
[Required]
public string DeviceId { get; set; }
[Required]
public string FirebaseToken { get; set; }
[Required]
public string OS { get; set; }
public int StoreId { get; set; }
}
Using the above class I have created one method. Now I want to return a custom object, but it's returning its own object.
API method is:
public ActionResult<bool> UpdateFirebaseToken(NotificationRequestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}
var result = _notificationService.InsertOrUpdateFirebaseToken(model);
return Ok(result);
}
Here FormatOutput method is format the output.
protected Base FormatOutput(object input, int code = 0, string message = "", string[] details = null)
{
Base baseResult = new Base();
baseResult.Status = code;
baseResult.Error = message;
baseResult.TimeStamp = CommonHelper.CurrentTimeStamp;
baseResult.Code = code;
baseResult.Details = details;
baseResult.Message = message; //Enum.Parse<APIResponseMessageEnum>(code.ToString(), true); // (enum of code get value from language)
return baseResult;
}
But the issue is it returns:
{
"errors": {
"DeviceId": [
"The DeviceId field is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "80000049-0001-fc00-b63f-84710c7967bb"
}
I want to customize this error with my model. I need error message and details from return output and passed it to my model. How can I do that? I had try to debug my code and found that breakpoint on API method is not calling. So I can't handle my custom method. Is there any solution? What am I doing wrong?
When using a controller with the ApiController attribute applied, ASP.NET Core automatically handles model validation errors by returning a 400 Bad Request with ModelState as the response body. As such, your conditional testing ModelState.IsValid is essentially always false (and therefore not entered) because the only requests that will ever get this far are valid ones.
You could simply remove the ApiController attribute, but that removes a bunch of other beneficial stuff the attributes adds as well. The better option is to use a custom response factory:
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
new BadRequestObjectResult(actionContext.ModelState);
});
That's essentially what's happening by default, so you'd simply need to change the action provided there accordingly to customize it to your whims.
As Chris analyzed, your issue is caused by Automatic HTTP 400
responses.
For the quick solution, you could suppress this feature by
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
For an efficient way, you could follow the suggestion from Chris, like below:
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
//options.SuppressModelStateInvalidFilter = true;
options.InvalidModelStateResponseFactory = actionContext =>
{
var modelState = actionContext.ModelState.Values;
return new BadRequestObjectResult(FormatOutput(modelState));
};
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
And, there isn't any need to define the code below any more in your action.
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}

Add a message to your HTTP 400 Bad Request in ASP.NET Core 3.1

Is there a way I can add a message to a BadRequest action result, and for that message to be visible to external clients, such as Postman? I am using ASP.NET Core 3.1.
Part of my code is included below. I want to say what the problem is—e.g., that the id sent in the body is not the same as the one taken from the URL. For now, I am using an Error object that I’ve made, which has the error code and message. But those aren't visible in Postman when I send the request.
public ActionResult PutColour(int id, Colour colour)
{
if (id != colour.Id)
{
return BadRequest(new Error("IDNotTheSame","ID from URL is not the same as in the body."));
}
}
What you pass to BadRequest is serialized and returned as the response body. If nothing is coming through, the only explanation is that you don't have any public properties on Error that can be serialized. For example, if you had something like:
public class Error
{
public Error(string type, string description)
{
Type = type;
Description = description;
}
public string Type { get; private set }
public string Description { get; private set; }
}
Then, you get a response like:
{
"type": "IDNotTheSame",
"description": "ID from URL is not the same as in the body."
}
Not sure what your Error class currently does. However, this is probably unnecessary anyways, as you can just use ModelState:
ModelState.AddModelError("Id", "ID from URL is not the same as in the body.");
return BadRequest(ModelState);
Finally, it should probably be said that this is a pointless validation in the first place. You shouldn't be sending an id with the model at all (always use a view model, not an entity class), and even if you do send it, you can simply just overwrite it with the value from the URL:
model.Id = id;
Done. No issues and you don't need to worry about sending an error back.
Please follow the following steps to show Bad Request with your custom Class.
Create constructor here of that class. And add manual description and many more
Step 1
Add a custom class and inherit this class with ValidationProblemDetails. After that initialize below base class constructor by passing context.ModelState.
public ValidationProblemDetails(ModelStateDictionary modelState)
Here is implementation-
public class CustomBadRequest : ValidationProblemDetails
{
public CustomBadRequest(ActionContext context) : base(context.ModelState)
{
Detail = "add details here";
Instance = "add extension here";
Status = 400;
Title = "add title here";
Type = "add type here";
}
}
Step 2
Add this Custom Bad request class in ConfigureServices method in Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomBadRequest(context);
return new BadRequestObjectResult(problems);
};
});
}
Step 3
Build and run.
and if any bad request come then you will see output like below-

How to a I redirect to Custom Error Handler Page

I want a generic error page for all application errors.
I have followed the guidelines to create a custom error handler in ASP.NET core and this catches the errors as expected. However, I cannot see how to redirect to a generic error handling the page. Examples seemed to be focused on Web API, not UI.
I have the following custom error handling code
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
int exceptionId = ExceptionManager.Publish(exception);
return context.Response.WriteAsync(new ErrorViewModel()
{
ExceptionId = exceptionId
}.ToString());
}
The exception details are logged to a database and return an Id. I have a controller action that displays the Id so the users can report it.
How do I redirect to my error view?
In Startup.cs method you need to call ExceptionHandlerMiddleware like below.
app.UseMiddleware(typeof(ExceptionHandlerMiddleware));
create a middleware class and write below code
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate next;
public ExceptionHandlerMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
//Write you logic
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = 500;
if (IsRequestAPI(context))
{
//when request api
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
{
State = 500,
message = exception.Message
}));
}
else
{
//when request page
context.Response.Redirect("/Home/Errorpage");
}
}
}
Middleware is "waterfalled" down through until either all have been executed, or one stops execution (in the case of our exception handling, we'll be writing ours so it stops the execution. More on that later).
The first things passed to your middleware is a request delegate. This is a delegate that takes the current HttpContext object and executes it. Your middleware saves this off upon creation and uses it in the Invoke() step.
Invoke() is where the work is done. Whatever you want to do to the request/response as part of your middleware is done here. Some other usages for middleware might be to authorize a request based on a header or inject a header into the request or response. For more examples, check out the Middleware documentation.