Wednesday, August 15, 2012

MVC 3 + Elmah - Handle Ajax/json action errors in a generic way

Elmah is cool! Go grab it and learn if you don’t use it yet. Yet, the moment comes when you have plenty of mvc actions returning json and you just want to integrate together that Elmah and MVC error handling to provide some unified JSON error handling in case of handled and unhandled exceptions.

Lets start with a handling code that we might put into every action with the following catch block:

catch (Exception ex)
{
    var ticketId = Guid.NewGuid(); // Lets issue a ticket to show the user and have in the log
 
    Request.ServerVariables["TTicketID"] = ticketId.ToString(); // Elmah will show this in a nice table
 
    ErrorSignal.FromCurrentContext().Raise(ex); //ELMAH Signaling
 
    ex.Data.Add("TTicketID", ticketId.ToString()); // Trying to see where this one gets in Elmah
 
    return Json(new { Error = String.Format("Support ticket: {0}\r\n Error: {1}", ticketId, ex.ToString()) }, JsonRequestBehavior.AllowGet);
}

So we want to create some trouble ticket id to present to the user, log the error, and return json with the Error field (my convention, not important here). With all that we have javascript in browser being able to cope with the error plus everything is logged.

But this “per method” solution doesn’t scale well at all, should you have plenty of those methods. A helper class method to the rescue? Possible. But ASP.NET MVC is so cool! Its stack is ready to solve the task!

So lets delete the above catch and introduce the catch it all error filter by extending the standard HandleErrorAttribute:

using System.Web.Mvc;
using Elmah;
using OneNetConfigurator.Controllers;
 
namespace OneNetConfigurator.Helpers
{
    public class OncHandleErrorAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            // Elmah-Log only handled exceptions
            if (context.ExceptionHandled)
                ErrorSignal.FromCurrentContext().Raise(context.Exception);
 
            if (context.HttpContext.Request.IsAjaxRequest())
            {
                // if request was an Ajax request, respond with json with Error field
                var jsonResult = new ErrorController { ControllerContext = context }.GetJsonError(context.Exception);
                jsonResult.ExecuteResult(context);
                context.ExceptionHandled = true;
            }
            else
            {
                // if not an ajax request, continue with logic implemented by MVC -> html error page
                base.OnException(context);
            }
        }
    }
}

Generally, very simple. We check if request is an Ajax request and if it is, we respond with json data generated via the ErrorController (note how context is being passed to the controller) crafted in the following way:

public class ErrorController : Controller
{
    public ActionResult GetJsonError(Exception ex)
    {
        var ticketId = Guid.NewGuid(); // Lets issue a ticket to show the user and have in the log
 
        Request.ServerVariables["TTicketID"] = ticketId.ToString(); // Elmah will show this in a nice table
 
        
 
        ex.Data.Add("TTicketID", ticketId.ToString()); // Trying to see where this one gets in Elmah
 
        return Json(new { Error = String.Format("Support ticket: {0}\r\n Error: {1}", ticketId, ex.ToString()) }, JsonRequestBehavior.AllowGet);
    }

See?! This is our catch block that we don’t need to copy between actions anymore. Of course, those using Ajax in MVC in order to bring html snippets back to browser would have to use/add an extra option and ExecuteResult on some error view instead of JsonResult. This answer on stackoverflow describes the case.

You may consider as well adding Response.StatusCode = 500; just before returning that Json. More on that in the next post on this subject.

With classes above we configure filters in the Global.asax.cs (note that generic HandleErrorAttribute is commented out now, as we have extended it and use our error filter now):

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new GlobalAuthorise());
        filters.Add(new OncHandleErrorAttribute());
        //filters.Add(new HandleErrorAttribute());
    }

Now, we have json with the Error field returned. And we don’t have to do anything in the action itself!:

image

With possible handling in browser like this:

image

One more thing is to test our filter for “non-json” actions. And it works!

image

This is it, hope it might help someone.

More from me on json in mvc: JSON data islands in ASP.NET MVC

1 comment:

Anonymous said...

thanks you. it help me a lot :x