@@ -9,7 +9,7 @@ module Servant.Server.Internal where
99
1010import Control.Applicative ((<$>))
1111import Control.Monad.Trans.Either (EitherT, runEitherT)
12−import Data.Aeson (ToJSON, FromJSON, encode, eitherDecode')
12+import Data.Aeson (ToJSON)
1313import qualified Data.ByteString as B
1414import qualified Data.ByteString.Lazy as BL
1515import Data.IORef (newIORef, readIORef, writeIORef)
@@ -24,12 +24,18 @@ import qualified Data.Text as T
2424import Data.Typeable
2525import GHC.TypeLits (KnownSymbol, symbolVal)
2626import Network.HTTP.Types hiding (Header)
27−import Network.Wai (Response, Request, ResponseReceived, Application, pathInfo, requestBody,
28− strictRequestBody, lazyRequestBody, requestHeaders, requestMethod,
27+import Network.Wai ( Response, Request, ResponseReceived, Application
28+ , pathInfo, requestBody, strictRequestBody
29+ , lazyRequestBody, requestHeaders, requestMethod,
2930 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(..),)
3136import Servant.Common.Text (FromText, fromText)
3237
38+
3339data ReqBodyState = Uncalled
3440 | Called !B.ByteString
3541 | Done !B.ByteString
@@ -66,39 +72,33 @@ toApplication ra request respond = do
6672 respond $ responseLBS methodNotAllowed405 [] "method not allowed"
6773 routingRespond (Left (InvalidBody err)) =
6874 respond $ responseLBS badRequest400 [] $ fromString $ "Invalid JSON in request body: " ++ err
75+ routingRespond (Left UnsupportedMediaType) =
76+ respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
6977 routingRespond (Left (HttpError status body)) =
7078 respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
7179 routingRespond (Right response) =
7280 respond response
7381
82+-- Note that the ordering of the constructors has great significance! It
83+-- determines the Ord instance and, consequently, the monoid instance.
7484-- * Route mismatch
7585data RouteMismatch =
7686 NotFound -- ^ the usual "not found" error
7787 | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
88+ | UnsupportedMediaType -- ^ request body has unsupported media type
7889 | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
7990 | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
80− deriving (Eq, Show)
91+ deriving (Eq, Ord, Show)
8192
82−-- |
83−-- @
84−-- > mempty = NotFound
85−-- >
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
92−-- @
9393instance Monoid RouteMismatch where
9494 mempty = NotFound
95+ -- The following isn't great, since it picks @InvalidBody@ based on
96+ -- alphabetical ordering, but any choice would be arbitrary.
97+ --
98+ -- "As one judge said to the other, 'Be just and if you can't be just, be
99+ -- arbitrary'" -- William Burroughs
100+ mappend = max
95101
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
102102
103103-- | A wrapper around @'Either' 'RouteMismatch' a@.
104104newtype RouteResult a =
@@ -171,8 +171,8 @@ class HasServer layout where
171171-- represented by @a@ and if it fails tries @b@. You must provide a request
172172-- handler for each route.
173173--
174−-- > type MyApi = "books" :> Get [Book] -- GET /books
175−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
174+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
175+-- > :<|> "books" :> ReqBody Book :> Post '[JSON] Book -- POST /books
176176-- >
177177-- > server :: Server MyApi
178178-- > server = listAllBooks :<|> postBook
@@ -203,7 +203,7 @@ captured _ = fromText
203203--
204204-- Example:
205205--
206−-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
206+-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
207207-- >
208208-- > server :: Server MyApi
209209-- > server = getBook
@@ -225,7 +225,7 @@ instance (KnownSymbol capture, FromText a, HasServer sublayout)
225225 _ -> respond $ failWith NotFound
226226
227227 where captureProxy = Proxy :: Proxy (Capture capture a)
228−
228+
229229
230230-- | If you have a 'Delete' endpoint in your API,
231231-- the handler for this endpoint is meant to delete
@@ -261,17 +261,24 @@ instance HasServer Delete where
261261-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
262262-- to quickly fail if some conditions are not met.
263263--
264−-- If successfully returning a value, we just require that its type has
265−-- a 'ToJSON' instance and servant takes care of encoding it for you,
266−-- yielding status code 200 along the way.
267−instance ToJSON result => HasServer (Get result) where
268− type Server (Get result) = EitherT (Int, String) IO result
264+-- If successfully returning a value, we use the type-level list, combined
265+-- with the request's @Accept@ header, to encode the value for you
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
268+-- list.
269+instance ( AllCTRender ctypes a
270+ ) => HasServer (Get ctypes a) where
271+ type Server (Get ctypes a) = EitherT (Int, String) IO a
269272 route Proxy action request respond
270273 | pathIsEmpty request && requestMethod request == methodGet = do
271274 e <- runEitherT action
272275 respond . succeedWith $ case e of
273− Right output ->
274− responseLBS ok200 [("Content-Type", "application/json")] (encode 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") [] ""
280+ Just (contentT, body) -> responseLBS ok200 [ ("Content-Type"
281+ , cs contentT)] body
275282 Left (status, message) ->
276283 responseLBS (mkStatus status (cs message)) [] (cs message)
277284 | pathIsEmpty request && requestMethod request /= methodGet =
@@ -292,7 +299,7 @@ instance ToJSON result => HasServer (Get result) where
292299-- > deriving (Eq, Show, FromText, ToText)
293300-- >
294301-- > -- 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
296303-- >
297304-- > server :: Server MyApi
298305-- > server = viewReferer
@@ -318,18 +325,25 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
318325-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
319326-- to quickly fail if some conditions are not met.
320327--
321−-- If successfully returning a value, we just require that its type has
322−-- a 'ToJSON' instance and servant takes care of encoding it for you,
323−-- yielding status code 201 along the way.
324−instance ToJSON a => HasServer (Post a) where
325− type Server (Post a) = EitherT (Int, String) IO a
328+-- If successfully returning a value, we use the type-level list, combined
329+-- with the request's @Accept@ header, to encode the value for you
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
332+-- list.
333+instance ( AllCTRender ctypes a
334+ ) => HasServer (Post ctypes a) where
335+ type Server (Post ctypes a) = EitherT (Int, String) IO a
326336
327337 route Proxy action request respond
328338 | pathIsEmpty request && requestMethod request == methodPost = do
329339 e <- runEitherT action
330340 respond . succeedWith $ case e of
331− Right out ->
332− responseLBS status201 [("Content-Type", "application/json")] (encode 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 "") [] ""
345+ Just (contentT, body) -> responseLBS status201 [ ("Content-Type"
346+ , cs contentT)] body
333347 Left (status, message) ->
334348 responseLBS (mkStatus status (cs message)) [] (cs message)
335349 | pathIsEmpty request && requestMethod request /= methodPost =
@@ -344,18 +358,25 @@ instance ToJSON a => HasServer (Post a) where
344358-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
345359-- to quickly fail if some conditions are not met.
346360--
347−-- If successfully returning a value, we just require that its type has
348−-- a 'ToJSON' instance and servant takes care of encoding it for you,
349−-- yielding status code 200 along the way.
350−instance ToJSON a => HasServer (Put a) where
351− type Server (Put a) = EitherT (Int, String) IO a
361+-- If successfully returning a value, we use the type-level list, combined
362+-- with the request's @Accept@ header, to encode the value for you
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
365+-- list.
366+instance ( AllCTRender ctypes a
367+ ) => HasServer (Put ctypes a) where
368+ type Server (Put ctypes a) = EitherT (Int, String) IO a
352369
353370 route Proxy action request respond
354371 | pathIsEmpty request && requestMethod request == methodPut = do
355372 e <- runEitherT action
356373 respond . succeedWith $ case e of
357− Right out ->
358− responseLBS ok200 [("Content-Type", "application/json")] (encode out)
374+ Right output -> do
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
359380 Left (status, message) ->
360381 responseLBS (mkStatus status (cs message)) [] (cs message)
361382 | pathIsEmpty request && requestMethod request /= methodPut =
@@ -374,15 +395,22 @@ instance ToJSON a => HasServer (Put a) where
374395-- If successfully returning a value, we just require that its type has
375396-- a 'ToJSON' instance and servant takes care of encoding it for you,
376397-- yielding status code 201 along the way.
377−instance (Typeable a, ToJSON a) => HasServer (Patch a) where
378− type Server (Patch a) = EitherT (Int, String) IO a
398+instance ( AllCTRender ctypes a
399+ , Typeable a
400+ , ToJSON a) => HasServer (Patch ctypes a) where
401+ type Server (Patch ctypes a) = EitherT (Int, String) IO a
379402
380403 route Proxy action request respond
381404 | pathIsEmpty request && requestMethod request == methodPost = do
382405 e <- runEitherT action
383406 respond . succeedWith $ case e of
384407 Right out -> case cast out of
385− Nothing -> responseLBS status200 [("Content-Type", "application/json")] (encode out)
408+ Nothing -> do
409+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
410+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) out of
411+ Nothing -> responseLBS (mkStatus 406 "") [] ""
412+ Just (contentT, body) -> responseLBS status200 [ ("Content-Type"
413+ , cs contentT)] body
386414 Just () -> responseLBS status204 [] ""
387415 Left (status, message) ->
388416 responseLBS (mkStatus status (cs message)) [] (cs message)
@@ -404,7 +432,7 @@ instance (Typeable a, ToJSON a) => HasServer (Patch a) where
404432--
405433-- Example:
406434--
407−-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
435+-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
408436-- >
409437-- > server :: Server MyApi
410438-- > server = getBooksBy
@@ -443,7 +471,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
443471--
444472-- Example:
445473--
446−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
474+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
447475-- >
448476-- > server :: Server MyApi
449477-- > server = getBooksBy
@@ -476,7 +504,7 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
476504--
477505-- Example:
478506--
479−-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
507+-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
480508-- >
481509-- > server :: Server MyApi
482510-- > server = getBooks
@@ -635,30 +663,42 @@ instance HasServer Raw where
635663-- | If you use 'ReqBody' in one of the endpoints for your API,
636664-- this automatically requires your server-side handler to be a function
637665-- 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@.
638669-- This lets servant worry about extracting it from the request and turning
639670-- it into a value of the type you specify.
640671--
672+--
641673-- All it asks is for a 'FromJSON' instance.
642674--
643675-- Example:
644676--
645−-- > type MyApi = "books" :> ReqBody Book :> Post Book
677+-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
646678-- >
647679-- > server :: Server MyApi
648680-- > server = postBook
649681-- > where postBook :: Book -> EitherT (Int, String) IO Book
650682-- > postBook book = ...insert into your db...
651−instance (FromJSON a, HasServer sublayout)
652− => HasServer (ReqBody a :> sublayout) where
683+instance ( AllCTUnrender list a, HasServer sublayout
684+ ) => HasServer (ReqBody list a :> sublayout) where
653685
654− type Server (ReqBody a :> sublayout) =
686+ type Server (ReqBody list a :> sublayout) =
655687 a -> Server sublayout
656688
657689 route Proxy subserver request respond = do
658− mrqbody <- eitherDecode' <$> lazyRequestBody request
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)
697+ <$> lazyRequestBody request
659698 case mrqbody of
660− Left e -> respond . failWith $ InvalidBody e
661− Right v -> route (Proxy :: Proxy sublayout) (subserver v) request respond
699+ Nothing -> respond . failWith $ UnsupportedMediaType
700+ Just (Left e) -> respond . failWith $ InvalidBody e
701+ Just (Right v) -> route (Proxy :: Proxy sublayout) (subserver v) request respond
662702
663703-- | Make sure the incoming request starts with @"/path"@, strip it and
664704-- pass the rest of the request path to @sublayout@.