2009-07-19 6 views
31

Sto implementando un servizio web RESTful usando WCF e WebHttpBinding. Attualmente sto lavorando sulla logica di gestione degli errori, implementando un gestore di errori personalizzato (IErrorHandler); l'obiettivo è di catturare eventuali eccezioni non rilevate generate dalle operazioni e quindi restituire un oggetto di errore JSON (incluso dire un codice di errore e un messaggio di errore - ad esempio {"errorCode": 123, "errorMessage": "bla"}) di nuovo al utente del browser insieme a un codice HTTP come BadRequest, InteralServerError o qualsiasi altra cosa (qualsiasi cosa diversa da 'OK'). Ecco il codice che sto usando all'interno del metodo ProvideFault del mio gestore di errori:Come rendere il gestore degli errori WCF personalizzato restituire la risposta JSON con codice http non OK?

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

-> Questo ritorna con Content-Type: application/json, tuttavia il codice di stato è 'OK' invece di '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); 

-> Questo restituisce con il codice di stato corretto, tuttavia il tipo di contenuto è ora 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; 

-> Questo restituisce con il codice di stato corretto e il tipo di contenuto corretto! Il problema è che il corpo http ora ha il testo "Impossibile caricare l'origine per: http://localhost:7000/bla .." invece dei dati effettivi JSON.

Qualche idea? Sto prendendo in considerazione l'utilizzo dell'ultimo approccio e l'inserimento del JSON nel campo dell'intestazione HTTP StatusMessage anziché nel corpo, ma questo non sembra altrettanto bello?

+4

Hai risolto il problema? Sto avendo lo stesso problema. – tucaz

risposta

0

Che aspetto ha la classe ErrorMessage?

Non utilizzare il campo StatusMessage per dati leggibili dalla macchina - vedere http://tools.ietf.org/html/rfc2616#section-6.1.1.

Inoltre, potrebbe essere corretto che "il corpo http ora ha il testo 'Impossibile caricare l'origine per: http://localhost:7000/bla ..' invece dei dati JSON effettivi .." - una stringa letterale è dati JSON se ricordo correttamente.

25

In realtà, questo funziona per me.

Ecco la mia 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; } 
    } 

In combinazione con l'ultimo frammento di sopra:

 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; 

Questo mi dà errori corretti come JSON. Grazie. :)

6

Nell'ultima versione di WCF (versione 11/2011) esiste un modo migliore per farlo utilizzando WebFaultException. Si può usare come segue nei vostri blocchi catch servizio:

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

Ecco una soluzione completa basata su alcune informazioni da sopra:

Sì avete. È possibile creare un gestore errori personalizzato e fare ciò che si desidera.

Vedere il codice allegato.

Questo è il gestore di errore personalizzato:

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

Questo è un comportamento servizio esteso per iniettare il gestore degli errori:

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

che è vincolante un costume in modo sarete in grado per configurarlo nel 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 
     }; 
    } 
} 

Questo è il 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: L'estensione comportamento deve essere in una riga esattamente come è (c'è un bug in WCF).

Questo è il mio lato client (parte del nostro proxy personalizzato)

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

Grazie per questo! Funziona e ridurrà il codice di gestione degli errori duplicato nella mia applicazione. Qualche idea su come procedere per testare questa implementazione? –

+0

Avrai bisogno del test dei componenti. Basta creare un servizio che genera un'eccezione e un client che lo invoca. Quindi, asserisci che la risposta è come previsto. – nadavy

1

doppio verificare che il vostro errorObject può essere serializzato dal DataContractJsonSerializer. Mi sono imbattuto in un problema in cui l'implementazione del mio contratto non forniva un setter per una delle proprietà e silenziosamente non riuscivo a serializzare, con conseguenti sintomi simili: "il server non ha inviato una risposta".

Ecco il codice che ho usato per avere maggiori dettagli circa l'errore di serializzazione (rende un buon test di unità, con un'affermazione e senza il try/catch per scopi breakpoint):

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

Grazie per questo suggerimento! Questo era il motivo per cui il mio caso non funzionava –

0

Ecco la soluzione mi è venuta con:

Catching exceptions from WCF Web Services

in sostanza, si ottiene il servizio web per impostare una variabile OutgoingWebResponseContext, e tornare null come risultato

(sì, davvero!)
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; 
     } 
} 

Quindi, il chiamante deve cercare errori, quindi verificare se è stato restituito un valore "statusText".

Ecco come ho fatto in angolare:

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

    }); 

Ed ecco ciò che il mio codice angolare visualizzata in IE:

Error in IE

fresco, eh?

Completamente generico e non è necessario aggiungere i campi Success o ErrorMessage ai dati [DataContract] restituiti dai servizi.

0

Per coloro che utilizzano le app Web per chiamare WFC, restituire sempre il JSON come Stream. Per errori, non c'è bisogno di un mucchio di codice di fantasia/brutto.Basta cambiare il codice di stato HTTP con:

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

Poi invece di buttare l'eccezione, formattare tale eccezione o un oggetto di errore personalizzato in JSON e restituirlo come System.IO.Stream.

Problemi correlati