packages feed

warp-effectful-1.0.0: src/Effectful/Wai/Handler/Warp.hs

{-# LANGUAGE Trustworthy #-}

-- |
-- Module      : Effectful.Wai.Handler.Warp
-- Copyright   : (c) 2026 Institute for Digital Autonomy
-- License     : EUPL-1.2
-- Maintainer  : IDA
--
-- Effectful bindings for the <http://hackage.haskell.org/package/warp warp library>.
--
-- = Example usage
--
-- Here is an 'Application' that serves requests using an arbitrary effect stack;
-- in this case it uses the 'State' effect to count the number of requests.
--
-- Run it using one of the 'run' functions:
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- > import Effectful
-- > import Effectful.State.Static.Local (State, evalState, modify)
-- > import Effectful.Wai (Application, responseLBS)
-- > import Effectful.Wai.Handler.Warp qualified as Warp
-- > import Network.HTTP.Types (status200)
-- >
-- > app :: (State Int :> es) => Application es
-- > app _ respond = do
-- >     modify @Int succ
-- >     respond $
-- >         responseLBS
-- >             status200
-- >             [("Content-Type", "text/plain")]
-- >             "Hello, Web!"
-- >
-- > main :: IO ()
-- > main = runEff . evalState @Int 0 . Warp.run 8080 $ app
module Effectful.Wai.Handler.Warp
    ( -- * Run a Warp server

      -- | All of these automatically serve the same 'Application' over HTTP\/1,
      -- HTTP\/1.1, and HTTP\/2.
      run
    , runEnv
    , runSettings
    , runSettingsSocket

      -- * Settings
    , Settings
    , defaultSettings

      -- ** Setters
    , setPort
    , setHost
    , setOnException
    , setOnExceptionResponse
    , setOnOpen
    , setOnClose
    , setTimeout
    , setManager
    , setFdCacheDuration
    , setFileInfoCacheDuration
    , setBeforeMainLoop
    , setNoParsePath
    , setInstallShutdownHandler
    , setServerName
    , setMaximumBodyFlush
    , setFork
    , setAccept
    , setProxyProtocolNone
    , setProxyProtocolRequired
    , setProxyProtocolOptional
    , setSlowlorisSize
    , setHTTP2Disabled
    , setLogger
    , setServerPushLogger
    , setGracefulShutdownTimeout
    , setGracefulCloseTimeout1
    , setGracefulCloseTimeout2
    , setMaxTotalHeaderLength
    , setAltSvc
    , setMaxBuilderResponseBufferSize

      -- ** Getters
    , getPort
    , getHost
    , getOnOpen
    , getOnClose
    , getOnException
    , getGracefulShutdownTimeout
    , getGracefulCloseTimeout1
    , getGracefulCloseTimeout2
    , getOpenConnectionCounter
    , getServerState

      -- ** Internal server state
    , ServerState
    , makeSettingsAndServerState
    , currentOpenConnections
    , currentShuttingDownState

      -- *** STM versions
    , Warp.currentOpenConnectionsSTM
    , Warp.currentShuttingDownStateSTM

      -- ** Connection counter
    , makeSettingsAndCounter
    , Counter
    , getCount

      -- ** Exception handler
    , defaultOnException
    , defaultShouldDisplayException

      -- ** Exception response handler
    , defaultOnExceptionResponse
    , exceptionResponseForDebug

      -- * Data types
    , HostPreference
    , Port
    , InvalidRequest (..)

      -- * Utilities
    , pauseTimeout
    , FileInfo (..)
    , getFileInfo
    , clientCertificate
    , withApplication
    , withApplicationSettings
    , testWithApplication
    , testWithApplicationSettings
    , openFreePort

      -- * Version
    , warpVersion

      -- * HTTP/2

      -- ** HTTP2 data
    , HTTP2Data
    , http2dataPushPromise
    , http2dataTrailers
    , defaultHTTP2Data
    , getHTTP2Data
    , setHTTP2Data
    , modifyHTTP2Data

      -- ** Push promise
    , PushPromise
    , promisedPath
    , promisedFile
    , promisedResponseHeaders
    , promisedWeight
    , defaultPushPromise
    )
where

import Control.Concurrent (forkIOWithUnmask)
import Control.Exception (SomeException)
import Control.Monad (void)
import Data.Bifunctor (second)
import Data.ByteString (ByteString)
import Data.X509 (CertificateChain)
import Effectful
import Effectful.Wai
    ( Application
    , Request
    , Response
    , liftRequest
    , liftResponse
    , unliftApplication
    , unliftRequestNoBody
    , unliftResponse
    )
import Network.HTTP.Types qualified as HTTP
import Network.Socket (SockAddr, Socket, accept)
import Network.Wai.Handler.Warp
    ( Counter
    , FileInfo
    , HTTP2Data (..)
    , HostPreference
    , InvalidRequest
    , Port
    , PushPromise (..)
    , ServerState
    , defaultHTTP2Data
    , defaultPushPromise
    , defaultShouldDisplayException
    , getCount
    , warpVersion
    )
import Network.Wai.Handler.Warp qualified as Warp
import Network.Wai.Handler.Warp.Internal qualified as WarpI
import System.TimeManager (Manager)
import Prelude

-- | Lifted 'Warp.Settings'.
data Settings es = Settings
    { settingsPort :: Port
    , settingsHost :: HostPreference
    , settingsOnException :: Maybe (Request es) -> SomeException -> Eff es ()
    , settingsOnExceptionResponse :: SomeException -> Response es
    , settingsOnOpen :: SockAddr -> Eff es Bool
    , settingsOnClose :: SockAddr -> Eff es ()
    , settingsTimeout :: Int
    , settingsManager :: Maybe Manager
    , settingsFdCacheDuration :: Int
    , settingsFileInfoCacheDuration :: Int
    , settingsBeforeMainLoop :: Eff es ()
    , settingsFork :: ((forall a. Eff es a -> Eff es a) -> Eff es ()) -> Eff es ()
    , settingsAccept :: Socket -> Eff es (Socket, SockAddr)
    , settingsNoParsePath :: Bool
    , settingsInstallShutdownHandler :: Eff es () -> Eff es ()
    , settingsServerName :: ByteString
    , settingsMaximumBodyFlush :: Maybe Int
    , settingsProxyProtocol :: WarpI.ProxyProtocol
    , settingsSlowlorisSize :: Int
    , settingsHTTP2Enabled :: Bool
    , settingsLogger :: Request es -> HTTP.Status -> Maybe Integer -> Eff es ()
    , settingsServerPushLogger :: Request es -> ByteString -> Integer -> Eff es ()
    , settingsGracefulShutdownTimeout :: Maybe Int
    , settingsGracefulCloseTimeout1 :: Int
    , settingsGracefulCloseTimeout2 :: Int
    , settingsMaxTotalHeaderLength :: Int
    , settingsAltSvc :: Maybe ByteString
    , settingsMaxBuilderResponseBufferSize :: Int
    , settingsConnectionCounter :: Maybe Counter
    , settingsServerState :: Maybe ServerState
    }

-- | Lifted 'Warp.defaultSettings'.
defaultSettings :: (IOE :> es) => Settings es
defaultSettings =
    Settings
        { settingsOnException = \_mReq e -> liftIO (settingsOnException Nothing e)
        , settingsOnExceptionResponse = liftResponse . settingsOnExceptionResponse
        , settingsOnOpen = liftIO . settingsOnOpen
        , settingsOnClose = liftIO . settingsOnClose
        , settingsBeforeMainLoop = liftIO settingsBeforeMainLoop
        , settingsFork = \inner ->
            withEffToIO (ConcUnlift Persistent Unlimited) \unlift ->
                void $ forkIOWithUnmask \unmask ->
                    unlift . inner $ liftIO . unmask . unlift
        , settingsAccept = liftIO . accept
        , settingsInstallShutdownHandler = \_close -> pure ()
        , settingsLogger = \req s ms -> liftIO (settingsLogger (unliftRequestNoBody req) s ms)
        , settingsServerPushLogger = \req p sz -> liftIO (settingsServerPushLogger (unliftRequestNoBody req) p sz)
        , ..
        }
  where
    WarpI.Settings{..} = Warp.defaultSettings

liftSettings :: (IOE :> es) => Warp.Settings -> Settings es
liftSettings WarpI.Settings{..} =
    Settings
        { settingsOnException = \_mReq -> liftIO . settingsOnException Nothing
        , settingsOnExceptionResponse = liftResponse . settingsOnExceptionResponse
        , settingsOnOpen = liftIO . settingsOnOpen
        , settingsOnClose = liftIO . settingsOnClose
        , settingsBeforeMainLoop = liftIO settingsBeforeMainLoop
        , settingsFork = \effInner ->
            withEffToIO (ConcUnlift Persistent Unlimited) \unlift ->
                settingsFork \ioUnmask ->
                    unlift . effInner $ liftIO . ioUnmask . unlift
        , settingsAccept = liftIO . settingsAccept
        , settingsInstallShutdownHandler = \closeEff ->
            withEffToIO SeqUnlift \unlift ->
                settingsInstallShutdownHandler (unlift closeEff)
        , settingsLogger = \req s ms -> liftIO (settingsLogger (unliftRequestNoBody req) s ms)
        , settingsServerPushLogger = ((liftIO .) .) . settingsServerPushLogger . unliftRequestNoBody
        , ..
        }

unliftSettings :: (IOE :> es) => (forall r. Eff es r -> IO r) -> Settings es -> Warp.Settings
unliftSettings unlift Settings{..} =
    WarpI.Settings
        { settingsOnException = (unlift .) . settingsOnException . fmap liftRequest
        , settingsOnExceptionResponse = unliftResponse unlift . settingsOnExceptionResponse
        , settingsOnOpen = unlift . settingsOnOpen
        , settingsOnClose = unlift . settingsOnClose
        , settingsBeforeMainLoop = unlift settingsBeforeMainLoop
        , settingsFork = \ioInner -> unlift $ settingsFork \effUnmask -> liftIO . ioInner $ unlift . effUnmask . liftIO
        , settingsAccept = unlift . settingsAccept
        , settingsInstallShutdownHandler = unlift . settingsInstallShutdownHandler . liftIO
        , settingsLogger = \req st ms -> unlift (settingsLogger (liftRequest req) st ms)
        , settingsServerPushLogger = ((unlift .) .) . settingsServerPushLogger . liftRequest
        , ..
        }

-- | Lifted 'Warp.run'.
run :: (IOE :> es) => Port -> Application es -> Eff es ()
run port = runSettings $ defaultSettings{settingsPort = port}

-- | Lifted 'Warp.runEnv'.
runEnv :: (IOE :> es) => Port -> Application es -> Eff es ()
runEnv port app =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift ->
        Warp.runEnv port (unliftApplication unlift app)

-- | Lifted 'Warp.runSettings'.
runSettings :: (IOE :> es) => Settings es -> Application es -> Eff es ()
runSettings settings app =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift ->
        Warp.runSettings (unliftSettings unlift settings) (unliftApplication unlift app)

-- | Lifted 'Warp.runSettingsSocket'.
runSettingsSocket :: (IOE :> es) => Settings es -> Socket -> Application es -> Eff es ()
runSettingsSocket settings socket app =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift ->
        Warp.runSettingsSocket (unliftSettings unlift settings) socket (unliftApplication unlift app)

-- | Lifted 'Warp.setPort'.
setPort :: Port -> Settings es -> Settings es
setPort v s = s{settingsPort = v}

-- | Lifted 'Warp.setHost'.
setHost :: HostPreference -> Settings es -> Settings es
setHost v s = s{settingsHost = v}

-- | Lifted 'Warp.setOnException'.
setOnException
    :: (Maybe (Request es) -> SomeException -> Eff es ())
    -> Settings es
    -> Settings es
setOnException v s = s{settingsOnException = v}

-- | Lifted 'Warp.setOnExceptionResponse'.
setOnExceptionResponse
    :: (SomeException -> Response es)
    -> Settings es
    -> Settings es
setOnExceptionResponse v s = s{settingsOnExceptionResponse = v}

-- | Lifted 'Warp.setOnOpen'.
setOnOpen :: (SockAddr -> Eff es Bool) -> Settings es -> Settings es
setOnOpen v s = s{settingsOnOpen = v}

-- | Lifted 'Warp.setOnClose'.
setOnClose :: (SockAddr -> Eff es ()) -> Settings es -> Settings es
setOnClose v s = s{settingsOnClose = v}

-- | Lifted 'Warp.setTimeout'.
setTimeout :: Int -> Settings es -> Settings es
setTimeout v s = s{settingsTimeout = v}

-- | Lifted 'Warp.setManager'.
setManager :: Manager -> Settings es -> Settings es
setManager v s = s{settingsManager = Just v}

-- | Lifted 'Warp.setFdCacheDuration'.
setFdCacheDuration :: Int -> Settings es -> Settings es
setFdCacheDuration v s = s{settingsFdCacheDuration = v}

-- | Lifted 'Warp.setFileInfoCacheDuration'.
setFileInfoCacheDuration :: Int -> Settings es -> Settings es
setFileInfoCacheDuration v s = s{settingsFileInfoCacheDuration = v}

-- | Lifted 'Warp.setBeforeMainLoop'.
setBeforeMainLoop :: Eff es () -> Settings es -> Settings es
setBeforeMainLoop v s = s{settingsBeforeMainLoop = v}

-- | Lifted 'Warp.setNoParsePath'.
setNoParsePath :: Bool -> Settings es -> Settings es
setNoParsePath v s = s{settingsNoParsePath = v}

-- | Lifted 'Warp.setInstallShutdownHandler'.
setInstallShutdownHandler :: (Eff es () -> Eff es ()) -> Settings es -> Settings es
setInstallShutdownHandler v s = s{settingsInstallShutdownHandler = v}

-- | Lifted 'Warp.setServerName'.
setServerName :: ByteString -> Settings es -> Settings es
setServerName v s = s{settingsServerName = v}

-- | Lifted 'Warp.setMaximumBodyFlush'.
setMaximumBodyFlush :: Maybe Int -> Settings es -> Settings es
setMaximumBodyFlush v s = s{settingsMaximumBodyFlush = v}

-- | Lifted 'Warp.setFork'.
setFork
    :: (((forall a. Eff es a -> Eff es a) -> Eff es ()) -> Eff es ())
    -> Settings es
    -> Settings es
setFork v s = s{settingsFork = v}

-- | Lifted 'Warp.setAccept'.
setAccept
    :: (Socket -> Eff es (Socket, SockAddr))
    -> Settings es
    -> Settings es
setAccept v s = s{settingsAccept = v}

-- | Lifted 'Warp.setProxyProtocolNone'.
setProxyProtocolNone :: Settings es -> Settings es
setProxyProtocolNone s = s{settingsProxyProtocol = WarpI.ProxyProtocolNone}

-- | Lifted 'Warp.setProxyProtocolRequired'.
setProxyProtocolRequired :: Settings es -> Settings es
setProxyProtocolRequired s = s{settingsProxyProtocol = WarpI.ProxyProtocolRequired}

-- | Lifted 'Warp.setProxyProtocolOptional'.
setProxyProtocolOptional :: Settings es -> Settings es
setProxyProtocolOptional s = s{settingsProxyProtocol = WarpI.ProxyProtocolOptional}

-- | Lifted 'Warp.setSlowlorisSize'.
setSlowlorisSize :: Int -> Settings es -> Settings es
setSlowlorisSize v s = s{settingsSlowlorisSize = v}

-- | Lifted 'Warp.setHTTP2Disabled'.
setHTTP2Disabled :: Settings es -> Settings es
setHTTP2Disabled s = s{settingsHTTP2Enabled = False}

-- | Lifted 'Warp.setLogger'.
setLogger
    :: (Request es -> HTTP.Status -> Maybe Integer -> Eff es ())
    -> Settings es
    -> Settings es
setLogger v s = s{settingsLogger = v}

-- | Lifted 'Warp.setServerPushLogger'.
setServerPushLogger
    :: (Request es -> ByteString -> Integer -> Eff es ())
    -> Settings es
    -> Settings es
setServerPushLogger v s = s{settingsServerPushLogger = v}

-- | Lifted 'Warp.setGracefulShutdownTimeout'.
setGracefulShutdownTimeout :: Maybe Int -> Settings es -> Settings es
setGracefulShutdownTimeout v s = s{settingsGracefulShutdownTimeout = v}

-- | Lifted 'Warp.setGracefulCloseTimeout1'.
setGracefulCloseTimeout1 :: Int -> Settings es -> Settings es
setGracefulCloseTimeout1 v s = s{settingsGracefulCloseTimeout1 = v}

-- | Lifted 'Warp.setGracefulCloseTimeout2'.
setGracefulCloseTimeout2 :: Int -> Settings es -> Settings es
setGracefulCloseTimeout2 v s = s{settingsGracefulCloseTimeout2 = v}

-- | Lifted 'Warp.setMaxTotalHeaderLength'.
setMaxTotalHeaderLength :: Int -> Settings es -> Settings es
setMaxTotalHeaderLength v s = s{settingsMaxTotalHeaderLength = v}

-- | Lifted 'Warp.setAltSvc'.
setAltSvc :: ByteString -> Settings es -> Settings es
setAltSvc v s = s{settingsAltSvc = Just v}

-- | Lifted 'Warp.setMaxBuilderResponseBufferSize'.
setMaxBuilderResponseBufferSize :: Int -> Settings es -> Settings es
setMaxBuilderResponseBufferSize v s = s{settingsMaxBuilderResponseBufferSize = v}

-- | Lifted 'Warp.getPort'.
getPort :: Settings es -> Port
getPort = settingsPort

-- | Lifted 'Warp.getHost'.
getHost :: Settings es -> HostPreference
getHost = settingsHost

-- | Lifted 'Warp.getOnOpen'.
getOnOpen :: Settings es -> SockAddr -> Eff es Bool
getOnOpen = settingsOnOpen

-- | Lifted 'Warp.getOnClose'.
getOnClose :: Settings es -> SockAddr -> Eff es ()
getOnClose = settingsOnClose

-- | Lifted 'Warp.getOnException'.
getOnException :: Settings es -> Maybe (Request es) -> SomeException -> Eff es ()
getOnException = settingsOnException

-- | Lifted 'Warp.getGracefulShutdownTimeout'.
getGracefulShutdownTimeout :: Settings es -> Maybe Int
getGracefulShutdownTimeout = settingsGracefulShutdownTimeout

-- | Lifted 'Warp.getGracefulCloseTimeout1'.
getGracefulCloseTimeout1 :: Settings es -> Int
getGracefulCloseTimeout1 = settingsGracefulCloseTimeout1

-- | Lifted 'Warp.getGracefulCloseTimeout2'.
getGracefulCloseTimeout2 :: Settings es -> Int
getGracefulCloseTimeout2 = settingsGracefulCloseTimeout2

-- | Lifted 'Warp.getOpenConnectionCounter'.
getOpenConnectionCounter :: Settings es -> Maybe Counter
getOpenConnectionCounter = settingsConnectionCounter

-- | Lifted 'Warp.getServerState'.
getServerState :: Settings es -> Maybe ServerState
getServerState = settingsServerState

-- | Lifted 'Warp.makeSettingsAndServerState'.
makeSettingsAndServerState :: (IOE :> es) => Eff es (ServerState, Settings es)
makeSettingsAndServerState = liftIO $ second liftSettings <$> Warp.makeSettingsAndServerState

-- | Lifted 'Warp.currentOpenConnections'.
currentOpenConnections :: (IOE :> es) => ServerState -> Eff es Int
currentOpenConnections = liftIO . Warp.currentOpenConnections

-- | Lifted 'Warp.currentShuttingDownState'.
currentShuttingDownState :: (IOE :> es) => ServerState -> Eff es Bool
currentShuttingDownState = liftIO . Warp.currentShuttingDownState

-- | Lifted 'Warp.makeSettingsAndCounter'.
makeSettingsAndCounter :: (IOE :> es) => Eff es (Counter, Settings es)
makeSettingsAndCounter = liftIO $ second liftSettings <$> Warp.makeSettingsAndCounter

-- | Lifted 'Warp.defaultOnException'.
defaultOnException :: (IOE :> es) => Maybe (Request es) -> SomeException -> Eff es ()
defaultOnException = (liftIO .) . Warp.defaultOnException . fmap unliftRequestNoBody

-- | Lifted 'Warp.defaultOnExceptionResponse'.
defaultOnExceptionResponse :: (IOE :> es) => SomeException -> Response es
defaultOnExceptionResponse = liftResponse . Warp.defaultOnExceptionResponse

-- | Lifted 'Warp.exceptionResponseForDebug'.
exceptionResponseForDebug :: (IOE :> es) => SomeException -> Response es
exceptionResponseForDebug = liftResponse . Warp.exceptionResponseForDebug

-- | Lifted 'Warp.pauseTimeout'.
pauseTimeout :: (IOE :> es) => Request es -> Eff es ()
pauseTimeout = liftIO . Warp.pauseTimeout . unliftRequestNoBody

-- | Lifted 'Warp.getFileInfo'.
getFileInfo :: (IOE :> es) => Request es -> FilePath -> Eff es FileInfo
getFileInfo = (liftIO .) . Warp.getFileInfo . unliftRequestNoBody

-- | Lifted 'Warp.clientCertificate'.
clientCertificate :: Request es -> Maybe CertificateChain
clientCertificate = Warp.clientCertificate . unliftRequestNoBody

-- | Lifted 'Warp.withApplication'.
withApplication :: (IOE :> es) => Eff es (Application es) -> (Port -> Eff es a) -> Eff es a
withApplication mkApp k =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift -> do
        app <- unlift mkApp
        Warp.withApplication (pure (unliftApplication unlift app)) (unlift . k)

-- | Lifted 'Warp.withApplicationSettings'.
withApplicationSettings
    :: (IOE :> es)
    => Settings es
    -> Eff es (Application es)
    -> (Port -> Eff es a)
    -> Eff es a
withApplicationSettings settings mkApp k =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift -> do
        app <- unlift mkApp
        Warp.withApplicationSettings
            (unliftSettings unlift settings)
            (pure (unliftApplication unlift app))
            (unlift . k)

-- | Lifted 'Warp.testWithApplication'.
testWithApplication :: (IOE :> es) => Eff es (Application es) -> (Port -> Eff es a) -> Eff es a
testWithApplication mkApp k =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift -> do
        app <- unlift mkApp
        Warp.testWithApplication (pure (unliftApplication unlift app)) (unlift . k)

-- | Lifted 'Warp.testWithApplicationSettings'.
testWithApplicationSettings
    :: (IOE :> es)
    => Settings es
    -> Eff es (Application es)
    -> (Port -> Eff es a)
    -> Eff es a
testWithApplicationSettings settings mkApp k =
    withEffToIO (ConcUnlift Persistent Unlimited) \unlift -> do
        app <- unlift mkApp
        Warp.testWithApplicationSettings
            (unliftSettings unlift settings)
            (pure (unliftApplication unlift app))
            (unlift . k)

-- | Lifted 'Warp.openFreePort'.
openFreePort :: (IOE :> es) => Eff es (Port, Socket)
openFreePort = liftIO Warp.openFreePort

-- | Lifted 'Warp.getHTTP2Data'.
getHTTP2Data :: (IOE :> es) => Request es -> Eff es (Maybe HTTP2Data)
getHTTP2Data = liftIO . Warp.getHTTP2Data . unliftRequestNoBody

-- | Lifted 'Warp.setHTTP2Data'.
setHTTP2Data :: (IOE :> es) => Request es -> Maybe HTTP2Data -> Eff es ()
setHTTP2Data = (liftIO .) . Warp.setHTTP2Data . unliftRequestNoBody

-- | Lifted 'Warp.modifyHTTP2Data'.
modifyHTTP2Data :: (IOE :> es) => Request es -> (Maybe HTTP2Data -> Maybe HTTP2Data) -> Eff es ()
modifyHTTP2Data = (liftIO .) . Warp.modifyHTTP2Data . unliftRequestNoBody