Proper handling of impure exceptions within HandlerError values

This commit is contained in:
Michael Snoyman 2016-06-22 18:24:18 +03:00
parent 85e7fd7e33
commit a3d9a13abe
5 changed files with 86 additions and 37 deletions

View File

@ -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)

View File

@ -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

View File

@ -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'.

View File

@ -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

View File

@ -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