ASP.NET MVC 5 Custom Error Page
Asked Answered
C

6

34

I am using a custom authorize attribute in a ASP.NET MVC 5 application like following:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext context)
    {
        if (context.HttpContext.Request.IsAuthenticated)
        {
            context.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);                
        }
        else
        {
            base.HandleUnauthorizedRequest(context);
        }
    }
}

In system.web section of my web.config I mentioned error paths like:

<system.web>
    <customErrors mode="On" defaultRedirect="/Error/Error">
      <error statusCode="403" redirect="/Error/NoPermissions"/>
    </customErrors>
</system.web>

But I am never redirected to my custom error page at /Error/NoPermissions. Instead the browser display the general error page saying "HTTP Error 403.0 - Forbidden".

Camisole answered 9/5, 2014 at 12:49 Comment(5)
Do you have the controller named Error with the Action NoPermissions inside?Carver
Change the config for <customerrors defaultredirect="Error" mode="On"> <error redirect="NoPermissions" statuscode="403"> </error></customerrors>Carver
It didn't work. Well in a MVC 4 application I had configurations like <customErrors mode="On" defaultRedirect="/Error/Error"> <error statusCode="403" redirect="/Error/UnauthorizedError" /> </customErrors> and it worked very well.Camisole
Well, there's no much difference between MVC 4 and MVC5 talking about custom errors. There's something else wrong there. Maybe your controller. Make sure it doesn't have the authorization attribute.Carver
my Error Controller/Actions don't have any attributes on them.Camisole
C
18

Thanks everyone, but problem is not with 403 code. Actually the problem was with the way i was trying to return 403. I just changed my code to throw an HttpException instead of returning the HttpStatusCodeResult and every things works now. I can return any HTTP status code by throwing HttpException exception and my customErrors configuration catches all of them. May be HttpStatusCodeResult is not doing the exact job I expected it to do.

I just replaced

context.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);

with

throw new HttpException((int)System.Net.HttpStatusCode.Forbidden, "Forbidden");

That's it.

Happy coding.

Camisole answered 9/5, 2014 at 20:34 Comment(2)
Thanks for sharing the final solution, that actually helped me out as well. Never thought about just throwing the error, although I wonder how costly it is to throw the exception all the time. Would be nice if the framework would allow you to just easily change the response instead of having to redirect or throw an error.Reformer
Hi, the problem is not with the HttpStatusCodeResult, but with customErrors. If you use httpErrors then it work both ways! To better understand the issue you can read this blog post: dusted.codes/…Folder
C
49

[1]: Remove all 'customErrors' & 'httpErrors' from Web.config

[2]: Check 'App_Start/FilterConfig.cs' looks like this:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }
}

[3]: in 'Global.asax' add this method:

public void Application_Error(Object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    Server.ClearError();

    var routeData = new RouteData();
    routeData.Values.Add("controller", "ErrorPage");
    routeData.Values.Add("action", "Error");
    routeData.Values.Add("exception", exception);

    if (exception.GetType() == typeof(HttpException))
    {
        routeData.Values.Add("statusCode", ((HttpException)exception).GetHttpCode());
    }
    else
    {
        routeData.Values.Add("statusCode", 500);
    }

    Response.TrySkipIisCustomErrors = true;
    IController controller = new ErrorPageController();
    controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
    Response.End();
}

[4]: Add 'Controllers/ErrorPageController.cs'

public class ErrorPageController : Controller
{
    public ActionResult Error(int statusCode, Exception exception)
    {
         Response.StatusCode = statusCode;
         ViewBag.StatusCode = statusCode + " Error";
         return View();
    }
}

[5]: in 'Views/Shared/Error.cshtml'

@model System.Web.Mvc.HandleErrorInfo
@{
    ViewBag.Title = (!String.IsNullOrEmpty(ViewBag.StatusCode)) ? ViewBag.StatusCode : "500 Error";
}

<h1 class="error">@(!String.IsNullOrEmpty(ViewBag.StatusCode) ? ViewBag.StatusCode : "500 Error"):</h1>

//@Model.ActionName
//@Model.ControllerName
//@Model.Exception.Message
//@Model.Exception.StackTrace

:D

Coheman answered 18/4, 2015 at 1:54 Comment(10)
just tried to implement this. accessing a non existing page returns nothing. (instead of 404)Raconteur
Just to make it clear, it return a 404 status code, with a blank page.Raconteur
@Dementic: Yes it's a blank page so you can customise it. I have it working OK on a few sites now.Coheman
Sorry, my bad. missed something.Raconteur
Does your solution work properly with AJAX requests? Also, you clear the server error before you recognize that it was an HttpException error.Wei
@Pavel: I don't think it handles AJAX requests; I store the exception as a local object first, then on the next line clear the server error - but I still have the 'exception' object saved.Coheman
Not working for me, unfortunately. I'm still getting the standard error pages.Backsight
Seems to be working for me, but the abundance of various handling techniques makes this very confusing. This one ended up working for me, or appears to be, while the redirect via .aspx files had failed with those files 'not being found' all the time.Calash
controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData)); causes exception on MVC 5 .NET 4.7.1Ine
System.ObjectDisposedException: 'Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed.'Ine
C
18

Thanks everyone, but problem is not with 403 code. Actually the problem was with the way i was trying to return 403. I just changed my code to throw an HttpException instead of returning the HttpStatusCodeResult and every things works now. I can return any HTTP status code by throwing HttpException exception and my customErrors configuration catches all of them. May be HttpStatusCodeResult is not doing the exact job I expected it to do.

I just replaced

context.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);

with

throw new HttpException((int)System.Net.HttpStatusCode.Forbidden, "Forbidden");

That's it.

Happy coding.

Camisole answered 9/5, 2014 at 20:34 Comment(2)
Thanks for sharing the final solution, that actually helped me out as well. Never thought about just throwing the error, although I wonder how costly it is to throw the exception all the time. Would be nice if the framework would allow you to just easily change the response instead of having to redirect or throw an error.Reformer
Hi, the problem is not with the HttpStatusCodeResult, but with customErrors. If you use httpErrors then it work both ways! To better understand the issue you can read this blog post: dusted.codes/…Folder
P
17

I also had this issue. Code in the OP’s question is perfectly working except the custom error code in <system.web> section in the web.config file. To fix the issue what I need to do was add the following code to <system.webServer>. Note that ‘webserver’ instead of ‘web’.

<httpErrors errorMode="Custom" existingResponse="Replace">
  <remove statusCode="403" />
  <error statusCode="403" responseMode="ExecuteURL" path="/Error/UnAuthorized" />
</httpErrors>

If someone is using following environment, here is the complete solution:

The Environment:

  • Visual Studio 2013 Update 4
  • Microsoft .NET Framework 4.5.1 with ASP.NET MVC 5
  • Project: ASP.NET Web Application with MVC & Authentication: Individual User Account template

Custom Attribute class:

Add the following class to your web site’s default namespace. The reason explained here in the accepted answer Stack Overflow question: Why does AuthorizeAttribute redirect to the login page for authentication and authorization failures?

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);

        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
} 

Then add the following code the web.config file

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

Following article explain more about this: ASP.NET MVC: Improving the Authorize Attribute (403 Forbidden)

And httpErrors in web.config section in this article: Demystifying ASP.NET MVC 5 Error Pages and Error Logging

Then add the ErrorController.cs to Controllers folder

public class ErrorController : Controller
{
    // GET: UnAuthorized
    public ActionResult UnAuthorized()
    {
        return View();
    }

    public ActionResult Error()
    {
        return View();
    }

}

Then add a UnAuthorized.cshtml to View/Shared folder

@{
    ViewBag.Title = "Your Request Unauthorized !"; //Customise as required
 }
 <h2>@ViewBag.Title.</h2> 

This will show customised error page instead of browser generated error page.

Also note that for the above environment, it is not required to comment the code inside RegisterGlobalFilters method added by the template as suggested in one of the answers.

Please note that I just cut and paste code from my working project therefore I used Unauthorized instead OP’s NoPermissions in the above code.

Paraphrase answered 29/7, 2015 at 0:41 Comment(2)
This is the way I have done it, but it seems that it's not possible to catch 404 URLs that are going to a file extension with this method.Reflective
Using ExecuteUrl is really bad. Because it is messing up SEO. So use file instead.Plaque
U
5

These seem to be complicated workarounds. Here is basically all you need to do to get your custom error pages (CEP) working:

  1. Add an Error Controller in your controllers folder.
  2. Inside your Error Controller annotate the class with [HandleError].
  3. For each error you want to display a CEP for, create an ActionResult method.
  4. Create a View for each CEP inside ~/Views/Shared/Error folder, and customize it how you like. (The Error folder should have been created when you created your controller. If it did not, you will need to create the Error folder first.)
  5. Open the web.config file at the root level *Note: there are two (2) web.config files. One in your Views folder, the other at the Root level of your application.
  6. Inside <system.web>, add <customError mode="On" defaultRedirect="~/Error/Error">. Any statusCode you don't have a CEP for will be handled by the defaultRedirect.
  7. For each error code you have a CEP for; add <error statusCode="[StatusCode]" redirect="~/Error/[CEP Name]">. You can leave off the file extension.

Controller Example:

namespace NAMESPACE_Name.Controllers
{
    [HandleError]
    public class ErrorController : Controller
    {
        // GET: Error
        public ActionResult BadRequest()
        {
            return View();
        }

        public ActionResult Error()
        {
            return View();
        }

        public ActionResult Forbidden()
        {
            return View();
        }

        public ActionResult InternalServerError()
        {
            return View();
        }

        public ActionResult NotFound()
        {
            return View();
        }

        public  ActionResult NotImplemented()
        {
            return View();
        }

        public ActionResult ServerBusyOrDown()
        {
            return View();
        }

        public ActionResult ServerUnavailable()
        {
            return View();
        }

        public ActionResult Timeout()
        {
            return View();
        }

