REST调用异常:设置内部异常时,出现401 UNAUTHORIZED循环

时间:2018-06-29 21:08:05

标签: c# rest exception-handling

作为一个好奇实验,我进行了一项测试,以从REST服务中抛出自定义WebFaultException<OurException>。过去,我从WCF服务中抛出了自定义异常,而不是使用DataContractDataMember创建伪“异常”。这样做在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);
}

1 个答案:

答案 0 :(得分:0)

没有发现为什么不应该这样做的原因,我们做到了。该服务最终实现为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");
        }

    }

}

这允许我们使用HTTP状态代码指示REST调用结果,从API引发自定义异常并将其序列化到调用方。我们将来可能会添加代码来记录内部异常,并有选择地在生成自定义响应之前将其剥离。

用法:

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;

}