2009-07-19 17 views
31

Estoy implementando un servicio web RESTful usando WCF y WebHttpBinding. Actualmente estoy trabajando en la lógica de manejo de errores, implementando un controlador de error personalizado (IErrorHandler); el objetivo es hacer que atrape las excepciones no detectadas lanzadas por las operaciones y luego devolver un objeto de error JSON (incluyendo decir un código de error y un mensaje de error, por ejemplo {"errorCode": 123, "errorMessage": "bla"}) volver al usuario del navegador junto con un código HTTP como BadRequest, InteralServerError o lo que sea (cualquier cosa que no sea 'OK' en realidad). Aquí está el código que estoy utilizando dentro del método ProvideFault de mi gestor de errores:¿Cómo hacer que el manejador de errores WCF personalizado devuelva la respuesta JSON con un código http incorrecto?

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

-> Esto devuelve con Content-Type: application/json, sin embargo, el código de estado es 'OK' en lugar 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); 

-> Esto devuelve el código de estado correcto; sin embargo, el tipo de contenido es ahora 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; 

-> Esto devuelve con el código de estado correcto y el tipo de contenido correcto! El problema es que el cuerpo http ahora tiene el texto 'No se pudo cargar el código fuente: http://localhost:7000/bla ..' en lugar de los datos JSON reales ..

¿Alguna idea? Estoy considerando utilizar el último enfoque y simplemente colocar el JSON en el campo de encabezado HTTP StatusMessage en lugar de colocarlo en el cuerpo, ¿pero esto no parece tan agradable?

+4

¿Pudo arreglar esto? Estoy teniendo el mismo problema. – tucaz

Respuesta

0

¿Cómo es la clase ErrorMessage?

No utilice el campo StatusMessage para datos legibles por máquina - consulte http://tools.ietf.org/html/rfc2616#section-6.1.1.

Además, puede estar bien que "el cuerpo http ahora tenga el texto 'Error al cargar la fuente para: http://localhost:7000/bla ..' en lugar de los datos JSON reales .." - una cadena literal es datos JSON si recuerdo correctamente.

25

En realidad, esto funciona para mí.

Aquí es mi clase 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; } 
    } 

En combinación con el último fragmento anterior:

 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; 

Esto me da errores propios como JSON. Gracias. :)

6

En la última versión de WCF (a partir de 11/2011) hay una mejor manera de hacerlo con WebFaultException. Se puede utilizar como sigue en sus bloques de captura de servicio:

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

Aquí es una solución completa basada en algo de información desde arriba:

Si usted tiene. Puede crear un manejador de errores personalizado y hacer lo que quiera.

Consulte el código adjunto.

Ese es el controlador de errores personalizado:

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

Eso es un comportamiento de servicio extendido para inyectar el gestor de errores:

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

Eso es vinculante una costumbre por lo que será capaz para configurarlo en el 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 
     }; 
    } 
} 

Ese es el 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> 

Nota: La extensión comportamiento estén situados en una línea exactamente como es (hay un error en WCF).

Esa es mi lado del cliente (parte de nuestro proxy personalizado)

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

¡Gracias por esto! Funciona y reducirá el código duplicado de manejo de errores en mi aplicación. ¿Alguna idea de cómo llevar a cabo la prueba unitaria de esta implementación? –

+0

Necesitará una prueba de componentes. Solo crea un servicio que arroje una excepción y un cliente que lo invoque. Luego, afirme que la respuesta es la esperada. – nadavy

1

vuelva a comprobar que su errorObject se puede serializar por DataContractJsonSerializer. Me encontré con un problema donde la implementación de mi contrato no proporcionaba un setter para una de las propiedades y silenciosamente no serializaba, lo que daba como resultado síntomas similares: 'el servidor no envió una respuesta'.

Aquí está el código que utiliza para obtener más detalles acerca del error de serialización (Hace una buena prueba de la unidad con una afirmación y sin el try/catch para fines de punto de ruptura):

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

¡Gracias por este consejo! Esta fue la razón por la cual mi caso no funcionaba –

0

Aquí está la solución que se me ocurrió con:

Catching exceptions from WCF Web Services

Básicamente, se obtiene el servicio web para establecer una variable OutgoingWebResponseContext, y volver null como resultado

(sí, de verdad!)
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; 
     } 
} 

Luego, le pide a la persona que llama que busque errores, luego verifique si se devolvió un valor "statusText".

Así es como lo hice en 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); 

    }); 

Y esto es lo que mi código angular representada en IE:

Error in IE

fresco, ey?

Completamente genérico, y no es necesario agregar Success o ErrorMessage campos a los datos [DataContract] que están devolviendo sus servicios.

0

Para aquellos que utilizan aplicaciones web para llamar a WFC, siempre devuelva su JSON como una secuencia. Para los errores, no hay necesidad de un montón de fantasía/código feo.Sólo cambia el código de estado HTTP con:

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

Entonces, en lugar de lanzar la excepción, formato que excepción o un objeto de error personalizado en JSON y devolverlo como una System.IO.Stream.

Cuestiones relacionadas