Comment faire personnalisé Gestionnaire d'erreurs WCF réponse JSON avec code http non-OK?

j'implémente un service Web RESTful en utilisant WCF et le WebHttpBinding. Actuellement je travaille sur la logique de gestion des erreurs, implémentant un gestionnaire d'erreurs personnalisé (IErrorHandler); le but est de le faire attraper toutes les exceptions non récupérées lancées par les opérations et ensuite retourner un objet d'erreur JSON (y compris dire un code d'erreur et un message d'erreur - par exemple { "errorCode": 123, "errorMessage": "bla" }) à l'utilisateur du navigateur avec un code HTTP tel que BadRequest, InteralServerError ou peu importe (autre chose que " OK " en fait). Voici le code que j'utilise à L'intérieur de la méthode ProvideFault de mon gestionnaire d'erreurs:

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);

-- > cela retourne avec Content-Type: application/json, cependant le code de statut est 'OK' au lieu de '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);

-- > ceci retourne avec le code d'état correct, cependant le type de contenu est maintenant 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;

-- > ceci retourne avec le statut correct le code et le type de contenu correct! Le problème est que le corps http a maintenant le message " Échec du chargement de la source: http://localhost:7000/bla ..'au lieu des données JSON réelles..

des idées? Je pense utiliser la dernière approche et simplement coller le JSON dans le champ D'en-tête HTTP StatusMessage plutôt que dans le corps, mais cela ne semble pas aussi agréable?

33
demandé sur Inferis 2009-07-19 05:19:26

8 réponses

en fait, ça me va.

voici ma classe ErrorMessage:

    [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; }
    }

combiné avec le dernier extrait ci-dessus:

        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; 

cela me donne des erreurs correctes comme json. Grâce. :)

26
répondu Inferis 2010-05-25 19:15:27

Voici une solution complète basée sur quelques informations d'en haut:

Oui. Vous pouvez créer custom error handler et faire ce que vous voulez.

voir le code ci-joint.

C'est le gestionnaire d'erreurs personnalisé:

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);
    }
}

c'est un comportement de service étendu pour injecter le gestionnaire d'erreurs:

/// <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
    }
}

C'est une liaison personnalisée donc vous serez en mesure de le configurer dans le 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
        };
    }
}

C'est le 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: l'extension de comportement doit être dans une ligne exactement comme elle est (il y a un bug dans WCF).

C'est mon côté client (partie de notre proxy personnalisé)

 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);
                }
            });
    }
15
répondu nadavy 2012-02-19 22:06:51

dans la dernière version de WCF (en date du 11/2011) Il ya une meilleure façon de le faire en utilisant WebFaultException. Vous pouvez l'utiliser comme suit dans votre service de blocs catch:

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; }
    }
7
répondu Jaime Botero 2011-11-08 17:32:04

j'ai eu exactement le même problème. Cela m'a été utile:

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

3
répondu ChrisCa 2011-05-31 11:22:06

Vérifiez deux fois que votre errorObject peut être sérialisé par DataContractJsonSerializer. J'ai rencontré un problème lorsque mon contrat ne prévoyait pas de setter pour l'une des propriétés et que je manquais silencieusement de sérialiser--ce qui a entraîné des symptômes similaires: "le serveur n'a pas envoyé de réponse".

voici le code que j'ai utilisé pour obtenir plus de détails sur l'erreur de sérialisation (fait un bon test d'unité avec une assertion et sans le try / catch pour break point objet):

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();
1
répondu steamer25 2012-08-09 14:27:16

à quoi ressemble la classe ErrorMessage?

N'utilisez pas le champ StatusMessage pour les données lisibles par machine -- Voir http://tools.ietf.org/html/rfc2616#section-6.1.1 .

aussi, il peut être correct que "le corps http a maintenant le texte 'N'a pas réussi à charger source pour: http://localhost:7000/bla ..'au lieu des données JSON réelles.."--une chaîne littérale est JSON data si je me souviens bien.

0
répondu JLamb 2009-08-04 08:12:00

Voici la solution que j'ai trouvée:

d'intercepter les exceptions de Services Web WCF

en gros, vous obtenez votre service web de définir une variable OutgoingWebResponseContext , et de retourner null comme le résultat (oui, vraiment !)

    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;
        }
}

ensuite, vous demandez à votre interlocuteur de rechercher les erreurs, puis de vérifier si une valeur" statusText " a été retournée.

Voici comment je l'ai fait en angle:

$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);

    });

et voici ce que mon code angulaire affiché dans IE:

Error in IE

Cool, hein ?

complètement générique, et pas besoin d'ajouter Success ou ErrorMessage aux données [DataContract] que vos services renvoient.

0
répondu Mike Gledhill 2017-05-23 10:30:11

pour ceux qui utilisent des applications web pour appeler WFC, toujours retourner votre JSON comme un flux. Pour les erreurs, pas besoin d'un tas de fantaisie/laid code. Il suffit de changer le code d'état http avec:

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

puis au lieu de lancer l'exception, formater cette exception ou un objet d'erreur personnalisé dans JSON et le retourner comme un système.IO.Stream.

0
répondu Roy Oliver 2016-05-03 16:56:36