REST 调用异常:设置内部异常时出现 401 UNAUTHORIZED 循环
REST call exception: 401 UNAUTHORIZED loop when inner exception is set
作为一个好奇心实验,我 运行 进行了从 REST 服务中抛出自定义 WebFaultException<OurException>
的测试。我过去曾从 WCF 服务中抛出自定义异常,而不是使用 DataContract
和 DataMember
创建虚假的 "exceptions"。这样做在 REST 中没有多大意义,但我很好奇。
我没想到的是在设置内部异常时卡在 401 UNAUTHORIZED
循环中。一个完美序列化的简单异常,即使对于我们自己的自定义异常也是如此。如果内部异常与外部异常的类型相同,则没有问题。但是我捕获和包装的任何东西都陷入了一个重复循环,即进行 REST 调用,抛出异常,对客户端的 401 UNAUTHORIZED
响应和密码提示,然后在输入我的密码后再次进行其余调用 - 重复.
我终于破解了 WebFaultException<T>
class 的源代码并找到了这个 gem:
[Serializable]
[System.Diagnostics.CodeAnalysis.SuppressMessage
("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "REST Exceptions cannot contain InnerExceptions or messages")]
public class WebFaultException {...}
那么为什么它们不能包含内部异常?一切都可以独立地很好地序列化,所以它必须是 WebFaultException
内部工作中的某些东西,要么没有实现,要么出于某些众所周知的原因明确阻止它。
什么给了?
界面:
[OperationContract]
[FaultContract(typeof(OurException))]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Xml,
BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "/TestCustomException")]
string TestCustomException();
服务方式:
public string TestCustomException() {
throw new WebFaultException<OurException>(new OurException("Nope.", new Exception("Still Nope.")), HttpStatusCode.InternalServerError);
}
在找不到不应该这样做的理由后,我们做到了。该服务最终实现为 Web API 2 服务。我们有一个自定义异常和一个模拟默认异常行为的异常过滤器,但允许我们指定 HTTP 状态代码(Microsoft 将所有内容设为 503)。内部异常序列化很好……有人可能会争辩说我们不应该包括它们,如果服务在我们部门之外公开,我们也不会。目前,调用者有责任确定联系域控制器失败是否是可以重试的暂时性问题。
RestSharp 前段时间删除了 Newtonsoft 依赖项,但不幸的是,它现在提供的反序列化器(在回答此问题时为 v106.4)无法处理基 public 成员 class - 它实际上只是为了反序列化简单的、非继承的类型而设计的。所以我们不得不自己添加 Newtonsoft 反序列化器......有了这些,内部异常序列化就好了。
无论如何,这是我们最终得到的代码:
[Serializable]
public class OurAdApiException : OurBaseWebException {
/// <summary>The result of the action in Active Directory </summary>
public AdActionResults AdResult { get; set; }
/// <summary>The result of the action in LDS </summary>
public AdActionResults LdsResult { get; set; }
// Other constructors snipped...
public OurAdApiException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
AdResult = (AdActionResults) info.GetValue("AdResult", typeof(AdActionResults));
LdsResult = (AdActionResults) info.GetValue("LdsResult", typeof(AdActionResults));
}
catch (ArgumentNullException) {
// blah
}
catch (InvalidCastException) {
// blah blah
}
catch (SerializationException) {
// yeah, yeah, yeah
}
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
// 'info' is guaranteed to be non-null by the above call to GetObjectData (will throw an exception there if null)
info.AddValue("AdResult", AdResult);
info.AddValue("LdsResult", LdsResult);
}
}
并且:
[Serializable]
public class OurBaseWebException : OurBaseException {
/// <summary>
/// Dictionary of properties relevant to the exception </summary>
/// <remarks>
/// Basically seals the property while leaving the class inheritable. If we don't do this,
/// we can't pass the dictionary to the constructors - we'd be making a virtual member call
/// from the constructor. This is because Microsoft makes the Data property virtual, but
/// doesn't expose a protected setter (the dictionary is instantiated the first time it is
/// accessed if null). #why
/// If you try to fully override the property, you get a serialization exception because
/// the base exception also tries to serialize its Data property </remarks>
public new IDictionary Data => base.Data;
/// <summary>The HttpStatusCode to return </summary>
public HttpStatusCode HttpStatusCode { get; protected set; }
public InformationSecurityWebException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
HttpStatusCode = (HttpStatusCode) info.GetValue("HttpStatusCode", typeof(HttpStatusCode));
}
catch (ArgumentNullException) {
// sure
}
catch (InvalidCastException) {
// fine
}
catch (SerializationException) {
// we do stuff here in the real code
}
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
info.AddValue(nameof(HttpStatusCode), HttpStatusCode, typeof(HttpStatusCode));
}
}
最后,我们的异常过滤器:
public override void OnException(HttpActionExecutedContext context) {
// Any custom AD API Exception thrown will be serialized into our custom response
// Any other exception will be handled by the Microsoft framework
if (context.Exception is OurAdApiException contextException) {
try {
// This lets us use HTTP Status Codes to indicate REST results.
// An invalid parameter value becomes a 400 BAD REQUEST, while
// a configuration error is a 503 SERVICE UNAVAILABLE, for example.
// (Code for CreateCustomErrorResponse available upon request...
// we basically copied the .NET framework code because it wasn't
// possible to modify/override it :(
context.Response = context.Request.CreateCustomErrorResponse(contextException.HttpStatusCode, contextException);
}
catch (Exception exception) {
exception.Swallow($"Caught an exception creating the custom response; IIS will generate the default response for the object");
}
}
}
这允许我们从 API 中抛出自定义异常并将它们序列化给调用者,使用 HTTP 状态代码指示 REST 调用结果。我们可能会在未来添加代码来记录内部异常,并在生成自定义响应之前选择性地剥离它。
用法:
catch (UserNotFoundException userNotFoundException) {
ldsResult = NotFound;
throw new OurAdApiException($"User '{userCN}' not found in LDS", HttpStatusCode.NotFound, adResult, ldsResult, userNotFoundException);
}
从 RestSharp 调用方反序列化:
public IRestResponse<T> Put<T, W, V>(string ResourceUri, W MethodParameter) where V : Exception
where T : new() {
// Handle to any logging context established by caller; null logger if none was configured
ILoggingContext currentContext = ContextStack<IExecutionContext>.CurrentContext as ILoggingContext ?? new NullLoggingContext();
currentContext.ThreadTraceInformation("Building the request...");
RestRequest request = new RestRequest(ResourceUri, Method.PUT) {
RequestFormat = DataFormat.Json,
OnBeforeDeserialization = serializedResponse => { serializedResponse.ContentType = "application/json"; }
};
request.AddBody(MethodParameter);
currentContext.ThreadTraceInformation($"Executing request: {request} ");
IRestResponse<T> response = _client.Execute<T>(request);
#region - Handle the Response -
if (response == null) {
throw new OurBaseException("The response from the REST service is null");
}
// If you're not following the contract, you'll get a serialization exception
// You can optionally work with the json directly, or use dynamic
if (!response.IsSuccessful) {
V exceptionData = JsonConvert.DeserializeObject<V>(response.Content);
throw exceptionData.ThreadTraceError();
}
// Timed out, aborted, etc.
if (response.ResponseStatus != ResponseStatus.Completed) {
throw new OurBaseException($"Request failed to complete: Status '{response.ResponseStatus}'").ThreadTraceError();
}
#endregion
return response;
}
作为一个好奇心实验,我 运行 进行了从 REST 服务中抛出自定义 WebFaultException<OurException>
的测试。我过去曾从 WCF 服务中抛出自定义异常,而不是使用 DataContract
和 DataMember
创建虚假的 "exceptions"。这样做在 REST 中没有多大意义,但我很好奇。
我没想到的是在设置内部异常时卡在 401 UNAUTHORIZED
循环中。一个完美序列化的简单异常,即使对于我们自己的自定义异常也是如此。如果内部异常与外部异常的类型相同,则没有问题。但是我捕获和包装的任何东西都陷入了一个重复循环,即进行 REST 调用,抛出异常,对客户端的 401 UNAUTHORIZED
响应和密码提示,然后在输入我的密码后再次进行其余调用 - 重复.
我终于破解了 WebFaultException<T>
class 的源代码并找到了这个 gem:
[Serializable]
[System.Diagnostics.CodeAnalysis.SuppressMessage
("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors",
Justification = "REST Exceptions cannot contain InnerExceptions or messages")]
public class WebFaultException {...}
那么为什么它们不能包含内部异常?一切都可以独立地很好地序列化,所以它必须是 WebFaultException
内部工作中的某些东西,要么没有实现,要么出于某些众所周知的原因明确阻止它。
什么给了?
界面:
[OperationContract]
[FaultContract(typeof(OurException))]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Xml,
BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "/TestCustomException")]
string TestCustomException();
服务方式:
public string TestCustomException() {
throw new WebFaultException<OurException>(new OurException("Nope.", new Exception("Still Nope.")), HttpStatusCode.InternalServerError);
}
在找不到不应该这样做的理由后,我们做到了。该服务最终实现为 Web API 2 服务。我们有一个自定义异常和一个模拟默认异常行为的异常过滤器,但允许我们指定 HTTP 状态代码(Microsoft 将所有内容设为 503)。内部异常序列化很好……有人可能会争辩说我们不应该包括它们,如果服务在我们部门之外公开,我们也不会。目前,调用者有责任确定联系域控制器失败是否是可以重试的暂时性问题。
RestSharp 前段时间删除了 Newtonsoft 依赖项,但不幸的是,它现在提供的反序列化器(在回答此问题时为 v106.4)无法处理基 public 成员 class - 它实际上只是为了反序列化简单的、非继承的类型而设计的。所以我们不得不自己添加 Newtonsoft 反序列化器......有了这些,内部异常序列化就好了。
无论如何,这是我们最终得到的代码:
[Serializable]
public class OurAdApiException : OurBaseWebException {
/// <summary>The result of the action in Active Directory </summary>
public AdActionResults AdResult { get; set; }
/// <summary>The result of the action in LDS </summary>
public AdActionResults LdsResult { get; set; }
// Other constructors snipped...
public OurAdApiException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
AdResult = (AdActionResults) info.GetValue("AdResult", typeof(AdActionResults));
LdsResult = (AdActionResults) info.GetValue("LdsResult", typeof(AdActionResults));
}
catch (ArgumentNullException) {
// blah
}
catch (InvalidCastException) {
// blah blah
}
catch (SerializationException) {
// yeah, yeah, yeah
}
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SerializationFormatter)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
// 'info' is guaranteed to be non-null by the above call to GetObjectData (will throw an exception there if null)
info.AddValue("AdResult", AdResult);
info.AddValue("LdsResult", LdsResult);
}
}
并且:
[Serializable]
public class OurBaseWebException : OurBaseException {
/// <summary>
/// Dictionary of properties relevant to the exception </summary>
/// <remarks>
/// Basically seals the property while leaving the class inheritable. If we don't do this,
/// we can't pass the dictionary to the constructors - we'd be making a virtual member call
/// from the constructor. This is because Microsoft makes the Data property virtual, but
/// doesn't expose a protected setter (the dictionary is instantiated the first time it is
/// accessed if null). #why
/// If you try to fully override the property, you get a serialization exception because
/// the base exception also tries to serialize its Data property </remarks>
public new IDictionary Data => base.Data;
/// <summary>The HttpStatusCode to return </summary>
public HttpStatusCode HttpStatusCode { get; protected set; }
public InformationSecurityWebException(SerializationInfo info, StreamingContext context) : base(info, context) {
try {
HttpStatusCode = (HttpStatusCode) info.GetValue("HttpStatusCode", typeof(HttpStatusCode));
}
catch (ArgumentNullException) {
// sure
}
catch (InvalidCastException) {
// fine
}
catch (SerializationException) {
// we do stuff here in the real code
}
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
info.AddValue(nameof(HttpStatusCode), HttpStatusCode, typeof(HttpStatusCode));
}
}
最后,我们的异常过滤器:
public override void OnException(HttpActionExecutedContext context) {
// Any custom AD API Exception thrown will be serialized into our custom response
// Any other exception will be handled by the Microsoft framework
if (context.Exception is OurAdApiException contextException) {
try {
// This lets us use HTTP Status Codes to indicate REST results.
// An invalid parameter value becomes a 400 BAD REQUEST, while
// a configuration error is a 503 SERVICE UNAVAILABLE, for example.
// (Code for CreateCustomErrorResponse available upon request...
// we basically copied the .NET framework code because it wasn't
// possible to modify/override it :(
context.Response = context.Request.CreateCustomErrorResponse(contextException.HttpStatusCode, contextException);
}
catch (Exception exception) {
exception.Swallow($"Caught an exception creating the custom response; IIS will generate the default response for the object");
}
}
}
这允许我们从 API 中抛出自定义异常并将它们序列化给调用者,使用 HTTP 状态代码指示 REST 调用结果。我们可能会在未来添加代码来记录内部异常,并在生成自定义响应之前选择性地剥离它。
用法:
catch (UserNotFoundException userNotFoundException) {
ldsResult = NotFound;
throw new OurAdApiException($"User '{userCN}' not found in LDS", HttpStatusCode.NotFound, adResult, ldsResult, userNotFoundException);
}
从 RestSharp 调用方反序列化:
public IRestResponse<T> Put<T, W, V>(string ResourceUri, W MethodParameter) where V : Exception
where T : new() {
// Handle to any logging context established by caller; null logger if none was configured
ILoggingContext currentContext = ContextStack<IExecutionContext>.CurrentContext as ILoggingContext ?? new NullLoggingContext();
currentContext.ThreadTraceInformation("Building the request...");
RestRequest request = new RestRequest(ResourceUri, Method.PUT) {
RequestFormat = DataFormat.Json,
OnBeforeDeserialization = serializedResponse => { serializedResponse.ContentType = "application/json"; }
};
request.AddBody(MethodParameter);
currentContext.ThreadTraceInformation($"Executing request: {request} ");
IRestResponse<T> response = _client.Execute<T>(request);
#region - Handle the Response -
if (response == null) {
throw new OurBaseException("The response from the REST service is null");
}
// If you're not following the contract, you'll get a serialization exception
// You can optionally work with the json directly, or use dynamic
if (!response.IsSuccessful) {
V exceptionData = JsonConvert.DeserializeObject<V>(response.Content);
throw exceptionData.ThreadTraceError();
}
// Timed out, aborted, etc.
if (response.ResponseStatus != ResponseStatus.Completed) {
throw new OurBaseException($"Request failed to complete: Status '{response.ResponseStatus}'").ThreadTraceError();
}
#endregion
return response;
}