How to make custom WCF error handler return JSON r

2019-01-13 02:36发布

问题:

I'm implementing a RESTful web service using WCF and the WebHttpBinding. Currently I'm working on the error handling logic, implementing a custom error handler (IErrorHandler); the aim is to have it catch any uncaught exceptions thrown by operations and then return a JSON error object (including say an error code and error message - e.g. { "errorCode": 123, "errorMessage": "bla" }) back to the browser user along with an an HTTP code such as BadRequest, InteralServerError or whatever (anything other than 'OK' really). Here is the code I am using inside the ProvideFault method of my error handler:

fault = Message.CreateMessage(version, "", errorObject, new DataContractJsonSerializer(typeof(ErrorMessage)));
var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);
var rmp = new HttpResponseMessageProperty();
rmp.StatusCode = System.Net.HttpStatusCode.InternalServerError;
rmp.Headers.Add(HttpRequestHeader.ContentType, "application/json");
fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);

--> This returns with Content-Type: application/json, however the status code is 'OK' instead of 'InternalServerError'.

fault = Message.CreateMessage(version, "", errorObject, new DataContractJsonSerializer(typeof(ErrorMessage)));
var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);
var rmp = new HttpResponseMessageProperty();
rmp.StatusCode = System.Net.HttpStatusCode.InternalServerError;
//rmp.Headers.Add(HttpRequestHeader.ContentType, "application/json");
fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);

--> This returns with the correct status code, however the content-type is now XML.

fault = Message.CreateMessage(version, "", errorObject, new DataContractJsonSerializer(typeof(ErrorMessage)));
var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);

var response = WebOperationContext.Current.OutgoingResponse;
response.ContentType = "application/json";
response.StatusCode = HttpStatusCode.InternalServerError;

--> This returns with the correct status code and the correct content-type! The problem is that the http body now has the text 'Failed to load source for: http://localhost:7000/bla..' instead of the actual JSON data..

Any ideas? I'm considering using the last approach and just sticking the JSON in the HTTP StatusMessage header field instead of in the body, but this doesn't seem quite as nice?

回答1:

Actually, this works for me.

Here's my ErrorMessage class:

    [DataContract]
    public class ErrorMessage
    {
        public ErrorMessage(Exception error)
        {
            Message = error.Message;
            StackTrace = error.StackTrace;
            Exception = error.GetType().Name;
        }

        [DataMember(Name="stacktrace")]
        public string StackTrace { get; set; }
        [DataMember(Name = "message")]
        public string Message { get; set; }
        [DataMember(Name = "exception-name")]
        public string Exception { get; set; }
    }

Combined with the last snippet above:

        fault = Message.CreateMessage(version, "", new ErrorMessage(error), new DataContractJsonSerializer(typeof(ErrorMessage)));
        var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
        fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);

        var response = WebOperationContext.Current.OutgoingResponse;
        response.ContentType = "application/json";
        response.StatusCode = HttpStatusCode.InternalServerError; 

This gives me proper errors as json. Thanks. :)



回答2:

Here's a complete solution based on some info from above:

Yes you have. You can create custom error handler and do what you feel like.

See the attached code.

That's the custom error handler:

public class JsonErrorHandler : IErrorHandler
{

    public bool HandleError(Exception error)
    {
        // Yes, we handled this exception...
        return true;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        // Create message
        var jsonError = new JsonErrorDetails { Message = error.Message, ExceptionType = error.GetType().FullName };
        fault = Message.CreateMessage(version, "", jsonError,
                                      new DataContractJsonSerializer(typeof(JsonErrorDetails)));

        // Tell WCF to use JSON encoding rather than default XML
        var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
        fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);

        // Modify response
        var rmp = new HttpResponseMessageProperty
                      {
                          StatusCode = HttpStatusCode.BadRequest,
                          StatusDescription = "Bad Request",
                      };
        rmp.Headers[HttpResponseHeader.ContentType] = "application/json";
        fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);
    }
}

That's an extended service behavior to inject the error handler:

/// <summary>
/// This class is a custom implementation of the WebHttpBehavior. 
/// The main of this class is to handle exception and to serialize those as requests that will be understood by the web application.
/// </summary>
public class ExtendedWebHttpBehavior : WebHttpBehavior
{
    protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        // clear default erro handlers.
        endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();

        // add our own error handler.
        endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new JsonErrorHandler());
        //BehaviorExtensionElement
    }
}

That's a custom binding so you'll be able to configure it in the web.config

