Thrift Exception Mapping

It is recommended in Finatra that you generally prefer to use exceptions for flow control in your controller and services and rely on the c.t.finatra.thrift.exceptions.ExceptionMapper to convert exceptions into proper finatra-thrift exceptions or thrift responses.

Please see Http Exception Mapping for why the framework provides this.

Support for Scala only

Thrift exception mapping is currently only supported for Scala servers using Scala Scrooge generated classes. The Scrooge generated Java stack does not yet provide enough flexibility to easily map responses to different types.

More information on the Scrooge thrift code generator can be found here.

How?

The Finatra framework adds a default to ExceptionMappingFilter which provides root-level mapping for exceptions. You can register additional mappers or override the default one altogether.

For instance, if you want to map a java.lang.ClassCastException to a ThriftException – e.g., ClientError(ClientErrorCause, errorMessage), which is defined in finatra_thrift_exceptions.thrift you could create the following ExceptionMapper:

@Singleton
class ClassCastExceptionMapper extends ExceptionMapper[ClassCastException, ClientError] {

  def handle(throwable: ClassCastException): Future[ClientError] = {
    Future.exception(ClientError(BadRequest, throwable.getMessage))
  }
}

Then register this exception mapper in your server.

class MyThriftServer extends ThriftServer {

  override def configureThrift(router: ThriftRouter): Unit = {
    router
      .filter[ExceptionMappingFilter]
      .exceptionMapper[ClassCastExceptionMapper]
    ...
  }

  ...
}

Two more examples mapping exceptions to actual thrift responses are located in the test mappers.

Also, you can see we register the exception mapper by type allowing the framework to instantiate an instance.

ExceptionMappingFilter

Using exception mappers requires you to include the c.t.finatra.thrift.filters.ExceptionMappingFilter in your server’s filter chain.

For information on how to add a filter to your ThriftServer see the Filters section.

Default Exception Mapper

The framework adds only the ThrowableExceptionMapper to the ExceptionManager by default which simply throws back any uncaught Throwable.

Throwable ThrowableExceptionMapper

The ExceptionManager walks the exception type hierarchy starting at the given exception type, moving up the inheritance chain until it finds mapper configured for the type. In this manner, an ExceptionMapper[Throwable] will be the last mapper invoked and acts as the “default”. Therefore to change the framework “default” mapper, simply add a new mapper over the Throwable type (i.e., ExceptionMapper[Throwable]) to the ExceptionManager.

As noted above the last registered mapper for a type wins.

The Finatra framework also provides a FinatraThriftExceptionMapper for mapping other exceptions to known ThriftExceptions. If you are also using finatra_thrift_exceptions.thrift, this mapper is recommended to be registered.

Override Default Behavior

The ExceptionManager is the class that handles registration of exception mappers. In the example above, the ThriftRouter#exceptionMapper method is simply registering the given mapper with the ExceptionManager.

The ExceptionManager is configured by the inclusion of the ExceptionManagerModule as a framework module in every ThriftServer.

If a new mapper is added over an exception type already registered in the ExceptionManager, the previous mapper will be overwritten.

Thus, the last registered mapper for an exception type wins.

Register an Exception Mapper

There are two ways to add a mapper.

Either directly through the ThriftRouter:

override def configureThrift(router: ThriftRouter): Unit = {
  router
    .filter[ExceptionMappingFilter]
    .exceptionMapper[MyThrowableExceptionMapper]
    .exceptionMapper[OtherExceptionMapper]
}

Or in a module which is then added to the Server, e.g.,

object MyExceptionMapperModule extends TwitterModule {
  override def singletonStartup(injector: Injector): Unit = {
    val manager = injector.instance[ExceptionManager]
    manager.add[MyThrowableExceptionMapper]
    manager.add[OtherExceptionMapper]
  }
}

...

override val modules = Seq(
  MyExceptionMapperModule)