6

I'm trying to fine-tune the error handling in my MVC application.

I've enabled custom errors in my web.config, and I added the following code to Application_Error.

Global.asax

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError() as Exception;
    if (exception != null)
    {
        Context.ClearError();
        Context.Response.TrySkipIisCustomErrors = true;

        string path = (exception is HttpException && (exception as HttpException).GetHttpCode() == 404) ?
            "~/Error/NotFound" :
            "~/Error/Index";
        Context.Server.TransferRequest(path, false);
    }
}

ErrorController.cs

[AllowAnonymous]
public ActionResult Index()
{
    Response.Clear();
    Response.StatusCode = 503;
    Response.TrySkipIisCustomErrors = true;
    return View();
}

[AllowAnonymous]
public ActionResult NotFound()
{
    Response.Clear();
    Response.StatusCode = 404;
    Response.TrySkipIisCustomErrors = true;
    return View();
}

Web.config

<system.web>
  <customErrors mode="RemoteOnly" defaultRedirect="~/Error">
    <error statusCode="404" redirect="~/Error/NotFound"/>
    <error statusCode="500" redirect="~/Error" />
  </customErrors>
</system.web>

This seems to be working fairly well. But how can I pass some error details along to my Error controller?

Also, extra points for tips on getting exception details to my Error controller for exceptions that occur within a controller.

Note: I do not want to use a redirect here. Doing so would tell crawlers like Google incorrect information about the URL.

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466

6 Answers6

6

If you want to get error details within error controller than do not clear error details (Context.ClearError()) inside Application_Error function.

Once you are in ErrorController Action fetch the last error again and then clear it.

HttpContext.Server.GetLastError()

If you want to get the the controller and action name under which exception occurred you can use below code to fetch detail

Request.RequestContext.RouteData.Values["controller"]
Request.RequestContext.RouteData.Values["action"]

Also if you want to run ErrorController and Specific Action from Application_Error function you can do something like below

   protected void Application_Error()
   {

Exception exception = Server.GetLastError();
var httpException = exception as HttpException;
Response.Clear();
Server.ClearError();
var routeData = new RouteData();
routeData.Values["controller"] = "Errors";
routeData.Values["action"] = "Common";
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;
  case 400:
    routeData.Values["action"] = "Http400";
    break;
  }
}

Response.TrySkipIisCustomErrors = true;
IController errorsController = new ErrorsController();
var rc = new RequestContext(new HttpContextWrapper(Context), routeData);

/* This will run specific action without redirecting */
errorsController.Execute(rc);

}

If you want to pass error as an object to error controller then you can add extra route data like below

routeData.Values["errorDetail"] = httpException;
3

Would adding parameters help ?

  • public ActionResult Index(string errorMessage)
  • public ActionResult NotFound(string errorMessage)

And then in Application_Error can look something like -

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError() as Exception;
    if (exception != null)
    {
        Context.ClearError();
        Context.Response.TrySkipIisCustomErrors = true;

        string path = (exception is HttpException && (exception as HttpException).GetHttpCode() == 404) ?
            "~/Error/NotFound?errorMessage="+exception.Message :
            "~/Error/Index?errorMessage="+exception.Message;
        Context.Server.TransferRequest(path, false);
    }
}

You can include additional parameter as per your requirement. Not the best approach though.

Pramod
  • 195
  • 1
  • 9
  • 1
    That's one approach, but certainly less than ideal. For example, some exceptions have inner exceptions. They also contain additional information as well. – Jonathan Wood Oct 21 '16 at 04:10
2

A simple approach would be to pass the Exception or ViewModel like this:

In your application_error:

HttpContext.Current.Items["Exception"] = exception;

in your error controller :

var exception = HttpContext.Current.Items["Exception"] as Exception;

Caveat: I am not a fan of using HttpContext.

Chad Grant
  • 44,326
  • 9
  • 65
  • 80
1

This is the set-up that I have. It will not redirect and it handles both application and some configured IIS errors in the same place. You can also pass in any information you want to your Error controller.

