Как сделать так, чтобы пользовательский обработчик ошибок WCF возвращал JSON-ответ с не-OK кодом http?
Я реализую веб-сервис RESTful, используя WCF и WebHttpBinding. В настоящее время я работаю над логикой обработки ошибок, реализуя собственный обработчик ошибок (IErrorHandler); цель состоит в том, чтобы он перехватывал любые неперехваченные исключения, генерируемые операциями, а затем возвращал объект ошибки JSON (включая, например, код ошибки и сообщение об ошибке - например, { "errorCode": 123, "errorMessage": "bla" }) обратно в пользователь браузера вместе с HTTP-кодом, таким как BadRequest, InteralServerError или чем-то еще (на самом деле, кроме "OK"). Вот код, который я использую внутри метода ProvideFault моего обработчика ошибок:
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);
-> Возвращается с Content-Type: application/json, однако код состояния "OK" вместо "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);
-> Возвращается с правильным кодом состояния, однако тип содержимого теперь 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;
-> Возвращается с правильным кодом статуса и правильным типом содержимого! Проблема заключается в том, что в теле http теперь вместо фактических данных JSON содержится текст "Не удалось загрузить источник для: http://localhost:7000/bla..".
Есть идеи? Я подумываю использовать последний подход и просто вставить JSON в поле заголовка HTTP StatusMessage, а не в тело, но это не так приятно?
8 ответов
На самом деле, это работает для меня.
Вот мой класс 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; }
}
В сочетании с последним фрагментом выше:
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;
Это дает мне правильные ошибки как JSON. Благодарю.:)
Вот полное решение, основанное на некоторой информации сверху:
Да у тебя есть. Вы можете создать собственный обработчик ошибок и делать то, что вы чувствуете.
Смотрите прикрепленный код.
Это специальный обработчик ошибок:
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);
}
}
Это расширенное поведение службы для добавления обработчика ошибок:
/// <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
}
}
Это пользовательская привязка, поэтому вы сможете настроить ее в файле 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
};
}
}
Это веб.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>
Примечание. Расширение поведения должно быть в ТОЧКУ в одной строке, как есть (в WCF есть ошибка).
Это моя клиентская часть (часть нашего собственного прокси)
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);
}
});
}
В последней версии WCF (по состоянию на 11/2011) есть лучший способ сделать это с помощью WebFaultException. Вы можете использовать его в блоках перехвата службы следующим образом:
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; }
}
У меня была точно такая же проблема. Это было полезно для меня:
http://zamd.net/2008/07/08/error-handling-with-webhttpbinding-for-ajaxjson/
Для тех, кто использует веб-приложения для вызова WFC, всегда возвращайте свой JSON в виде потока. Для ошибок нет необходимости в куче причудливого / уродливого кода. Просто измените код статуса http:
System.ServiceModel.Web.WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError
Затем вместо того, чтобы выдавать исключение, отформатируйте это исключение или пользовательский объект ошибки в JSON и верните его как System.IO.Stream.
Дважды проверьте, что ваш errorObject может быть сериализован DataContractJsonSerializer. Я столкнулся с проблемой, когда моя реализация контракта не предоставила установщик для одного из свойств и не смог сериализоваться, что привело к похожим симптомам: "сервер не отправил ответ".
Вот код, который я использовал для получения более подробной информации об ошибке сериализации (делает хороший модульный тест с утверждением и без try/catch для целей точки останова):
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();
Вот решение, которое я придумал:
Перехват исключений из веб-служб WCF
По сути, вы получаете ваш веб-сервис для установки OutgoingWebResponseContext
переменная и возврат null
в результате (да, действительно!)
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;
}
}
Затем вы заставляете своего вызывающего абонента искать ошибки, а затем проверяете, было ли возвращено значение "statusText".
Вот как я это сделал в 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);
});
И вот что мой угловой код отображается в IE:
Круто, эй?
Полностью универсальный, и не нужно добавлять Success
или же ErrorMessage
поля к [DataContract]
данные, которые возвращают ваши услуги.
Как выглядит класс ErrorMessage?
Не используйте поле StatusMessage для машиночитаемых данных - см. http://tools.ietf.org/html/rfc2616.
Кроме того, может быть хорошо, что "тело http теперь содержит текст" Не удалось загрузить источник для: http://localhost:7000/bla.. "вместо фактических данных JSON.." - буквальная строка - JSON данные, если я правильно помню.