Response wrapping and error handling done properly

This commit is contained in:
Michael Snoyman 2009-09-18 09:36:47 +03:00
parent 86ca811ac5
commit c75c72d9cb
3 changed files with 42 additions and 42 deletions

View File

@ -23,8 +23,8 @@ module Web.Restful.Application
) where ) where
import Web.Encodings import Web.Encodings
import Data.ByteString.Class
import qualified Data.ByteString.Lazy as B import qualified Data.ByteString.Lazy as B
import Data.Object
import qualified Hack import qualified Hack
import Hack.Middleware.CleanPath import Hack.Middleware.CleanPath
@ -59,15 +59,19 @@ class ResourceName a b => RestfulApp a b | a -> b where
, methodOverride , methodOverride
] ]
-- | How to generate 404 pages. FIXME make more user-friendly.
response404 :: a -> Hack.Env -> IO Hack.Response
response404 _ = default404
-- | Wrappers for cleaning up responses. Especially intended for -- | Wrappers for cleaning up responses. Especially intended for
-- beautifying static HTML. FIXME more user friendly. -- beautifying static HTML. FIXME more user friendly.
responseWrapper :: a -> String -> B.ByteString -> IO B.ByteString responseWrapper :: a -> String -> B.ByteString -> IO B.ByteString
responseWrapper _ _ = return responseWrapper _ _ = return
-- | Output error response pages.
errorHandler :: a -> RawRequest -> ErrorResult -> HasRepsW
errorHandler _ rr NotFound = HasRepsW $ toObject $ "Not found: " ++ show rr
errorHandler _ _ (Redirect url) =
HasRepsW $ toObject $ "Redirect to: " ++ url
errorHandler _ _ (InternalError e) =
HasRepsW $ toObject $ "Internal server error: " ++ e
-- | Given a sample resource name (purely for typing reasons), generating -- | Given a sample resource name (purely for typing reasons), generating
-- a Hack application. -- a Hack application.
toHackApp :: RestfulApp resourceName modelType toHackApp :: RestfulApp resourceName modelType
@ -107,19 +111,20 @@ toHackApplication :: RestfulApp resourceName model
-> Hack.Application -> Hack.Application
toHackApplication sampleRN hm env = do toHackApplication sampleRN hm env = do
let (Right resource) = splitPath $ Hack.pathInfo env let (Right resource) = splitPath $ Hack.pathInfo env
case findResourceNames resource of let (handler, urlParams') =
[] -> response404 sampleRN $ env case findResourceNames resource of
[(rn, urlParams')] -> do [] -> (noHandler, [])
let verb :: Verb [(rn, urlParams'')] ->
verb = toVerb $ Hack.requestMethod env let verb = toVerb $ Hack.requestMethod env
rr :: RawRequest in (hm rn verb, urlParams'')
rr = envToRawRequest urlParams' env x -> error $ "Invalid findResourceNames: " ++ show x
handler :: Handler let rr = envToRawRequest urlParams' env
handler = hm rn verb let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env ctypes' = parseHttpAccept rawHttpAccept
ctypes' = parseHttpAccept rawHttpAccept runResponse (errorHandler sampleRN rr)
runResponse (handler rr) ctypes' (responseWrapper sampleRN)
x -> error $ "Invalid matches: " ++ show x ctypes'
(handler rr)
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest urlParams' env = envToRawRequest urlParams' env =
@ -132,10 +137,3 @@ envToRawRequest urlParams' env =
rawCookie = tryLookup "" "Cookie" $ Hack.http env rawCookie = tryLookup "" "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)] cookies' = decodeCookies rawCookie :: [(String, String)]
in RawRequest rawPieces urlParams' gets' posts cookies' files env in RawRequest rawPieces urlParams' gets' posts cookies' files env
default404 :: Hack.Env -> IO Hack.Response
default404 env = return $
Hack.Response
404
[("Content-Type", "text/plain")]
$ toLazyByteString $ "Not found: " ++ Hack.pathInfo env

View File

@ -1,6 +1,7 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverlappingInstances #-} {-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE StandaloneDeriving #-}
--------------------------------------------------------- ---------------------------------------------------------
-- --
-- Module : Web.Restful.Request -- Module : Web.Restful.Request
@ -210,6 +211,9 @@ data RawRequest = RawRequest
, rawFiles :: [(ParamName, FileInfo)] , rawFiles :: [(ParamName, FileInfo)]
, rawEnv :: Hack.Env , rawEnv :: Hack.Env
} }
deriving Show
deriving instance Show FileInfo
-- | All GET paramater values with the given name. -- | All GET paramater values with the given name.
getParams :: RawRequest -> ParamName -> [ParamValue] getParams :: RawRequest -> ParamName -> [ParamValue]

View File

@ -29,6 +29,8 @@ module Web.Restful.Response
, header , header
, GenResponse (..) , GenResponse (..)
, liftIO , liftIO
, ErrorResult (..)
, HasRepsW (..)
) where ) where
import Data.ByteString.Class import Data.ByteString.Class
@ -61,47 +63,43 @@ data HasRepsW = forall a. HasReps a => HasRepsW a
instance HasReps HasRepsW where instance HasReps HasRepsW where
reps (HasRepsW r) = reps r reps (HasRepsW r) = reps r
-- | The result of a request. This does not include possible headers. data ErrorResult =
data Result =
Redirect String Redirect String
| NotFound | NotFound
| InternalError String | InternalError String
| Content HasRepsW
instance HasReps Result where getStatus :: ErrorResult -> Int
reps (Redirect s) = [("text/plain", toLazyByteString s)]
reps NotFound = [("text/plain", toLazyByteString "not found")] -- FIXME use the real 404 page
reps (InternalError s) = [("text/plain", toLazyByteString s)]
reps (Content r) = reps r
getStatus :: Result -> Int
getStatus (Redirect _) = 303 getStatus (Redirect _) = 303
getStatus NotFound = 404 getStatus NotFound = 404
getStatus (InternalError _) = 500 getStatus (InternalError _) = 500
getStatus (Content _) = 200
getHeaders :: Result -> [Header] getHeaders :: ErrorResult -> [Header]
getHeaders (Redirect s) = [Header "Location" s] getHeaders (Redirect s) = [Header "Location" s]
getHeaders _ = [] getHeaders _ = []
newtype ResponseT m a = ResponseT (m (Either Result a, [Header])) newtype ResponseT m a = ResponseT (m (Either ErrorResult a, [Header]))
type ResponseIO = ResponseT IO type ResponseIO = ResponseT IO
type Response = ResponseIO HasRepsW type Response = ResponseIO HasRepsW
runResponse :: Response -> [ContentType] -> IO Hack.Response runResponse :: (ErrorResult -> HasRepsW)
runResponse (ResponseT inside) ctypesAll = do -> (ContentType -> B.ByteString -> IO B.ByteString)
-> [ContentType]
-> Response
-> IO Hack.Response
runResponse eh wrapper ctypesAll (ResponseT inside) = do
(x, headers') <- inside (x, headers') <- inside
let extraHeaders = let extraHeaders =
case x of case x of
Left r -> getHeaders r Left r -> getHeaders r
Right _ -> [] Right _ -> []
headers <- mapM toPair (headers' ++ extraHeaders) headers <- mapM toPair (headers' ++ extraHeaders)
let outReps = either reps reps x let outReps = either (reps . eh) reps x
let statusCode = let statusCode =
case x of case x of
Left r -> getStatus r Left r -> getStatus r
Right _ -> 200 Right _ -> 200
(ctype, finalRep) <- chooseRep outReps ctypesAll (ctype, selectedRep) <- chooseRep outReps ctypesAll
finalRep <- wrapper ctype selectedRep
let headers'' = ("Content-Type", ctype) : headers let headers'' = ("Content-Type", ctype) : headers
return $! Hack.Response statusCode headers'' finalRep return $! Hack.Response statusCode headers'' finalRep