In Web.config:

<system.web>
  <customErrors mode="Off" />
  ...
</system.web>

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Auto">
    <remove statusCode="403" />
    <remove statusCode="404" />
    <remove statusCode="500" />
    <error statusCode="403" responseMode="ExecuteURL" path="/Error/Display/403" />
    <error statusCode="404" responseMode="ExecuteURL" path="/Error/Display/404" />
    <error statusCode="500" responseMode="ExecuteURL" path="/Error/Display/500" />
  </httpErrors>
...
</system.webServer>

In ErrorController (showing method signatures only for brevity):

// This one gets called from Application_Error
// You can add additional parameters to this action if needed
public ActionResult Index(Exception exception)
{
   ...
}

// This one gets called by IIS (see Web.config)
public ActionResult Display([Bind(Prefix = "id")] HttpStatusCode statusCode)
{
    ...
}

Additionally, I have an ErrorViewModel and an Index view.

In Application_Error:

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

    var httpContext = new HttpContextWrapper(Context);

    httpContext.ClearError();

    var routeData = new RouteData();
    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = "Index";
    routeData.Values["exception"] = exception;
    // Here you can add additional route values as necessary.
    // Make sure you add them as parameters to the action you're executing

    IController errorController = DependencyResolver.Current.GetService<ErrorController>();
    var context = new RequestContext(httpContext, routeData);
    errorController.Execute(context);
}

So far, this is the basic set-up that I have. This will not perform a redirect (the error controller action is executed from Application_Error), and it handles controller exceptions as well as, for example, IIS 404s (such as yourwebsite.com/blah.html).

From this point on, whatever happens inside your ErrorController would be based on your needs.


As an example, I will add some extra details of my implementation. As I was saying, I have an ErrorViewModel.

My ErrorViewModel:

public class ErrorViewModel
{
    public string Title { get; set; }

    public string Text { get; set; }

    // This is only relevant to my business needs
    public string ContentResourceKey { get; set; }

    // I am including the actual exception in here so that in the view,
    // when the request is local, I am displaying the exception for
    // debugging purposes.
    public Exception Exception { get; set; }
}

My ErrorController (relevant parts):

public ActionResult Index(Exception exception)
{
    ErrorViewModel model;

    var statusCode = HttpStatusCode.InternalServerError;

    if (exception is HttpException)
    {
        statusCode = (HttpStatusCode)(exception as HttpException).GetHttpCode();

        // More details on this below
        if (exception is DisplayableException)
        {
            model = CreateErrorModel(exception as DisplayableException);
        }
        else
        {
            model = CreateErrorModel(statusCode);
            model.Exception = exception;
        }
    }
    else
    {
        model = new ErrorViewModel { Exception = exception };
    }

    return ErrorResult(model, statusCode);
}

public ActionResult Display([Bind(Prefix = "id")] HttpStatusCode statusCode)
{
    var model = CreateErrorModel(statusCode);

    return ErrorResult(model, statusCode);
}

private ErrorViewModel CreateErrorModel(HttpStatusCode statusCode)
{
    var model = new ErrorViewModel();

    switch (statusCode)
    {
        case HttpStatusCode.NotFound:
            // Again, this is only relevant to my business logic.
            // You can do whatever you want here
            model.ContentResourceKey = "error-page-404";
            break;
        case HttpStatusCode.Forbidden:
            model.Title = "Unauthorised.";
            model.Text = "Your are not authorised to access this resource.";
            break;

        // etc...
    }

    return model;
}


private ErrorViewModel CreateErrorModel(DisplayableException exception)
{
    if (exception == null)
    {
        return new ErrorViewModel();
    }

    return new ErrorViewModel
    {
        Title = exception.DisplayTitle,
        Text = exception.DisplayDescription,
        Exception = exception.InnerException
    };
}

