Proper handling of impure exceptions within HandlerError values
This commit is contained in:
parent
85e7fd7e33
commit
a3d9a13abe
@ -1,3 +1,7 @@
|
|||||||
|
## 1.4.22
|
||||||
|
|
||||||
|
* Proper handling of impure exceptions within `HandlerError` values
|
||||||
|
|
||||||
## 1.4.21
|
## 1.4.21
|
||||||
|
|
||||||
* Add support for `Encoding` from `aeson-0.11` [#1241](https://github.com/yesodweb/yesod/pull/1241)
|
* Add support for `Encoding` from `aeson-0.11` [#1241](https://github.com/yesodweb/yesod/pull/1241)
|
||||||
|
|||||||
@ -15,8 +15,7 @@ import Yesod.Core.Internal.Response
|
|||||||
import Blaze.ByteString.Builder (toByteString)
|
import Blaze.ByteString.Builder (toByteString)
|
||||||
import Control.Exception (fromException, evaluate)
|
import Control.Exception (fromException, evaluate)
|
||||||
import qualified Control.Exception as E
|
import qualified Control.Exception as E
|
||||||
import Control.Exception.Lifted (catch)
|
import Control.Monad (mplus, (<=<))
|
||||||
import Control.Monad (mplus)
|
|
||||||
import Control.Monad.IO.Class (MonadIO)
|
import Control.Monad.IO.Class (MonadIO)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Logger (LogLevel (LevelError), LogSource,
|
import Control.Monad.Logger (LogLevel (LevelError), LogSource,
|
||||||
@ -38,9 +37,6 @@ import Language.Haskell.TH.Syntax (Loc, qLocation)
|
|||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Network.Wai.Internal
|
import Network.Wai.Internal
|
||||||
#if !MIN_VERSION_base(4, 6, 0)
|
|
||||||
import Prelude hiding (catch)
|
|
||||||
#endif
|
|
||||||
import System.Log.FastLogger (LogStr, toLogStr)
|
import System.Log.FastLogger (LogStr, toLogStr)
|
||||||
import Yesod.Core.Content
|
import Yesod.Core.Content
|
||||||
import Yesod.Core.Class.Yesod
|
import Yesod.Core.Class.Yesod
|
||||||
@ -62,6 +58,36 @@ newtype WrappedBS = WrappedBS { unWrappedBS :: S8.ByteString }
|
|||||||
instance NFData WrappedBS
|
instance NFData WrappedBS
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
-- | Catch all synchronous exceptions, ignoring asynchronous
|
||||||
|
-- exceptions.
|
||||||
|
--
|
||||||
|
-- Ideally we'd use this from a different library
|
||||||
|
catchSync :: IO a -> (E.SomeException -> IO a) -> IO a
|
||||||
|
catchSync thing after = thing `E.catch` \e ->
|
||||||
|
if isAsyncException e
|
||||||
|
then E.throwIO e
|
||||||
|
else after e
|
||||||
|
|
||||||
|
-- | Determine if an exception is asynchronous
|
||||||
|
--
|
||||||
|
-- Also worth being upstream
|
||||||
|
isAsyncException :: E.SomeException -> Bool
|
||||||
|
isAsyncException e =
|
||||||
|
case fromException e of
|
||||||
|
Just E.SomeAsyncException{} -> True
|
||||||
|
Nothing -> False
|
||||||
|
|
||||||
|
-- | Convert an exception into an ErrorResponse
|
||||||
|
toErrorHandler :: E.SomeException -> IO ErrorResponse
|
||||||
|
toErrorHandler e0 = flip catchSync errFromShow $
|
||||||
|
case fromException e0 of
|
||||||
|
Just (HCError x) -> evaluate $!! x
|
||||||
|
_
|
||||||
|
| isAsyncException e0 -> E.throwIO e0
|
||||||
|
| otherwise -> errFromShow e0
|
||||||
|
where
|
||||||
|
errFromShow x = return $! InternalError $! T.pack $! show x
|
||||||
|
|
||||||
-- | Function used internally by Yesod in the process of converting a
|
-- | Function used internally by Yesod in the process of converting a
|
||||||
-- 'HandlerT' into an 'Application'. Should not be needed by users.
|
-- 'HandlerT' into an 'Application'. Should not be needed by users.
|
||||||
runHandler :: ToTypedContent c
|
runHandler :: ToTypedContent c
|
||||||
@ -69,10 +95,6 @@ runHandler :: ToTypedContent c
|
|||||||
-> HandlerT site IO c
|
-> HandlerT site IO c
|
||||||
-> YesodApp
|
-> YesodApp
|
||||||
runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -> do
|
runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -> do
|
||||||
let toErrorHandler e =
|
|
||||||
case fromException e of
|
|
||||||
Just (HCError x) -> x
|
|
||||||
_ -> InternalError $ T.pack $ show e
|
|
||||||
istate <- liftIO $ I.newIORef GHState
|
istate <- liftIO $ I.newIORef GHState
|
||||||
{ ghsSession = reqSession yreq
|
{ ghsSession = reqSession yreq
|
||||||
, ghsRBC = Nothing
|
, ghsRBC = Nothing
|
||||||
@ -88,41 +110,44 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
, handlerToParent = const ()
|
, handlerToParent = const ()
|
||||||
, handlerResource = resState
|
, handlerResource = resState
|
||||||
}
|
}
|
||||||
contents' <- catch (fmap Right $ unHandlerT handler hd)
|
contents' <- catchSync (fmap Right $ unHandlerT handler hd)
|
||||||
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
|
(\e -> do
|
||||||
$ fromException e)
|
eh <- toErrorHandler e
|
||||||
|
return $ Left $ maybe (HCError eh) id $ fromException e)
|
||||||
state <- liftIO $ I.readIORef istate
|
state <- liftIO $ I.readIORef istate
|
||||||
|
|
||||||
(finalSession, mcontents1) <- (do
|
(finalSession, mcontents1) <- (do
|
||||||
finalSession <- returnDeepSessionMap (ghsSession state)
|
finalSession <- returnDeepSessionMap (ghsSession state)
|
||||||
return (finalSession, Nothing)) `E.catch` \e -> return
|
return (finalSession, Nothing)) `catchSync` \e -> return
|
||||||
(Map.empty, Just $! HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
(Map.empty, Just $! HCError $! InternalError $! T.pack $! show e)
|
||||||
|
|
||||||
(headers, mcontents2) <- (do
|
(headers, mcontents2) <- (do
|
||||||
headers <- return $!! appEndo (ghsHeaders state) []
|
headers <- return $!! appEndo (ghsHeaders state) []
|
||||||
return (headers, Nothing)) `E.catch` \e -> return
|
return (headers, Nothing)) `catchSync` \e -> return
|
||||||
([], Just $! HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
([], Just $! HCError $! InternalError $! T.pack $! show e)
|
||||||
|
|
||||||
let contents =
|
let contents =
|
||||||
case mcontents1 `mplus` mcontents2 of
|
case mcontents1 `mplus` mcontents2 of
|
||||||
Just x -> x
|
Just x -> x
|
||||||
Nothing -> either id (HCContent defaultStatus . toTypedContent) contents'
|
Nothing -> either id (HCContent defaultStatus . toTypedContent) contents'
|
||||||
let handleError e = flip runInternalState resState $ do
|
let handleError e0 = do
|
||||||
yar <- rheOnError e yreq
|
e <- (evaluate $!! e0) `catchSync` \e -> return $! InternalError $! T.pack $! show e
|
||||||
{ reqSession = finalSession
|
flip runInternalState resState $ do
|
||||||
}
|
yar <- rheOnError e yreq
|
||||||
case yar of
|
{ reqSession = finalSession
|
||||||
YRPlain status' hs ct c sess ->
|
}
|
||||||
let hs' = headers ++ hs
|
case yar of
|
||||||
status
|
YRPlain status' hs ct c sess ->
|
||||||
| status' == defaultStatus = getStatus e
|
let hs' = headers ++ hs
|
||||||
| otherwise = status'
|
status
|
||||||
in return $ YRPlain status hs' ct c sess
|
| status' == defaultStatus = getStatus e
|
||||||
YRWai _ -> return yar
|
| otherwise = status'
|
||||||
YRWaiApp _ -> return yar
|
in return $ YRPlain status hs' ct c sess
|
||||||
|
YRWai _ -> return yar
|
||||||
|
YRWaiApp _ -> return yar
|
||||||
let sendFile' ct fp p =
|
let sendFile' ct fp p =
|
||||||
return $ YRPlain H.status200 headers ct (ContentFile fp p) finalSession
|
return $ YRPlain H.status200 headers ct (ContentFile fp p) finalSession
|
||||||
contents1 <- evaluate contents `E.catch` \e -> return
|
contents1 <- evaluate contents `catchSync` \e -> return
|
||||||
(HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
(HCError $! InternalError $! T.pack $! show (e :: E.SomeException))
|
||||||
case contents1 of
|
case contents1 of
|
||||||
HCContent status (TypedContent ct c) -> do
|
HCContent status (TypedContent ct c) -> do
|
||||||
@ -141,9 +166,9 @@ runHandler rhe@RunHandlerEnv {..} handler yreq = withInternalState $ \resState -
|
|||||||
return $ YRPlain
|
return $ YRPlain
|
||||||
status hs typePlain emptyContent
|
status hs typePlain emptyContent
|
||||||
finalSession
|
finalSession
|
||||||
HCSendFile ct fp p -> catch
|
HCSendFile ct fp p -> catchSync
|
||||||
(sendFile' ct fp p)
|
(sendFile' ct fp p)
|
||||||
(handleError . toErrorHandler)
|
(handleError <=< toErrorHandler)
|
||||||
HCCreated loc -> do
|
HCCreated loc -> do
|
||||||
let hs = Header "Location" (encodeUtf8 loc) : headers
|
let hs = Header "Location" (encodeUtf8 loc) : headers
|
||||||
return $ YRPlain
|
return $ YRPlain
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
@ -41,6 +42,7 @@ import qualified Data.Text as T
|
|||||||
import qualified Data.Text.Lazy.Builder as TBuilder
|
import qualified Data.Text.Lazy.Builder as TBuilder
|
||||||
import Data.Time (UTCTime)
|
import Data.Time (UTCTime)
|
||||||
import Data.Typeable (Typeable)
|
import Data.Typeable (Typeable)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import Language.Haskell.TH.Syntax (Loc)
|
import Language.Haskell.TH.Syntax (Loc)
|
||||||
import qualified Network.HTTP.Types as H
|
import qualified Network.HTTP.Types as H
|
||||||
import Network.Wai (FilePart,
|
import Network.Wai (FilePart,
|
||||||
@ -314,7 +316,8 @@ data ErrorResponse =
|
|||||||
| NotAuthenticated
|
| NotAuthenticated
|
||||||
| PermissionDenied Text
|
| PermissionDenied Text
|
||||||
| BadMethod H.Method
|
| BadMethod H.Method
|
||||||
deriving (Show, Eq, Typeable)
|
deriving (Show, Eq, Typeable, Generic)
|
||||||
|
instance NFData ErrorResponse
|
||||||
|
|
||||||
----- header stuff
|
----- header stuff
|
||||||
-- | Headers to be added to a 'Result'.
|
-- | Headers to be added to a 'Result'.
|
||||||
|
|||||||
@ -6,6 +6,8 @@ module YesodCoreTest.Exceptions (exceptionsTest, Widget) where
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
|
import Yesod.Core.Types (HandlerContents (HCError))
|
||||||
|
import Control.Exception (throwIO)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Network.Wai.Test
|
import Network.Wai.Test
|
||||||
import Network.HTTP.Types (status301)
|
import Network.HTTP.Types (status301)
|
||||||
@ -14,11 +16,15 @@ data Y = Y
|
|||||||
mkYesod "Y" [parseRoutes|
|
mkYesod "Y" [parseRoutes|
|
||||||
/ RootR GET
|
/ RootR GET
|
||||||
/redirect RedirR GET
|
/redirect RedirR GET
|
||||||
|
/impure ImpureR GET
|
||||||
|]
|
|]
|
||||||
|
|
||||||
instance Yesod Y where
|
instance Yesod Y where
|
||||||
approot = ApprootStatic "http://test"
|
approot = ApprootStatic "http://test"
|
||||||
errorHandler (InternalError e) = return $ toTypedContent e
|
errorHandler (InternalError e) = do
|
||||||
|
_ <- return $! e
|
||||||
|
addHeader "ERROR" "HANDLER"
|
||||||
|
return $ toTypedContent e
|
||||||
errorHandler x = defaultErrorHandler x
|
errorHandler x = defaultErrorHandler x
|
||||||
|
|
||||||
getRootR :: Handler ()
|
getRootR :: Handler ()
|
||||||
@ -29,10 +35,14 @@ getRedirR = do
|
|||||||
addHeader "foo" "bar"
|
addHeader "foo" "bar"
|
||||||
redirectWith status301 RootR
|
redirectWith status301 RootR
|
||||||
|
|
||||||
|
getImpureR :: Handler ()
|
||||||
|
getImpureR = liftIO $ throwIO $ HCError $ InternalError $ error "impure!"
|
||||||
|
|
||||||
exceptionsTest :: Spec
|
exceptionsTest :: Spec
|
||||||
exceptionsTest = describe "Test.Exceptions" $ do
|
exceptionsTest = describe "Test.Exceptions" $ do
|
||||||
it "500" case500
|
it "500" case500
|
||||||
it "redirect keeps headers" caseRedirect
|
it "redirect keeps headers" caseRedirect
|
||||||
|
it "deals with impure InternalError values" caseImpure
|
||||||
|
|
||||||
runner :: Session () -> IO ()
|
runner :: Session () -> IO ()
|
||||||
runner f = toWaiApp Y >>= runSession f
|
runner f = toWaiApp Y >>= runSession f
|
||||||
@ -48,3 +58,10 @@ caseRedirect = runner $ do
|
|||||||
res <- request defaultRequest { pathInfo = ["redirect"] }
|
res <- request defaultRequest { pathInfo = ["redirect"] }
|
||||||
assertStatus 301 res
|
assertStatus 301 res
|
||||||
assertHeader "foo" "bar" res
|
assertHeader "foo" "bar" res
|
||||||
|
|
||||||
|
caseImpure :: IO ()
|
||||||
|
caseImpure = runner $ do
|
||||||
|
res <- request defaultRequest { pathInfo = ["impure"] }
|
||||||
|
assertStatus 500 res
|
||||||
|
assertBodyContains "impure!" res
|
||||||
|
assertHeader "ERROR" "HANDLER" res
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: yesod-core
|
name: yesod-core
|
||||||
version: 1.4.21
|
version: 1.4.22
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michael Snoyman <michael@snoyman.com>
|
author: Michael Snoyman <michael@snoyman.com>
|
||||||
@ -22,7 +22,7 @@ extra-source-files:
|
|||||||
README.md
|
README.md
|
||||||
|
|
||||||
library
|
library
|
||||||
build-depends: base >= 4.3 && < 5
|
build-depends: base >= 4.6 && < 5
|
||||||
, time >= 1.1.4
|
, time >= 1.1.4
|
||||||
, wai >= 3.0
|
, wai >= 3.0
|
||||||
, wai-extra >= 3.0.7
|
, wai-extra >= 3.0.7
|
||||||
@ -63,7 +63,7 @@ library
|
|||||||
, unix-compat
|
, unix-compat
|
||||||
, conduit-extra
|
, conduit-extra
|
||||||
, exceptions >= 0.6
|
, exceptions >= 0.6
|
||||||
, deepseq
|
, deepseq >= 1.4
|
||||||
, mwc-random
|
, mwc-random
|
||||||
, primitive
|
, primitive
|
||||||
, word8
|
, word8
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user