首页 > 解决方案 > REST 调用异常:设置内部异常时出现 401 UNAUTHORIZED 循环

问题描述

作为一个好奇的实验,我运行了一个测试以WebFaultException<OurException>从 REST 服务中抛出一个自定义。我过去曾从 WCF 服务中抛出自定义异常,而不是使用DataContractand创建虚假“异常” 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);
}

标签: c#restexception-handling

解决方案


没有找到不应该这样做的理由,我们做到了。该服务最终实现为 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;

}

推荐阅读