当 HTTP 状态代码为 401 Unauthorized 时,IErrorHandler 返回错误消息 body
IErrorHandler returning wrong message body when HTTP status code is 401 Unauthorized
我已经实现了 IErrorHandler 来处理我的 restful WCF 服务的构造函数中抛出的授权异常。当捕获到一般异常时,我的自定义类型按预期 returned,但 ContentType header 不正确。
HTTP/1.1 500 Internal Server Error
Content-Type: application/xml;
...
{"ErrorMessage":"Error!"}
然而,当错误处理程序尝试 return 401 未经授权的 http 状态代码时,消息 body 被覆盖为默认类型,但 ContentType header 是应该的。
HTTP/1.1 401 Unauthorized
Content-Type: application/json;
...
{"Message":"Authentication failed.","StackTrace":null,"ExceptionType":"System.InvalidOperationException"}
显然这里有问题,但我不确定是什么。
如何实现 IErrorHandler,使其 return 在 json 中使用正确的 header 自定义类型?
BaseDataResponseContractObject:
[Serializable]
[DataContract( Name = "BaseDataResponseContract" )]
public class BaseDataResponseContract
{
[DataMember]
public string ErrorMessage { get; set; }
} // end
这就是我想要的objectreturn。我的应用程序中的所有其他 object 都继承自此 object。当抛出异常时,我们真正关心的是 http 状态代码和错误消息。
IErrorHandler 实现(为简洁起见未显示日志记录):
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 实现:
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>
最后,我在使用 WebFaultException 时看到了类似的行为。我的想法是,这是一些深藏不露的 .Net 恶作剧的结果。我选择实现 IErrorHandler,这样我就可以捕获任何其他可能无法处理的异常。
参考:
https://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.ierrorhandler(v=vs.100).aspx
其他示例:
IErrorHandler doesn't seem to be handling my errors in WCF .. any ideas?
我不太确定您的应用程序是如何实现的。根据您的描述,我建议使用 visual studio 调试您的 ErrorHandler 以查看异常是否到达您的回调。
如果是,请按照您想要的方式手动构建 soap 错误或响应。
如果不是,则意味着异常发生在到达您的服务操作之前,它可能已经在 Channel 堆栈中失败,在这种情况下,一种简单的方法是添加额外的 HttpModule 来自定义或映射响应。或者您可以尝试在频道堆栈中自定义编码器。
根据您所写的内容,您在服务实现的构造函数中抛出了异常。因为 WCF 使用反射来创建您的服务实现,除非您的服务是 Singleton,否则您将得到 TargetInvocationException。
示例(使用 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
{
}
基本上,避免在构造函数中根据业务逻辑抛出异常。仅在处理编程错误时这样做。
在为此苦苦挣扎了将近一整天后,我发现这是由 IIS 设置引起的。
在 IIS 中的 API 项目下,在身份验证菜单下,我将 'Forms Authentication' 设置为 'Enabled'。我关闭了这个 'feature' 并且上面的代码开始按预期工作。我发现这是由于我团队中的另一位开发人员将代码放入 web.config 文件中,从而更改了 IIS 中的设置。具体来说:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
...
<system.web>
<authentication mode="Forms" />
</system.web>
...
</configuration>
此外,通过在 WebOperationContext OutgoingResponse object.
上使用 ContentType 属性,我能够使 Content-Type header 正确显示
// 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();
我已经实现了 IErrorHandler 来处理我的 restful WCF 服务的构造函数中抛出的授权异常。当捕获到一般异常时,我的自定义类型按预期 returned,但 ContentType header 不正确。
HTTP/1.1 500 Internal Server Error
Content-Type: application/xml;
...
{"ErrorMessage":"Error!"}
然而,当错误处理程序尝试 return 401 未经授权的 http 状态代码时,消息 body 被覆盖为默认类型,但 ContentType header 是应该的。
HTTP/1.1 401 Unauthorized
Content-Type: application/json;
...
{"Message":"Authentication failed.","StackTrace":null,"ExceptionType":"System.InvalidOperationException"}
显然这里有问题,但我不确定是什么。
如何实现 IErrorHandler,使其 return 在 json 中使用正确的 header 自定义类型?
BaseDataResponseContractObject:
[Serializable]
[DataContract( Name = "BaseDataResponseContract" )]
public class BaseDataResponseContract
{
[DataMember]
public string ErrorMessage { get; set; }
} // end
这就是我想要的objectreturn。我的应用程序中的所有其他 object 都继承自此 object。当抛出异常时,我们真正关心的是 http 状态代码和错误消息。
IErrorHandler 实现(为简洁起见未显示日志记录):
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 实现:
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>
最后,我在使用 WebFaultException 时看到了类似的行为。我的想法是,这是一些深藏不露的 .Net 恶作剧的结果。我选择实现 IErrorHandler,这样我就可以捕获任何其他可能无法处理的异常。
参考:
https://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.ierrorhandler(v=vs.100).aspx
其他示例:
IErrorHandler doesn't seem to be handling my errors in WCF .. any ideas?
我不太确定您的应用程序是如何实现的。根据您的描述,我建议使用 visual studio 调试您的 ErrorHandler 以查看异常是否到达您的回调。
如果是,请按照您想要的方式手动构建 soap 错误或响应。
如果不是,则意味着异常发生在到达您的服务操作之前,它可能已经在 Channel 堆栈中失败,在这种情况下,一种简单的方法是添加额外的 HttpModule 来自定义或映射响应。或者您可以尝试在频道堆栈中自定义编码器。
根据您所写的内容,您在服务实现的构造函数中抛出了异常。因为 WCF 使用反射来创建您的服务实现,除非您的服务是 Singleton,否则您将得到 TargetInvocationException。
示例(使用 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
{
}
基本上,避免在构造函数中根据业务逻辑抛出异常。仅在处理编程错误时这样做。
在为此苦苦挣扎了将近一整天后,我发现这是由 IIS 设置引起的。
在 IIS 中的 API 项目下,在身份验证菜单下,我将 'Forms Authentication' 设置为 'Enabled'。我关闭了这个 'feature' 并且上面的代码开始按预期工作。我发现这是由于我团队中的另一位开发人员将代码放入 web.config 文件中,从而更改了 IIS 中的设置。具体来说:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
...
<system.web>
<authentication mode="Forms" />
</system.web>
...
</configuration>
此外,通过在 WebOperationContext OutgoingResponse object.
上使用 ContentType 属性,我能够使 Content-Type header 正确显示// 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();