Skip to content

Instantly share code, notes, and snippets.

@noelwelsh
Created February 20, 2015 16:06
Show Gist options
  • Save noelwelsh/9cacc8683bf3231b9219 to your computer and use it in GitHub Desktop.
Save noelwelsh/9cacc8683bf3231b9219 to your computer and use it in GitHub Desktop.
Robust Error Handling in Scala
import scalaz.\/
import scalaz.syntax.either._
object Example2 {
// This example simulates error handling for a simple three tier web application
//
// The tiers are:
// - the HTTP service
// - a user authentication layer
// - a database layer
//
// Each stage defines its own error types
// A few type aliases for clarity
type Query = String
type Host = String
type Username = String
type Password = String
final case class User(username: Username, firstName: String, lastName: String, password: Password)
object HttpService {
sealed trait HttpError extends Exception
final case class CouldNotAuthenticate(username: Username) extends HttpError
// We don't store any infomration about the cause of an internal
// error as we don't want to leak internal information to
// attackers.
final case object InternalError extends HttpError
// After calling login we would have a conversion to some kind of
// HTTP response (not shown). The type is:
// \/[HttpError, User] => HttpResponse
// or in abstract
// A => B
// Readers of Essential Scala should recognise this as a fold
def login(username: Username, password: Password): \/[HttpError, User] =
// We use leftMap to convert the error type from the layer below
// into the error type used at this layer. This is boilerplate to
// some extent, but there are good reasons for changing how we
// report errors, which we see here as we want to avoid leaking
// information to attackers. In a real system we'd also have
// some kind of logging to record information we need for
// debugging.
AuthenticationService.login(username, password).leftMap {
// Don't leak information about why a login attempt
// failed. Letting an attacker know they have found a valid
// username allows them to focus attacks.
case AuthenticationService.NotFound(username) =>
CouldNotAuthenticate(username)
case AuthenticationService.BadPassword(username, password) =>
CouldNotAuthenticate(username)
case AuthenticationService.DatabaseError(error) =>
InternalError
}
}
object AuthenticationService {
sealed trait ServiceError
final case class BadPassword(username: Username, password: Password) extends ServiceError
final case class NotFound(username: Username) extends ServiceError
final case class DatabaseError(error: DatabaseService.DatabaseError) extends ServiceError
def login(username: Username, password: Password): \/[ServiceError, User] =
for {
user <- DatabaseService.findUserByUsername(username).leftMap {
case DatabaseService.NotFound(u) => NotFound(u)
case e @ DatabaseService.CouldNotAuthenticate(u, p) => DatabaseError(e)
case e @ DatabaseService.CouldNotConnect(h) => DatabaseError(e)
}
_ <- checkPassword(user, password)
} yield user
def checkPassword(user: User, password: Password): \/[ServiceError, User] =
if(user.password == password)
user.right
else
BadPassword(user.username, password).left
}
object DatabaseService {
sealed trait DatabaseError
final case class NotFound(username: Username) extends DatabaseError
final case class CouldNotConnect(host: Host) extends DatabaseError
final case class CouldNotAuthenticate(username: Username, password: Password) extends DatabaseError
def findUserByUsername(username: Username): \/[DatabaseError, User] =
// Simulate network and system errors
Math.random() match {
case p if p < 0.1 => CouldNotConnect("database.server").left
case p if p < 0.2 => CouldNotAuthenticate("user", "password").left
case _ =>
username match {
case "noelw" => User("noelw", "Noel", "Welsh", "soverysecret").right
case "daveg" => User("daveg", "Dave", "Gurnell", "\\m/").right
case _ => NotFound(username).left
}
}
}
}
@pjurczenko
Copy link

This is a great example, but I am really curious about one thing: since login method is impure, how would you test it? Even if we accept that impurity, we can't override DatabaseService object, because AuthenticationService is an object itself. I can think of a few solutions (e.g. making AuthService a trait, or passing \/[DatabaseError, User] as a param), but none of them fully satisfies me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment