Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correctly handling custom errors in ASP.NET MVC?

Tags:

asp.net-mvc

I've spent the day going through a dozen or more sites, blogs, and SO answers, all claiming to provide the "right" way to implement custom error handling, in ASP.NET MVC.

None of them agree with each other, and most of them don't seem to work.

What I want:

  • Custom pages for the standard HTTP errors
  • That have the same look-and-feel as the standard pages of my site
  • Without having to completely redo everything I have in my Shared/_Layout.cspx
  • That work whether the user is authenticated or not
  • But that don't provide access to anything the user should be required to be authenticated to access
  • That should properly return the URL requested with the appropriate HTTP error code, rather than redirecting to a different page that returns HTTP 200
  • That redirects anything that isn't a valid route to the 404 page, whether it is a missing action, a missing controller, or a missing route
  • That allows for handling any sub-codes, like 404.3, that we need handled separately
  • That works the same in IIS7, IIS Express, and in Casini
  • That works in MVC5, but will continue to work in MVC6, MVC7, and whatever.
  • That doesn't involve separate code being added to each and every page

And, I want this to be the canonical answer. Not just "this is what I do", but "this is what Microsoft's developers intended and expected to be done".

Any ideas?

like image 904
Jeff Dege Avatar asked Aug 26 '14 22:08

Jeff Dege


3 Answers

I'm not sure if this is the best way but it works for me and its seo friendly: web.config

 <system.webServer>
   <httpErrors errorMode="Custom" existingResponse="Replace">
      <remove statusCode="404" />
      <error statusCode="404" responseMode="ExecuteURL" path="/Error/NotFound" />
    </httpErrors>
  </system.webServer>

you can define sub status codes too:

<error statusCode="404" subStatusCode="2" responseMode="ExecuteURL" path="/Error/NotFound" />

and this is how my error controller looks like:

public class ErrorController : Controller
{
    //
    // GET: /Error/      

    public ActionResult NotFound()
    {
        // you have to set your error codes manually
        Response.StatusCode = (int)HttpStatusCode.NotFound;

        return View();
    }
}
like image 123
Marian Ban Avatar answered Oct 22 '22 04:10

Marian Ban


The following solution meets most of your requirements except:

  • But that don't provide access to anything the user should be required to be authenticated to access This is very dependent on your architecture of your application, however this method does not add any risk of this happening
  • That works the same in IIS7, IIS Express, and in Casini not used Casini so can't guarantee this
  • That works in MVC5, but will continue to work in MVC6, MVC7, and whatever. Haven't tried this in MVC6
  • That allows for handling any sub-codes, like 404.3, that we need handled separately It depends what you mean by this - you can return a Response.SubStatusCode to the client in your action results. If you mean to handle it within the application, then provided it is in the exception object thrown, you can access it.

Create an ErrorController - this allows you to tailor your end-user error pages and status codes.:

[AllowAnonymous]
public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View();
    }

    public ActionResult ServerError()
    {
        Response.StatusCode = 500;
        return View();
    }

    public ActionResult UnauthorisedRequest()
    {
        Response.StatusCode = 403;
        return View();
    }

    //Any other errors you want to specifically handle here.

    public ActionResult CatchAllUrls()
    {
        //throwing an exception here pushes the error through the Application_Error method for centralised handling/logging
        throw new HttpException(404, "The requested url " + Request.Url.ToString() + " was not found");
    }
}

Add a route to catch all urls to the end of your route config - this captures all 404's that are not already caught by matching existing routes:

routes.MapRoute("CatchAllUrls", "{*url}", new { controller = "Error", action = "CatchAllUrls" });

In your global.asax:

protected void Application_Error(object sender, EventArgs e)
    {
        Exception exception = Server.GetLastError();

        //Error logging omitted

        HttpException httpException = exception as HttpException;
        RouteData routeData = new RouteData();
        IController errorController = new Controllers.ErrorController();
        routeData.Values.Add("controller", "Error");
        routeData.Values.Add("area", "");
        routeData.Values.Add("ex", exception);

        if (httpException != null)
        {
            //this is a basic exampe of how you can choose to handle your errors based on http status codes.
            switch (httpException.GetHttpCode())
            {
                case 404:
                    Response.Clear();

                    // page not found
                    routeData.Values.Add("action", "PageNotFound");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

                    break;
                case 500:
                    // server error
                    routeData.Values.Add("action", "ServerError");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
                 case 403:
                    // server error
                    routeData.Values.Add("action", "UnauthorisedRequest");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
                 //add cases for other http errors you want to handle, otherwise HTTP500 will be returned as the default.
                default:
                    // server error
                    routeData.Values.Add("action", "ServerError");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
            }
        }
        //All other exceptions should result in a 500 error as they are issues with unhandled exceptions in the code
        else
        {
            routeData.Values.Add("action", "ServerError");
            Server.ClearError();
            // Call the controller with the route
            errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
        }
    }

In Summary:

  • Custom pages for the standard HTTP errors You have full control of the page returned to the user
  • That have the same look-and-feel as the standard pages of my site You can use your existign layout if you want to for the custom pages
  • Without having to completely redo everything I have in my Shared/_Layout.cspx as above
  • That work whether the user is authenticated or not This is partly down to your architecture but there is nothing in the proposed method that will interfere with user authentication
  • That should properly return the URL requested with the appropriate HTTP error code, rather than -redirecting to a different page that returns HTTP 200 There is no redirect to the error page so the correct http status code is retured
  • That redirects anything that isn't a valid route to the 404 page, whether it is a missing action, a missing controller, or a missing route
  • That doesn't involve separate code being added to each and every page.
like image 4
Carl Avatar answered Oct 22 '22 04:10

Carl


There are 2 types of error handling.

  1. Page-level errors
  2. Application-level erros

For page-level errors you can use both web.config or you can create base controller and override OnException event.

protected override void OnException(ExceptionContext filterContext) { ... }

For application-level errors you can use global.asax Application_Error event

void Application_Error(object sender, EventArgs e) { ... }

Also you can check this link: https://www.simple-talk.com/dotnet/asp.net/handling-errors-effectively-in-asp.net-mvc/

like image 2
Kadir Avatar answered Oct 22 '22 06:10

Kadir