Chase Florell
Chase Florell

Reputation: 47377

How to catch an error in a controller when thrown from an Attribute?

I've got an ActionFilterAttribute that throws an error if the request is invalid.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if((!filterContext.HttpContext.Request.IsAjaxRequest() ||
            !filterContext.HttpContext.Request.IsXMLHttpRequest()) &&
           (!filterContext.HttpContext.Request.IsLocal))
        {
            throw new InvalidOperationException("This operation can only be accessed via Ajax requests.");
        }
    }
}

Now in my controller, I'd like to catch the error and pass it on to the view

[AjaxOnly]
public JsonpResult List()
{
    try
    {
    var vimeoService = new VimeoService();
    var videos = vimeoService.GetVideosFromChannel(this._vimeoChannelId);

    return this.Jsonp(videos);
    }
    catch (Exception ex)
    {
        var err = new ErrorsModel() { ErrorMessage = ex.Message };
        return this.Jsonp(err, false);
    }
}

Now because the attribute runs before the controller action fires, I can never "catch" the error in the controller, and thereby I cannot relay the error to the view.

How can I achieve this?

Upvotes: 1

Views: 1173

Answers (1)

Chase Florell
Chase Florell

Reputation: 47377

Alright, so the short answer (tl;dr) is that you need to handle all exceptions in a clean and organized manner. And that is NOT to do it individually in each controller action as I (as a dummy) was originally planning.

Instead, I've wired up my Global Application_Error method to handle all errors, and punch them over to an Error controller with a single index action.

    private void Application_Error()
    {
        // setup the route to Send the error to
        var routeData = new RouteData();
        routeData.Values.Add("action", "Index");

        // Execute the ErrorController instead of the intended controller
        IController errorController = new Controllers.ErrorController();
        errorController.Execute(new RequestContext(new HttpContextWrapper(this.Context), routeData));

        // After the controller executes, clear the error.
        this.Server.ClearError();
    }

My error controller is pretty basic. Basically it takes the last error thrown and pushes the relevant data to the client (In my case, serializes it as JsonP).

public class ErrorController : Controller
{
    public JsonpResult Index()
    {
        var lastError = Server.GetLastError();
        var message = lastError.Message;
        int statusCode;

        // If the lastError is a System.Exception, then we
        // need to manually set the Http StatusCode.
        if (lastError.GetType() == typeof(System.Exception))
        {
            statusCode = 500;
        }
        else
        {
            var httpException = (HttpException)this.Server.GetLastError();
            statusCode = httpException.GetHttpCode();
        }

        // Set the status code header.
        this.Response.StatusCode = statusCode;

        // create a new ErrorsModel that can be past to the client
        var err = new ErrorsModel()
            {
                ErrorMessage = message, 
                StatusCode = statusCode
            };
        return this.Jsonp(err, false);
    }
}

Now to add a little robustness to my app, I've created custom Exceptions that correspond to Http StatusCodes.

public sealed class UnauthorizedException : HttpException
{
    /// <summary>
    /// Similar to 403 Forbidden, but specifically for use when authentication is possible but has failed or not yet been provided
    /// </summary>
    public UnauthorizedException(string message) : base((int)StatusCode.Unauthorized, message) { }
}

Now I can throw exceptions throughout my app wherever applicable, and they will all get picked up and sent to the client in an easily managable way.

Here's an example of throwing one of said errors.

if (!User.Identity.IsAuthenticated)
    throw new UnauthorizedException("You are not authorized");

Upvotes: 1

Related Questions