scotty-0.30: Web/Scotty/Body.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
module Web.Scotty.Body (
newBodyInfo,
cloneBodyInfo
, getFormParamsAndFilesAction
, getBodyAction
, getBodyChunkAction
-- wai-extra
, W.RequestParseException(..)
) where
import Control.Concurrent.MVar
import Control.Monad.IO.Class
import Control.Monad.Trans.Resource (InternalState)
import Data.Bifunctor (first, bimap)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified GHC.Exception as E (throw)
import Network.Wai (Request(..), getRequestBodyChunk)
import qualified Network.Wai.Handler.Warp as Warp (InvalidRequest(..))
import qualified Network.Wai.Parse as W (File, Param, getRequestBodyType, tempFileBackEnd, RequestBodyType(..), sinkRequestBodyEx, RequestParseException(..), ParseRequestBodyOptions)
-- import UnliftIO (MonadUnliftIO(..))
import UnliftIO.Exception (Handler(..), catches, throwIO)
import Web.Scotty.Internal.Types (BodyInfo(..), BodyChunkBuffer(..), BodyPartiallyStreamed(..), RouteOptions(..), File, ScottyException(..), Param)
import Web.Scotty.Util (readRequestBody, decodeUtf8Lenient)
-- | Make a new BodyInfo with readProgress at 0 and an empty BodyChunkBuffer.
newBodyInfo :: (MonadIO m) => Request -> m BodyInfo
newBodyInfo req = liftIO $ do
readProgress <- newMVar 0
chunkBuffer <- newMVar (BodyChunkBuffer False [])
return $ BodyInfo readProgress chunkBuffer (getRequestBodyChunk req)
-- | Make a copy of a BodyInfo, sharing the previous BodyChunkBuffer but with the
-- readProgress MVar reset to 0.
cloneBodyInfo :: (MonadIO m) => BodyInfo -> m BodyInfo
cloneBodyInfo (BodyInfo _ chunkBufferVar getChunk) = liftIO $ do
cleanReadProgressVar <- newMVar 0
return $ BodyInfo cleanReadProgressVar chunkBufferVar getChunk
-- | Get the form params and files from the request.
--
-- NB : catches exceptions from 'warp' and 'wai-extra' and wraps them into 'ScottyException'
getFormParamsAndFilesAction ::
InternalState
-> W.ParseRequestBodyOptions
-> Request -- ^ only used for its body type
-> BodyInfo -- ^ the request body contents are read from here
-> RouteOptions
-> IO ([Param], [File FilePath])
getFormParamsAndFilesAction istate prbo req bodyInfo opts = do
let
bs2t = decodeUtf8Lenient
convertBoth = bimap bs2t bs2t
convertKey = first bs2t
bs <- getBodyAction bodyInfo opts
let
wholeBody = BL.toChunks bs
(formparams, fs) <- parseRequestBodyExBS istate prbo wholeBody (W.getRequestBodyType req) `catches` handleWaiParseSafeExceptions
return (convertBoth <$> formparams, convertKey <$> fs)
-- | Wrap exceptions from upstream libraries into 'ScottyException'
handleWaiParseSafeExceptions :: MonadIO m => [Handler m a]
handleWaiParseSafeExceptions = [h1, h2]
where
h1 = Handler (\ (e :: W.RequestParseException ) -> throwIO $ WaiRequestParseException e)
h2 = Handler (\(e :: Warp.InvalidRequest) -> throwIO $ WarpRequestException e)
-- | Adapted from wai-extra's Network.Wai.Parse, modified to accept body as list of Bytestrings.
-- Reason: WAI's requestBody is an IO action that returns the body as chunks. Once read,
-- they can't be read again. We read them into a lazy Bytestring, so Scotty user can get
-- the raw body, even if they also want to call wai-extra's parsing routines.
parseRequestBodyExBS :: MonadIO m =>
InternalState
-> W.ParseRequestBodyOptions
-> [B.ByteString]
-> Maybe W.RequestBodyType
-> m ([W.Param], [W.File FilePath])
parseRequestBodyExBS istate o bl rty =
case rty of
Nothing -> return ([], [])
Just rbt -> do
mvar <- liftIO $ newMVar bl -- MVar is a bit of a hack so we don't have to inline
-- large portions of Network.Wai.Parse
let provider = modifyMVar mvar $ \bsold -> case bsold of
[] -> return ([], B.empty)
(b:bs) -> return (bs, b)
liftIO $ W.sinkRequestBodyEx o (W.tempFileBackEnd istate) rbt provider
-- | Retrieve the entire body, using the cached chunks in the BodyInfo and reading any other
-- chunks if they still exist.
-- Mimic the previous behavior by throwing 'BodyPartiallyStreamed' if the user has already
-- started reading the body by chunks.
--
-- throw 'ScottyException' if request body too big
getBodyAction :: BodyInfo -> RouteOptions -> IO (BL.ByteString)
getBodyAction (BodyInfo readProgress chunkBufferVar getChunk) opts =
modifyMVar readProgress $ \index ->
modifyMVar chunkBufferVar $ \bcb@(BodyChunkBuffer hasFinished chunks) -> do
if | index > 0 -> E.throw BodyPartiallyStreamed
| hasFinished -> return (bcb, (index, BL.fromChunks chunks))
| otherwise -> do
newChunks <- readRequestBody getChunk return (maxRequestBodySize opts)
return $ (BodyChunkBuffer True (chunks ++ newChunks), (index, BL.fromChunks (chunks ++ newChunks)))
-- | Retrieve a chunk from the body at the index stored in the readProgress MVar.
-- Serve the chunk from the cached array if it's already present; otherwise read another
-- chunk from WAI and advance the index.
getBodyChunkAction :: BodyInfo -> IO BS.ByteString
getBodyChunkAction (BodyInfo readProgress chunkBufferVar getChunk) =
modifyMVar readProgress $ \index ->
modifyMVar chunkBufferVar $ \bcb@(BodyChunkBuffer hasFinished chunks) -> do
if | index < length chunks -> return (bcb, (index + 1, chunks !! index))
| hasFinished -> return (bcb, (index, mempty))
| otherwise -> do
newChunk <- getChunk
return (BodyChunkBuffer (B.null newChunk) (chunks ++ [newChunk]), (index + 1, newChunk))