Search code examples
haskellservant

How to receive POST with Reqbody in servant


Below code is supposed to be able to send a json body. But I always get a error with the following request:

curl -X POST -i http://localhost:8080/comtrade --data 'name=nut&age=12'

The error message is:

Status Code: 405 Method Not Allowed
content-type: text/plain
date: Fri, 12 Mar 2021 18:49:04 GMT
server: Warp/3.3.14
transfer-encoding: chunked
data User = User {
    age :: Int,
    name :: String
} deriving Generic

instance FromJSON User where
  parseJSON = withObject "User" parseUser

parseUser :: Object -> Parser User
parseUser o = do
  n <- (o .: "name")
  a <- (o .: "age")
  return (User a n) 

instance ToJSON  User where
  toJSON user = object 
    [ "age" .= age user
    , "name" .= name user
    ]

type ComTradeAPI =
  "comtrade" :> ReqBody '[JSON] User :> Post '[JSON] Int
  :<|> "test" :> Get '[JSON] User
  :<|> Raw

myServer :: Server ComTradeAPI
myServer = getUser
           :<|> test
           :<|> serveDirectoryWebApp "site"
    where
      test :: Handler User
      test = return (User 12 "nut")
      getUser :: User -> Handler Int
      getUser usr = return 12

main :: IO ()
main = openBrowser "http://localhost:8080/index.html"
    >> run 8080 (serve (Proxy :: Proxy ComTradeAPI) myServer)

Could anyone tell me how to make servant-server receive POST messages?


Solution

  • As Fyodor Soikin points out in the comment, the cURL example in the OP doesn't post JSON, but URL-encoded data. You can see this if you use the -v (verbose) option for cURL instead of -i:

    $ curl -v http://localhost:8080/comtrade -d "{ \"name\": \"nut\", \"age\": 12 }"
    *   Trying ::1:8080...
    * TCP_NODELAY set
    *   Trying 127.0.0.1:8080...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 8080 (#0)
    > POST /comtrade HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.67.0
    > Accept: */*
    > Content-Length: 28
    > Content-Type: application/x-www-form-urlencoded
    >
    * upload completely sent off: 28 out of 28 bytes
    * Mark bundle as not supporting multiuse
    < HTTP/1.1 405 Method Not Allowed
    < Transfer-Encoding: chunked
    < Date: Sat, 13 Mar 2021 12:52:59 GMT
    < Server: Warp/3.2.28
    < Content-Type: text/plain
    <
    Only GET or HEAD is supported
    

    Notice that Content-Type is application/x-www-form-urlencoded.

    The ReqBody '[JSON] User type declares that the API expects the body as JSON. The first thing you need to do, then, is to post JSON instead of URL-encoded data.

    That, in itself, is, however, not enough:

    $ curl -v http://localhost:8080/comtrade -d "{ \"name\": \"nut\", \"age\": 12 }"
    *   Trying ::1:8080...
    * TCP_NODELAY set
    *   Trying 127.0.0.1:8080...
    * TCP_NODELAY set
    * Connected to localhost (127.0.0.1) port 8080 (#0)
    > POST /comtrade HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.67.0
    > Accept: */*
    > Content-Length: 28
    > Content-Type: application/x-www-form-urlencoded
    >
    * upload completely sent off: 28 out of 28 bytes
    * Mark bundle as not supporting multiuse
    < HTTP/1.1 405 Method Not Allowed
    < Transfer-Encoding: chunked
    < Date: Sat, 13 Mar 2021 12:56:42 GMT
    < Server: Warp/3.2.28
    < Content-Type: text/plain
    <
    Only GET or HEAD is supported
    

    Notice that cURL still defaults the Content-Type to application/x-www-form-urlencoded. Since the API is declared to receive JSON, you must explicitly tell it that here comes JSON:

    $ curl -i http://localhost:8080/comtrade -H "Content-Type: application/json" -d "{ \"name\": \"nut\", \"age\": 12 }"
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    Date: Sat, 13 Mar 2021 12:58:09 GMT
    Server: Warp/3.2.28
    Content-Type: application/json;charset=utf-8
    
    12
    

    As far as I can tell, there's nothing wrong with the Haskell code. It's a question of using the HTTP protocol correctly.