问题
I have implemented IErrorHandler to handle authorization exceptions thrown within the constructor of my restful WCF service. When a general exception is caught my custom type is returned as expected, but the ContentType header is incorrect.
HTTP/1.1 500 Internal Server Error
Content-Type: application/xml;
...
{"ErrorMessage":"Error!"}
However when the error handler tries to return a 401 Unauthorized http status code the message body is overridden to the default type but the ContentType header is as it should be.
HTTP/1.1 401 Unauthorized
Content-Type: application/json;
...
{"Message":"Authentication failed.","StackTrace":null,"ExceptionType":"System.InvalidOperationException"}
Obviously something is wrong here, but I'm not sure what.
How do I implement IErrorHandler such that it returns my custom type in json with the correct headers?
BaseDataResponseContract Object:
[Serializable]
[DataContract( Name = "BaseDataResponseContract" )]
public class BaseDataResponseContract
{
[DataMember]
public string ErrorMessage { get; set; }
} // end
This is the object I want to return. All of the other objects in my application inherit from this object. When an exception is thrown all we really care about is the http status code and the error message.
IErrorHandler Implementation (logging is not shown for brevity):
namespace WebServices.BehaviorsAndInspectors
{
public class ErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return true;
} // end
public void ProvideFault(Exception ex, MessageVersion version, ref Message fault)
{
// Create a new instance of the object I would like to return with a default message
var baseDataResponseContract = new BaseDataResponseContract { ErrorMessage = "Error!" };
// Get the outgoing response portion of the current context
var response = WebOperationContext.Current.OutgoingResponse;
// Set the http status code
response.StatusCode = HttpStatusCode.InternalServerError;
// If the exception is a specific type change the default settings
if (ex.GetType() == typeof(UserNotFoundException))
{
baseDataResponseContract.ErrorMessage = "Invalid Username!";
response.StatusCode = HttpStatusCode.Unauthorized;
}
// Create the fault message that is returned (note the ref parameter)
fault = Message.CreateMessage(version, "", baseDataResponseContract, new DataContractJsonSerializer(typeof(BaseDataResponseContract)));
// Tell WCF to use JSON encoding rather than default XML
var webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
fault.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty);
// Add ContentType header that specifies we are using json
var httpResponseMessageProperty = new HttpResponseMessageProperty();
httpResponseMessageProperty.Headers[HttpResponseHeader.ContentType] = "application/json";
fault.Properties.Add(HttpResponseMessageProperty.Name, httpResponseMessageProperty);
} // end
} // end class
} // end namespace
IServiceBehavior Implementation:
namespace WebServices.BehaviorsAndInspectors
{
public class ErrorHandlerExtensionBehavior : BehaviorExtensionElement, IServiceBehavior
{
public override Type BehaviorType
{
get { return GetType(); }
}
protected override object CreateBehavior()
{
return this;
}
private IErrorHandler GetInstance()
{
return new ErrorHandler();
}
void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } // end
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
var errorHandlerInstance = GetInstance();
foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
{
dispatcher.ErrorHandlers.Add(errorHandlerInstance);
}
}
void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } // end
} // end class
} // end namespace
Web.Config:
<system.serviceModel>
<services>
<service name="WebServices.MyService">
<endpoint binding="webHttpBinding" contract="WebServices.IMyService" />
</service>
</services>
<extensions>
<behaviorExtensions>
<!-- This extension if for the WCF Error Handling-->
<add name="ErrorHandlerBehavior" type="WebServices.BehaviorsAndInspectors.ErrorHandlerExtensionBehavior, WebServices, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<ErrorHandlerBehavior />
</behavior>
</serviceBehaviors>
</behaviors>
....
</system.serviceModel>
Finally, I am seeing similar behavior when using WebFaultException. My thought is that this is the result of some deeply buried .Net shenanigans. I am choosing to implement IErrorHandler so that I can catch any other exceptions that may not be handled.
Reference:
https://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.ierrorhandler(v=vs.100).aspx
http://www.brainthud.com/cards/5218/25441/which-four-behavior-interfaces-exist-for-interacting-with-a-service-or-client-description-what-methods-do-they-implement-and
Other Examples:
IErrorHandler doesn't seem to be handling my errors in WCF .. any ideas?
How to make custom WCF error handler return JSON response with non-OK http code?
How do you set the Content-Type header for an HttpClient request?
回答1:
I am not quite sure how your application is implemented. Based on your description, I suggest using visual studio to debug your ErrorHandler to see whether the exception arrive your callback.
If yes, manually construct your soap fault or response in the way you want.
If not, it means the exception happens before arriving your service operation, it may fail already in Channel stack, in this case, an easy approach is add extra HttpModule to custom or map the response. Or you can try custom the encoder in Channel stack.
回答2:
Base on what you write, you throw an exception in the constructor of the service implementation. Because WCF uses reflection to create your service implementation, unless your service are Singleton, you will get a TargetInvocationException.
Example (use LINQPad):
void Main()
{
try
{
Activator.CreateInstance(typeof(Foo));
}
catch(Exception e)
{
e.Message.Dump();
e.GetType().Name.Dump();
}
}
public class Foo
{
public Foo()
{
throw new AuthorizationFailedException();
}
}
public class AuthorizationFailedException : Exception
{
}
Basically, avoid throwing exceptions based on business logic in a constructor. Only do that for handling programming errors.
回答3:
After struggling with this for almost a full day I discovered that this was caused by an IIS setting.
Under my API project in IIS, under the Authentication menu I had 'Forms Authentication' set to 'Enabled'. I turned off this 'feature' and the code above started working as expected. I found that this was due to another developer on my team putting code within the web.config file that altered the settings in IIS. Specifically:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
...
<system.web>
<authentication mode="Forms" />
</system.web>
...
</configuration>
Further, I was able to get the Content-Type header to appear correctly by using the ContentType Property on the WebOperationContext OutgoingResponse object.
// Get the outgoing response portion of the current context
var response = WebOperationContext.Current.OutgoingResponse;
// Add ContentType header that specifies we are using JSON
response.ContentType = new MediaTypeHeaderValue("application/json").ToString();
来源:https://stackoverflow.com/questions/38231970/ierrorhandler-returning-wrong-message-body-when-http-status-code-is-401-unauthor