{-# LANGUAGE CPP #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE LambdaCase #-} {-# language ScopedTypeVariables #-} module Web.Scotty.Action ( addHeader , body , bodyReader , file , rawResponse , files , filesOpts , W.ParseRequestBodyOptions, W.defaultParseRequestBodyOptions , finish , header , headers , html , htmlLazy , json , jsonData , formData , next , pathParam , captureParam , formParam , queryParam , pathParamMaybe , captureParamMaybe , formParamMaybe , queryParamMaybe , pathParams , captureParams , formParams , queryParams , throw , raw , nested , readEither , redirect , redirect300 , redirect301 , redirect302 , redirect303 , redirect304 , redirect307 , redirect308 , request , setHeader , status , stream , text , textLazy , getResponseStatus , getResponseHeaders , getResponseContent , Param , Parsable(..) , ActionT -- private to Scotty , runAction ) where import Blaze.ByteString.Builder (fromLazyByteString) import qualified Control.Exception as E import Control.Monad (when) import Control.Monad.IO.Class (MonadIO(..)) import UnliftIO (MonadUnliftIO(..)) import Control.Monad.Reader (MonadReader(..), ReaderT(..), asks) import Control.Monad.Trans.Resource (withInternalState, runResourceT) import Control.Concurrent.MVar import qualified Data.Aeson as A import Data.Bool (bool) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.CaseInsensitive as CI import Data.Traversable (for) import qualified Data.Map.Strict as Map import qualified Data.HashMap.Strict as HM import Data.Int import Data.List (foldl') import Data.Maybe (maybeToList) import qualified Data.Text as T import Data.Text.Encoding as STE import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TLE import Data.Time (UTCTime) import Data.Time.Format (parseTimeM, defaultTimeLocale) import Data.Typeable (typeOf) import Data.Word import Network.HTTP.Types -- not re-exported until version 0.11 #if !MIN_VERSION_http_types(0,11,0) import Network.HTTP.Types.Status #endif import Network.Wai (Request, Response, StreamingBody, Application, requestHeaders) import Network.Wai.Handler.Warp (InvalidRequest(..)) import qualified Network.Wai.Parse as W (FileInfo(..), ParseRequestBodyOptions, defaultParseRequestBodyOptions) import Numeric.Natural import Web.FormUrlEncoded (Form(..), FromForm(..)) import Web.Scotty.Internal.Types import Web.Scotty.Util (mkResponse, addIfNotPresent, add, replace, lazyTextToStrictByteString, decodeUtf8Lenient) import UnliftIO.Exception (Handler(..), catches, throwIO) import System.IO (hPutStrLn, stderr) import Network.Wai.Internal (ResponseReceived(..)) -- | Evaluate a route, catch all exceptions (user-defined ones, internal and all remaining, in this order) -- and construct the 'Response' -- -- 'Nothing' indicates route failed (due to Next) and pattern matching should try the next available route. -- 'Just' indicates a successful response. runAction :: MonadUnliftIO m => Options -> Maybe (ErrorHandler m) -- ^ this handler (if present) is in charge of user-defined exceptions -> ActionEnv -> ActionT m () -- ^ Route action to be evaluated -> m (Maybe Response) runAction options mh env action = do ok <- flip runReaderT env $ runAM $ tryNext $ action `catches` concat [ [actionErrorHandler] , maybeToList mh , [scottyExceptionHandler options, someExceptionHandler options] ] res <- getResponse env return $ bool Nothing (Just $ mkResponse res) ok -- | Exception handler in charge of 'ActionError'. Rethrowing 'Next' here is caught by 'tryNext'. -- All other cases of 'ActionError' are converted to HTTP responses. actionErrorHandler :: MonadIO m => ErrorHandler m actionErrorHandler = Handler $ \case AERedirect s url -> do status s setHeader "Location" url AENext -> next AEFinish -> return () -- | Default handler for exceptions from scotty scottyExceptionHandler :: MonadIO m => Options -> ErrorHandler m scottyExceptionHandler Options{jsonMode} = Handler $ \case RequestTooLarge -> do status status413 if jsonMode then json $ A.object ["status" A..= (413 :: Int), "description" A..= ("Request body is too large" :: T.Text)] else text "Request body is too large" MalformedJSON bs err -> do status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= ("jsonData: malformed" :: T.Text) , "body" A..= decodeUtf8Lenient (BL.toStrict bs) , "error" A..= err ] else raw $ BL.unlines [ "jsonData: malformed" , "Body: " <> bs , "Error: " <> BL.fromStrict (encodeUtf8 err) ] FailedToParseJSON bs err -> do status status422 if jsonMode then json $ A.object [ "status" A..= (422 :: Int) , "description" A..= ("jsonData: failed to parse" :: T.Text) , "body" A..= decodeUtf8Lenient (BL.toStrict bs) , "error" A..= err ] else raw $ BL.unlines [ "jsonData: failed to parse" , "Body: " <> bs , "Error: " <> BL.fromStrict (encodeUtf8 err) ] MalformedForm err -> do status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= ("formData: malformed" :: T.Text) , "error" A..= err ] else raw $ BL.unlines [ "formData: malformed" , "Error: " <> BL.fromStrict (encodeUtf8 err) ] PathParameterNotFound k -> do status status500 if jsonMode then json $ A.object [ "status" A..= (500 :: Int) , "description" A..= T.unwords [ "Path parameter", k, "not found"] ] else text $ T.unwords [ "Path parameter", k, "not found"] QueryParameterNotFound k -> do status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= T.unwords [ "Query parameter", k, "not found"] ] else text $ T.unwords [ "Query parameter", k, "not found"] FormFieldNotFound k -> do status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= T.unwords [ "Form field", k, "not found"] ] else text $ T.unwords [ "Form field", k, "not found"] FailedToParseParameter k v e -> do status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= T.unwords [ "Failed to parse parameter", k, v, ":", e] ] else text $ T.unwords [ "Failed to parse parameter", k, v, ":", e] WarpRequestException we -> case we of RequestHeaderFieldsTooLarge -> do status status413 if jsonMode then json $ A.object [ "status" A..= (413 :: Int) , "description" A..= ("Request header fields too large" :: T.Text) ] else text "Request header fields too large" weo -> do -- FIXME fall-through case on InvalidRequest, it would be nice to return more specific error messages and codes here status status400 if jsonMode then json $ A.object [ "status" A..= (400 :: Int) , "description" A..= T.unwords ["Request Exception:", T.pack (show weo)] ] else text $ T.unwords ["Request Exception:", T.pack (show weo)] WaiRequestParseException we -> do status status413 -- 413 Content Too Large https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413 if jsonMode then json $ A.object [ "status" A..= (413 :: Int) , "description" A..= T.unwords ["wai-extra Exception:", T.pack (show we)] ] else text $ T.unwords ["wai-extra Exception:", T.pack (show we)] ResourceTException rte -> do status status500 if jsonMode then json $ A.object [ "status" A..= (500 :: Int) , "description" A..= T.unwords ["resourcet Exception:", T.pack (show rte)] ] else text $ T.unwords ["resourcet Exception:", T.pack (show rte)] -- | Uncaught exceptions turn into HTTP 500 Server Error codes someExceptionHandler :: MonadIO m => Options -> ErrorHandler m someExceptionHandler Options{verbose, jsonMode} = Handler $ \(E.SomeException e) -> do when (verbose > 0) $ liftIO $ hPutStrLn stderr $ "Unhandled exception of " <> show (typeOf e) <> ": " <> show e status status500 if jsonMode then json $ A.object [ "status" A..= (500 :: Int) , "description" A..= ("Internal Server Error" :: T.Text) ] else text "Internal Server Error" -- | Throw an exception which can be caught within the scope of the current Action with 'catch'. -- -- If the exception is not caught locally, another option is to implement a global 'Handler' (with 'defaultHandler') that defines its interpretation and a translation to HTTP error codes. -- -- Uncaught exceptions turn into HTTP 500 responses. throw :: (MonadIO m, E.Exception e) => e -> ActionT m a throw = E.throw -- | Abort execution of this action and continue pattern matching routes. -- Like an exception, any code after 'next' is not executed. -- -- NB : Internally, this is implemented with an exception that can only be -- caught by the library, but not by the user. -- -- As an example, these two routes overlap. The only way the second one will -- ever run is if the first one calls 'next'. -- -- > get "/foo/:bar" $ do -- > w :: Text <- pathParam "bar" -- > unless (w == "special") next -- > text "You made a request to /foo/special" -- > -- > get "/foo/:baz" $ do -- > w <- pathParam "baz" -- > text $ "You made a request to: " <> w next :: Monad m => ActionT m a next = E.throw AENext -- | Synonym for 'redirect302'. -- If you are unsure which redirect to use, you probably want this one. -- -- > redirect "http://www.google.com" -- -- OR -- -- > redirect "/foo/bar" redirect :: (Monad m) => T.Text -> ActionT m a redirect = redirect302 -- | Redirect to given URL with status 300 (Multiple Choices). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect300 :: (Monad m) => T.Text -> ActionT m a redirect300 = redirectStatus status300 -- | Redirect to given URL with status 301 (Moved Permanently). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect301 :: (Monad m) => T.Text -> ActionT m a redirect301 = redirectStatus status301 -- | Redirect to given URL with status 302 (Found). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect302 :: (Monad m) => T.Text -> ActionT m a redirect302 = redirectStatus status302 -- | Redirect to given URL with status 303 (See Other). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect303 :: (Monad m) => T.Text -> ActionT m a redirect303 = redirectStatus status303 -- | Redirect to given URL with status 304 (Not Modified). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect304 :: (Monad m) => T.Text -> ActionT m a redirect304 = redirectStatus status304 -- | Redirect to given URL with status 307 (Temporary Redirect). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect307 :: (Monad m) => T.Text -> ActionT m a redirect307 = redirectStatus status307 -- | Redirect to given URL with status 308 (Permanent Redirect). Like throwing -- an uncatchable exception. Any code after the call to -- redirect will not be run. redirect308 :: (Monad m) => T.Text -> ActionT m a redirect308 = redirectStatus status308 redirectStatus :: (Monad m) => Status -> T.Text -> ActionT m a redirectStatus s = E.throw . AERedirect s -- | Finish the execution of the current action. Like throwing an uncatchable -- exception. Any code after the call to finish will not be run. -- -- /Since: 0.10.3/ finish :: (Monad m) => ActionT m a finish = E.throw AEFinish -- | Get the 'Request' object. request :: Monad m => ActionT m Request request = ActionT $ envReq <$> ask -- | Get list of uploaded files. -- -- NB: Loads all file contents in memory with options 'W.defaultParseRequestBodyOptions' files :: MonadUnliftIO m => ActionT m [File BL.ByteString] files = runResourceT $ withInternalState $ \istate -> do (_, fs) <- formParamsAndFilesWith istate W.defaultParseRequestBodyOptions for fs (\(fname, f) -> do bs <- liftIO $ BL.readFile (W.fileContent f) pure (fname, f{ W.fileContent = bs}) ) -- | Get list of uploaded temp files and form parameters decoded from multipart payloads. -- -- NB the temp files are deleted when the continuation exits. filesOpts :: MonadUnliftIO m => W.ParseRequestBodyOptions -> ([Param] -> [File FilePath] -> ActionT m a) -- ^ temp files validation, storage etc -> ActionT m a filesOpts prbo io = runResourceT $ withInternalState $ \istate -> do (ps, fs) <- formParamsAndFilesWith istate prbo io ps fs -- | Get a request header. Header name is case-insensitive. header :: (Monad m) => T.Text -> ActionT m (Maybe T.Text) header k = do hs <- requestHeaders <$> request return $ fmap decodeUtf8Lenient $ lookup (CI.mk (encodeUtf8 k)) hs -- | Get all the request headers. Header names are case-insensitive. headers :: (Monad m) => ActionT m [(T.Text, T.Text)] headers = do hs <- requestHeaders <$> request return [ ( decodeUtf8Lenient (CI.original k) , decodeUtf8Lenient v) | (k,v) <- hs ] -- | Get the request body. -- -- NB This loads the whole request body in memory at once. body :: (MonadIO m) => ActionT m BL.ByteString body = ActionT ask >>= (liftIO . envBody) -- | Get an IO action that reads body chunks -- -- * This is incompatible with 'body' since 'body' consumes all chunks. bodyReader :: Monad m => ActionT m (IO B.ByteString) bodyReader = ActionT $ envBodyChunk <$> ask -- | Parse the request body as a JSON object and return it. -- -- If the JSON object is malformed, this sets the status to -- 400 Bad Request, and throws an exception. -- -- If the JSON fails to parse, this sets the status to -- 422 Unprocessable Entity. -- -- These status codes are as per https://www.restapitutorial.com/httpstatuscodes.html. -- -- NB : Internally this uses 'body'. jsonData :: (A.FromJSON a, MonadIO m) => ActionT m a jsonData = do b <- body when (b == "") $ throwIO $ MalformedJSON b "no data" case A.eitherDecode b of Left err -> throwIO $ MalformedJSON b $ T.pack err Right value -> case A.fromJSON value of A.Error err -> throwIO $ FailedToParseJSON b $ T.pack err A.Success a -> return a -- | Parse the request body as @x-www-form-urlencoded@ form data and return it. -- -- The form is parsed using 'urlDecodeAsForm'. If that returns 'Left', the -- status is set to 400 and an exception is thrown. formData :: (FromForm a, MonadUnliftIO m) => ActionT m a formData = do form <- paramListToForm <$> formParams case fromForm form of Left err -> throwIO $ MalformedForm err Right value -> return value where -- This rather contrived implementation uses cons and reverse to avoid -- quadratic complexity when constructing a Form from a list of Param. -- It's equivalent to using HashMap.insertWith (++) which does have -- quadratic complexity due to appending at the end of list. paramListToForm :: [Param] -> Form paramListToForm = Form . fmap reverse . foldl' (\f (k, v) -> HM.alter (prependValue v) k f) HM.empty prependValue :: a -> Maybe [a] -> Maybe [a] prependValue v = Just . maybe [v] (v :) -- | Synonym for 'pathParam' captureParam :: (Parsable a, MonadIO m) => T.Text -> ActionT m a captureParam = pathParam -- | Look up a path parameter. -- -- * Raises an exception which can be caught by 'catch' if parameter is not found. If the exception is not caught, scotty will return a HTTP error code 500 ("Internal Server Error") to the client. -- -- * If the parameter is found, but 'parseParam' fails to parse to the correct type, 'next' is called. -- -- /Since: 0.20/ pathParam :: (Parsable a, MonadIO m) => T.Text -> ActionT m a pathParam k = do val <- ActionT $ lookup k . envPathParams <$> ask case val of Nothing -> throwIO $ PathParameterNotFound k Just v -> case parseParam $ TL.fromStrict v of Left _ -> next Right a -> pure a -- | Look up a form parameter. -- -- * Raises an exception which can be caught by 'catch' if parameter is not found. If the exception is not caught, scotty will return a HTTP error code 400 ("Bad Request") to the client. -- -- * This function raises a code 400 also if the parameter is found, but 'parseParam' fails to parse to the correct type. -- -- /Since: 0.20/ formParam :: (MonadUnliftIO m, Parsable b) => T.Text -> ActionT m b formParam k = runResourceT $ withInternalState $ \istate -> do (ps, _) <- formParamsAndFilesWith istate W.defaultParseRequestBodyOptions case lookup k ps of Nothing -> throwIO $ FormFieldNotFound k Just v -> case parseParam $ TL.fromStrict v of Left e -> throwIO $ FailedToParseParameter k v (TL.toStrict e) Right a -> pure a -- | Look up a query parameter. -- -- * Raises an exception which can be caught by 'catch' if parameter is not found. If the exception is not caught, scotty will return a HTTP error code 400 ("Bad Request") to the client. -- -- * This function raises a code 400 also if the parameter is found, but 'parseParam' fails to parse to the correct type. -- -- /Since: 0.20/ queryParam :: (Parsable a, MonadIO m) => T.Text -> ActionT m a queryParam = paramWith QueryParameterNotFound envQueryParams -- | Look up a path parameter. Returns 'Nothing' if the parameter is not found or cannot be parsed at the right type. -- -- NB : Doesn't throw exceptions. In particular, route pattern matching will not continue, so developers -- must 'raiseStatus' or 'throw' to signal something went wrong. -- -- /Since: 0.21/ pathParamMaybe :: (Parsable a, Monad m) => T.Text -> ActionT m (Maybe a) pathParamMaybe = paramWithMaybe envPathParams -- | Look up a capture parameter. Returns 'Nothing' if the parameter is not found or cannot be parsed at the right type. -- -- NB : Doesn't throw exceptions. In particular, route pattern matching will not continue, so developers -- must 'raiseStatus' or 'throw' to signal something went wrong. -- -- /Since: 0.21/ captureParamMaybe :: (Parsable a, Monad m) => T.Text -> ActionT m (Maybe a) captureParamMaybe = paramWithMaybe envPathParams -- | Look up a form parameter. Returns 'Nothing' if the parameter is not found or cannot be parsed at the right type. -- -- NB : Doesn't throw exceptions, so developers must 'raiseStatus' or 'throw' to signal something went wrong. -- -- /Since: 0.21/ formParamMaybe :: (MonadUnliftIO m, Parsable a) => T.Text -> ActionT m (Maybe a) formParamMaybe k = runResourceT $ withInternalState $ \istate -> do (ps, _) <- formParamsAndFilesWith istate W.defaultParseRequestBodyOptions case lookup k ps of Nothing -> pure Nothing Just v -> either (const $ pure Nothing) (pure . Just) $ parseParam $ TL.fromStrict v -- | Look up a query parameter. Returns 'Nothing' if the parameter is not found or cannot be parsed at the right type. -- -- NB : Doesn't throw exceptions, so developers must 'raiseStatus' or 'throw' to signal something went wrong. -- -- /Since: 0.21/ queryParamMaybe :: (Parsable a, Monad m) => T.Text -> ActionT m (Maybe a) queryParamMaybe = paramWithMaybe envQueryParams data ParamType = PathParam | FormParam | QueryParam instance Show ParamType where show = \case PathParam -> "path" FormParam -> "form" QueryParam -> "query" paramWith :: (MonadIO m, Parsable b) => (T.Text -> ScottyException) -> (ActionEnv -> [Param]) -> T.Text -- ^ parameter name -> ActionT m b paramWith toError f k = do val <- ActionT $ (lookup k . f) <$> ask case val of Nothing -> throwIO $ toError k Just v -> case parseParam $ TL.fromStrict v of Left e -> throwIO $ FailedToParseParameter k v (TL.toStrict e) Right a -> pure a -- | Look up a parameter. Returns 'Nothing' if the parameter is not found or cannot be parsed at the right type. -- -- NB : Doesn't throw exceptions. -- -- /Since: 0.21/ paramWithMaybe :: (Monad m, Parsable b) => (ActionEnv -> [Param]) -> T.Text -- ^ parameter name -> ActionT m (Maybe b) paramWithMaybe f k = do val <- ActionT $ asks (lookup k . f) case val of Nothing -> pure Nothing Just v -> either (const $ pure Nothing) (pure . Just) $ parseParam $ TL.fromStrict v -- | Get path parameters pathParams :: Monad m => ActionT m [Param] pathParams = paramsWith envPathParams -- | Get path parameters captureParams :: Monad m => ActionT m [Param] captureParams = paramsWith envPathParams -- | Get form parameters formParams :: MonadUnliftIO m => ActionT m [Param] formParams = runResourceT $ withInternalState $ \istate -> do fst <$> formParamsAndFilesWith istate W.defaultParseRequestBodyOptions -- | Get query parameters queryParams :: Monad m => ActionT m [Param] queryParams = paramsWith envQueryParams paramsWith :: Monad m => (ActionEnv -> a) -> ActionT m a paramsWith f = ActionT (asks f) -- === access the fields of the Response being constructed -- | Access the HTTP 'Status' of the Response -- -- /SINCE 0.21/ getResponseStatus :: (MonadIO m) => ActionT m Status getResponseStatus = srStatus <$> getResponseAction -- | Access the HTTP headers of the Response -- -- /SINCE 0.21/ getResponseHeaders :: (MonadIO m) => ActionT m ResponseHeaders getResponseHeaders = srHeaders <$> getResponseAction -- | Access the content of the Response -- -- /SINCE 0.21/ getResponseContent :: (MonadIO m) => ActionT m Content getResponseContent = srContent <$> getResponseAction -- | Minimum implemention: 'parseParam' class Parsable a where -- | Take a 'T.Text' value and parse it as 'a', or fail with a message. parseParam :: TL.Text -> Either TL.Text a -- | Default implementation parses comma-delimited lists. -- -- > parseParamList t = mapM parseParam (T.split (== ',') t) parseParamList :: TL.Text -> Either TL.Text [a] parseParamList t = mapM parseParam (TL.split (== ',') t) -- No point using 'read' for Text, ByteString, Char, and String. instance Parsable T.Text where parseParam = Right . TL.toStrict instance Parsable TL.Text where parseParam = Right instance Parsable B.ByteString where parseParam = Right . lazyTextToStrictByteString instance Parsable BL.ByteString where parseParam = Right . TLE.encodeUtf8 -- | Overrides default 'parseParamList' to parse String. instance Parsable Char where parseParam t = case TL.unpack t of [c] -> Right c _ -> Left "parseParam Char: no parse" parseParamList = Right . TL.unpack -- String -- | Checks if parameter is present and is null-valued, not a literal '()'. -- If the URI requested is: '/foo?bar=()&baz' then 'baz' will parse as (), where 'bar' will not. instance Parsable () where parseParam t = if TL.null t then Right () else Left "parseParam Unit: no parse" instance (Parsable a) => Parsable [a] where parseParam = parseParamList instance Parsable Bool where parseParam t = if t' == TL.toCaseFold "true" || t' == TL.toCaseFold "on" then Right True else if t' == TL.toCaseFold "false" then Right False else Left "parseParam Bool: no parse" where t' = TL.toCaseFold t instance Parsable Double where parseParam = readEither instance Parsable Float where parseParam = readEither instance Parsable Int where parseParam = readEither instance Parsable Int8 where parseParam = readEither instance Parsable Int16 where parseParam = readEither instance Parsable Int32 where parseParam = readEither instance Parsable Int64 where parseParam = readEither instance Parsable Integer where parseParam = readEither instance Parsable Word where parseParam = readEither instance Parsable Word8 where parseParam = readEither instance Parsable Word16 where parseParam = readEither instance Parsable Word32 where parseParam = readEither instance Parsable Word64 where parseParam = readEither instance Parsable Natural where parseParam = readEither -- | parse a UTCTime timestamp formatted as a ISO 8601 timestamp: -- -- @yyyy-mm-ddThh:mm:ssZ@ , where the seconds can have a decimal part with up to 12 digits and no trailing zeros. instance Parsable UTCTime where parseParam t = let fmt = "%FT%T%QZ" in case parseTimeM True defaultTimeLocale fmt (TL.unpack t) of Just d -> Right d _ -> Left $ "parseParam UTCTime: no parse of \"" <> t <> "\"" -- | Useful for creating 'Parsable' instances for things that already implement 'Read'. Ex: -- -- > instance Parsable Int where parseParam = readEither readEither :: Read a => TL.Text -> Either TL.Text a readEither t = case [ x | (x,"") <- reads (TL.unpack t) ] of [x] -> Right x [] -> Left "readEither: no parse" _ -> Left "readEither: ambiguous parse" -- | Set the HTTP response status. status :: MonadIO m => Status -> ActionT m () status = modifyResponse . setStatus -- Not exported, but useful in the functions below. changeHeader :: MonadIO m => (CI.CI B.ByteString -> B.ByteString -> [(HeaderName, B.ByteString)] -> [(HeaderName, B.ByteString)]) -> T.Text -> T.Text -> ActionT m () changeHeader f k = modifyResponse . setHeaderWith . f (CI.mk $ encodeUtf8 k) . encodeUtf8 -- | Add to the response headers. Header names are case-insensitive. addHeader :: MonadIO m => T.Text -> T.Text -> ActionT m () addHeader = changeHeader add -- | Set one of the response headers. Will override any previously set value for that header. -- Header names are case-insensitive. setHeader :: MonadIO m => T.Text -> T.Text -> ActionT m () setHeader = changeHeader replace -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/plain; charset=utf-8\" if it has not already been set. text :: (MonadIO m) => T.Text -> ActionT m () text t = do changeHeader addIfNotPresent "Content-Type" "text/plain; charset=utf-8" raw $ BL.fromStrict $ encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/plain; charset=utf-8\" if it has not already been set. textLazy :: (MonadIO m) => TL.Text -> ActionT m () textLazy t = do changeHeader addIfNotPresent "Content-Type" "text/plain; charset=utf-8" raw $ TLE.encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/html; charset=utf-8\" if it has not already been set. html :: (MonadIO m) => T.Text -> ActionT m () html t = do changeHeader addIfNotPresent "Content-Type" "text/html; charset=utf-8" raw $ BL.fromStrict $ encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/html; charset=utf-8\" if it has not already been set. htmlLazy :: (MonadIO m) => TL.Text -> ActionT m () htmlLazy t = do changeHeader addIfNotPresent "Content-Type" "text/html; charset=utf-8" raw $ TLE.encodeUtf8 t -- | Send a file as the response. Doesn't set the \"Content-Type\" header, so you probably -- want to do that on your own with 'setHeader'. Setting a status code will have no effect -- because Warp will overwrite that to 200 (see 'Network.Wai.Handler.Warp.Internal.sendResponse'). file :: MonadIO m => FilePath -> ActionT m () file = modifyResponse . setContent . ContentFile rawResponse :: MonadIO m => Response -> ActionT m () rawResponse = modifyResponse . setContent . ContentResponse -- | Set the body of the response to the JSON encoding of the given value. Also sets \"Content-Type\" -- header to \"application/json; charset=utf-8\" if it has not already been set. json :: (A.ToJSON a, MonadIO m) => a -> ActionT m () json v = do changeHeader addIfNotPresent "Content-Type" "application/json; charset=utf-8" raw $ A.encode v -- | Set the body of the response to a Source. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'setHeader'. stream :: MonadIO m => StreamingBody -> ActionT m () stream = modifyResponse . setContent . ContentStream -- | Set the body of the response to the given 'BL.ByteString' value. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'setHeader'. raw :: MonadIO m => BL.ByteString -> ActionT m () raw = modifyResponse . setContent . ContentBuilder . fromLazyByteString -- | Nest a whole WAI application inside a Scotty handler. -- See Web.Scotty for further documentation nested :: (MonadIO m) => Network.Wai.Application -> ActionT m () nested app = do -- Is MVar really the best choice here? Not sure. r <- request ref <- liftIO $ newEmptyMVar _ <- liftIO $ app r (\res -> putMVar ref res >> return ResponseReceived) res <- liftIO $ readMVar ref rawResponse res