Search code examples
haskellscotty

How do I use a global state in WAI middleware?


I am writing a web server using Scotty. The server should have a login route that once user logs in, a token is dispatched and recorded in the server. Obviously the recorded tokens should be in a global state (not using Redis kind of thing because the server is run on single machine and has small view counts).

Now I know the Scotty examples have shown how to define and access global states in routes, but I can't find out how to do the same thing for middlewares.

I tried using the same way from the official example but neither the middleware function nor the app function has a WebM monad context so I can't really do this:

app = do
  tokensList <- webM $ gets tokens
  middleware $ authMiddleware tokensList

Solution

  • That "official example" isn't so great. I mean, it's a good example of using a custom monad, but it's a lousy example of how to provide global state. You don't need the custom monad at all. You just need the TVar.

    Ultimately, all you need to do is create the newTVar in main, and then pass it to the app function to construct the application and middleware, which can access the TVar directly with lifted IO operations in the application and IO operations in the middleware. It'll look something like the following:

    main :: IO ()
    main = do
      state <- newTVar startState
      scotty 3000 $ app state
    
    app :: TVar AppState -> ScottyM ()
    app state = do
      middleware (authMiddleware state)
      get "..." $ do
        x <- liftIO $ readTVarIO state
        ...
    
    authMiddleware :: TVar State -> MiddleWare
    authMiddleware state app req resp = do
      x <- readTVarIO state
      ...
      app req resp
    

    To illustrate, here's a rewrite of that official example without the monad that operates on the TVar directly in the handler. (No middleware yet, but see the example further below.)

    {-# LANGUAGE OverloadedStrings #-}
    
    module Main (main) where
    
    import Web.Scotty
    import Control.Concurrent.STM
    import Control.Monad.IO.Class
    import Data.String
    
    newtype AppState = AppState { tickCount :: Int }
    
    main :: IO ()
    main = do
      state <- newTVarIO (AppState 0)
      scotty 3000 $ app state
    
    app :: TVar AppState -> ScottyM ()
    app state = do
      get "/" $ do
        c <- liftIO $ tickCount <$> readTVarIO state
        text $ fromString $ show c
      get "/plusone" $ do
        liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+1))
        redirect "/"
      get "/plustwo" $ do
        liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+2))
        redirect "/"
    

    and here's a version that uses middleware to count page accesses and queries and resets the value from the handlers, all directly through the TVar without requiring any custom monad:

    {-# LANGUAGE OverloadedStrings #-}
    
    module Main (main) where
    
    import Web.Scotty
    import Control.Concurrent.STM
    import Control.Monad.IO.Class
    import Data.String
    import Network.Wai
    
    newtype AppState = AppState { tickCount :: Int }
    
    main = do
      state <- newTVarIO (AppState 0)
      scotty 3000 $ app state
    
    app :: TVar AppState -> ScottyM ()
    app state = do
      middleware $ countMiddleware state
      get "/" $ do
        c <- liftIO $ tickCount <$> readTVarIO state
        text $ fromString $ show c
      get "/reset" $ do
        liftIO . atomically $ writeTVar state (AppState 0)
        redirect "/"
    
    countMiddleware :: TVar AppState -> Middleware
    countMiddleware state appl req resp = do
      liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+1))
      appl req resp