Posts tagged "servant":
Validation of data in a servant server
I've been playing around with adding more validation of data received by an HTTP
endpoint in a servant server. Defining a type with a FromJSON instance is very
easy, just derive a Generic instance and it just works. Here's a simple
example
data Person = Person
{ name :: Text
, age :: Int
, occupation :: Occupation
}
deriving (Generic, Show)
deriving (FromJSON, ToJSON) via (Generically Person)
data Occupation = UnderAge | Student | Unemployed | SelfEmployed | Retired | Occupation Text
deriving (Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON) via (Generically Occupation)
However, the validation is rather limited, basically it's just checking that
each field is present and of the correct type. For the type above I'd like to
enforce some constraints for the combination of age and occupation.
The steps I thought of are
- Hide the default constructor and define a smart one. (This is the standard suggestion for placing extra constraints values.)
- Manually define the
FromJSONinstance using theGenericinstance to limit the amount of code and the smart constructor.
The smart constructor
I give the constructor the result type Either String Person to make sure it
can both be usable in code and when defining parseJSON.
mkPerson :: Text -> Int -> Occupation -> Either String Person
mkPerson name age occupation = do
guardE mustBeUnderAge
guardE notUnderAge
guardE tooOldToBeStudent
guardE mustBeRetired
pure $ Person name age occupation
where
guardE (pred, err) = when pred $ Left err
mustBeUnderAge = (age < 8 && occupation > UnderAge, "too young for occupation")
notUnderAge = (age > 15 && occupation == UnderAge, "too old to be under age")
tooOldToBeStudent = (age > 45 && occupation == Student, "too old to be a student")
mustBeRetired = (age > 65 && occupation /= Retired, "too old to not be retired")
Here I'm making use of Either e being a Monad and use when to apply the
constraints and ensure the reason for failure is given to the caller.
The FromJSON instance
When defining the instance I take advantage of the Generic instance to make
the implementation short and simple.
instance FromJSON Person where
parseJSON v = do
Person{name, age, occupation} <- genericParseJSON defaultOptions v
either fail pure $ mkPerson name age occupation
If there are many more fields in the type I'd consider using RecordWildCards.
Conclusion
No, it's nothing ground-breaking but I think it's a fairly nice example of how things can fit together in Haskell.
Servant and a weirdness in Keycloak
When writing a small tool to interface with Keycloak I found an endpoint that
require the content type to be application/json while the body should be plain
text. (The details are in the issue.) Since servant assumes that the content
type and the content match (I know, I'd always thought that was a safe
assumption to make too) it doesn't work with ReqBody '[JSON] Text. Instead I
had to create a custom type that's a combination of JSON and PlainText,
something that turned out to required surprisingly little code:
data KeycloakJSON deriving (Typeable) instance Accept KeycloakJSON where contentType _ = "application" // "json" instance MimeRender KeycloakJSON Text where mimeRender _ = fromStrict . encodeUtf8
The bug has already been fixed in Keycloak, but I'm sure there are other APIs with similar weirdness so maybe this will be useful to someone else.
The timeout manager exception
The other day I bumped the dependencies of a Haskell project at work and noticed a new exception being thrown:
Thread killed by timeout manager
After a couple of false starts (it wasn't the connection pool, nor was it servant) I realised that a better approach would be to look at the list of packages that were updated as part of the dependency bumping.1 Most of them I thought would be very unlikely sources of it, but two in the list stood out:
| Package | Pre | Post |
|---|---|---|
| unliftio | 0.2.14 | 0.2.18 |
| warp | 3.3.15 | 3.3.16 |
warp since the exception seemed to be thrown shortly after handling an HTTP
request, and unliftio since the exception was caught by the handler for
uncaught exceptions and its description contains "thread". Also, when looking at
the code changes in warp on GitHub2 I found that some of the changes
introduced was increased use of unliftio for async stuff. The changes contain
mentions of TimeoutThread and System.TimeManager. That sounded promising,
and it lead me to the TimeoutThread exception in time-manager.
With that knowledge I could quickly adjust the handler for uncaught exceptions
to not log TimeoutThread as fatal:
lastExceptionHandler :: LoggerSet -> SomeException -> IO () lastExceptionHandler logger e | Just TimeoutThread <- fromException e = return () | otherwise = do logFatalIoS logger $ pack $ "uncaught exception: " <> displayException e flushLogStr logger
I have to say it was a bit more work to arrive at this than I'd have liked. I reckon there are easier ways to track down the information I needed. So I'd love to hear what tricks and tips others have.
Footnotes:
As a bonus it gave me a good reason to reach for comm, a command that I
rarely use but for some reason always enjoy.
GitHub's compare feature isn't very easy to discover, but a URL like this https://github.com/yesodweb/wai/compare/warp-3.3.15…warp-3.3.16 (note the 3 dots!) does the trick.
Custom monad with servant and throwing errors
In the past I've always used scotty when writing web services. This was mostly due to laziness, I found working out how to use scotty a lot easier than servant, so basically I was being lazy. Fairly quickly I bumped into some limitations in scotty, but at first the workarounds didn't add too much complexity and were acceptable. A few weeks ago they started weighing on me though and I decided to look into servant and since I really liked what I found I've started moving all projects to use servant.
In several of the projects I've used tagless final style and defined a type
based on ReaderT holding configuration over IO, that is something like
newtype AppM a = AppM {unAppM : ReaderT Config IO a} deriving ( Functor, Applicative, Monad, MonadIO, MonadReader Config ) runAppM :: AppM a -> Config -> IO a runAppM app = runReaderT (unAppM app)
I found that servant is very well suited to this style through hoistServer and
there are several examples on how to use it with a ReaderT-based type like
above. The first one I found is in the servant cookbook. However, as I realised
a bit later, using a simple type like this doesn't make it easy to trigger
responses with status other than 200 OK. When I looked at the definition of
the type for writing handlers that ships with servant, Handler, I decided to
try to use the following type in my service
newtype AppM a = AppM {unAppM : ReaderT Config (ExceptT ServerError IO) a} deriving ( Functor, Applicative, Monad, MonadIO, MonadReader Config ) runAppM :: AppM a -> Config -> IO (Either ServerError a) runAppM app = runExceptT . runReaderT (unAppM app)
The natural transformation required by hoistServer can then be written like
nt :: AppM a -> Handler a nt x = liftIO (runAppM x cfg) >>= \case Right v -> pure v Left err -> throwError err
I particularly like how clearly this suggests a way to add custom errors if I want that.
- Swap out
ServerErrorfor my custom error type inAppM. - Write a function to transform my custom error type into a
ServerError,transformCustomError :: CustomError -> ServerError. - use
throwError $ transformCustomError errin theLeftbranch ofnt.
A slight complication with MonadUnliftIO
I was using unliftio in my service, and as long as I based my monad stack only
on ReaderT that worked fine. I even got the MonadUnliftIO instance for free
through automatic deriving. ExceptT isn't a stateless monad though, so using
unliftio is out of the question, instead I had to switch to MonadBaseControl
and the packages that work with it. Defining and instance of MonadBaseControl
looked a bit daunting, but luckily Handler has an instance of it that I used
as inspiration.
First off MonadBaseControl requires the type to also be an instance of
MonadBase. There's an explicit implementation for Handler, but I found that
it can be derived automatically, so I took the lazy route.
The instance of MonadBaseControl for AppM ended up looking like this
instance MonadBaseControl IO AppM where type StM AppM a = Either ServerError a liftBaseWith f = AppM (liftBaseWith (\g -> f (g . unAppM))) restoreM = AppM . restoreM
I can't claim to really understand what's going on in that definition, but I have Alexis King's article on Demystifying MonadBaseControl on my list of things to read.