ASP.NET Core: global handling for showing exception message in current page - asp.net-core

In my application I have a set of errors that are "expected" and thus they shouldn't redirect the user to a full error page when they occur. For example, I would like to show the error message in a red box above the current page.
I can easily catch an error on a model action and show the message on the current view, but I was wondering how to do it globally.
I tried a custom middleware with TempData and a filter with ModelState, but can't wrap my head around how to actually pass the error data back to the current page.
This works for a single model (setting the error data in TempData):
public async Task<IActionResult> OnPost() {
try {
// methodThatThrows
}
catch (ApplicationError e) {
TempData["Error"] = e.Message;
return RedirectToPage("Current_Page");
}
return RedirectToPage("Other_Page");
}
For some reason, this doesn't work (in a global middleware), as TempData is empty when the redirect completes. Also setting the middleware doesn't really work with showing the other, more critical errors in the normal error page:
public class ApplicationErrorMiddleware {
private readonly RequestDelegate _next;
private readonly ITempDataDictionaryFactory _tempFactory;
public ApplicationErrorMiddleware(RequestDelegate next, ITempDataDictionaryFactory tempFactory) {
_next = next;
_tempFactory = tempFactory;
}
public async Task InvokeAsync(HttpContext httpContext) {
try {
await _next(httpContext);
}
catch (ApplicationError ex) {
HandleError(httpContext, ex);
}
}
private void HandleError(HttpContext context, ApplicationError error) {
var tempData = _tempFactory.GetTempData(context);
tempData.Add("Error", error.Message);
context.Response.Redirect(context.Request.Path);
}
}

By the tip of #hiiru, I went through a wild goose chase through the configuration options to find a working solution.
My issue was a missing call from the middleware HandleError-method:
private void HandleError(HttpContext context, ApplicationError error) {
var tempData = _tempFactory.GetTempData(context);
tempData.Add("Error", error.Message);
tempData.Save(); // this call was missing
context.Response.Redirect(context.Request.Path);
}
After popping that in there, the tempdata is sent with the redirection back to the original page. Note that this is using the default cookie-based temp data, so no specific configuration is needed.
Now, this works, but it might not be the best way to do this.

Related

Asp.Net AuthorizationHandler's response upsets Chrome, causes "net::ERR_HTTP2_PROTOCOL_ERROR"

I've decided to write a custom AuthorizationHandler for a custom Policy I'm using :
// I pass this to AddPolicy in startup.cs
public class MyRequirement : IAuthorizationRequirement {
public MyRequirement () { ... }
}
public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement> {
public MyAuthorizationHandler() { }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {
if (context.Resource is HttpContext httpContext) {
var endpoint = httpContext.GetEndpoint();
if ( /* conditions for hard failure */ ) { context.Fail(); return; }
if ( /* conditions for success */) { context.Succeed(requirement); return; }
// Neither a success nor a failure, simply a different response.
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);
return;
}
context.Fail();
}
}
I've seen similar code snippets in other StackOverlflow answers. (e.g. here : How to change status code & add message from failed AuthorizationHandler policy )
Problem : this doesn't seem to generate a "valid" 404 response.
I think so for two reasons:
When I look at Chrome's network tab, the response is NOT "404", instead it's net::ERR_HTTP2_PROTOCOL_ERROR 404
When I look at the response data, there's only headers. My custom error text ("Blah blah NotFound") does not appear anywhere.
What am I doing wrong?
Note : I've tried returning immediately after setting the 404, without doing context.Fail() but I get the same result.
The root cause:
My Web Api had several middlewares working with the response value. Those middleware were chained up in Startup.cs, using the traditional app.UseXXX().
Chrome was receiving 404 (along with my custom response body) from my Requirement middleware (hurray!), but Chrome is "smart" and by design continues to receive the response even after that initial 404 -- for as long as the server continues generating some response data.
Because of that, Chrome eventually came across a different response added by another of the chained up middlewares. The 404 was still there, but the response body was slightly changed.
And since chrome is paranoid, it would display this net::ERR_HTTP2_PROTOCOL_ERROR to indicate that someone had messed up the consistency of the response somewhere along the chain of responders.
==========
The solution :
Finalize your response with Response.CompleteAsync() to prevent any other subsequent middleware from changing it further :
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement) {
if (context.Resource is HttpContext httpContext) {
var endpoint = httpContext.GetEndpoint();
if ( /* conditions for hard failure */ ) { context.Fail(); return; }
if ( /* conditions for success */) {
context.Succeed(requirement);
return;
}
// Neither a requirement success nor a requirement failure, just a different response :
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync("Blah blah NotFound");
await httpContext.Response.CompleteAsync(); // <-- THIS!!!
return;
}
context.Fail();
}
Please note : if your 'HandleRequirementAsync' function does not have the 'async' keyword, then do not use 'await' inside of it, and do return Task.CompletedTask; instead of just return;
Below is a work demo based on your code, you can refer to it.
public class MyAuthorizationHandler : AuthorizationHandler<MyRequirement>
{
private readonly IHttpContextAccessor httpContextAccessor;
public MyAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement)
{
if ( /* conditions for success */) {
context.Succeed(requirement);
return;
}
// If it fails at this point, I want to return 404 because of reasons.
else
{
var httpContext = httpContextAccessor.HttpContext;
httpContext.Response.StatusCode = 404;
httpContext.Response.ContentType = "application/json";
httpContext.Response.WriteAsync("Blah blah NotFound").ConfigureAwait(false);
}
return Task.CompletedTask;
}
}
result:

