What should a JSON service return on failure / error

后端 未结 11 1115
温柔的废话
温柔的废话 2020-12-02 05:32

I\'m writing a JSON service in C# (.ashx file). On a successful request to the service I return some JSON data. If the request fails, either because an exception was thrown

11条回答
  •  抹茶落季
    2020-12-02 06:17

    I've spend some hours solving this problem. My solution is based on the following wishes/requirements:

    • Don't have repetitive boilerplate error handling code in all JSON controller actions.
    • Preserve HTTP (error) status codes. Why? Because higher level concerns should not affect lower level implementation.
    • Be able to get JSON data when an error/exception occur on the server. Why? Because I might want rich error information. E.g. error message, domain specific error status code, stack trace (in debug/development environment).
    • Ease of use client side - preferable using jQuery.

    I create a HandleErrorAttribute (see code comments for explanation of the details). A few details including "usings" has been left out, so the code might not compile. I add the filter to the global filters during application initialization in Global.asax.cs like this:

    GlobalFilters.Filters.Add(new UnikHandleErrorAttribute());
    

    Attribute:

    namespace Foo
    {
      using System;
      using System.Diagnostics;
      using System.Linq;
      using System.Net;
      using System.Reflection;
      using System.Web;
      using System.Web.Mvc;
    
      /// 
      /// Generel error handler attribute for Foo MVC solutions.
      /// It handles uncaught exceptions from controller actions.
      /// It outputs trace information.
      /// If custom errors are enabled then the following is performed:
      /// 
      ///
    • If the controller action return type is then a object with a message property is returned. /// If the exception is of type it's message will be used as the message property value. /// Otherwise a localized resource text will be used.
    • ///
    /// Otherwise the exception will pass through unhandled. ///
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class FooHandleErrorAttribute : HandleErrorAttribute { private readonly TraceSource _TraceSource; /// /// must not be null. /// /// public FooHandleErrorAttribute(TraceSource traceSource) { if (traceSource == null) throw new ArgumentNullException(@"traceSource"); _TraceSource = traceSource; } public TraceSource TraceSource { get { return _TraceSource; } } /// /// Ctor. /// public FooHandleErrorAttribute() { var className = typeof(FooHandleErrorAttribute).FullName ?? typeof(FooHandleErrorAttribute).Name; _TraceSource = new TraceSource(className); } public override void OnException(ExceptionContext filterContext) { var actionMethodInfo = GetControllerAction(filterContext.Exception); // It's probably an error if we cannot find a controller action. But, hey, what should we do about it here? if(actionMethodInfo == null) return; var controllerName = filterContext.Controller.GetType().FullName; // filterContext.RouteData.Values[@"controller"]; var actionName = actionMethodInfo.Name; // filterContext.RouteData.Values[@"action"]; // Log the exception to the trace source var traceMessage = string.Format(@"Unhandled exception from {0}.{1} handled in {2}. Exception: {3}", controllerName, actionName, typeof(FooHandleErrorAttribute).FullName, filterContext.Exception); _TraceSource.TraceEvent(TraceEventType.Error, TraceEventId.UnhandledException, traceMessage); // Don't modify result if custom errors not enabled //if (!filterContext.HttpContext.IsCustomErrorEnabled) // return; // We only handle actions with return type of JsonResult - I don't use AjaxRequestExtensions.IsAjaxRequest() because ajax requests does NOT imply JSON result. // (The downside is that you cannot just specify the return type as ActionResult - however I don't consider this a bad thing) if (actionMethodInfo.ReturnType != typeof(JsonResult)) return; // Handle JsonResult action exception by creating a useful JSON object which can be used client side // Only provide error message if we have an MySpecialExceptionWithUserMessage. var jsonMessage = FooHandleErrorAttributeResources.Error_Occured; if (filterContext.Exception is MySpecialExceptionWithUserMessage) jsonMessage = filterContext.Exception.Message; filterContext.Result = new JsonResult { Data = new { message = jsonMessage, // Only include stacktrace information in development environment stacktrace = MyEnvironmentHelper.IsDebugging ? filterContext.Exception.StackTrace : null }, // Allow JSON get requests because we are already using this approach. However, we should consider avoiding this habit. JsonRequestBehavior = JsonRequestBehavior.AllowGet }; // Exception is now (being) handled - set the HTTP error status code and prevent caching! Otherwise you'll get an HTTP 200 status code and running the risc of the browser caching the result. filterContext.ExceptionHandled = true; filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; // Consider using more error status codes depending on the type of exception filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); // Call the overrided method base.OnException(filterContext); } /// /// Does anybody know a better way to obtain the controller action method info? /// See http://stackoverflow.com/questions/2770303/how-to-find-in-which-controller-action-an-error-occurred. /// /// /// private static MethodInfo GetControllerAction(Exception exception) { var stackTrace = new StackTrace(exception); var frames = stackTrace.GetFrames(); if(frames == null) return null; var frame = frames.FirstOrDefault(f => typeof(IController).IsAssignableFrom(f.GetMethod().DeclaringType)); if (frame == null) return null; var actionMethod = frame.GetMethod(); return actionMethod as MethodInfo; } } }

    I've developed the following jQuery plugin for client side ease of use:

    (function ($, undefined) {
      "using strict";
    
      $.FooGetJSON = function (url, data, success, error) {
        /// 
        /// **********************************************************
        /// * UNIK GET JSON JQUERY PLUGIN.                           *
        /// **********************************************************
        /// This plugin is a wrapper for jQuery.getJSON.
        /// The reason is that jQuery.getJSON success handler doesn't provides access to the JSON object returned from the url
        /// when a HTTP status code different from 200 is encountered. However, please note that whether there is JSON
        /// data or not depends on the requested service. if there is no JSON data (i.e. response.responseText cannot be
        /// parsed as JSON) then the data parameter will be undefined.
        ///
        /// This plugin solves this problem by providing a new error handler signature which includes a data parameter.
        /// Usage of the plugin is much equal to using the jQuery.getJSON method. Handlers can be added etc. However,
        /// the only way to obtain an error handler with the signature specified below with a JSON data parameter is
        /// to call the plugin with the error handler parameter directly specified in the call to the plugin.
        ///
        /// success: function(data, textStatus, jqXHR)
        /// error: function(data, jqXHR, textStatus, errorThrown)
        ///
        /// Example usage:
        ///
        ///   $.FooGetJSON('/foo', { id: 42 }, function(data) { alert('Name :' + data.name); }, function(data) { alert('Error: ' + data.message); });
        /// 
    
        // Call the ordinary jQuery method
        var jqxhr = $.getJSON(url, data, success);
    
        // Do the error handler wrapping stuff to provide an error handler with a JSON object - if the response contains JSON object data
        if (typeof error !== "undefined") {
          jqxhr.error(function(response, textStatus, errorThrown) {
            try {
              var json = $.parseJSON(response.responseText);
              error(json, response, textStatus, errorThrown);
            } catch(e) {
              error(undefined, response, textStatus, errorThrown);
            }
          });
        }
    
        // Return the jQueryXmlHttpResponse object
        return jqxhr;
      };
    })(jQuery);
    

    What do I get from all this? The final result is that

    • None of my controller actions has requirements on HandleErrorAttributes.
    • None of my controller actions contains any repetitive boiler plate error handling code.
    • I have a single point of error handling code allowing me to easily change logging and other error handling related stuff.
    • A simple requirement: Controller actions returning JsonResult's must have return type JsonResult and not some base type like ActionResult. Reason: See code comment in FooHandleErrorAttribute.

    Client side example:

    var success = function(data) {
      alert(data.myjsonobject.foo);
    };
    var onError = function(data) {
      var message = "Error";
      if(typeof data !== "undefined")
        message += ": " + data.message;
      alert(message);
    };
    $.FooGetJSON(url, params, onSuccess, onError);
    

    Comments are most welcome! I'll probably blog about this solution some day...

提交回复
热议问题