Search code examples
haskellservant

Answer a request with 200 or 404 based on the content of `Maybe` using Servant


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!


Solution

  • 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