Guilherme Molin
Guilherme Molin

Reputation: 403

How to throw a deserialized exception?

I'm serializing a Exception at the server with JsonConvert.SerializeObject and then encoding to a byte[] and deserializing in the client using JsonConvert.DeserializeObject. Everything works fine so far... The problem is when I throw the Exception the stacktrace being replaced, let me demostrate:

public void HandleException(RpcException exp)
{
    // Get the exception byte[]
    string exceptionString = exp.Trailer.GetBytes("exception-bin");
    
    // Deserialize the exception
    Exception exception = JsonConvert.DeserializeObject<Exception>(exceptionString, new 
    JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
    
    // Log the Exception: The stacktrace is correct. Ex.: at ServerMethod()
    Console.WriteLine(exception);
    
    // Throw the same Exception: The stacktrace is changed. Ex.: at HandleException()
    ExceptionDispatchInfo.Capture(exception).Throw();
}

Upvotes: 4

Views: 2236

Answers (2)

Guilherme Molin
Guilherme Molin

Reputation: 403

Just a small case that I want to point out: I'm calling this seralization/deserialization from two apps one is Blazor (.net 5) and another one is WinForms (.net framework 4.7). In the blazor one the method of the accepted answer did not work. What I do in this case is set te RemoteStackTrace via reflection.

// Convert string para exception
Exception exception = JsonConvert.DeserializeObject<Exception>(exceptionString, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

// Set RemoteStackTrace
exception.GetType().GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, exception.StackTrace);

// Throw the Exception with original stacktrace
ExceptionDispatchInfo.Capture(exception).Throw();

Upvotes: 0

dbc
dbc

Reputation: 117200

If you deserialize an Exception and set JsonSerializerSettings.Context = new StreamingContext(StreamingContextStates.CrossAppDomain) then the deserialized stack trace string will get prepended to the displayed StackTrace even after the exception is thrown:

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    Context = new StreamingContext(StreamingContextStates.CrossAppDomain),
};
var exception = JsonConvert.DeserializeObject<Exception>(exceptionString, settings);

Notes:

  • This works because, in the streaming constructor for Exception, the deserialized stack trace string is saved into a _remoteStackTraceString which is later prepended to the regular stack trace:

    if (context.State == StreamingContextStates.CrossAppDomain)
    {
        // ...this new exception may get thrown.  It is logically a re-throw, but 
        //  physically a brand-new exception.  Since the stack trace is cleared 
        //  on a new exception, the "_remoteStackTraceString" is provided to 
        //  effectively import a stack trace from a "remote" exception.  So,
        //  move the _stackTraceString into the _remoteStackTraceString.  Note
        //  that if there is an existing _remoteStackTraceString, it will be 
        //  preserved at the head of the new string, so everything works as 
        //  expected.
        // Even if this exception is NOT thrown, things will still work as expected
        //  because the StackTrace property returns the concatenation of the
        //  _remoteStackTraceString and the _stackTraceString.
        _remoteStackTraceString = _remoteStackTraceString + _stackTraceString;
        _stackTraceString = null;
    }
    
  • While the serialization stream for Exception does contain the stack trace string, it does not attempt to capture the private Object _stackTrace which is used by the runtime to identify where in the executing assembly the exception was thrown. This would seem to be why ExceptionDispatchInfo is unable to copy and use this information when throwing the exception. Thus it seems to be impossible to throw a deserialized exception and restore its "real" stack trace from the serialization stream.

  • In order Json.NET to deserialize a type using its streaming constructor (and thus set the remote trace string as required), the type must be marked with [Serializable] and implement ISerializable. System.Exception meets both requirements, but some derived classes of Exception do not always add the [Serializable] attribute. If your specific serialized exception lacks the attribute, see Deserializing custom exceptions in Newtonsoft.Json.

  • Deserializing an exception with TypeNameHandling.All is insecure and may lead to injection of attack types when deserializing from untrusted sources. See: External json vulnerable because of Json.Net TypeNameHandling auto? whose answer specifically discusses deserialization of exceptions.

Demo fiddle here.

Upvotes: 1

Related Questions