I'm currently trying to implement a simple web server with servant. At the moment, I have a IO (Maybe String)
that I want to expose via a GET endpoint (this might be a database lookup that may or may not return a result, hence IO
and Maybe
). If the Maybe
contains a value, the response should contain this value with a 200 OK response status. If the Maybe
is Nothing
, a 404 Not Found should be returned.
So far I was following the tutorial, which also describes handling errors using throwError
. However, I haven't managed to get it to compile. What I have is the following code:
type MaybeAPI = "maybe" :> Get '[ JSON] String
server :: Server MaybeAPI
server = stringHandler
maybeAPI :: Proxy MaybeAPI
maybeAPI = Proxy
app :: Application
app = serve maybeAPI server
stringHandler :: Handler String
stringHandler = liftIO $ fmap (\s -> (fromMaybe (throwError err404) s)) ioMaybeString
ioMaybeString :: IO (Maybe String)
ioMaybeString = return $ Just "foo"
runServer :: IO ()
runServer = run 8081 app
I know this is probably more verbose than it needs to be, but I guess it can be simplified as soon as it is working. The problem is the stringHandler
, for which the compilation fails with:
No instance for (MonadError ServerError []) arising from a use of ‘throwError’
So my question is: Is this the way to implement such an endpoint in Servant? If so, how can I fix the implementation? My Haskell knowledge is rather limited and I never used throwError
before, so it's entirely possible that I'm missing something here. Any help is appreciated!
As I mentioned in my comment, the problem is that in the offending line:
stringHandler :: Handler String
stringHandler = liftIO $ fmap (\s -> (fromMaybe (throwError err404) s)) ioMaybeString
s
is a Maybe String
, so to use it as the second argument to fromMaybe
, the first argument must be a String
- and throwError
is never going to produce a string.
Although you talked about your code perhaps being too verbose and you would look at simplifying it later, I think part of the problem here is that in this particular handler you are trying to be too concise. Let's try to write this in a more basic, pseudo-imperative style. Since Handler
is a monad, we can write this in a do
block which checks the value of s
and takes the appropriate action:
stringHandler :: Handler String
stringHandler = do
s <- liftIO ioMaybeString
case s of
Just str -> return str
Nothing -> throwError err404
Note that throwError
can produce a value of type Handler a
for any type it needs to be, which in this case is String
. And that we have to use liftIO
on ioMaybeString
to lift it into the Handler
monad, else this won't typecheck.
I can understand why you might have thought fromMaybe
was a good fit here, but fundamentally it isn't - the reason being that it's a "pure" function, that doesn't involve IO at all, whereas when you're talking about throwing server errors then you are absolutely unavoidably doing IO. These things essentially can't mix within a single function. (Which makes the fmap
inappropriate too - that can certainly be used to "lift" a pure computation to work on IO actions, but here, as I've said, the computation you needed fundamentally isn't pure.)
And if you want to make the stringHandler
function above more concise, while I don't think it's really an improvement, you could still use >>=
explicitly instead of the do
block, without making the code too unreadable:
stringHandler = liftIO ioMaybeString >>= f
where f (Just str) = return str
f Nothing = throwError err404