private ActionResult ErrorResult(ErrorViewModel model, HttpStatusCode statusCode)
{
    HttpContext.Response.Clear();
    HttpContext.Response.StatusCode = (int)statusCode;
    HttpContext.Response.TrySkipIisCustomErrors = true;

    return View("Index", model);
}

In some cases I need to display a custom message when an error happens. I have a custom exception for this purpose:

[Serializable]
public class DisplayableException : HttpException
{
    public string DisplayTitle { get; set; }

    public string DisplayDescription { get; set; }

    public DisplayableException(string title, string description)
        : this(title, description, HttpStatusCode.InternalServerError, null, null)
    {
    }

    public DisplayableException(string title, string description, Exception exception)
        : this(title, description, HttpStatusCode.InternalServerError, null, exception)
    {
    }

    public DisplayableException(string title, string description, string message, Exception exception)
        : this(title, description, HttpStatusCode.InternalServerError, message, exception)
    {
    }

    public DisplayableException(string title, string description, HttpStatusCode statusCode, string message, Exception inner)
        : base((int)statusCode, message, inner)
    {
        DisplayTitle = title;
        DisplayDescription = description;
    }
}

Then I use it like this:

catch(SomeException ex)
{
    throw new DisplayableException("My Title", "My  custom display message", "An error occurred and I must display something", ex)
}

In my ErrorController I handle this exception separately, setting the ErrorViewModel's Title and Text properties from this DisplayableException.

Andrei Olariu
  • 556
  • 4
  • 8
  • Seems like your code needs a little more error checking, such as checking for `null` in a couple of places. One place you don't want to introduce new exceptions is in your error-handling code. – Jonathan Wood Oct 25 '16 at 17:13
  • I completely agree that we don't want to get exceptions inside exception handling code. However, looking back at my code, I think that the only two places that could potentially create problems are the `ExecuteErrorAction` method (in the HttpModule) or in the `CreateErrorModel(DisplayableException)` overload (in `ErrorController`). In the first case, the problem could arise if a new instance of the `ErrorController` cannot be created (which I think it's unlikely and it would be immediately obvious during dev). The second one would fail for an unlikely `null` param. What do you think? – Andrei Olariu Oct 26 '16 at 07:50
  • I know that "unlikely" is generally not a good reason to not do something but for the case of `ExecuteErrorAction` I think it's safe enough to assume that an instance will be created. For the second case (of `CreateErrorModel(DisplayableException)`), ok, it is more possible for it to happen. I say "unlikely" because of the way the method is called in this particular case. However, I know it can't stand as a good argument because a method should not be aware of how it's being called. This applies to `ExecuteErrorAction` as well. I'll update the code to reflect this. – Andrei Olariu Oct 26 '16 at 08:11
  • I removed `ExecuteErrorAction` and moved the code inside `Applicaiton_Error` (the method was kind of pointless, anyways). I also added a null check in `CreateErrorModel` for the exception. If it's null, I'm returning a new empty model. – Andrei Olariu Oct 26 '16 at 08:19
0

Can you use Session object like Session["AppError"]=exception. Then you can retrieve it in your Error Controller. Keep in mind that exception is not Serializable but you can use other tricks. Couple of them is here: How to serialize an Exception object in C#? or What is the correct way to make a custom .NET Exception serializable?

Community
  • 1
  • 1
Yuri S
  • 5,355
  • 1
  • 15
  • 23
0

Try like this;

        protected void Application_Error(Object sender, EventArgs e)
    {
        var exception = Server.GetLastError();
        var statusCode = exception.GetType() == typeof (HttpException) ? ((HttpException) exception).GetHttpCode() : 500;
        var routeData = new RouteData
        {
            Values =
            {
                {"controller", "Error"},
                {"action", "Index"},
                {"statusCode", statusCode},
                {"exception", exception}
            }
        };

        Server.ClearError();
        Response.TrySkipIisCustomErrors = true;

        IController errorController = new ErrorController();
        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

        Response.End();
    }
}

Then write a little business code in Index method in ErrorController. (if StatusCode == 400 ... else ...)

kaya
  • 724
  • 10
  • 24