15

I'm trying to throw an HTTP 403 error code back at the client. I've read that HttpException is the cleanest way to accomplish this, but it's not working for me. I throw the exception from within a page like this:

throw new HttpException(403,"You must be logged in to access this resource.");

However, this will only give a standard ASP.Net stack trace(with 500 error) when CustomErrors is off. If CustomErrors is on, then this will not redirect to the page I have setup to be displayed when a 403 error occurs. Should I forget about HttpException and instead set all the HTTP codes myself? How do I fix this?

The custom errors part of my Web.Config is this:

<customErrors mode="On" defaultRedirect="GenericErrorPage.html">
      <error statusCode="403" redirect="Forbidden.html" />
</customErrors>

Instead of getting Forbidden.html, I'll get GenericErrorPage.html

Earlz
  • 62,085
  • 98
  • 303
  • 499

3 Answers3

8

You need to override Application error like this:

    protected void Application_Error()
    {
        var exception = Server.GetLastError();
        var httpException = exception as HttpException;
        Response.Clear();
        Server.ClearError();
        
        var routeData = new RouteData();
        routeData.Values["controller"] = "Errors";
        routeData.Values["action"] = "General";
        routeData.Values["exception"] = exception;
        Response.StatusCode = 500;

        if (httpException != null)
        {
            Response.StatusCode = httpException.GetHttpCode();
            switch (Response.StatusCode)
            {
                case 403:
                    routeData.Values["action"] = "Http403";
                    break;
                case 404:
                    routeData.Values["action"] = "Http404";
                    break;
            }
        }

        IController errorsController = new ErrorsController();
        var rc = new RequestContext(new HttpContextWrapper(Context), routeData);
        errorsController.Execute(rc);
    }

Then you've got to add the errorsController:

public class ErrorsController : Controller
{
    public ActionResult General(Exception exception)
    {
        ViewBag.ErrorCode = Response.StatusCode;
        ViewBag.Message = "Error Happened";

        //you should log your exception here

        return View("Index");
    }

    public ActionResult Http404()
    {
        ViewBag.ErrorCode = Response.StatusCode;
        ViewBag.Message = "Not Found";
        return View("Index");
    }

    public ActionResult Http403()
    {
        ViewBag.Message = Response.StatusCode;
        ViewBag.Message = "Forbidden";
        return View("Index");
    }

}

And last create a view in for errorsController. I created just one view called index in Views/Errors/.

Dale K
  • 25,246
  • 15
  • 42
  • 71
OscarVGG
  • 2,632
  • 2
  • 27
  • 34
3

With this code included in the element configuration/system.web of Web.config file:

  <customErrors mode="On">
    <error statusCode="403" redirect="~/errors/Forbidden.aspx"/>
  </customErrors>

I managed it to work as expected.

You can find a good tutorial with examples (example 3 is the right one) here: http://aspnetresources.com/articles/CustomErrorPages

Or you may use Response.Status to do so: Asp Classic return specific http status code

Community
  • 1
  • 1
