@@ -9,7 +9,7 @@ module Servant.Server.Internal where
10import Control.Applicative ((<$>))
10import Control.Applicative ((<$>))
11import Control.Monad.Trans.Either (EitherT, runEitherT)
11import Control.Monad.Trans.Either (EitherT, runEitherT)
12−import Data.Aeson (ToJSON, FromJSON, encode, eitherDecode')
12+import Data.Aeson (ToJSON)
13import qualified Data.ByteString as B
13import qualified Data.ByteString as B
14import qualified Data.ByteString.Lazy as BL
14import qualified Data.ByteString.Lazy as BL
15import Data.IORef (newIORef, readIORef, writeIORef)
15import Data.IORef (newIORef, readIORef, writeIORef)
@@ -24,12 +24,18 @@ import qualified Data.Text as T
24import Data.Typeable
24import Data.Typeable
25import GHC.TypeLits (KnownSymbol, symbolVal)
25import GHC.TypeLits (KnownSymbol, symbolVal)
26import Network.HTTP.Types hiding (Header)
26import Network.HTTP.Types hiding (Header)
27−import Network.Wai (Response, Request, ResponseReceived, Application, pathInfo, requestBody,
27+import Network.Wai ( Response, Request, ResponseReceived, Application
28+ , pathInfo, requestBody, strictRequestBody
28− strictRequestBody, lazyRequestBody, requestHeaders, requestMethod,
29+ , lazyRequestBody, requestHeaders, requestMethod,
29 rawQueryString, responseLBS)
30 rawQueryString, responseLBS)
30−import Servant.API (QueryParams, QueryParam, QueryFlag, MatrixParams, MatrixParam, MatrixFlag, ReqBody, Header, Capture, Get, Delete, Put, Post, Patch, Raw, (:>), (:<|>)(..))
31+import Servant.API ( QueryParams, QueryParam, QueryFlag, ReqBody, Header
32+ , MatrixParams, MatrixParam, MatrixFlag
33+ , Capture, Get, Delete, Put, Post, Patch, Raw, (:>), (:<|>)(..))
34+import Servant.API.ContentTypes ( AllCTRender(..), AcceptHeader(..)
35+ , AllCTUnrender(..),)
31import Servant.Common.Text (FromText, fromText)
36import Servant.Common.Text (FromText, fromText)
33data ReqBodyState = Uncalled
39data ReqBodyState = Uncalled
34 | Called !B.ByteString
40 | Called !B.ByteString
35 | Done !B.ByteString
41 | Done !B.ByteString
@@ -66,39 +72,33 @@ toApplication ra request respond = do
66 respond $ responseLBS methodNotAllowed405 [] "method not allowed"
72 respond $ responseLBS methodNotAllowed405 [] "method not allowed"
67 routingRespond (Left (InvalidBody err)) =
73 routingRespond (Left (InvalidBody err)) =
68 respond $ responseLBS badRequest400 [] $ fromString $ "Invalid JSON in request body: " ++ err
74 respond $ responseLBS badRequest400 [] $ fromString $ "Invalid JSON in request body: " ++ err
75+ routingRespond (Left UnsupportedMediaType) =
76+ respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
69 routingRespond (Left (HttpError status body)) =
77 routingRespond (Left (HttpError status body)) =
70 respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
78 respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
71 routingRespond (Right response) =
79 routingRespond (Right response) =
72 respond response
80 respond response
82+-- Note that the ordering of the constructors has great significance! It
83+-- determines the Ord instance and, consequently, the monoid instance.
74-- * Route mismatch
84-- * Route mismatch
75data RouteMismatch =
85data RouteMismatch =
76 NotFound -- ^ the usual "not found" error
86 NotFound -- ^ the usual "not found" error
77 | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
87 | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
88+ | UnsupportedMediaType -- ^ request body has unsupported media type
78 | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
89 | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
79 | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
90 | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
80− deriving (Eq, Show)
91+ deriving (Eq, Ord, Show)
84−-- > mempty = NotFound
86−-- > _ `mappend` HttpError s b = HttpError s b
87−-- > HttpError s b `mappend` _ = HttpError s b
88−-- > NotFound `mappend` x = x
89−-- > WrongMethod `mappend` InvalidBody s = InvalidBody s
90−-- > WrongMethod `mappend` _ = WrongMethod
91−-- > InvalidBody s `mappend` _ = InvalidBody s
93instance Monoid RouteMismatch where
93instance Monoid RouteMismatch where
94 mempty = NotFound
94 mempty = NotFound
95+ -- The following isn't great, since it picks @InvalidBody@ based on
96+ -- alphabetical ordering, but any choice would be arbitrary.
98+ -- "As one judge said to the other, 'Be just and if you can't be just, be
99+ -- arbitrary'" -- William Burroughs
96− _ `mappend` HttpError s b = HttpError s b
97− HttpError s b `mappend` _ = HttpError s b
98− NotFound `mappend` x = x
99− WrongMethod `mappend` InvalidBody s = InvalidBody s
100− WrongMethod `mappend` _ = WrongMethod
101− InvalidBody s `mappend` _ = InvalidBody s
103-- | A wrapper around @'Either' 'RouteMismatch' a@.
103-- | A wrapper around @'Either' 'RouteMismatch' a@.
104newtype RouteResult a =
104newtype RouteResult a =
@@ -171,8 +171,8 @@ class HasServer layout where
171-- represented by @a@ and if it fails tries @b@. You must provide a request
171-- represented by @a@ and if it fails tries @b@. You must provide a request
172-- handler for each route.
172-- handler for each route.
174−-- > type MyApi = "books" :> Get [Book] -- GET /books
174+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
175−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
175+-- > :<|> "books" :> ReqBody Book :> Post '[JSON] Book -- POST /books
177-- > server :: Server MyApi
177-- > server :: Server MyApi
178-- > server = listAllBooks :<|> postBook
178-- > server = listAllBooks :<|> postBook
@@ -203,7 +203,7 @@ captured _ = fromText
204-- Example:
204-- Example:
206−-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
206+-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
208-- > server :: Server MyApi
208-- > server :: Server MyApi
209-- > server = getBook
209-- > server = getBook
@@ -225,7 +225,7 @@ instance (KnownSymbol capture, FromText a, HasServer sublayout)
225 _ -> respond $ failWith NotFound
225 _ -> respond $ failWith NotFound
227 where captureProxy = Proxy :: Proxy (Capture capture a)
227 where captureProxy = Proxy :: Proxy (Capture capture a)
230-- | If you have a 'Delete' endpoint in your API,
230-- | If you have a 'Delete' endpoint in your API,
231-- the handler for this endpoint is meant to delete
231-- the handler for this endpoint is meant to delete
@@ -261,17 +261,24 @@ instance HasServer Delete where
261-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
261-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
262-- to quickly fail if some conditions are not met.
262-- to quickly fail if some conditions are not met.
264−-- If successfully returning a value, we just require that its type has
264+-- If successfully returning a value, we use the type-level list, combined
265−-- a 'ToJSON' instance and servant takes care of encoding it for you,
265+-- with the request's @Accept@ header, to encode the value for you
266−-- yielding status code 200 along the way.
266+-- (returning a status code of 200). If there was no @Accept@ header or it
267+-- was @*/*@, we return encode using the first @Content-Type@ type on the
269+instance ( AllCTRender ctypes a
267−instance ToJSON result => HasServer (Get result) where
270+ ) => HasServer (Get ctypes a) where
268− type Server (Get result) = EitherT (Int, String) IO result
271+ type Server (Get ctypes a) = EitherT (Int, String) IO a
269 route Proxy action request respond
272 route Proxy action request respond
270 | pathIsEmpty request && requestMethod request == methodGet = do
273 | pathIsEmpty request && requestMethod request == methodGet = do
271 e <- runEitherT action
274 e <- runEitherT action
272 respond . succeedWith $ case e of
275 respond . succeedWith $ case e of
273− Right output ->
276+ Right output -> do
277+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
278+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
279+ Nothing -> responseLBS (mkStatus 406 "Not Acceptable") [] ""
274− responseLBS ok200 [("Content-Type", "application/json")] (encode output)
280+ Just (contentT, body) -> responseLBS ok200 [ ("Content-Type"
281+ , cs contentT)] body
275 Left (status, message) ->
282 Left (status, message) ->
276 responseLBS (mkStatus status (cs message)) [] (cs message)
283 responseLBS (mkStatus status (cs message)) [] (cs message)
277 | pathIsEmpty request && requestMethod request /= methodGet =
284 | pathIsEmpty request && requestMethod request /= methodGet =
@@ -292,7 +299,7 @@ instance ToJSON result => HasServer (Get result) where
292-- > deriving (Eq, Show, FromText, ToText)
299-- > deriving (Eq, Show, FromText, ToText)
294-- > -- GET /view-my-referer
301-- > -- GET /view-my-referer
295−-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
302+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get '[JSON] Referer
297-- > server :: Server MyApi
304-- > server :: Server MyApi
298-- > server = viewReferer
305-- > server = viewReferer
@@ -318,18 +325,25 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
318-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
325-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
319-- to quickly fail if some conditions are not met.
326-- to quickly fail if some conditions are not met.
321−-- If successfully returning a value, we just require that its type has
328+-- If successfully returning a value, we use the type-level list, combined
322−-- a 'ToJSON' instance and servant takes care of encoding it for you,
329+-- with the request's @Accept@ header, to encode the value for you
323−-- yielding status code 201 along the way.
330+-- (returning a status code of 201). If there was no @Accept@ header or it
331+-- was @*/*@, we return encode using the first @Content-Type@ type on the
333+instance ( AllCTRender ctypes a
324−instance ToJSON a => HasServer (Post a) where
334+ ) => HasServer (Post ctypes a) where
325− type Server (Post a) = EitherT (Int, String) IO a
335+ type Server (Post ctypes a) = EitherT (Int, String) IO a
327 route Proxy action request respond
337 route Proxy action request respond
328 | pathIsEmpty request && requestMethod request == methodPost = do
338 | pathIsEmpty request && requestMethod request == methodPost = do
329 e <- runEitherT action
339 e <- runEitherT action
330 respond . succeedWith $ case e of
340 respond . succeedWith $ case e of
331− Right out ->
341+ Right output -> do
342+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
343+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
344+ Nothing -> responseLBS (mkStatus 406 "") [] ""
332− responseLBS status201 [("Content-Type", "application/json")] (encode out)
345+ Just (contentT, body) -> responseLBS status201 [ ("Content-Type"
346+ , cs contentT)] body
333 Left (status, message) ->
347 Left (status, message) ->
334 responseLBS (mkStatus status (cs message)) [] (cs message)
348 responseLBS (mkStatus status (cs message)) [] (cs message)
335 | pathIsEmpty request && requestMethod request /= methodPost =
349 | pathIsEmpty request && requestMethod request /= methodPost =
@@ -344,18 +358,25 @@ instance ToJSON a => HasServer (Post a) where
344-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
358-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
345-- to quickly fail if some conditions are not met.
359-- to quickly fail if some conditions are not met.
347−-- If successfully returning a value, we just require that its type has
361+-- If successfully returning a value, we use the type-level list, combined
348−-- a 'ToJSON' instance and servant takes care of encoding it for you,
362+-- with the request's @Accept@ header, to encode the value for you
349−-- yielding status code 200 along the way.
363+-- (returning a status code of 201). If there was no @Accept@ header or it
364+-- was @*/*@, we return encode using the first @Content-Type@ type on the
366+instance ( AllCTRender ctypes a
350−instance ToJSON a => HasServer (Put a) where
367+ ) => HasServer (Put ctypes a) where
351− type Server (Put a) = EitherT (Int, String) IO a
368+ type Server (Put ctypes a) = EitherT (Int, String) IO a
353 route Proxy action request respond
370 route Proxy action request respond
354 | pathIsEmpty request && requestMethod request == methodPut = do
371 | pathIsEmpty request && requestMethod request == methodPut = do
355 e <- runEitherT action
372 e <- runEitherT action
356 respond . succeedWith $ case e of
373 respond . succeedWith $ case e of
357− Right out ->
374+ Right output -> do
358− responseLBS ok200 [("Content-Type", "application/json")] (encode out)
375+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
376+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
377+ Nothing -> responseLBS (mkStatus 406 "") [] ""
378+ Just (contentT, body) -> responseLBS status200 [ ("Content-Type"
379+ , cs contentT)] body
359 Left (status, message) ->
380 Left (status, message) ->
360 responseLBS (mkStatus status (cs message)) [] (cs message)
381 responseLBS (mkStatus status (cs message)) [] (cs message)
361 | pathIsEmpty request && requestMethod request /= methodPut =
382 | pathIsEmpty request && requestMethod request /= methodPut =
@@ -374,15 +395,22 @@ instance ToJSON a => HasServer (Put a) where
374-- If successfully returning a value, we just require that its type has
395-- If successfully returning a value, we just require that its type has
375-- a 'ToJSON' instance and servant takes care of encoding it for you,
396-- a 'ToJSON' instance and servant takes care of encoding it for you,
376-- yielding status code 201 along the way.
397-- yielding status code 201 along the way.
398+instance ( AllCTRender ctypes a
377−instance (Typeable a, ToJSON a) => HasServer (Patch a) where
400+ , ToJSON a) => HasServer (Patch ctypes a) where
378− type Server (Patch a) = EitherT (Int, String) IO a
401+ type Server (Patch ctypes a) = EitherT (Int, String) IO a
380 route Proxy action request respond
403 route Proxy action request respond
381 | pathIsEmpty request && requestMethod request == methodPost = do
404 | pathIsEmpty request && requestMethod request == methodPost = do
382 e <- runEitherT action
405 e <- runEitherT action
383 respond . succeedWith $ case e of
406 respond . succeedWith $ case e of
384 Right out -> case cast out of
407 Right out -> case cast out of
409+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
410+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) out of
411+ Nothing -> responseLBS (mkStatus 406 "") [] ""
385− Nothing -> responseLBS status200 [("Content-Type", "application/json")] (encode out)
412+ Just (contentT, body) -> responseLBS status200 [ ("Content-Type"
413+ , cs contentT)] body
386 Just () -> responseLBS status204 [] ""
414 Just () -> responseLBS status204 [] ""
387 Left (status, message) ->
415 Left (status, message) ->
388 responseLBS (mkStatus status (cs message)) [] (cs message)
416 responseLBS (mkStatus status (cs message)) [] (cs message)
@@ -404,7 +432,7 @@ instance (Typeable a, ToJSON a) => HasServer (Patch a) where
405-- Example:
433-- Example:
407−-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
435+-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
409-- > server :: Server MyApi
437-- > server :: Server MyApi
410-- > server = getBooksBy
438-- > server = getBooksBy
@@ -443,7 +471,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
444-- Example:
472-- Example:
446−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
474+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
448-- > server :: Server MyApi
476-- > server :: Server MyApi
449-- > server = getBooksBy
477-- > server = getBooksBy
@@ -476,7 +504,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
477-- Example:
505-- Example:
479−-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
507+-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
481-- > server :: Server MyApi
509-- > server :: Server MyApi
482-- > server = getBooks
510-- > server = getBooks
@@ -635,30 +663,42 @@ instance HasServer Raw where
635-- | If you use 'ReqBody' in one of the endpoints for your API,
663-- | If you use 'ReqBody' in one of the endpoints for your API,
636-- this automatically requires your server-side handler to be a function
664-- this automatically requires your server-side handler to be a function
637-- that takes an argument of the type specified by 'ReqBody'.
665-- that takes an argument of the type specified by 'ReqBody'.
666+-- The @Content-Type@ header is inspected, and the list provided is used to
667+-- attempt deserialization. If the request does not have a @Content-Type@
668+-- header, it is treated as @application/octet-stream@.
638-- This lets servant worry about extracting it from the request and turning
669-- This lets servant worry about extracting it from the request and turning
639-- it into a value of the type you specify.
670-- it into a value of the type you specify.
641-- All it asks is for a 'FromJSON' instance.
673-- All it asks is for a 'FromJSON' instance.
643-- Example:
675-- Example:
645−-- > type MyApi = "books" :> ReqBody Book :> Post Book
677+-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
647-- > server :: Server MyApi
679-- > server :: Server MyApi
648-- > server = postBook
680-- > server = postBook
649-- > where postBook :: Book -> EitherT (Int, String) IO Book
681-- > where postBook :: Book -> EitherT (Int, String) IO Book
650-- > postBook book = ...insert into your db...
682-- > postBook book = ...insert into your db...
651−instance (FromJSON a, HasServer sublayout)
683+instance ( AllCTUnrender list a, HasServer sublayout
652− => HasServer (ReqBody a :> sublayout) where
684+ ) => HasServer (ReqBody list a :> sublayout) where
654− type Server (ReqBody a :> sublayout) =
686+ type Server (ReqBody list a :> sublayout) =
655 a -> Server sublayout
687 a -> Server sublayout
657 route Proxy subserver request respond = do
689 route Proxy subserver request respond = do
690+ -- See HTTP RFC 2616, section 7.2.1
691+ -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
692+ -- See also "W3C Internet Media Type registration, consistency of use"
693+ -- http://www.w3.org/2001/tag/2002/0129-mime
694+ let contentTypeH = fromMaybe "application/octet-stream"
695+ $ lookup hContentType $ requestHeaders request
696+ mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
658− mrqbody <- eitherDecode' <$> lazyRequestBody request
697+ <$> lazyRequestBody request
659 case mrqbody of
698 case mrqbody of
699+ Nothing -> respond . failWith $ UnsupportedMediaType
660− Left e -> respond . failWith $ InvalidBody e
700+ Just (Left e) -> respond . failWith $ InvalidBody e
661− Right v -> route (Proxy :: Proxy sublayout) (subserver v) request respond
701+ Just (Right v) -> route (Proxy :: Proxy sublayout) (subserver v) request respond
663-- | Make sure the incoming request starts with @"/path"@, strip it and
703-- | Make sure the incoming request starts with @"/path"@, strip it and
664-- pass the rest of the request path to @sublayout@.
704-- pass the rest of the request path to @sublayout@.