Exception Serialization In Lagom

Lagom is an open source framework for building systems of Reactive microservices both in Java or Scala. It provides many out of the box although opinionated APIs, implementation of supporting features and appropriate defaults to build, test and deploy entire systems of Reactive microservices at a fast, yet reliable pace. In this blog post we will discuss one such appropriate default that Lagom provides us out of the box. We will try to understand how Lagom translates exceptions to Json responses with appropriate HTTP error codes.

Lagom provides an ExceptionSerializer trait that describes conversion of exceptions to a RawExceptionMessage implementation.

trait ExceptionSerializer {
def serialize(exception: Throwable, accept: immutable.Seq[MessageProtocol]): RawExceptionMessage
}

The RawExceptionMessage trait models a transport error code, a protocol, and a message body. Lagom framework eventually makes use of RawExceptionMessage instances returned by ExceptionSerializer implementations to form HTTP responses with appropriate headers, body and error code.

trait RawExceptionMessage {
val errorCode: TransportErrorCode
val protocol: MessageProtocol
val message: ByteString
}

The appropriate default implementation by Lagom which implements this ExceptionSerializer trait is the DefaultExceptionSerializer . In the following code snippet you can see how DefaultExceptionSerializer serializes exception messages to Json. The message of type ByteString modeled by RawExceptionMessage is actually a stringified Json that captures the exception message.

class DefaultExceptionSerializer(environment: Environment) extends ExceptionSerializer {
override def serialize(exception: Throwable, accept: Seq[MessageProtocol]): RawExceptionMessage = {
val (errorCode, message) = exception match {
case te: TransportException =>
(te.errorCode, te.exceptionMessage)
case e if environment.mode == Mode.Prod =>
// By default, don't give out information about generic exceptions.
(TransportErrorCode.InternalServerError, new ExceptionMessage("Exception", ""))
case e =>
// Ok to give out exception information in dev and test
val writer = new CharArrayWriter
e.printStackTrace(new PrintWriter(writer))
val detail = writer.toString
(TransportErrorCode.InternalServerError, new ExceptionMessage(s"${exception.getClass.getName}: ${exception.getMessage}", detail))
}
val messageBytes = ByteString.fromString(Json.stringify(Json.obj(
"name" -> message.name,
"detail" -> message.detail
)))
RawExceptionMessage(errorCode, MessageProtocol(Some("application/json"), None, None), messageBytes)
}
}

The DefaultExceptionSerializer implementation can also be seen speaking a lot about Lagom’s built in error handling. It is only the instances and subclass instances of TransportException that Lagom considers safe to return the exception details of. For the rest of the exceptions it returns nothing unless in development with a generic HTTP error code of 500. In production, for security reasons, Lagom censors error messages which otherwise could be used by an attacker to gain information on how a service is implemented. There infact are a few useful built in subclasses of TransportException that Lagom provides out of the box, these include NotFound and PolicyViolation.

class TransportException(val errorCode: TransportErrorCode, val exceptionMessage: ExceptionMessage, cause: Throwable)
extends RuntimeException(exceptionMessage.detail, cause)
class NotFound(errorCode: TransportErrorCode, exceptionMessage: ExceptionMessage, cause: Throwable)
extends TransportException(errorCode, exceptionMessage, cause)

Now let's see how this DefaultExceptionSerializer is made available to your service implementation.

The service descriptor that we use to describe our services also has a provision to supply an exception serializer via it's withExceptionSerializer method.

trait Descriptor {
def withExceptionSerializer(exceptionSerializer: ExceptionSerializer): Descriptor
}

Lagom supplies the DefaultExceptionSerializer to our service descriptors as part of service resolution while loading our application in development mode and voila a default exception serializer is made available to you out of the box by Lagom.

class ScaladslServiceResolver(defaultExceptionSerializer: ExceptionSerializer) extends ServiceResolver {
override def resolve(descriptor: Descriptor): Descriptor = {
val withExceptionSerializer: Descriptor = if (descriptor.exceptionSerializer == DefaultExceptionSerializer.Unresolved) {
descriptor.withExceptionSerializer(defaultExceptionSerializer)
} else descriptor
}
Show Comments