How to make custom WCF error handler return JSON response with non-OK http code?

前端 未结 8 1600
春和景丽
春和景丽 2020-12-04 12: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 (IErrorHand

相关标签:
8条回答
  • 2020-12-04 12:53

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

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

    0 讨论(0)
  • 2020-12-04 12:55

    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();
    
    0 讨论(0)
  • 2020-12-04 12:58

    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);
                    }
                });
        }
    
    0 讨论(0)
  • 2020-12-04 13:02

    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.

    0 讨论(0)
  • 2020-12-04 13:06

    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.

    0 讨论(0)
  • 2020-12-04 13:07

    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. :)

    0 讨论(0)
提交回复
热议问题