Application_Error and ExceptionFilter

1.3k Views Asked by At

So I have recently built at ExceptionFilter which handles all errors except Api Errors. The ExceptionFilter looks like this:

public class ExceptionAttribute : IExceptionFilter
{

    /// <summary>
    /// Handles any exception
    /// </summary>
    /// <param name="filterContext">The current context</param>
    public void OnException(ExceptionContext filterContext)
    {

        // If our exception has been handled, exit the function
        if (filterContext.ExceptionHandled)
            return;

        // If our exception is not an ApiException
        if (!(filterContext.Exception is ApiException))
        {

            // Set our base status code
            var statusCode = (int)HttpStatusCode.InternalServerError;

            // If our exception is an http exception
            if (filterContext.Exception is HttpException)
            {

                // Cast our exception as an HttpException
                var exception = (HttpException)filterContext.Exception;

                // Get our real status code
                statusCode = exception.GetHttpCode();
            }

            // Set our context result
            var result = CreateActionResult(filterContext, statusCode);

            // Set our handled property to true
            filterContext.ExceptionHandled = true;
        }
    }

    /// <summary>
    /// Creats an action result from the status code
    /// </summary>
    /// <param name="filterContext">The current context</param>
    /// <param name="statusCode">The status code of the error</param>
    /// <returns></returns>
    protected virtual ActionResult CreateActionResult(ExceptionContext filterContext, int statusCode)
    {

        // Create our context
        var context = new ControllerContext(filterContext.RequestContext, filterContext.Controller);
        var statusCodeName = ((HttpStatusCode)statusCode).ToString();

        // Create our route
        var controller = (string)filterContext.RouteData.Values["controller"];
        var action = (string)filterContext.RouteData.Values["action"];
        var model = new HandleErrorInfo(filterContext.Exception, controller, action);

        // Create our result
        var view = SelectFirstView(context, string.Format("~/Views/Error/{0}.cshtml", statusCodeName), "~/Views/Error/Index.cshtml", statusCodeName);
        var result = new ViewResult { ViewName = view, ViewData = new ViewDataDictionary<HandleErrorInfo>(model) };

        // Return our result
        return result;
    }

    /// <summary>
    /// Gets the first view name that matches the supplied names
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="viewNames">A list of view names</param>
    /// <returns></returns>
    protected string SelectFirstView(ControllerContext context, params string[] viewNames)
    {
        return viewNames.First(view => ViewExists(context, view));
    }

    /// <summary>
    /// Checks to see if a view exists
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="name">The name of the view to check</param>
    /// <returns></returns>
    protected bool ViewExists(ControllerContext context, string name)
    {
        var result = ViewEngines.Engines.FindView(context, name, null);

        return result.View != null;
    }
}

As you can see, if the error is not an ApiException then I route the user to the error controller. The ApiException is just an error that happens when I make an API call from within MVC. When these errors happen I would like to return the error as JSON back to the client so that the JavaScript can handle it.

I thought not handling the error would do this, but instead it generates a server error (albeit with the JSON error in it) like so:

Server Error in '/' Application.

{"message":"validateMove validation failure:\r\nThe item is despatched and cannot be moved"}

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: SapphirePlus.Web.ApiException: {"message":"validateMove validation failure:\r\nThe item is despatched and cannot be moved"}

Source Error:

Line 181: if (response.StatusCode != HttpStatusCode.OK)

Line 182: throw new ApiException(result);

So my question is, can I get the Application_Error method to get errors that ARE ApiExceptions and return the error as JSON?

2

There are 2 best solutions below

0
On

In the end I didn't need to use Global.asax at all, I was able to handle it all inside my ExceptionAttribute class like this:

public class ExceptionAttribute : IExceptionFilter
{