Custom Result in Net 6 Minimal API

In ASP.NET Core 5 I had a custom Action Result as follows:
public class ErrorResult : ActionResult {
private readonly IList<Error> _errors;
public ErrorResult(IList<Error> errors) {
_errors = errors;
}
public override async Task ExecuteResultAsync(ActionContext context) {
// Code that creates Response
await result.ExecuteResultAsync(context);
}
}
Then on a Controller action I would have:
return new ErrorResult(errors);
How to do something similar in NET 6 Minimal APIs?
I have been looking at it and I think I should implement IResult.
But I am not sure if that is the solution or how to do it.
I have recently been playing around with minimal APIs and and working on global exception handling. Here is what I have come up with so far.
Create a class implementation of IResult
Create a constructor which will take an argument of the details you want going into your IResult response. APIErrorDetails is a custom implementation of mine similar to what you'd see in ProblemDetails in MVC. Method implementation is open to whatever your requirements are.
public class ExceptionAllResult : IResult
{
private readonly ApiErrorDetails _details;
public ExceptionAllResult(ApiErrorDetails details)
{
_details = details;
}
public async Task ExecuteAsync(HttpContext httpContext)
{
var jsonDetails = JsonSerializer.Serialize(_details);
httpContext.Response.ContentType = MediaTypeNames.Application.Json;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(jsonDetails);
httpContext.Response.StatusCode = _details.StatusCode;
await httpContext.Response.WriteAsync(jsonDetails);
}
}
Return result in your exception handling middleware in your Program.cs file.
app.UseExceptionHandler(
x =>
{
x.Run(
async context =>
{
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
// Whatever you want for null handling
if (exceptionFeature is null) throw new Exception();
// My result service for creating my API details from the HTTP context and exception. This returns the Result class seen in the code snippet above
var result = resultService.GetErrorResponse(exceptionFeature.Error, context);
await result.ExecuteAsync(context); // returns the custom result
});
}
);
If you still want to use MVC (Model-View-Controller), you still can use Custom ActionResult.
If you just want to use Minimal APIs to do the response, then you have to implement IResult, Task<IResult> or ValueTask<IResult>.
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
The following example uses the built-in result types to customize the response:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
You can find more IResult implementation samples here: https://github.com/dotnet/aspnetcore/tree/main/src/Http/Http.Results/src
Link: Minimal APIs overview | Microsoft Docs

If criteria not met redirect to different Razor Page from public async Task OnGetAsync()

