What should a JSON service return on failure / error

后端 未结 11 1112
温柔的废话
温柔的废话 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;
    
      /// <summary>
      /// 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:
      /// <ul>
      ///   <li>If the controller action return type is <see cref="JsonResult"/> then a <see cref="JsonResult"/> object with a <c>message</c> property is returned.
      ///       If the exception is of type <see cref="MySpecialExceptionWithUserMessage"/> it's message will be used as the <see cref="JsonResult"/> <c>message</c> property value.
      ///       Otherwise a localized resource text will be used.</li>
      /// </ul>
      /// Otherwise the exception will pass through unhandled.
      /// </summary>
      [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
      public sealed class FooHandleErrorAttribute : HandleErrorAttribute
      {
        private readonly TraceSource _TraceSource;
    
        /// <summary>
        /// <paramref name="traceSource"/> must not be null.
        /// </summary>
        /// <param name="traceSource"></param>
        public FooHandleErrorAttribute(TraceSource traceSource)
        {
          if (traceSource == null)
            throw new ArgumentNullException(@"traceSource");
          _TraceSource = traceSource;
        }
    
        public TraceSource TraceSource
        {
          get
          {
            return _TraceSource;
          }
        }
    
        /// <summary>
        /// Ctor.
        /// </summary>
        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);
        }
    
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="exception"></param>
        /// <returns></returns>
        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) {
        /// <summary>
        /// **********************************************************
        /// * 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); });
        /// </summary>
    
        // 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...

    0 讨论(0)
  • 2020-12-02 06:17

    I would definitely return a 500 error with a JSON object describing the error condition, similar to how an ASP.NET AJAX "ScriptService" error returns. I believe this is fairly standard. It's definitely nice to have that consistency when handling potentially unexpected error conditions.

    Aside, why not just use the built in functionality in .NET, if you're writing it in C#? WCF and ASMX services make it easy to serialize data as JSON, without reinventing the wheel.

    0 讨论(0)
  • 2020-12-02 06:17

    Rails scaffolds use 422 Unprocessable Entity for these kinds of errors. See RFC 4918 for more information.

    0 讨论(0)
  • 2020-12-02 06:18

    For server/protocol errors I would try to be as REST/HTTP as possible (Compare this with you typing in URL's in your browser):

    • a non existing item is called (/persons/{non-existing-id-here}). Return a 404.
    • an unexpected error on the server (code bug) occured. Return a 500.
    • the client user is not authorised to get the resource. Return a 401.

    For domain/business logic specific errors I would say the protocol is used in the right way and there's no server internal error, so respond with an error JSON/XML object or whatever you prefer to describe your data with (Compare this with you filling in forms on a website):

    • a user wants to change its account name but the user did not yet verify its account by clicking a link in an email which was sent to the user. Return {"error":"Account not verified"} or whatever.
    • a user wants to order a book, but the book was sold (state changed in DB) and can't be ordered anymore. Return {"error":"Book already sold"}.
    0 讨论(0)
  • 2020-12-02 06:23

    I don't think you should be returning any http error codes, rather custom exceptions that are useful to the client end of the application so the interface knows what had actually occurred. I wouldn't try and mask real issues with 404 error codes or something to that nature.

    0 讨论(0)
  • 2020-12-02 06:27

    The HTTP status code you return should depend on the type of error that has occurred. If an ID doesn't exist in the database, return a 404; if a user doesn't have enough privileges to make that Ajax call, return a 403; if the database times out before being able to find the record, return a 500 (server error).

    jQuery automatically detects such error codes, and runs the callback function that you define in your Ajax call. Documentation: http://api.jquery.com/jQuery.ajax/

    Short example of a $.ajax error callback:

    $.ajax({
      type: 'POST',
      url: '/some/resource',
      success: function(data, textStatus) {
        // Handle success
      },
      error: function(xhr, textStatus, errorThrown) {
        // Handle error
      }
    });
    
    0 讨论(0)
提交回复
热议问题