packages feed

servant-server 0.15 → 0.16

raw patch · 16 files changed

+997/−900 lines, 16 filesdep ~QuickCheckdep ~hspecdep ~http-media

Dependency ranges changed: QuickCheck, hspec, http-media, network, network-uri, servant

Files

CHANGELOG.md view
@@ -1,6 +1,18 @@ [The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-server/CHANGELOG.md) [Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md) +0.16+----++- Rename `ServantError` to `ClientError`, `ServantErr` to `ServerError`+  [#1131](https://github.com/haskell-servant/pull/1131)+- *servant-server* Reorder HTTP failure code priorities+  [#1103](https://github.com/haskell-servant/servant/pull/1103)+- *servant-server* Re-organise internal modules+  [#1139](https://github.com/haskell-servant/servant/pull/1139)+- Allow `network-3.0`+  [#1107](https://github.com/haskell-servant/pull/1107)+ 0.15 ---- 
servant-server.cabal view
@@ -1,6 +1,6 @@ cabal-version:       >=1.10 name:                servant-server-version:             0.15+version:             0.16  synopsis:            A family of combinators for defining webservices APIs and serving them category:            Servant, Web@@ -21,13 +21,13 @@ license-file:        LICENSE author:              Servant Contributors maintainer:          haskell-servant-maintainers@googlegroups.com-copyright:           2014-2016 Zalora South East Asia Pte Ltd, 2016-2018 Servant Contributors+copyright:           2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors build-type:          Custom tested-with:   GHC ==8.0.2    || ==8.2.2    || ==8.4.4-   || ==8.6.2+   || ==8.6.3  extra-source-files:   CHANGELOG.md@@ -52,10 +52,13 @@     Servant.Server.Internal     Servant.Server.Internal.BasicAuth     Servant.Server.Internal.Context+    Servant.Server.Internal.Delayed+    Servant.Server.Internal.DelayedIO     Servant.Server.Internal.Handler     Servant.Server.Internal.Router+    Servant.Server.Internal.RouteResult     Servant.Server.Internal.RoutingApplication-    Servant.Server.Internal.ServantErr+    Servant.Server.Internal.ServerError     Servant.Server.StaticFiles    -- deprecated@@ -76,7 +79,7 @@   -- Servant dependencies   -- strict dependency as we re-export 'servant' things.   build-depends:-      servant             >= 0.15     && < 0.15.1+      servant             >= 0.16     && < 0.17.1     , http-api-data       >= 0.4      && < 0.4.1    -- Other dependencies: Lower bound around what is in the latest Stackage LTS.@@ -87,9 +90,9 @@     , exceptions          >= 0.10.0   && < 0.11     , http-media          >= 0.7.1.3  && < 0.8     , http-types          >= 0.12.2   && < 0.13-    , network-uri         >= 2.6.1.0  && < 2.7+    , network-uri         >= 2.6.1.0  && < 2.8     , monad-control       >= 1.0.2.3  && < 1.1-    , network             >= 2.8      && < 2.9+    , network             >= 2.8      && < 3.1     , string-conversions  >= 0.4.0.1  && < 0.5     , resourcet           >= 1.2.2    && < 1.3     , tagged              >= 0.8.6    && < 0.9@@ -160,7 +163,7 @@   build-depends:       aeson                >= 1.4.1.0  && < 1.5     , directory            >= 1.3.0.0  && < 1.4-    , hspec                >= 2.6.0    && < 2.7+    , hspec                >= 2.6.0    && < 2.8     , hspec-wai            >= 0.9.0    && < 0.10     , QuickCheck           >= 2.12.6.1 && < 2.13     , should-not-typecheck >= 2.1.0    && < 2.2@@ -168,7 +171,7 @@     , wai-extra            >= 3.0.24.3 && < 3.1    build-tool-depends:-    hspec-discover:hspec-discover >= 2.6.0 && <2.7+    hspec-discover:hspec-discover >= 2.6.0 && <2.8  test-suite doctests   build-depends:
src/Servant/Server.hs view
@@ -49,7 +49,7 @@   -- , mkAuthHandler      -- * Default error type-  , ServantErr(..)+  , ServerError(..)     -- ** 3XX   , err300   , err301
src/Servant/Server/Experimental/Auth.hs view
@@ -27,11 +27,9 @@                  ((:>)) import           Servant.API.Experimental.Auth import           Servant.Server.Internal-                 (HasContextEntry, HasServer (..), getContextEntry)-import           Servant.Server.Internal.Handler-                 (Handler, runHandler)-import           Servant.Server.Internal.RoutingApplication-                 (DelayedIO, addAuthCheck, delayedFailFatal, withRequest)+                 (DelayedIO, Handler, HasContextEntry, HasServer (..),+                 addAuthCheck, delayedFailFatal, getContextEntry, runHandler,+                 withRequest)  -- * General Auth 
src/Servant/Server/Internal.hs view
@@ -22,10 +22,13 @@   ( module Servant.Server.Internal   , module Servant.Server.Internal.BasicAuth   , module Servant.Server.Internal.Context+  , module Servant.Server.Internal.Delayed+  , module Servant.Server.Internal.DelayedIO   , module Servant.Server.Internal.Handler   , module Servant.Server.Internal.Router+  , module Servant.Server.Internal.RouteResult   , module Servant.Server.Internal.RoutingApplication-  , module Servant.Server.Internal.ServantErr+  , module Servant.Server.Internal.ServerError   ) where  import           Control.Monad@@ -88,10 +91,13 @@  import           Servant.Server.Internal.BasicAuth import           Servant.Server.Internal.Context+import           Servant.Server.Internal.Delayed+import           Servant.Server.Internal.DelayedIO import           Servant.Server.Internal.Handler import           Servant.Server.Internal.Router+import           Servant.Server.Internal.RouteResult import           Servant.Server.Internal.RoutingApplication-import           Servant.Server.Internal.ServantErr+import           Servant.Server.Internal.ServerError  #ifdef HAS_TYPE_ERROR import           GHC.TypeLits
src/Servant/Server/Internal/BasicAuth.hs view
@@ -9,7 +9,7 @@                  (guard) import           Control.Monad.Trans                  (liftIO)-import qualified Data.ByteString                            as BS+import qualified Data.ByteString                     as BS import           Data.ByteString.Base64                  (decodeLenient) import           Data.Monoid@@ -26,8 +26,8 @@  import           Servant.API.BasicAuth                  (BasicAuthData (BasicAuthData))-import           Servant.Server.Internal.RoutingApplication-import           Servant.Server.Internal.ServantErr+import           Servant.Server.Internal.DelayedIO+import           Servant.Server.Internal.ServerError  -- * Basic Auth 
+ src/Servant/Server/Internal/Delayed.hs view
@@ -0,0 +1,272 @@+{-# LANGUAGE CPP                   #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE TypeOperators         #-}+{-# LANGUAGE UndecidableInstances  #-}+module Servant.Server.Internal.Delayed where++import           Control.Monad.IO.Class+                 (MonadIO (..))+import           Control.Monad.Reader+                 (ask)+import           Control.Monad.Trans.Resource+                 (ResourceT, runResourceT)+import           Network.Wai+                 (Request, Response)++import           Servant.Server.Internal.DelayedIO+import           Servant.Server.Internal.Handler+import           Servant.Server.Internal.RouteResult+import           Servant.Server.Internal.ServerError++-- | A 'Delayed' is a representation of a handler with scheduled+-- delayed checks that can trigger errors.+--+-- Why would we want to delay checks?+--+-- There are two reasons:+--+-- 1. In a straight-forward implementation, the order in which we+-- perform checks will determine the error we generate. This is+-- because once an error occurs, we would abort and not perform+-- any subsequent checks, but rather return the current error.+--+-- This is not a necessity: we could continue doing other checks,+-- and choose the preferred error. However, that would in general+-- mean more checking, which leads us to the other reason.+--+-- 2. We really want to avoid doing certain checks too early. For+-- example, captures involve parsing, and are much more costly+-- than static route matches. In particular, if several paths+-- contain the "same" capture, we'd like as much as possible to+-- avoid trying the same parse many times. Also tricky is the+-- request body. Again, this involves parsing, but also, WAI makes+-- obtaining the request body a side-effecting operation. We+-- could/can work around this by manually caching the request body,+-- but we'd rather keep the number of times we actually try to+-- decode the request body to an absolute minimum.+--+-- We prefer to have the following relative priorities of error+-- codes:+--+-- @+-- 404+-- 405 (bad method)+-- 401 (unauthorized)+-- 415 (unsupported media type)+-- 406 (not acceptable)+-- 400 (bad request)+-- @+--+-- Therefore, while routing, we delay most checks so that they+-- will ultimately occur in the right order.+--+-- A 'Delayed' contains many delayed blocks of tests, and+-- the actual handler:+--+-- 1. Delayed captures. These can actually cause 404, and+-- while they're costly, they should be done first among the+-- delayed checks (at least as long as we do not decouple the+-- check order from the error reporting, see above). Delayed+-- captures can provide inputs to the actual handler.+--+-- 2. Method check(s). This can cause a 405. On success,+-- it does not provide an input for the handler. Method checks+-- are comparatively cheap.+--+-- 3. Authentication checks. This can cause 401.+--+-- 4. Accept and content type header checks. These checks+-- can cause 415 and 406 errors.+--+-- 5. Query parameter checks. They require parsing and can cause 400 if the+-- parsing fails. Query parameter checks provide inputs to the handler+--+-- 6. Header Checks. They also require parsing and can cause 400 if parsing fails.+--+-- 7. Body check. The request body check can cause 400.+--+data Delayed env c where+  Delayed :: { capturesD :: env -> DelayedIO captures+             , methodD   :: DelayedIO ()+             , authD     :: DelayedIO auth+             , acceptD   :: DelayedIO ()+             , contentD  :: DelayedIO contentType+             , paramsD   :: DelayedIO params+             , headersD  :: DelayedIO headers+             , bodyD     :: contentType -> DelayedIO body+             , serverD   :: captures+                         -> params+                         -> headers+                         -> auth+                         -> body+                         -> Request+                         -> RouteResult c+             } -> Delayed env c++instance Functor (Delayed env) where+  fmap f Delayed{..} =+    Delayed+      { serverD = \ c p h a b req -> f <$> serverD c p h a b req+      , ..+      } -- Note [Existential Record Update]++-- | A 'Delayed' without any stored checks.+emptyDelayed :: RouteResult a -> Delayed env a+emptyDelayed result =+  Delayed (const r) r r r r r r (const r) (\ _ _ _ _ _ _ -> result)+  where+    r = return ()++-- | Add a capture to the end of the capture block.+addCapture :: Delayed env (a -> b)+           -> (captured -> DelayedIO a)+           -> Delayed (captured, env) b+addCapture Delayed{..} new =+  Delayed+    { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt+    , serverD   = \ (x, v) p h a b req -> ($ v) <$> serverD x p h a b req+    , ..+    } -- Note [Existential Record Update]++-- | Add a parameter check to the end of the params block+addParameterCheck :: Delayed env (a -> b)+                  -> DelayedIO a+                  -> Delayed env b+addParameterCheck Delayed {..} new =+  Delayed+    { paramsD = (,) <$> paramsD <*> new+    , serverD = \c (p, pNew) h a b req -> ($ pNew) <$> serverD c p h a b req+    , ..+    }++-- | Add a parameter check to the end of the params block+addHeaderCheck :: Delayed env (a -> b)+               -> DelayedIO a+               -> Delayed env b+addHeaderCheck Delayed {..} new =+  Delayed+    { headersD = (,) <$> headersD <*> new+    , serverD = \c p (h, hNew) a b req -> ($ hNew) <$> serverD c p h a b req+    , ..+    }++-- | Add a method check to the end of the method block.+addMethodCheck :: Delayed env a+               -> DelayedIO ()+               -> Delayed env a+addMethodCheck Delayed{..} new =+  Delayed+    { methodD = methodD <* new+    , ..+    } -- Note [Existential Record Update]++-- | Add an auth check to the end of the auth block.+addAuthCheck :: Delayed env (a -> b)+             -> DelayedIO a+             -> Delayed env b+addAuthCheck Delayed{..} new =+  Delayed+    { authD   = (,) <$> authD <*> new+    , serverD = \ c p h (y, v) b req -> ($ v) <$> serverD c p h y b req+    , ..+    } -- Note [Existential Record Update]++-- | Add a content type and body checks around parameter checks.+--+-- We'll report failed content type check (415), before trying to parse+-- query parameters (400). Which, in turn, happens before request body parsing.+addBodyCheck :: Delayed env (a -> b)+             -> DelayedIO c         -- ^ content type check+             -> (c -> DelayedIO a)  -- ^ body check+             -> Delayed env b+addBodyCheck Delayed{..} newContentD newBodyD =+  Delayed+    { contentD = (,) <$> contentD <*> newContentD+    , bodyD    = \(content, c) -> (,) <$> bodyD content <*> newBodyD c+    , serverD  = \ c p h a (z, v) req -> ($ v) <$> serverD c p h a z req+    , ..+    } -- Note [Existential Record Update]+++-- | Add an accept header check before handling parameters.+-- In principle, we'd like+-- to take a bad body (400) response take precedence over a+-- failed accept check (406). BUT to allow streaming the body,+-- we cannot run the body check and then still backtrack.+-- We therefore do the accept check before the body check,+-- when we can still backtrack. There are other solutions to+-- this, but they'd be more complicated (such as delaying the+-- body check further so that it can still be run in a situation+-- where we'd otherwise report 406).+addAcceptCheck :: Delayed env a+               -> DelayedIO ()+               -> Delayed env a+addAcceptCheck Delayed{..} new =+  Delayed+    { acceptD = acceptD *> new+    , ..+    } -- Note [Existential Record Update]++-- | Many combinators extract information that is passed to+-- the handler without the possibility of failure. In such a+-- case, 'passToServer' can be used.+passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b+passToServer Delayed{..} x =+  Delayed+    { serverD = \ c p h a b req -> ($ x req) <$> serverD c p h a b req+    , ..+    } -- Note [Existential Record Update]++-- | Run a delayed server. Performs all scheduled operations+-- in order, and passes the results from the capture and body+-- blocks on to the actual handler.+--+-- This should only be called once per request; otherwise the guarantees about+-- effect and HTTP error ordering break down.+runDelayed :: Delayed env a+           -> env+           -> Request+           -> ResourceT IO (RouteResult a)+runDelayed Delayed{..} env = runDelayedIO $ do+    r <- ask+    c <- capturesD env+    methodD+    a <- authD+    acceptD+    content <- contentD+    p <- paramsD       -- Has to be before body parsing, but after content-type checks+    h <- headersD+    b <- bodyD content+    liftRouteResult (serverD c p h a b r)++-- | Runs a delayed server and the resulting action.+-- Takes a continuation that lets us send a response.+-- Also takes a continuation for how to turn the+-- result of the delayed server into a response.+runAction :: Delayed env (Handler a)+          -> env+          -> Request+          -> (RouteResult Response -> IO r)+          -> (a -> RouteResult Response)+          -> IO r+runAction action env req respond k = runResourceT $+    runDelayed action env req >>= go >>= liftIO . respond+  where+    go (Fail e)      = return $ Fail e+    go (FailFatal e) = return $ FailFatal e+    go (Route a)     = liftIO $ do+      e <- runHandler a+      case e of+        Left err -> return . Route $ responseServerError err+        Right x  -> return $! k x++{- Note [Existential Record Update]+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~++Due to GHC issue <https://ghc.haskell.org/trac/ghc/ticket/2595 2595>, we cannot+do the more succint thing - just update the records we actually change.+-}
+ src/Servant/Server/Internal/DelayedIO.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE TypeFamilies               #-}+module Servant.Server.Internal.DelayedIO where++import           Control.Monad.Base+                 (MonadBase (..))+import           Control.Monad.Catch+                 (MonadThrow (..))+import           Control.Monad.Reader+                 (MonadReader (..), ReaderT (..), runReaderT)+import           Control.Monad.Trans+                 (MonadIO (..), MonadTrans (..))+import           Control.Monad.Trans.Control+                 (ComposeSt, MonadBaseControl (..), MonadTransControl (..),+                 defaultLiftBaseWith, defaultRestoreM)+import           Control.Monad.Trans.Resource+                 (MonadResource (..), ResourceT, runInternalState,+                 runResourceT, transResourceT, withInternalState)+import           Network.Wai+                 (Application, Request, Response, ResponseReceived)++import           Servant.Server.Internal.RouteResult+import           Servant.Server.Internal.ServerError++-- | Computations used in a 'Delayed' can depend on the+-- incoming 'Request', may perform 'IO', and result in a+-- 'RouteResult', meaning they can either suceed, fail+-- (with the possibility to recover), or fail fatally.+--+newtype DelayedIO a = DelayedIO { runDelayedIO' :: ReaderT Request (ResourceT (RouteResultT IO)) a }+  deriving+    ( Functor, Applicative, Monad+    , MonadIO, MonadReader Request+    , MonadThrow+    , MonadResource+    )++instance MonadBase IO DelayedIO where+    liftBase = liftIO++liftRouteResult :: RouteResult a -> DelayedIO a+liftRouteResult x = DelayedIO $ lift . lift $ RouteResultT . return $ x++instance MonadBaseControl IO DelayedIO where+    -- type StM DelayedIO a = StM (ReaderT Request (ResourceT (RouteResultT IO))) a+    -- liftBaseWith f = DelayedIO $ liftBaseWith $ \g -> f (g . runDelayedIO')+    -- restoreM       = DelayedIO . restoreM++    type StM DelayedIO a = RouteResult a+    liftBaseWith f = DelayedIO $ ReaderT $ \req -> withInternalState $ \s ->+        liftBaseWith $ \runInBase -> f $ \x ->+            runInBase (runInternalState (runReaderT (runDelayedIO' x) req) s)+    restoreM      = DelayedIO . lift . withInternalState . const . restoreM+++runDelayedIO :: DelayedIO a -> Request -> ResourceT IO (RouteResult a)+runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO' m) req++-- | Fail with the option to recover.+delayedFail :: ServerError -> DelayedIO a+delayedFail err = liftRouteResult $ Fail err++-- | Fail fatally, i.e., without any option to recover.+delayedFailFatal :: ServerError -> DelayedIO a+delayedFailFatal err = liftRouteResult $ FailFatal err++-- | Gain access to the incoming request.+withRequest :: (Request -> DelayedIO a) -> DelayedIO a+withRequest f = do+    req <- ask+    f req
src/Servant/Server/Internal/Handler.hs view
@@ -22,13 +22,13 @@                  (ExceptT, runExceptT) import           GHC.Generics                  (Generic)-import           Servant.Server.Internal.ServantErr-                 (ServantErr)+import           Servant.Server.Internal.ServerError+                 (ServerError) -newtype Handler a = Handler { runHandler' :: ExceptT ServantErr IO a }+newtype Handler a = Handler { runHandler' :: ExceptT ServerError IO a }   deriving     ( Functor, Applicative, Monad, MonadIO, Generic-    , MonadError ServantErr+    , MonadError ServerError     , MonadThrow, MonadCatch, MonadMask     ) @@ -36,7 +36,7 @@   liftBase = Handler . liftBase  instance MonadBaseControl IO Handler where-  type StM Handler a = Either ServantErr a+  type StM Handler a = Either ServerError a    -- liftBaseWith :: (RunInBase Handler IO -> IO a) -> Handler a   liftBaseWith f = Handler (liftBaseWith (\g -> f (g . runHandler')))@@ -44,5 +44,5 @@   -- restoreM :: StM Handler a -> Handler a   restoreM st = Handler (restoreM st) -runHandler :: Handler a -> IO (Either ServantErr a)+runHandler :: Handler a -> IO (Either ServerError a) runHandler = runExceptT . runHandler'
+ src/Servant/Server/Internal/RouteResult.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE DeriveFunctor         #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE UndecidableInstances  #-}+module Servant.Server.Internal.RouteResult where++import           Control.Monad+                 (ap, liftM)+import           Control.Monad.Base+                 (MonadBase (..))+import           Control.Monad.Catch+                 (MonadThrow (..))+import           Control.Monad.Trans+                 (MonadIO (..), MonadTrans (..))+import           Control.Monad.Trans.Control+                 (ComposeSt, MonadBaseControl (..), MonadTransControl (..),+                 defaultLiftBaseWith, defaultRestoreM)++import           Servant.Server.Internal.ServerError++-- | The result of matching against a path in the route tree.+data RouteResult a =+    Fail ServerError           -- ^ Keep trying other paths.+                               --   The 'ServantError' should only be 404, 405 or 406.+  | FailFatal !ServerError     -- ^ Don't try other paths.+  | Route !a+  deriving (Eq, Show, Read, Functor)++instance Applicative RouteResult where+    pure = return+    (<*>) = ap++instance Monad RouteResult where+    return = Route+    Route a     >>= f = f a+    Fail e      >>= _ = Fail e+    FailFatal e >>= _ = FailFatal e++newtype RouteResultT m a = RouteResultT { runRouteResultT :: m (RouteResult a) }+  deriving (Functor)++instance MonadTrans RouteResultT where+    lift = RouteResultT . liftM Route++instance (Functor m, Monad m) => Applicative (RouteResultT m) where+    pure  = return+    (<*>) = ap++instance Monad m => Monad (RouteResultT m) where+    return = RouteResultT . return . Route+    m >>= k = RouteResultT $ do+        a <- runRouteResultT m+        case a of+            Fail e      -> return $ Fail e+            FailFatal e -> return $ FailFatal e+            Route b     -> runRouteResultT (k b)++instance MonadIO m => MonadIO (RouteResultT m) where+    liftIO = lift . liftIO++instance MonadBase b m => MonadBase b (RouteResultT m) where+    liftBase = lift . liftBase++instance MonadBaseControl b m => MonadBaseControl b (RouteResultT m) where+    type StM (RouteResultT m) a = ComposeSt RouteResultT m a+    liftBaseWith = defaultLiftBaseWith+    restoreM     = defaultRestoreM++instance MonadTransControl RouteResultT where+    type StT RouteResultT a = RouteResult a+    liftWith f = RouteResultT $ liftM return $ f runRouteResultT+    restoreT = RouteResultT++instance MonadThrow m => MonadThrow (RouteResultT m) where+    throwM = lift . throwM
src/Servant/Server/Internal/Router.hs view
@@ -7,6 +7,8 @@ import           Prelude () import           Prelude.Compat +import           Data.Function+                 (on) import           Data.Map                  (Map) import qualified Data.Map                                   as M@@ -16,7 +18,8 @@ import           Network.Wai                  (Response, pathInfo) import           Servant.Server.Internal.RoutingApplication-import           Servant.Server.Internal.ServantErr+import           Servant.Server.Internal.RouteResult+import           Servant.Server.Internal.ServerError  type Router env = Router' env RoutingApplication @@ -208,7 +211,14 @@  -- Priority on HTTP codes. ----- It just so happens that 404 < 405 < 406 as far as--- we are concerned here, so we can use (<). worseHTTPCode :: Int -> Int -> Bool-worseHTTPCode = (<)+worseHTTPCode = on (<) toPriority+  where+    toPriority :: Int -> Int+    toPriority 404 = 0 -- not found+    toPriority 405 = 1 -- method not allowed+    toPriority 401 = 2 -- unauthorized+    toPriority 415 = 3 -- unsupported media type+    toPriority 406 = 4 -- not acceptable+    toPriority 400 = 6 -- bad request+    toPriority _   = 5
src/Servant/Server/Internal/RoutingApplication.hs view
@@ -1,399 +1,20 @@-{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveFunctor              #-}-{-# LANGUAGE FlexibleInstances          #-}-{-# LANGUAGE GADTs                      #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses      #-}-{-# LANGUAGE RecordWildCards            #-}-{-# LANGUAGE TypeFamilies               #-}-{-# LANGUAGE TypeOperators              #-}-{-# LANGUAGE UndecidableInstances       #-} module Servant.Server.Internal.RoutingApplication where -import           Control.Monad-                 (ap, liftM)-import           Control.Monad.Base-                 (MonadBase (..))-import           Control.Monad.Catch-                 (MonadThrow (..))-import           Control.Monad.Reader-                 (MonadReader (..), ReaderT (..), runReaderT)-import           Control.Monad.Trans-                 (MonadIO (..), MonadTrans (..))-import           Control.Monad.Trans.Control-                 (ComposeSt, MonadBaseControl (..), MonadTransControl (..),-                 defaultLiftBaseWith, defaultRestoreM)-import           Control.Monad.Trans.Resource-                 (MonadResource (..), ResourceT, runInternalState,-                 runResourceT, transResourceT, withInternalState) import           Network.Wai                  (Application, Request, Response, ResponseReceived) import           Prelude () import           Prelude.Compat-import           Servant.Server.Internal.Handler-import           Servant.Server.Internal.ServantErr+import           Servant.Server.Internal.RouteResult+import           Servant.Server.Internal.ServerError  type RoutingApplication =      Request -- ^ the request, the field 'pathInfo' may be modified by url routing   -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived --- | The result of matching against a path in the route tree.-data RouteResult a =-    Fail ServantErr           -- ^ Keep trying other paths. The @ServantErr@-                              -- should only be 404, 405 or 406.-  | FailFatal !ServantErr     -- ^ Don't try other paths.-  | Route !a-  deriving (Eq, Show, Read, Functor)--instance Applicative RouteResult where-    pure = return-    (<*>) = ap--instance Monad RouteResult where-    return = Route-    Route a     >>= f = f a-    Fail e      >>= _ = Fail e-    FailFatal e >>= _ = FailFatal e--newtype RouteResultT m a = RouteResultT { runRouteResultT :: m (RouteResult a) }-  deriving (Functor)--instance MonadTrans RouteResultT where-    lift = RouteResultT . liftM Route--instance (Functor m, Monad m) => Applicative (RouteResultT m) where-    pure  = return-    (<*>) = ap--instance Monad m => Monad (RouteResultT m) where-    return = RouteResultT . return . Route-    m >>= k = RouteResultT $ do-        a <- runRouteResultT m-        case a of-            Fail e      -> return $ Fail e-            FailFatal e -> return $ FailFatal e-            Route b     -> runRouteResultT (k b)--instance MonadIO m => MonadIO (RouteResultT m) where-    liftIO = lift . liftIO--instance MonadBase b m => MonadBase b (RouteResultT m) where-    liftBase = lift . liftBase--instance MonadBaseControl b m => MonadBaseControl b (RouteResultT m) where-    type StM (RouteResultT m) a = ComposeSt RouteResultT m a-    liftBaseWith = defaultLiftBaseWith-    restoreM     = defaultRestoreM--instance MonadTransControl RouteResultT where-    type StT RouteResultT a = RouteResult a-    liftWith f = RouteResultT $ liftM return $ f runRouteResultT-    restoreT = RouteResultT--instance MonadThrow m => MonadThrow (RouteResultT m) where-    throwM = lift . throwM- toApplication :: RoutingApplication -> Application toApplication ra request respond = ra request routingRespond  where   routingRespond :: RouteResult Response -> IO ResponseReceived-  routingRespond (Fail err)      = respond $ responseServantErr err-  routingRespond (FailFatal err) = respond $ responseServantErr err+  routingRespond (Fail err)      = respond $ responseServerError err+  routingRespond (FailFatal err) = respond $ responseServerError err   routingRespond (Route v)       = respond v---- | A 'Delayed' is a representation of a handler with scheduled--- delayed checks that can trigger errors.------ Why would we want to delay checks?------ There are two reasons:------ 1. In a straight-forward implementation, the order in which we--- perform checks will determine the error we generate. This is--- because once an error occurs, we would abort and not perform--- any subsequent checks, but rather return the current error.------ This is not a necessity: we could continue doing other checks,--- and choose the preferred error. However, that would in general--- mean more checking, which leads us to the other reason.------ 2. We really want to avoid doing certain checks too early. For--- example, captures involve parsing, and are much more costly--- than static route matches. In particular, if several paths--- contain the "same" capture, we'd like as much as possible to--- avoid trying the same parse many times. Also tricky is the--- request body. Again, this involves parsing, but also, WAI makes--- obtaining the request body a side-effecting operation. We--- could/can work around this by manually caching the request body,--- but we'd rather keep the number of times we actually try to--- decode the request body to an absolute minimum.------ We prefer to have the following relative priorities of error--- codes:------ @--- 404--- 405 (bad method)--- 401 (unauthorized)--- 415 (unsupported media type)--- 406 (not acceptable)--- 400 (bad request)--- @------ Therefore, while routing, we delay most checks so that they--- will ultimately occur in the right order.------ A 'Delayed' contains many delayed blocks of tests, and--- the actual handler:------ 1. Delayed captures. These can actually cause 404, and--- while they're costly, they should be done first among the--- delayed checks (at least as long as we do not decouple the--- check order from the error reporting, see above). Delayed--- captures can provide inputs to the actual handler.------ 2. Method check(s). This can cause a 405. On success,--- it does not provide an input for the handler. Method checks--- are comparatively cheap.------ 3. Authentication checks. This can cause 401.------ 4. Accept and content type header checks. These checks--- can cause 415 and 406 errors.------ 5. Query parameter checks. They require parsing and can cause 400 if the--- parsing fails. Query parameter checks provide inputs to the handler------ 6. Header Checks. They also require parsing and can cause 400 if parsing fails.------ 7. Body check. The request body check can cause 400.----data Delayed env c where-  Delayed :: { capturesD :: env -> DelayedIO captures-             , methodD   :: DelayedIO ()-             , authD     :: DelayedIO auth-             , acceptD   :: DelayedIO ()-             , contentD  :: DelayedIO contentType-             , paramsD   :: DelayedIO params-             , headersD  :: DelayedIO headers-             , bodyD     :: contentType -> DelayedIO body-             , serverD   :: captures-                         -> params-                         -> headers-                         -> auth-                         -> body-                         -> Request-                         -> RouteResult c-             } -> Delayed env c--instance Functor (Delayed env) where-  fmap f Delayed{..} =-    Delayed-      { serverD = \ c p h a b req -> f <$> serverD c p h a b req-      , ..-      } -- Note [Existential Record Update]---- | Computations used in a 'Delayed' can depend on the--- incoming 'Request', may perform 'IO, and result in a--- 'RouteResult, meaning they can either suceed, fail--- (with the possibility to recover), or fail fatally.----newtype DelayedIO a = DelayedIO { runDelayedIO' :: ReaderT Request (ResourceT (RouteResultT IO)) a }-  deriving-    ( Functor, Applicative, Monad-    , MonadIO, MonadReader Request-    , MonadThrow-    , MonadResource-    )--instance MonadBase IO DelayedIO where-    liftBase = liftIO--liftRouteResult :: RouteResult a -> DelayedIO a-liftRouteResult x = DelayedIO $ lift . lift $ RouteResultT . return $ x--instance MonadBaseControl IO DelayedIO where-    -- type StM DelayedIO a = StM (ReaderT Request (ResourceT (RouteResultT IO))) a-    -- liftBaseWith f = DelayedIO $ liftBaseWith $ \g -> f (g . runDelayedIO')-    -- restoreM       = DelayedIO . restoreM--    type StM DelayedIO a = RouteResult a-    liftBaseWith f = DelayedIO $ ReaderT $ \req -> withInternalState $ \s ->-        liftBaseWith $ \runInBase -> f $ \x ->-            runInBase (runInternalState (runReaderT (runDelayedIO' x) req) s)-    restoreM      = DelayedIO . lift . withInternalState . const . restoreM---runDelayedIO :: DelayedIO a -> Request -> ResourceT IO (RouteResult a)-runDelayedIO m req = transResourceT runRouteResultT $ runReaderT (runDelayedIO' m) req---- | A 'Delayed' without any stored checks.-emptyDelayed :: RouteResult a -> Delayed env a-emptyDelayed result =-  Delayed (const r) r r r r r r (const r) (\ _ _ _ _ _ _ -> result)-  where-    r = return ()---- | Fail with the option to recover.-delayedFail :: ServantErr -> DelayedIO a-delayedFail err = liftRouteResult $ Fail err---- | Fail fatally, i.e., without any option to recover.-delayedFailFatal :: ServantErr -> DelayedIO a-delayedFailFatal err = liftRouteResult $ FailFatal err---- | Gain access to the incoming request.-withRequest :: (Request -> DelayedIO a) -> DelayedIO a-withRequest f = do-    req <- ask-    f req---- | Add a capture to the end of the capture block.-addCapture :: Delayed env (a -> b)-           -> (captured -> DelayedIO a)-           -> Delayed (captured, env) b-addCapture Delayed{..} new =-  Delayed-    { capturesD = \ (txt, env) -> (,) <$> capturesD env <*> new txt-    , serverD   = \ (x, v) p h a b req -> ($ v) <$> serverD x p h a b req-    , ..-    } -- Note [Existential Record Update]---- | Add a parameter check to the end of the params block-addParameterCheck :: Delayed env (a -> b)-                  -> DelayedIO a-                  -> Delayed env b-addParameterCheck Delayed {..} new =-  Delayed-    { paramsD = (,) <$> paramsD <*> new-    , serverD = \c (p, pNew) h a b req -> ($ pNew) <$> serverD c p h a b req-    , ..-    }---- | Add a parameter check to the end of the params block-addHeaderCheck :: Delayed env (a -> b)-               -> DelayedIO a-               -> Delayed env b-addHeaderCheck Delayed {..} new =-  Delayed-    { headersD = (,) <$> headersD <*> new-    , serverD = \c p (h, hNew) a b req -> ($ hNew) <$> serverD c p h a b req-    , ..-    }---- | Add a method check to the end of the method block.-addMethodCheck :: Delayed env a-               -> DelayedIO ()-               -> Delayed env a-addMethodCheck Delayed{..} new =-  Delayed-    { methodD = methodD <* new-    , ..-    } -- Note [Existential Record Update]---- | Add an auth check to the end of the auth block.-addAuthCheck :: Delayed env (a -> b)-             -> DelayedIO a-             -> Delayed env b-addAuthCheck Delayed{..} new =-  Delayed-    { authD   = (,) <$> authD <*> new-    , serverD = \ c p h (y, v) b req -> ($ v) <$> serverD c p h y b req-    , ..-    } -- Note [Existential Record Update]---- | Add a content type and body checks around parameter checks.------ We'll report failed content type check (415), before trying to parse--- query parameters (400). Which, in turn, happens before request body parsing.-addBodyCheck :: Delayed env (a -> b)-             -> DelayedIO c         -- ^ content type check-             -> (c -> DelayedIO a)  -- ^ body check-             -> Delayed env b-addBodyCheck Delayed{..} newContentD newBodyD =-  Delayed-    { contentD = (,) <$> contentD <*> newContentD-    , bodyD    = \(content, c) -> (,) <$> bodyD content <*> newBodyD c-    , serverD  = \ c p h a (z, v) req -> ($ v) <$> serverD c p h a z req-    , ..-    } -- Note [Existential Record Update]----- | Add an accept header check before handling parameters.--- In principle, we'd like--- to take a bad body (400) response take precedence over a--- failed accept check (406). BUT to allow streaming the body,--- we cannot run the body check and then still backtrack.--- We therefore do the accept check before the body check,--- when we can still backtrack. There are other solutions to--- this, but they'd be more complicated (such as delaying the--- body check further so that it can still be run in a situation--- where we'd otherwise report 406).-addAcceptCheck :: Delayed env a-               -> DelayedIO ()-               -> Delayed env a-addAcceptCheck Delayed{..} new =-  Delayed-    { acceptD = acceptD *> new-    , ..-    } -- Note [Existential Record Update]---- | Many combinators extract information that is passed to--- the handler without the possibility of failure. In such a--- case, 'passToServer' can be used.-passToServer :: Delayed env (a -> b) -> (Request -> a) -> Delayed env b-passToServer Delayed{..} x =-  Delayed-    { serverD = \ c p h a b req -> ($ x req) <$> serverD c p h a b req-    , ..-    } -- Note [Existential Record Update]---- | Run a delayed server. Performs all scheduled operations--- in order, and passes the results from the capture and body--- blocks on to the actual handler.------ This should only be called once per request; otherwise the guarantees about--- effect and HTTP error ordering break down.-runDelayed :: Delayed env a-           -> env-           -> Request-           -> ResourceT IO (RouteResult a)-runDelayed Delayed{..} env = runDelayedIO $ do-    r <- ask-    c <- capturesD env-    methodD-    a <- authD-    acceptD-    content <- contentD-    p <- paramsD       -- Has to be before body parsing, but after content-type checks-    h <- headersD-    b <- bodyD content-    liftRouteResult (serverD c p h a b r)---- | Runs a delayed server and the resulting action.--- Takes a continuation that lets us send a response.--- Also takes a continuation for how to turn the--- result of the delayed server into a response.-runAction :: Delayed env (Handler a)-          -> env-          -> Request-          -> (RouteResult Response -> IO r)-          -> (a -> RouteResult Response)-          -> IO r-runAction action env req respond k = runResourceT $-    runDelayed action env req >>= go >>= liftIO . respond-  where-    go (Fail e)      = return $ Fail e-    go (FailFatal e) = return $ FailFatal e-    go (Route a)     = liftIO $ do-      e <- runHandler a-      case e of-        Left err -> return . Route $ responseServantErr err-        Right x  -> return $! k x--{- Note [Existential Record Update]-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--Due to GHC issue <https://ghc.haskell.org/trac/ghc/ticket/2595 2595>, we cannot-do the more succint thing - just update the records we actually change.--}
− src/Servant/Server/Internal/ServantErr.hs
@@ -1,475 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE OverloadedStrings  #-}-{-# LANGUAGE RecordWildCards    #-}-module Servant.Server.Internal.ServantErr where--import           Control.Exception-                 (Exception)-import qualified Data.ByteString.Char8 as BS-import qualified Data.ByteString.Lazy  as LBS-import           Data.Typeable-                 (Typeable)-import qualified Network.HTTP.Types    as HTTP-import           Network.Wai-                 (Response, responseLBS)--data ServantErr = ServantErr { errHTTPCode     :: Int-                             , errReasonPhrase :: String-                             , errBody         :: LBS.ByteString-                             , errHeaders      :: [HTTP.Header]-                             } deriving (Show, Eq, Read, Typeable)--instance Exception ServantErr--responseServantErr :: ServantErr -> Response-responseServantErr ServantErr{..} = responseLBS status errHeaders errBody-  where-    status = HTTP.mkStatus errHTTPCode (BS.pack errReasonPhrase)---- | 'err300' Multiple Choices------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err300 { errBody = "I can't choose." }----err300 :: ServantErr-err300 = ServantErr { errHTTPCode = 300-                    , errReasonPhrase = "Multiple Choices"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err301' Moved Permanently------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err301----err301 :: ServantErr-err301 = ServantErr { errHTTPCode = 301-                    , errReasonPhrase = "Moved Permanently"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err302' Found------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err302----err302 :: ServantErr-err302 = ServantErr { errHTTPCode = 302-                    , errReasonPhrase = "Found"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err303' See Other------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err303----err303 :: ServantErr-err303 = ServantErr { errHTTPCode = 303-                    , errReasonPhrase = "See Other"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err304' Not Modified------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err304----err304 :: ServantErr-err304 = ServantErr { errHTTPCode = 304-                    , errReasonPhrase = "Not Modified"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err305' Use Proxy------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err305----err305 :: ServantErr-err305 = ServantErr { errHTTPCode = 305-                    , errReasonPhrase = "Use Proxy"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err307' Temporary Redirect------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err307----err307 :: ServantErr-err307 = ServantErr { errHTTPCode = 307-                    , errReasonPhrase = "Temporary Redirect"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err400' Bad Request------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err400 { errBody = "Your request makes no sense to me." }----err400 :: ServantErr-err400 = ServantErr { errHTTPCode = 400-                    , errReasonPhrase = "Bad Request"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err401' Unauthorized------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err401 { errBody = "Your credentials are invalid." }----err401 :: ServantErr-err401 = ServantErr { errHTTPCode = 401-                    , errReasonPhrase = "Unauthorized"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err402' Payment Required------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err402 { errBody = "You have 0 credits. Please give me $$$." }----err402 :: ServantErr-err402 = ServantErr { errHTTPCode = 402-                    , errReasonPhrase = "Payment Required"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err403' Forbidden------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err403 { errBody = "Please login first." }----err403 :: ServantErr-err403 = ServantErr { errHTTPCode = 403-                    , errReasonPhrase = "Forbidden"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err404' Not Found------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err404 { errBody = "(╯°□°)╯︵ ┻━┻)." }----err404 :: ServantErr-err404 = ServantErr { errHTTPCode = 404-                    , errReasonPhrase = "Not Found"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err405' Method Not Allowed------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err405 { errBody = "Your account privileges does not allow for this.  Please pay $$$." }----err405 :: ServantErr-err405 = ServantErr { errHTTPCode = 405-                    , errReasonPhrase = "Method Not Allowed"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err406' Not Acceptable------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err406----err406 :: ServantErr-err406 = ServantErr { errHTTPCode = 406-                    , errReasonPhrase = "Not Acceptable"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err407' Proxy Authentication Required------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err407----err407 :: ServantErr-err407 = ServantErr { errHTTPCode = 407-                    , errReasonPhrase = "Proxy Authentication Required"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err409' Conflict------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err409 { errBody = "Transaction conflicts with 59879cb56c7c159231eeacdd503d755f7e835f74" }----err409 :: ServantErr-err409 = ServantErr { errHTTPCode = 409-                    , errReasonPhrase = "Conflict"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err410' Gone------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err410 { errBody = "I know it was here at some point, but.. I blame bad luck." }----err410 :: ServantErr-err410 = ServantErr { errHTTPCode = 410-                    , errReasonPhrase = "Gone"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err411' Length Required------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError err411----err411 :: ServantErr-err411 = ServantErr { errHTTPCode = 411-                    , errReasonPhrase = "Length Required"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err412' Precondition Failed------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err412 { errBody = "Precondition fail: x < 42 && y > 57" }----err412 :: ServantErr-err412 = ServantErr { errHTTPCode = 412-                    , errReasonPhrase = "Precondition Failed"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err413' Request Entity Too Large------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err413 { errBody = "Request exceeded 64k." }----err413 :: ServantErr-err413 = ServantErr { errHTTPCode = 413-                    , errReasonPhrase = "Request Entity Too Large"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err414' Request-URI Too Large------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err414 { errBody = "Maximum length is 64." }----err414 :: ServantErr-err414 = ServantErr { errHTTPCode = 414-                    , errReasonPhrase = "Request-URI Too Large"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err415' Unsupported Media Type------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err415 { errBody = "Supported media types:  gif, png" }----err415 :: ServantErr-err415 = ServantErr { errHTTPCode = 415-                    , errReasonPhrase = "Unsupported Media Type"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err416' Request range not satisfiable------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err416 { errBody = "Valid range is [0, 424242]." }----err416 :: ServantErr-err416 = ServantErr { errHTTPCode = 416-                    , errReasonPhrase = "Request range not satisfiable"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err417' Expectation Failed------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err417 { errBody = "I found a quux in the request.  This isn't going to work." }----err417 :: ServantErr-err417 = ServantErr { errHTTPCode = 417-                    , errReasonPhrase = "Expectation Failed"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err418' Expectation Failed------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err418 { errBody = "Apologies, this is not a webserver but a teapot." }----err418 :: ServantErr-err418 = ServantErr { errHTTPCode = 418-                    , errReasonPhrase = "I'm a teapot"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err422' Unprocessable Entity------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err422 { errBody = "I understood your request, but can't process it." }----err422 :: ServantErr-err422 = ServantErr { errHTTPCode = 422-                    , errReasonPhrase = "Unprocessable Entity"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err500' Internal Server Error------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err500 { errBody = "Exception in module A.B.C:55.  Have a great day!" }----err500 :: ServantErr-err500 = ServantErr { errHTTPCode = 500-                    , errReasonPhrase = "Internal Server Error"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err501' Not Implemented------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err501 { errBody = "/v1/foo is not supported with quux in the request." }----err501 :: ServantErr-err501 = ServantErr { errHTTPCode = 501-                    , errReasonPhrase = "Not Implemented"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err502' Bad Gateway------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err502 { errBody = "Tried gateway foo, bar, and baz.  None responded." }----err502 :: ServantErr-err502 = ServantErr { errHTTPCode = 502-                    , errReasonPhrase = "Bad Gateway"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err503' Service Unavailable------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err503 { errBody = "We're rewriting in PHP." }----err503 :: ServantErr-err503 = ServantErr { errHTTPCode = 503-                    , errReasonPhrase = "Service Unavailable"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err504' Gateway Time-out------ Example:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err504 { errBody = "Backend foobar did not respond in 5 seconds." }----err504 :: ServantErr-err504 = ServantErr { errHTTPCode = 504-                    , errReasonPhrase = "Gateway Time-out"-                    , errBody = ""-                    , errHeaders = []-                    }---- | 'err505' HTTP Version not supported------ Example usage:------ > failingHandler :: Handler ()--- > failingHandler = throwError $ err505 { errBody = "I support HTTP/4.0 only." }----err505 :: ServantErr-err505 = ServantErr { errHTTPCode = 505-                    , errReasonPhrase = "HTTP Version not supported"-                    , errBody = ""-                    , errHeaders = []-                    }
+ src/Servant/Server/Internal/ServerError.hs view
@@ -0,0 +1,477 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE RecordWildCards    #-}+module Servant.Server.Internal.ServerError where++import           Control.Exception+                 (Exception)+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy  as LBS+import           Data.Typeable+                 (Typeable)+import qualified Network.HTTP.Types    as HTTP+import           Network.Wai+                 (Response, responseLBS)++data ServerError = ServerError+    { errHTTPCode     :: Int+    , errReasonPhrase :: String+    , errBody         :: LBS.ByteString+    , errHeaders      :: [HTTP.Header]+    }+  deriving (Show, Eq, Read, Typeable)++instance Exception ServerError++responseServerError :: ServerError -> Response+responseServerError ServerError{..} = responseLBS status errHeaders errBody+  where+    status = HTTP.mkStatus errHTTPCode (BS.pack errReasonPhrase)++-- | 'err300' Multiple Choices+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err300 { errBody = "I can't choose." }+--+err300 :: ServerError+err300 = ServerError { errHTTPCode = 300+                    , errReasonPhrase = "Multiple Choices"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err301' Moved Permanently+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err301+--+err301 :: ServerError+err301 = ServerError { errHTTPCode = 301+                    , errReasonPhrase = "Moved Permanently"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err302' Found+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err302+--+err302 :: ServerError+err302 = ServerError { errHTTPCode = 302+                    , errReasonPhrase = "Found"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err303' See Other+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err303+--+err303 :: ServerError+err303 = ServerError { errHTTPCode = 303+                    , errReasonPhrase = "See Other"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err304' Not Modified+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err304+--+err304 :: ServerError+err304 = ServerError { errHTTPCode = 304+                    , errReasonPhrase = "Not Modified"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err305' Use Proxy+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err305+--+err305 :: ServerError+err305 = ServerError { errHTTPCode = 305+                    , errReasonPhrase = "Use Proxy"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err307' Temporary Redirect+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err307+--+err307 :: ServerError+err307 = ServerError { errHTTPCode = 307+                    , errReasonPhrase = "Temporary Redirect"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err400' Bad Request+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err400 { errBody = "Your request makes no sense to me." }+--+err400 :: ServerError+err400 = ServerError { errHTTPCode = 400+                    , errReasonPhrase = "Bad Request"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err401' Unauthorized+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err401 { errBody = "Your credentials are invalid." }+--+err401 :: ServerError+err401 = ServerError { errHTTPCode = 401+                    , errReasonPhrase = "Unauthorized"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err402' Payment Required+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err402 { errBody = "You have 0 credits. Please give me $$$." }+--+err402 :: ServerError+err402 = ServerError { errHTTPCode = 402+                    , errReasonPhrase = "Payment Required"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err403' Forbidden+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err403 { errBody = "Please login first." }+--+err403 :: ServerError+err403 = ServerError { errHTTPCode = 403+                    , errReasonPhrase = "Forbidden"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err404' Not Found+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err404 { errBody = "(╯°□°)╯︵ ┻━┻)." }+--+err404 :: ServerError+err404 = ServerError { errHTTPCode = 404+                    , errReasonPhrase = "Not Found"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err405' Method Not Allowed+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err405 { errBody = "Your account privileges does not allow for this.  Please pay $$$." }+--+err405 :: ServerError+err405 = ServerError { errHTTPCode = 405+                    , errReasonPhrase = "Method Not Allowed"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err406' Not Acceptable+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err406+--+err406 :: ServerError+err406 = ServerError { errHTTPCode = 406+                    , errReasonPhrase = "Not Acceptable"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err407' Proxy Authentication Required+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err407+--+err407 :: ServerError+err407 = ServerError { errHTTPCode = 407+                    , errReasonPhrase = "Proxy Authentication Required"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err409' Conflict+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err409 { errBody = "Transaction conflicts with 59879cb56c7c159231eeacdd503d755f7e835f74" }+--+err409 :: ServerError+err409 = ServerError { errHTTPCode = 409+                    , errReasonPhrase = "Conflict"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err410' Gone+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err410 { errBody = "I know it was here at some point, but.. I blame bad luck." }+--+err410 :: ServerError+err410 = ServerError { errHTTPCode = 410+                    , errReasonPhrase = "Gone"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err411' Length Required+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError err411+--+err411 :: ServerError+err411 = ServerError { errHTTPCode = 411+                    , errReasonPhrase = "Length Required"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err412' Precondition Failed+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err412 { errBody = "Precondition fail: x < 42 && y > 57" }+--+err412 :: ServerError+err412 = ServerError { errHTTPCode = 412+                    , errReasonPhrase = "Precondition Failed"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err413' Request Entity Too Large+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err413 { errBody = "Request exceeded 64k." }+--+err413 :: ServerError+err413 = ServerError { errHTTPCode = 413+                    , errReasonPhrase = "Request Entity Too Large"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err414' Request-URI Too Large+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err414 { errBody = "Maximum length is 64." }+--+err414 :: ServerError+err414 = ServerError { errHTTPCode = 414+                    , errReasonPhrase = "Request-URI Too Large"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err415' Unsupported Media Type+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err415 { errBody = "Supported media types:  gif, png" }+--+err415 :: ServerError+err415 = ServerError { errHTTPCode = 415+                    , errReasonPhrase = "Unsupported Media Type"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err416' Request range not satisfiable+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err416 { errBody = "Valid range is [0, 424242]." }+--+err416 :: ServerError+err416 = ServerError { errHTTPCode = 416+                    , errReasonPhrase = "Request range not satisfiable"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err417' Expectation Failed+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err417 { errBody = "I found a quux in the request.  This isn't going to work." }+--+err417 :: ServerError+err417 = ServerError { errHTTPCode = 417+                    , errReasonPhrase = "Expectation Failed"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err418' Expectation Failed+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err418 { errBody = "Apologies, this is not a webserver but a teapot." }+--+err418 :: ServerError+err418 = ServerError { errHTTPCode = 418+                    , errReasonPhrase = "I'm a teapot"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err422' Unprocessable Entity+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err422 { errBody = "I understood your request, but can't process it." }+--+err422 :: ServerError+err422 = ServerError { errHTTPCode = 422+                    , errReasonPhrase = "Unprocessable Entity"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err500' Internal Server Error+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err500 { errBody = "Exception in module A.B.C:55.  Have a great day!" }+--+err500 :: ServerError+err500 = ServerError { errHTTPCode = 500+                    , errReasonPhrase = "Internal Server Error"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err501' Not Implemented+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err501 { errBody = "/v1/foo is not supported with quux in the request." }+--+err501 :: ServerError+err501 = ServerError { errHTTPCode = 501+                    , errReasonPhrase = "Not Implemented"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err502' Bad Gateway+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err502 { errBody = "Tried gateway foo, bar, and baz.  None responded." }+--+err502 :: ServerError+err502 = ServerError { errHTTPCode = 502+                    , errReasonPhrase = "Bad Gateway"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err503' Service Unavailable+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err503 { errBody = "We're rewriting in PHP." }+--+err503 :: ServerError+err503 = ServerError { errHTTPCode = 503+                    , errReasonPhrase = "Service Unavailable"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err504' Gateway Time-out+--+-- Example:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err504 { errBody = "Backend foobar did not respond in 5 seconds." }+--+err504 :: ServerError+err504 = ServerError { errHTTPCode = 504+                    , errReasonPhrase = "Gateway Time-out"+                    , errBody = ""+                    , errHeaders = []+                    }++-- | 'err505' HTTP Version not supported+--+-- Example usage:+--+-- > failingHandler :: Handler ()+-- > failingHandler = throwError $ err505 { errBody = "I support HTTP/4.0 only." }+--+err505 :: ServerError+err505 = ServerError { errHTTPCode = 505+                    , errReasonPhrase = "HTTP Version not supported"+                    , errBody = ""+                    , errHeaders = []+                    }
test/Servant/Server/Internal/RoutingApplicationSpec.hs view
@@ -23,7 +23,7 @@ import           Network.Wai                  (defaultRequest) import           Servant-import           Servant.Server.Internal.RoutingApplication+import           Servant.Server.Internal import           Test.Hspec import           Test.Hspec.Wai                  (request, shouldRespondWith, with)
test/Servant/Server/RouterSpec.hs view
@@ -30,20 +30,45 @@  routerSpec :: Spec routerSpec = do-  let app' :: Application-      app' = toApplication $ runRouter router'+  describe "tweakResponse" $ do+    let app' :: Application+        app' = toApplication $ runRouter router' -      router', router :: Router ()-      router' = tweakResponse (fmap twk) router-      router = leafRouter $ \_ _ cont -> cont (Route $ responseBuilder (Status 201 "") [] "")+        router', router :: Router ()+        router' = tweakResponse (fmap twk) router+        router = leafRouter $ \_ _ cont -> cont (Route $ responseBuilder (Status 201 "") [] "") -      twk :: Response -> Response-      twk (ResponseBuilder (Status i s) hs b) = ResponseBuilder (Status (i + 1) s) hs b-      twk b = b+        twk :: Response -> Response+        twk (ResponseBuilder (Status i s) hs b) = ResponseBuilder (Status (i + 1) s) hs b+        twk b = b -  describe "tweakResponse" . with (return app') $ do-    it "calls f on route result" $ do-      get "" `shouldRespondWith` 202+    with (return app') $ do+      it "calls f on route result" $ do+        get "" `shouldRespondWith` 202++  describe "runRouter" $ do+    let toApp :: Router () -> Application+        toApp = toApplication . runRouter++        cap :: Router ()+        cap = CaptureRouter $+          let delayed = addCapture (emptyDelayed $ Route pure) (const $ delayedFail err400)+          in leafRouter+             $ \env req res ->+                 runAction delayed env req res+                 . const+                 $ Route success++        router :: Router ()+        router = leafRouter (\_ _ res -> res $ Route success)+          `Choice` cap++        success :: Response+        success = responseBuilder (Status 200 "") [] ""++    with (pure $ toApp router) $ do+      it "capture failure returns a 400 response" $ do+        get "/badcapture" `shouldRespondWith` 400  distributivitySpec :: Spec distributivitySpec =