    /// <summary>
    /// Handles any exception
    /// </summary>
    /// <param name="filterContext">The current context</param>
    public void OnException(ExceptionContext filterContext)
    {

        // If our exception has been handled, exit the function
        if (filterContext.ExceptionHandled)
            return;

        // Set our base status code
        var statusCode = (int)HttpStatusCode.BadRequest;

        // If our exception is an http exception
        if (filterContext.Exception is HttpException)
        {

            // Cast our exception as an HttpException
            var exception = (HttpException)filterContext.Exception;

            // Get our real status code
            statusCode = exception.GetHttpCode();
        }

        // Set our context result
        var result = CreateActionResult(filterContext, statusCode);

        // Set our handled property to true
        filterContext.Result = result;
        filterContext.ExceptionHandled = true;
    }

    /// <summary>
    /// Creats an action result from the status code
    /// </summary>
    /// <param name="filterContext">The current context</param>
    /// <param name="statusCode">The status code of the error</param>
    /// <returns></returns>
    protected virtual ActionResult CreateActionResult(ExceptionContext filterContext, int statusCode)
    {

        // Create our context
        var context = new ControllerContext(filterContext.RequestContext, filterContext.Controller);
        var statusCodeName = ((HttpStatusCode)statusCode).ToString();

        // Create our route
        var controller = (string)filterContext.RouteData.Values["controller"];
        var action = (string)filterContext.RouteData.Values["action"];
        var model = new HandleErrorInfo(filterContext.Exception, controller, action);

        // Create our result
        var view = SelectFirstView(context, string.Format("~/Views/Error/{0}.cshtml", statusCodeName), "~/Views/Error/Index.cshtml", statusCodeName);
        var result = new ViewResult { ViewName = view, ViewData = new ViewDataDictionary<IError>(this.Factorize(model)) };

        // Return our result
        return result;
    }

    /// <summary>
    /// Factorizes the HandleErrorInfo
    /// </summary>
    /// <param name="error"></param>
    /// <returns></returns>
    protected virtual IError Factorize(HandleErrorInfo error)
    {

        // Get the error
        var model = new Error
        {
            Message = "There was an unhandled exception."
        };

        // If we have an error
        if (error != null)
        {

            // Get our exception
            var exception = error.Exception;

            // If we are dealing with an ApiException
            if (exception is ApiException || exception is HttpException)
            {

                // Get our JSON
                var json = JObject.Parse(exception.Message);
                var message = json["exceptionMessage"] != null ? json["exceptionMessage"] : json["message"];

                // If we have a message
                if (message != null)
                {

                    // Update our model message
                    model.Message = message.ToString();
                }
            }
            else
            {

                // Update our message
                model.Message = exception.Message;
            }
        }

        // Return our response
        return model;
    }

    /// <summary>
    /// Gets the first view name that matches the supplied names
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="viewNames">A list of view names</param>
    /// <returns></returns>
    protected string SelectFirstView(ControllerContext context, params string[] viewNames)
    {
        return viewNames.First(view => ViewExists(context, view));
    }

    /// <summary>
    /// Checks to see if a view exists
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="name">The name of the view to check</param>
    /// <returns></returns>
    protected bool ViewExists(ControllerContext context, string name)
    {
        var result = ViewEngines.Engines.FindView(context, name, null);

        return result.View != null;
    }
}

This handled any Mvc error and for my Api calls, I did this:

    /// <summary>
    /// Used to handle the api response
    /// </summary>
    /// <param name="response">The HttpResponseMessage</param>
    /// <returns>Returns a string</returns>
    private async Task<string> HandleResponse(HttpResponseMessage response)
    {

        // Read our response content
        var result = await response.Content.ReadAsStringAsync();

        // If there was an error, throw an HttpException
        if (response.StatusCode != HttpStatusCode.OK)
            throw new ApiException(result);

        // Return our result if there are no errors
        return result;
    }

This allowed me to capture the ApiError and handle the response differently than with any other exception.

3
On

So my question is, can I get the Application_Error method to get errors that ARE ApiExceptions and return the error as JSON?

Of course:

protected void Application_Error()
{
    var apiException = Server.GetLastError() as ApiException;
    if (apiException != null)
    {
        Response.Clear();
        Server.ClearError();

        Response.StatusCode = 400;
        Context.Response.ContentType = "application/json";
        Context.Response.Write("YOUR JSON HERE");
    }
}