/// <summary>
/// Enables the ExtendedWebHttpBehavior for an endpoint through configuration.
/// Note: Since the ExtendedWebHttpBehavior is derived of the WebHttpBehavior we wanted to have the exact same configuration.  
/// However during the coding we've relized that the WebHttpElement is sealed so we've grabbed its code using reflector and
/// modified it to our needs.
/// </summary>
public sealed class ExtendedWebHttpElement : BehaviorExtensionElement
{
    private ConfigurationPropertyCollection properties;
    /// <summary>Gets or sets a value that indicates whether help is enabled.</summary>
    /// <returns>true if help is enabled; otherwise, false. </returns>
    [ConfigurationProperty("helpEnabled")]
    public bool HelpEnabled
    {
        get
        {
            return (bool)base["helpEnabled"];
        }
        set
        {
            base["helpEnabled"] = value;
        }
    }
    /// <summary>Gets and sets the default message body style.</summary>
    /// <returns>One of the values defined in the <see cref="T:System.ServiceModel.Web.WebMessageBodyStyle" /> enumeration.</returns>
    [ConfigurationProperty("defaultBodyStyle")]
    public WebMessageBodyStyle DefaultBodyStyle
    {
        get
        {
            return (WebMessageBodyStyle)base["defaultBodyStyle"];
        }
        set
        {
            base["defaultBodyStyle"] = value;
        }
    }
    /// <summary>Gets and sets the default outgoing response format.</summary>
    /// <returns>One of the values defined in the <see cref="T:System.ServiceModel.Web.WebMessageFormat" /> enumeration.</returns>
    [ConfigurationProperty("defaultOutgoingResponseFormat")]
    public WebMessageFormat DefaultOutgoingResponseFormat
    {
        get
        {
            return (WebMessageFormat)base["defaultOutgoingResponseFormat"];
        }
        set
        {
            base["defaultOutgoingResponseFormat"] = value;
        }
    }
    /// <summary>Gets or sets a value that indicates whether the message format can be automatically selected.</summary>
    /// <returns>true if the message format can be automatically selected; otherwise, false. </returns>
    [ConfigurationProperty("automaticFormatSelectionEnabled")]
    public bool AutomaticFormatSelectionEnabled
    {
        get
        {
            return (bool)base["automaticFormatSelectionEnabled"];
        }
        set
        {
            base["automaticFormatSelectionEnabled"] = value;
        }
    }
    /// <summary>Gets or sets the flag that specifies whether a FaultException is generated when an internal server error (HTTP status code: 500) occurs.</summary>
    /// <returns>Returns true if the flag is enabled; otherwise returns false.</returns>
    [ConfigurationProperty("faultExceptionEnabled")]
    public bool FaultExceptionEnabled
    {
        get
        {
            return (bool)base["faultExceptionEnabled"];
        }
        set
        {
            base["faultExceptionEnabled"] = value;
        }
    }
    protected override ConfigurationPropertyCollection Properties
    {
        get
        {
            if (this.properties == null)
            {
                this.properties = new ConfigurationPropertyCollection
                {
                    new ConfigurationProperty("helpEnabled", typeof(bool), false, null, null, ConfigurationPropertyOptions.None), 
                    new ConfigurationProperty("defaultBodyStyle", typeof(WebMessageBodyStyle), WebMessageBodyStyle.Bare, null, null, ConfigurationPropertyOptions.None), 
                    new ConfigurationProperty("defaultOutgoingResponseFormat", typeof(WebMessageFormat), WebMessageFormat.Xml, null, null, ConfigurationPropertyOptions.None), 
                    new ConfigurationProperty("automaticFormatSelectionEnabled", typeof(bool), false, null, null, ConfigurationPropertyOptions.None), 
                    new ConfigurationProperty("faultExceptionEnabled", typeof(bool), false, null, null, ConfigurationPropertyOptions.None)
                };
            }
            return this.properties;
        }
    }
    /// <summary>Gets the type of the behavior enabled by this configuration element.</summary>
    /// <returns>The <see cref="T:System.Type" /> for the behavior enabled with the configuration element: <see cref="T:System.ServiceModel.Description.WebHttpBehavior" />.</returns>
    public override Type BehaviorType
    {
        get
        {
            return typeof(ExtendedWebHttpBehavior);
        }
    }
    protected override object CreateBehavior()
    {
        return new ExtendedWebHttpBehavior
        {
            HelpEnabled = this.HelpEnabled,
            DefaultBodyStyle = this.DefaultBodyStyle,
            DefaultOutgoingResponseFormat = this.DefaultOutgoingResponseFormat,
            AutomaticFormatSelectionEnabled = this.AutomaticFormatSelectionEnabled,
            FaultExceptionEnabled = this.FaultExceptionEnabled
        };
    }
}

That's the web.config

  <system.serviceModel>
<diagnostics>
  <messageLogging logMalformedMessages="true" logMessagesAtTransportLevel="true" />
</diagnostics>
<bindings>
  <webHttpBinding>
    <binding name="regularService" />
  </webHttpBinding>
</bindings>
<behaviors>
  <endpointBehaviors>
    <behavior name="AjaxBehavior">
      <extendedWebHttp />
    </behavior>
  </endpointBehaviors>
  <serviceBehaviors>
    <behavior>
      <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
      <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
      <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
      <serviceDebug includeExceptionDetailInFaults="true"/>
    </behavior>
  </serviceBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions>
    <add name="extendedWebHttp" type="MyNamespace.ExtendedWebHttpElement, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </behaviorExtensions>
