Search code examples
haskellservant

catching IO exceptions in servant


I use servant for a simple JSON api, that allows you to create users, whose names must be unique. This is enforced by a unique constraint in SQLite. I have a function DB.saveUser :: UserReq -> IO Int that (unsurprisingly) saves users to SQLite and returns the generated id. It will throw a SQLError ErrorConstraint _ _ if the name has already been taken. I want to return a HTTP response code 409 if that occurs. So my question would be, is there some way to catch the SQLError in the Handler Monad? If not, what would be the cleanest way to achieve what I'm looking for? I thought about making DB.saveUser return a Maybe Int but somehow I think there must be a better solution.

createUser :: UserReq -> Handler (Headers '[Header "Location" Text] NoContent)
createUser ur = do id <- liftIO (DB.saveUser ur)
                   return . addHeader (T.pack ("/user/" ++ show id)) $ NoContent

saveUser (UR.UserReq name) = withConnection database $ \conn ->
  do executeNamed conn "INSERT INTO users (name) VALUES (:name)" [":name" := name]
     fromIntegral <$> lastInsertRowId conn

Solution

  • Servant's Handler is a newtype wrapper:

    newtype Handler a = Handler { runHandler' :: ExceptT ServantErr IO a }
    

    inside, there is a ExceptT ServantErr, where ServantErr is:

    data ServantErr = ServantErr { errHTTPCode     :: Int
                                 , errReasonPhrase :: String
                                 , errBody         :: LBS.ByteString
                                 , errHeaders      :: [HTTP.Header]
                                 } deriving (Show, Eq, Read, Typeable)
    

    So, you can run your DB operations in eg try or, to handle releasing resources: bracket, and map your DB exceptions to ServantErr to get the HTTP response code that you want.

    How to throw ServantErr after you catch DB exception: https://haskell-servant.readthedocs.io/en/stable/tutorial/Server.html#failing-through-servanterr