        public ActionResult Unauthorized()
        {
            return View();
        }
    }
}

View Example:

@{ 
    Layout = "~/Views/Shared/_FullWidthLayout.cshtml";
    ViewBag.Title = "404 Error";
}
<div class="opensans margin-sides text-center">
    <div class="text-center">
        <h1 class="text-normal">Uh oh! Something went wrong!</h1>
        <div class="img-container text-center">
            <div class="centered">
                <h1 class="bold">404 - Not Found</h1>
            </div>
            <img class="img text-center" src="~/Images/BackgroundImg.png" style="opacity: 0.15;" />
        </div>
        <p class="text-left">
            This is usually the result of a broken link, a web page that has been moved or deleted, or a mistyped URL.
            <ol class="text-left">
                <li>Check the URL you entered in the address bar for typos,</li>
                <li>If the address you entered is correct, the problem is on our end.</li>
                <li>Please check back later as the resource you requested could be getting worked on,</li>
                <li>However, if this continues for the resource you requested, please submit a <a href="mailto:EmailAddress?subject=Website%20Error">trouble ticket</a>.</li>
            </ol>
        </p>
    </div>
</div>

Web.Config Example:

<customErrors mode="On" defaultRedirect="~/Error/Error">
  <!--The defaultRedirect page will display for any error not listed below.-->
  <error statusCode="400" redirect="~/Error/BadRequest"/>
  <error statusCode="401" redirect="~/Error/Unauthorized"/>
  <error statusCode="403" redirect="~/Error/Forbidden"/>
  <error statusCode="404" redirect="~/Error/NotFound"/>
  <error statusCode="408" redirect="~/Error/Timeout"/>
  <error statusCode="500" redirect="~/Error/InternalServerError"/>
  <error statusCode="501" redirect="~/Error/NotImplemented"/>
  <error statusCode="502" redirect="~/Error/ServerUnavailable"/>
  <error statusCode="503" redirect="~/Error/ServerBusyOrDown"/>
</customErrors>

That's it! Step-by-step solution to a problem that really shouldn't be a problem! Again, any statusCode you don't have a CEP for, will be handled by the defaultRedirect page.

Ulrike answered 3/2, 2020 at 20:24 Comment(4)
Perfect! I don't get why this answer doesn't have more upvotes.Daggerboard
i have all of these steps implemented but for some reason I don't seem to ever hit a breakpoint inside my Error controller I only ever end up seeing the shared error page any clue why?Nicolle
@d0rf47, you would need to share your code so we can understand why.Ulrike
@Nicolle are you returning HTTP status codes from your actions or throwing HTTPExceptions? If you're doing the former, then you need to use the IIS 7.x httpErrors section. The customErrors section only responds to HTTPExceptions.Emboss
F
2

since I ran into a very similar issue I wanted to shed more light on it.

customErrors will only capture actual http exceptions thrown in your ASP.NET application. The HttpStatusCodeResult doesn't throw an exception though. It just writes a response with the according status code, which makes more sense in your example.

If you are running on IIS 7.0 or higher you should be using httpErrors now, as this will show you custom error pages in all cases. This is an IIS level setting.

I wrote a whole blog post about this to explain the differences: http://dusted.codes/demystifying-aspnet-mvc-5-error-pages-and-error-logging

Folder answered 6/4, 2015 at 23:3 Comment(0)
R
0

Update

You only need to do that special redirect for 403 errors. All other 500 errors should take effect through your defaultRedirect="/Error/Error" setting in customErrors. However, you need to remove or comment out the HandleErrorAttribute registration in the App_Start/FilterConfig.cs file for custom errors to actually work. Otherwise, that attribute will redirect all errors to the Error.cshtml file in the Views/Shared directory.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        // remove this line below
        //filters.Add(new HandleErrorAttribute());
    }
}

Original Answer

As far as I know, you cannot use customErrors in the web.config to handle 403 errors for some reason. I feel your pain as it seems like something that should be as simple as the code you already have, but apparently 403 errors are treated as a web server concern.

What you can do instead is just redirect the user to your desired "NoPermissions" page like this:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext context)
    {
        if (context.HttpContext.Request.IsAuthenticated)
        {
            context.Result = new RedirectToRouteResult(new RouteValueDictionary(new
            {
                action = "NoPermissions",
                controller = "Error",
                area = ""
            }));
        }
        else
        {
            base.HandleUnauthorizedRequest(context);
        }
    }
}

The request will have a 200 status code instead of a 403, but if you can live with that, this is an easy workaround.

Here is a similar SO question for more info: Returning custom errors.

Also, this article explains how to go the IIS route: http://kitsula.com/Article/MVC-Custom-Error-Pages

Reformer answered 9/5, 2014 at 13:32 Comment(2)
That will only work for 403. What about 500 and other codes? Moreover, httpErrors are also not working for me.Camisole
I have updated my answer to address the 500 issue. Make sure you are actually on IIS when using httpErrors. I'm not sure if the local development web server, cassini, works with httpErrors.Reformer

© 2022 - 2024 — McMap. All rights reserved.