J.T. Taylor
J.T. Taylor

Reputation: 4277

How to return JSON for errors outside the WebApi pipeline?

I'm creating a public-facing API using WebApi 2.1, have a dedicated WebApi project (no MVC), have the API hosted in IIS 7.5 on its own server, and have the design goal of returning only JSON or empty content, and never returning HTML.

I am happily using ExceptionFilterAttribute to deal with exceptions arising within the WebApi pipeline, as follows:

public class GlobalExceptionHandler : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        // Log the exception to Elmah
        Elmah.Error error = new Elmah.Error(context.Exception, HttpContext.Current);
        error.Detail = ActionState.GetRequestParameters(context) + error.Detail;
        Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(error);

        if (context.Exception is NotImplementedException)
        {
            context.Response = context.Request.CreateErrorResponse(
                HttpStatusCode.NotImplemented
                , "The API method has not yet been implemented"
            );
        }
        else
        {
            context.Response = context.Request.CreateErrorResponse(
                HttpStatusCode.InternalServerError
                , "A " + context.Exception.GetType().ToString() + " was thrown"
            );
        }

        base.OnException(context);
    }
}

The filter is properly added in App_Start:

config.Filters.Add(new SecureVideoApiGlobalExceptionHandler());

The problem comes when an error arises that is outside the WebApi pipeline. For example, a request comes with the URI of the web root, https://mysite.com/, and the response is a 403.11 with an HTML body stating "The Web server is configured to not list the contents of this directory." Another possibility is that there is an error in code called in the Application_Start method of global.asax, for example in the AutoMapper code, which is asserting that all the mappings are valid.

My question is: how can I make it such that when any error occurs on the API server, only a JSON error message is returned, and never an HTML error message?

I have tried

<modules runAllManagedModulesForAllRequests="true">

This allows me to handle any error in Application_Error() in global.asax, but I do not have the ability to access a Response object there to emit JSON.

Upvotes: 3

Views: 2929

Answers (1)

oCcSking
oCcSking

Reputation: 928

You can do somthing like this Attribute

[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class ExceptionActionFilter : ExceptionFilterAttribute
{
    private static Logger _log ;//= LogManager.GetLogger("mysite");

    public override void OnException(HttpActionExecutedContext contex)
    {
        if (_log == null)
            _log = LogManager.GetCurrentClassLogger();

        var ex = contex.Exception;

        _log.Error(ex);

        contex.Response = contex.Request.CreateResponse(HttpStatusCode.OK,
           new 
           {
               ErrorMessage = contex.Exception.Message,
               RealStatusCode = (int)(ex is NotImplementedException || ex is ArgumentNullException ? HttpStatusCode.NoContent : HttpStatusCode.BadRequest), 
               ReturnUrl = CommonContext.ErrorUrl
           },
           new JsonMediaTypeFormatter());

        base.OnException(contex);
    }      
}

And then put the Attribute on a class exc and on the clien side

or with filter

 public class ExceptionLoggerFilter : System.Web.Http.Filters.IExceptionFilter
{
    private static Logger _log;
    public ExceptionLoggerFilter()
    {
        if (_log == null)
            _log = LogManager.GetCurrentClassLogger();
    }

    public bool AllowMultiple { get { return true; } }

    public System.Threading.Tasks.Task ExecuteExceptionFilterAsync(
            System.Web.Http.Filters.HttpActionExecutedContext contex,
            System.Threading.CancellationToken cancellationToken)
    {
        return System.Threading.Tasks.Task.Factory.StartNew(() =>
        {
            _log.Error(contex.Exception);

            contex.Response = contex.Request.CreateResponse(HttpStatusCode.OK, 
                new { RealStatusCode = (int)HttpStatusCode.Forbidden, ReturnUrl = "#/error.html"},
                contex.ActionContext.ControllerContext.Configuration.Formatters.JsonFormatter);

        }, cancellationToken);
    }
}

And then in Global.asax.cs

protected void Application_Start()
{
    GlobalConfiguration.Configure(WebApiConfig.Register);
    Database.SetInitializer<MySiteContext>(null);
}

and

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)//RouteCollection config)//
    {
        config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        config.Filters.Add(new ExceptionLoggerFilter());
        // Web API routes
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional });

    }
}

And in the client side maybee somthing like

$.controller.ajax.promise = controller.ajax.promise = function ( obj )
{
    var deferred = q.defer();
    $.ajax( {
        type: obj.type || "GET",
        url: obj.url,
        context: obj.context || null,
        data: obj.data || null,
        contentType: obj.contentType || "application/json; charset=utf-8",
        dataType: obj.dataType || "json",
        success: function ( res, textStatus, jqXHR )
        {
            if ( res.RealStatusCode )
            {
                switch ( res.RealStatusCode )
                {
                    case 400://x error
                        res.ClientMessage = res.ErrorMessage;
                        deferred.reject(res);
                        break;
                    case 408://y errors
                        location.href = res.ReturnUrl;
                        return false;
                    case 403://ext
                        msgbox.alert( {
                            message: 'Ma belle msg',
                            title: "Error"
                        } );
                        deferred.reject();
                        location.href = res.ReturnUrl;
                        return false;
                    default:
                        deferred.reject();
                        location.href = res.ReturnUrl;
                        break;
                }
            }
            deferred.resolve( res );
            return true;
        },
        error: function ( jqXHR, textStatus, errorThrown )
        {
            deferred.reject( { msg: jqXHR.statusText, jqXHR: jqXHR, textStatus:textStatus, errorThrown:errorThrown } );
        }
    } );

    return deferred.promise;
};

I hope this helps other googlers out there! (Like @Thomas C. G. de Vilhena said =)

Upvotes: 5

Related Questions