Binus
  • 1,065
  • 7
  • 14
  • This seems to get overridden by the HttpException though. Are you suggesting using Response.End or something similar as well? (basically completely doing away with ASP.Net's customErrors section?) – Earlz Apr 10 '11 at 16:11
  • It depends on your application. Some say you should never throw an exception in application's regular execution path, as they make code less readable. But In case of 403 exception it is OK. – Binus Apr 10 '11 at 19:51
  • @Binus, I ended up using a similar tutorial to build my own answer. – Earlz Apr 10 '11 at 20:25
  • @Earlz: Yeah, but the solution from the tutorial does not use any helper class, only _Web.config_ files and asp pages. When I tried it first time, I add the configuration only to _system.webServer_, but _system.web_ was the right place. – Binus Apr 10 '11 at 20:38
  • @Binus well I could've easily crammed it all into the Global.Asax as he did(note, he doesn't implement everything) but I prefer it to be a bit more clean. Also, I needed this fixed not just for myself but because a project I'm selling would basically cause this problem. Now instead of leaving it up to customers to figure it out, I just say "yea add this line to Global.asax" instead. – Earlz Apr 10 '11 at 21:42
  • @Earlz: If you want to search the right exception page by the error number, use your code and be happy. But I thought you just want to handle 403 exception by redirecting to one page and all other errors redirect to another. In that case you can just modify _Web.config_ (no _Global.asax_ modification required) and create target asp pages. I think this solution is cleaner than adding custom error handling class, but not so customer foolproof. Also adding further error page would require _Web.config_ update. But this code redirects you instead of standard 500 error, which was what you ask ;) – Binus Apr 10 '11 at 22:58
  • @Binus actually it's both. What I'm seeing is that when throwing an HttpException for a 403(or really any error but 404 or 500) ASP.Net will handle it like a default 500 error. So I'll get my generic error page and also get a 500 HTTP status code. I think the ASP.Net custom errors code is completely screwed up – Earlz Apr 11 '11 at 01:44
  • @Earlz: I think there was something wrong in your configuration. For me the option redirects the server to the error page where I can set whatever HTTP response status code I want. Moreover if I set __ I am able to read the exception from _Server.GetLastError()_. Maybe you use different .NET/VisualStudio/IIS combination. – Binus Apr 11 '11 at 11:18
  • @Binus, well actually I'm experiencing this in Mono/xsp/mod_mono+apache so that's possible. It could be a bug in Mono, regardless, my answer fixes the improper status code error in ASP.Net – Earlz Apr 12 '11 at 04:03
  • @Earlz: Well, it is possible that this particular option does not work in Mono. If you mention it sooner, I would not try to convince you, that it is pointless to write class for something that can be configured :). – Binus Apr 12 '11 at 09:55
1

I've actually ended up making my own nifty little class to fix this problem. It doesn't handle everything and I'm not sure it plays nice with MVC, but it works for my use. Basically, instead of relying on ASP.Net to output the correct error page and error code, it will clear the error and then do a server-side transfer and display the appropriate Web.Config error page. It also recognizes the customErrors mode and reacts accordingly.

public static class CustomErrorsFixer
{
    static public void HandleErrors(HttpContext Context)
    {
        if(RunIt(Context)==false){
            return;
        }
        HttpException ex = Context.Error as HttpException;
        if(ex==null){
            try{
                ex=Context.Error.InnerException as HttpException;
            }catch{
                ex=null;
            }
        }

        if (ex != null){
            Context.Response.StatusCode = ex.GetHttpCode();
        }else{
            Context.Response.StatusCode = 500;
        }
        Context.ClearError();

        Context.Server.Transfer(GetCustomError(Context.Response.StatusCode.ToString()));
        HttpContext.Current.Response.End();
    }
    static protected string GetCustomError(string code)
    {
        CustomErrorsSection section = ConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;

        if (section != null)
        {
            CustomError page = section.Errors[code];

            if (page != null)
            {
                return page.Redirect;
            }
        }
        return section.DefaultRedirect;
    }
    static protected bool RunIt(HttpContext context){
        CustomErrorsSection section = ConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
        switch(section.Mode){
            case CustomErrorsMode.Off:
                return false;
            case CustomErrorsMode.On:
                return true;
            case CustomErrorsMode.RemoteOnly:
                return !(context.Request.UserHostAddress=="127.0.0.1");
            default:
                return true;
        }
    }

}

And then to activate it, just add a small thing to Global.asax

    protected virtual void Application_Error (Object sender, EventArgs e)
    {
        CustomErrorsFixer.HandleErrors(Context);
    }
Earlz
  • 62,085
  • 98
  • 303
  • 499
  • I have tried the same solution but getting the error "Server cannot set status after HTTP headers have been sent." at `context.Response.StatusCode = ex != null ? ex.GetHttpCode() : 500;` – user1211185 Feb 27 '15 at 11:46