简体   繁体   中英

REST call exception: 401 UNAUTHORIZED loop when inner exception is set

As a curiosity experiment, I ran a test to throw a custom WebFaultException<OurException> from a REST service. I've thrown custom exceptions from WCF services in the past instead of creating faux "exceptions" using DataContract and DataMember . Doing this doesn't make as much sense in REST, but I was curious.

What I didn't expect was getting stuck in a 401 UNAUTHORIZED loop when an inner exception was set. A simple exception serialized perfectly, even for our own custom exceptions. If the inner exception was the same type as the outer exception, no problem. But anything I caught and wrapped got stuck in a repeat loop of making the REST call, throwing the exception, a 401 UNAUTHORIZED response to the client with a password prompt, followed by making the rest call again after entering my password - repeat.

I finally cracked open the source for the WebFaultException<T> class and found this gem:

[Serializable]
[System.Diagnostics.CodeAnalysis.SuppressMessage
       ("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", 
        Justification = "REST Exceptions cannot contain InnerExceptions or messages")]
public class WebFaultException {...}

So why can't they contain inner exceptions? Everything serializes fine independently, so it has to be something in the inner workings of WebFaultException that either isn't implemented or explicitly prevents this for some clearly-known-to-someone reason.

What gives?

The interface:

[OperationContract]
[FaultContract(typeof(OurException))]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Xml, 
           BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "/TestCustomException")]
string TestCustomException();

The service method:

public string TestCustomException() {
    throw new WebFaultException<OurException>(new OurException("Nope.", new Exception("Still Nope.")), HttpStatusCode.InternalServerError);
}

Having found no reason why this shouldn't be done, we did it. The service was ultimately implemented as a Web API 2 service. We have a custom exception and an exception filter that mimics the default exception behavior but allows us to specify the HTTP status code (Microsoft makes everything a 503). Inner exceptions serialize fine... one could argue that we shouldn't include them, and if the service were exposed outside of our department, we wouldn't. For the moment, it's the caller's responsibility to decide whether a failure to contact the domain controller is a transient issue that can be retried.

RestSharp removed the Newtonsoft dependencies some while back, but unfortunately, the deserializers it provides now (v106.4 at the time of this answer) can't handle public members in a base class - it was literally only designed to deserialize simple, non-inheriting types. So we had to add the Newtonsoft deserializers back ourselves... with those, inner exceptions serialize just fine.

In any case, here's the code we ended up with:

[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);

    }

}

And:

[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));         
    }

}

Finally, our exception filter:

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");
        }

    }

}

This allows us to throw custom exceptions from the API and have them serialized to the caller, using HTTP status codes to indicate the REST call results. We may add code in the future to log the inner exception and optionally strip it before generating the custom response.

Usage:

catch (UserNotFoundException userNotFoundException) {
    ldsResult = NotFound;
    throw new OurAdApiException($"User '{userCN}' not found in LDS", HttpStatusCode.NotFound, adResult, ldsResult, userNotFoundException);
}

Deserializing from the RestSharp caller:

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;

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM