Monday, October 20, 2008

Leaky Exceptions

Take a look at this exception:

System.Runtime.Serialization.SerializationException: Unable to find assembly 'Castle.MicroKernel, Version=1.0.3.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc'.

<snip>

at System.Runtime.Remoting.Messaging.SmuggledMethodReturnMessage.FixupForNewAppDomain()
at System.Runtime.Remoting.Channels.CrossAppDomainSink.SyncProcessMessage(IMessage reqMsg)

Exception rethrown at [0]: 
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at Gallio.TDNetRunner.Core.IProxyTestRunner.Run(IFacadeTestListener testListener, String assemblyPath, String cref)

What Happened

  1. We made a call into a remote AppDomain.
  2. The call was processed and now we are trying to produce a return message.
  3. The call threw an exception of some type.
  4. The exception could not be deserialized by the caller because the exception is defined in the Castle.MicroKernel assembly.
  5. As a result, the remoting library "helpfully" replaced the real exception with a SerializationException instead.

Diagnosis

The server is leaking an exception of a type that the client does not understand.

The remote call interface is a Leaky Abstraction.  Technically, the implementation of that interface is leaky.

Fixes

Case 1: Wrap the inner exception.

There are several things we can do to fix leaky exceptions.  In most cases, it's simple enough to wrap the exception.

public void Wibble(string wibbleUser, string wibbleParam)
{
    try
    {
        wibbleAuditor.RecordWibble(wibbleUser, wibbleParam);
    }
    catch (AuditException ex)
    {
        throw new WibbleException("Could not write audit log message before wibbling.  Wibble aborted.");
    }
    try
    {
        wibbleHandler.Execute(wibbleParam);
    }
    catch (WibbleProcessorException ex)
    {
        throw new WibbleException("An error occurred while wibbling.", ex);
    }
}

Case 2: Capture a description of the inner exception as a string.

As we saw in the case of our SerializationException above, in some cases we cannot actually wrap the exception since the "InnerException" itself might not be serializable or deserializable.

In just those cases, we can do something like the following instead:

throw new WibbleException(String.Format("An error occurred while wibbling.  {0}", ex));

Capturing the exception as a string lets us smuggle it over into places where the exception type might not be accessible.  However, we do lose some flexibility in how the client can handle the exception.

Case 3: Discard the inner exception.

There is such a thing as too much information.

Sometimes revealing the contents of the inner exception to the client at all may constitute a security risk (leaking sensitive data in the stack trace or the message) or it may just provide way more details than the client can understand.

In these cases, it's best to provide a clear description of the exception in-situ without relying on the information from the internal exception at all.  Such an exception might instead refer to another source for obtaining the nitty-gritty details for low-level diagnosis.

throw new WibbleException("An error occurred while wibbling.  Consult the wibble processor log for details.");

Similar Problems

A method that throws an exception of an internal type.  This effectively prevents the client from filtering by exception type.

A method that throws an exception of an undocumented type.  While the exception may be caught, the client does not know to expect it.

A method that throws an exception of a vague type or with a vague message.  While the exception may be filtered and may be expected, the client will not be able to make sense of its contents.

An improperly defined custom exception type that lacks the special deserialization constructor and [Serializable].  Such an exception cannot cross remoting boundaries even if it was expected to.

An exception that reveals sensitive information as part of the exception message.

Examples:

  • Allowing a generic platform exception such as Win32Exception or COMException to escape through a high-level supposedly platform-independent interface.
  • Allowing an exception thrown by a some lower-level component or implementation detail that the client does not know about to escape.
  • Allowing an exception of a type not specified in the published documentation to escape.
  • Allowing a FileNotFoundException from an internal (non-obvious) I/O operation to be returned to the client without wrapping it with a higher-level exception type to explain what was going on.
  • Throwing an exception that contains a database connection string or user password in the message.

Full Circle

In this case I modified my code to return a SafeException containing the inner exception message as a string (applied case #2 above).  Here's what was hiding behind that SerializationException.

Gallio.Loader.SafeException: Gallio.Runtime.RuntimeException: Could not resolve service of type Gallio.Runner.ITestRunnerFactory. ---> Castle.MicroKernel.Resolvers.DependencyResolverException: Could not resolve non-optional dependency for 'NCoverIntegration2.NCoverTestRunnerFactory' (Gallio.Runner.DefaultTestRunnerFactory). Parameter 'name' type 'System.String'
   at Castle.MicroKernel.Resolvers.DefaultDependencyResolver.Resolve(CreationContext context, ISubDependencyResolver parentResolver, ComponentModel model, DependencyModel dependency)

<snip>

Ahh!  Now I know what's going on so I can fix it...

1 comment:

ulu said...

Hi,

I think I've just spotted another leaky exception in Gallio -- see issue 329 on Google code.

Thanks for the great framework.