</extensions>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<services>
  <service name="MyWebService">
    <endpoint address="" behaviorConfiguration="AjaxBehavior"
      binding="webHttpBinding" bindingConfiguration="regularService"
      contract="IMyWebService" />
  </service>
</services>

Note: The behavior extension should be in one line EXACTLY as is (there's a bug in WCF).

That's my client side (part of our custom proxy)

 public void Invoke<T>(string action, object prms, JsAction<T> successCallback, JsAction<WebServiceException> errorCallback = null, JsBoolean webGet = null)
    {
        Execute(new WebServiceRequest { Action = action, Parameters = prms, UseGetMethod = webGet },
            t =>
            {
                successCallback(t.As<T>());
            },
            (req, message, err)=>
            {
                if (req.status == 400) //Bad request - that's what we've specified in the WCF error handler.
                {
                    var details = JSON.parse(req.responseText).As<JsonErrorDetails>();
                    var ex = new WebServiceException()
                    {
                        Message = details.Message,
                        StackTrace = details.StackTrace,
                        Type = details.ExceptionType
                    };

                    errorCallback(ex);
                }
            });
    }


回答3:

In the latest version of WCF (As of 11/2011) there's a better way of doing this using WebFaultException. You can use it as follows in your service catch blocks:

throw new WebFaultException<ServiceErrorDetail>(new ServiceErrorDetail(ex), HttpStatusCode.SeeOther);


[DataContract]
    public class ServiceErrorDetail
    {
        public ServiceErrorDetail(Exception ex)
        {
            Error = ex.Message;
            Detail = ex.Source;
        }
        [DataMember]
        public String Error { get; set; }
        [DataMember]
        public String Detail { get; set; }
    }


回答4:

I had the exact same problem. This was useful for me:

http://zamd.net/2008/07/08/error-handling-with-webhttpbinding-for-ajaxjson/



回答5:

Double-check that your errorObject can be serialized by DataContractJsonSerializer. I ran into a problem where my contract implementation didn't provide a setter for one of the properties and was silently failing to serialize--resulting in similar symptoms: 'server did not send a response'.

Here's the code I used to get more details about the serialization error (makes a good unit test with an assertion and without the try/catch for breakpoint purposes):

Stream s = new MemoryStream();
try
{
    new DataContractJsonSerializer(typeof(ErrorObjectDataContractClass)).WriteObject(s, errorObject);
} catch(Exception e)
{
    e.ToString();
}
s.Seek(0, SeekOrigin.Begin);
var json = new StreamReader(s, Encoding.UTF8).ReadToEnd();


回答6:

What does the ErrorMessage class look like?

Don't use the StatusMessage field for machine-readable data -- see http://tools.ietf.org/html/rfc2616#section-6.1.1 .

Also, it may be okay that "the http body now has the text 'Failed to load source for: http://localhost:7000/bla..' instead of the actual JSON data.." -- a literal string is JSON data if I remember correctly.



回答7:

Here's the solution I came up with:

Catching exceptions from WCF Web Services

Basically, you get your web service to set a OutgoingWebResponseContext variable, and return null as the result (yes, really !)

    public List<string> GetAllCustomerNames()
    {
        //  Get a list of unique Customer names.
        //
        try 
        {
            //  As an example, let's throw an exception, for our Angular to display..
            throw new Exception("Oh heck, something went wrong !");

            NorthwindDataContext dc = new NorthwindDataContext();
            var results = (from cust in dc.Customers select cust.CompanyName).Distinct().OrderBy(s => s).ToList();

            return results;
        }  
        catch (Exception ex)
        {
            OutgoingWebResponseContext response = WebOperationContext.Current.OutgoingResponse;
            response.StatusCode = System.Net.HttpStatusCode.Forbidden;
            response.StatusDescription = ex.Message;
            return null;
        }
}

Then, you get your caller to look out for errors, then check if a "statusText" value was returned.

Here's how I did it in Angular:

$http.get('http://localhost:15021/Service1.svc/getAllCustomerNames')
    .then(function (data) {
        //  We successfully loaded the list of Customer names.
        $scope.ListOfCustomerNames = data.GetAllCustomerNamesResult;

    }, function (errorResponse) {

        //  The WCF Web Service returned an error

        var HTTPErrorNumber = errorResponse.status;
        var HTTPErrorStatusText = errorResponse.statusText;

        alert("An error occurred whilst fetching Customer Names\r\nHTTP status code: " + HTTPErrorNumber + "\r\nError: " + HTTPErrorStatusText);

    });

And here's what my Angular code displayed in IE:

Cool, hey ?

Completely generic, and no need to add Success or ErrorMessage fields to the [DataContract] data which your services are returning.



回答8:

For those using web apps to call WFC, always return your JSON as a Stream. For errors, no need for a bunch of fancy/ugly code. Just change the http status code with:

System.ServiceModel.Web.WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError

Then instead of throwing the exception, format that exception or a custom error object into JSON and return it as a System.IO.Stream.