c# - REST 调用异常:设置内部异常时出现 401 UNAUTHORIZED 循环
问题描述
作为一个好奇的实验,我运行了一个测试以WebFaultException<OurException>
从 REST 服务中抛出一个自定义。我过去曾从 WCF 服务中抛出自定义异常,而不是使用DataContract
and创建虚假“异常” DataMember
。这样做在 REST 中没有多大意义,但我很好奇。
401 UNAUTHORIZED
当设置内部异常时,我没想到会陷入循环。一个简单的异常完美序列化,即使是我们自己的自定义异常。如果内部异常与外部异常的类型相同,则没有问题。但是我捕获和包装的任何东西都陷入了重复循环,即进行 REST 调用、抛出异常、401 UNAUTHORIZED
使用密码提示响应客户端,然后在输入我的密码后再次进行其余调用 - 重复。
我终于破解了该WebFaultException<T>
课程的源代码并找到了这个宝石:
[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)无法处理基类中的公共成员 - 它实际上只是为了反序列化简单的、非继承类型。所以我们必须自己添加 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;
}
推荐阅读
- sql - 如何加入 SQL-SERVER
- r - 出错时退出函数时自动 sink()
- asp.net-core - identityserver4 关联失败
- javascript - 如果请求需要一定时间,如何显示通知?
- javascript - 使用 react-router v6 和 useNavigate 动态生成查询参数
- java - 使用 java 在 Sharepoint 中读取 Excel 文件
- flutter - 如何更改 ElevatedButton 和 OutlinedButton 的禁用颜色
- excel - 我在 Vlookup 源工作表上的范围说什么
- php - 当我将一个文件包含到另一个文件中时,为什么会收到一条错误消息“您无法序列化或反序列化 PDO 实例”?
- google-cloud-platform - BigQuery 显示错误的结果 - 从 Cloud Function 复制数据?