I'm going through the servant tutorial here: https://docs.servant.dev/en/stable/tutorial/Server.html#from-combinators-to-handler-arguments
Which roughly has code like below:
app1 :: Application
app1 = serve (Proxy :: Proxy API) server3
main' :: IO ()
main' = run 8081 app1
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
data Position = Position
{ xCoord :: Int
, yCoord :: Int
} deriving Generic
instance ToJSON Position
newtype HelloMessage = HelloMessage { msg :: String }
deriving Generic
instance ToJSON HelloMessage
data ClientInfo = ClientInfo
{ clientName :: String
, clientEmail :: String
, clientAge :: Int
, clientInterestedIn :: [String]
} deriving Generic
instance FromJSON ClientInfo
instance ToJSON ClientInfo
data Email = Email
{ from :: String
, to :: String
, subject :: String
, body :: String
} deriving Generic
instance ToJSON Email
emailForClient :: ClientInfo -> Email
emailForClient c = Email from' to' subject' body'
where from' = "[email protected]"
to' = clientEmail c
subject' = "Hey " ++ clientName c ++ ", we miss you!"
body' = "Hi " ++ clientName c ++ ",\n\n"
++ "Since you've recently turned " ++ show (clientAge c)
++ ", have you checked out our latest "
++ intercalate ", " (clientInterestedIn c)
++ " products? Give us a visit!"
server3 :: Server API
server3 = position
:<|> hello
:<|> marketing
where position :: Int -> Int -> Handler Position
position x y = return (Position x y)
hello :: Maybe String -> Handler HelloMessage
hello mname = return . HelloMessage $ case mname of
Nothing -> "Hello, anonymous coward"
Just n -> "Hello, " ++ n
marketing :: ClientInfo -> Handler Email
marketing clientinfo = return (emailForClient clientinfo)
Given a simple input works great:
curl http://localhost:8081/position/1/2
{"yCoord":2,"xCoord":1}
Given an simple invalid input works not so great (replacing 2
with a string test
:
curl -v http://localhost:8081/position/1/test
* Trying ::1:8081...
* TCP_NODELAY set
* connect to ::1 port 8081 failed: Connection refused
* Trying 127.0.0.1:8081...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /position/1/test HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Transfer-Encoding: chunked
< Date: Mon, 16 Dec 2019 18:01:00 GMT
< Server: Warp/3.2.28
<
* Connection #0 to host localhost left intact
How can I add error handling / validation to the response in the second case? So ideally rather than just a blank HTTP 400, it responds with "error expecting int, got string". Would this be to do with the ExceptT
functionality? Is there a simple example of this anywhere?
In general, I think, this is not worth doing, because in general, there may be very complicated combinations of routes, yielding very unintuitive error messages. For example, consider the following API:
type API =
"position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
:<|> "position" :> "foo" :> "test" :> Get '[JSON] Position
This API admits /position/1/2
and /position/foo/test
, but rejects /position/1/test
, and there is no sane error message you can generate for this last case. It would have to be something like "expecting either an Int at the very end, or a "foo" in the second-from-end position, but got "test" at the end and "1" at second-from-end". Not helpful for the consumer.
But if you just want to handle this one particular path, you can just create a second "catch-all" route that would return an appropriately formatted message:
type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
:<|> "position" :> Capture "x" Text :> Capture "y" Text :> Get '[JSON] ()
:<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
...
server3 = position
:<|> badPosition
:<|> hello
:<|> marketing
where
...
badPosition x y =
throwError $ err400 { errBody = "Expected ints, got '" <> x <> "' and '" <> y <> "'" }