I am new to the "async" and "task" stuff.
I can't seem to get working a simple if{} else{} inside the OnGetAsync().
public async Task OnGetAsync()
{
if (HttpContext.Session.GetString("LoggedStatus") != null)
{
//KEEP GOING
Accounts = await _context.Accounts.ToListAsync();
}
else
{
RedirectToPage("./Index");
}
}
The error I get is from the Accounts page, which I am trying to avoid even going near by using the "RedirectToPage("./Index")" which is my Home page.
I tried putting "return" word in front of RedirectToPage but it turns red when I do that. Also, if first condition is met (there is a value in the Session object) the Accounts pages shows up with no errors. So, I'm pretty sure the problem is in my attempt to redirect in the "else" statment.
NullReferenceException: Object reference not set to an instance of an object.
OESAC.Pages.Accounts.Pages_Accounts_Index.ExecuteAsync() in Index.cshtml
+
#foreach (var item in Model.Accounts)
The error above is in Accounts right where it loops thru and displays rows.
I'm not sure why it even gets to the Accounts.chstml.
You need to use Task<IActionResult> in public async Task<IActionResult> OnGetAsync(), combined with a return statement.
public async Task<IActionResult> OnGetAsync()
{
if (HttpContext.Session.GetString("LoggedStatus") != null)
{
//KEEP GOING
Accounts = await _context.Accounts.ToListAsync();
return Page();
}
else
{
return RedirectToPage("./Index");
}
}
Microsoft's docs has some good read on this here:
https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-2.2&tabs=visual-studio
Based on a comment, you can run this w/o async.
public IActionResult OnGet()
{
if (HttpContext.Session.GetString("LoggedStatus") != null)
{
//KEEP GOING
Accounts = _context.Accounts.ToList();
return Page();
}
else
{
return RedirectToPage("./Index");
}
}

How can I successfully use HttpClient to access iTunes search service in MVC4 app

I am writing an MVC4 app, and in the controller, I need to be able to make a call to a web service to access the iTunes search service in order to get some cover art for a DVD.
I have the following very simple code
public static string GetAlbumArt(Movie movie) {
Task<string> lookupTask = LookupAlbumArt(movie.Title, movie.Year);
lookupTask.Wait(5000);
if (lookupTask.IsCompleted)
{
return lookupTask.Result;
}
else
{
return Timeout;
}
}
private static async Task<string> LookupAlbumArt(string title, int year)
{
using (var client = new HttpClient())
{
string response= await client.GetStringAsync(
"http://itunes.apple.com/search?term=hoosiers&media=movie"
);
return response;
}
}
When I run this and set breakpoints at the return Timeout line in GetAlbumArt and also at the return response line in LookupAlbumArt, the Timeout breakpoint is hit first, and then the LookupAlbumArt return statement is hit, and the correct content is in the response variable.
If I remove the timeout parameter from lookupTask.Wait(5000), the wait GetStringAsync call never returns, and the page never loads.
This feels like a pretty straightforward task, and I'm stumped as to what I'm doing wrong.

Custom error pages in mvc 4 application, setup with Windows authentication

I have an intranet application setup with windows authentication. Like in most applications, certain parts of the application are accessible to specific roles only. When a user not in desired role would try to access that area, he should be shown a friendly "You do not have permission to view this page" view.
I searched and looked at several resources that guides to extend the Authorize Attribute. I tried that approach, but it simply doesn't work. I still get the IIS error message and the breakpoint in this custom attributes never gets hit. The breakpoint in my extended attibute doen't get hit even when a user in role visits the page. So, I am wondering if I am missing anything ?
This is what I have -
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeRedirect : AuthorizeAttribute
{
private const string IS_AUTHORIZED = "isAuthorized";
public string RedirectUrl = "~Areas/Errors/Http401";
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool isAuthorized = base.AuthorizeCore(httpContext);
httpContext.Items.Add(IS_AUTHORIZED, isAuthorized);
return isAuthorized;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
var isAuthorized = filterContext.HttpContext.Items[IS_AUTHORIZED] != null ? Convert.ToBoolean(filterContext.HttpContext.Items[IS_AUTHORIZED]) : false;
if(!isAuthorized && filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl);
}
}
}
CONTROLLER -
[AuthorizeRedirect]
[HttpPost, ValidateInput(true)]
public ActionResult NewPart(PartsViewModel vmodel) {..}
Any ideas?
Thanks
I think you could use custom error pages instead. Use AuthorizeAttribute to restrict access by callers to an action method.
[Authorize (Roles="Editor, Moderator", Users="Ann, Gohn")]
public ActionResult RestrictedAction()
{
// action logic
}
Then you could use one of the ways those are proposed by #Marco. I like handle HTTP status code within Application_EndRequest. So, it is possible to solve your problem using by following:
protected void Application_EndRequest()
{
int status = Response.StatusCode;
if (Response.StatusCode == 401)
{
Response.Clear();
var rd = new RouteData();
rd.DataTokens["area"] = "Areas";
rd.Values["controller"] = "Errors";
rd.Values["action"] = "Http401";
IController c = new ErrorsController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
}
}
To clearly specifiey what happens to an existing response when the HTTP status code is an error, you should use existingResponse attribute of <httpErrors> element in your configuration file. If you want to the error page appears immediately, then use Replace value, in otherwise - PassThrough (see details in my issue).