Sessions
HTTP is a stateless protocol. While some view this as a disadvantage, advocates of RESTful web development laud this as an plus. When state is removed from the picture, it is easier to scale applications, caching can happen automatically, and many other nice side effects occur. You can draw many parallels with the non-mutable nature of Haskell in general.
As much as possible, RESTful applications should avoid storing state about an interaction with a client. However, it is sometimes unavoidable. Features like shopping carts are the classic example, but other more mundane interactions like proper login handling can be greatly enhanced by proper usage of sessions.
This chapter will describe how Yesod stores session data, how you can access this data, and some special functions to help you make the most of sessions.
Clientsession
One of the earliest packages spun off from Yesod was clientsession. This package uses encryption and signatures to store data in a client-side cookie. The encryption prevents the user from inspecting the data, and the signature ensures that the session can be neither hijacked nor tampered with.
It might sound like a bad idea from an efficiency standpoint to store data in a cookie: after all, this means that the data must be sent on every request. However, in practice, clientsession can be a great boon for performance.
No server side database lookup is required to service a request.
We can easily scale horizontally: each request contains all the information we need to send a response.
To avoid undue bandwidth overhead, production sites can serve their static content from a separate domain name to avoid the overhead of transmitting the session cookie for each request.
Storing megabytes of information in the session will be a bad idea. But for that matter, most session implementations recommend against such practices. If you really need massive storage for a user, it is best to store a lookup key in the session, and put the actual data in a database.
All of the interaction with clientsession is handled by Yesod internally, but there are a few spots where you can tweak the behavior just a bit.
Controlling sessions
There are three functions in the Yesod typeclass that control how sessions work. encryptKey returns the encryption key used. By default, it will take this from a local file, so that sessions can persist between database shutdowns. This file will be automatically created and filled with random data if it does not exist. And if you override this function to return Nothing
, sessions will be disabled.
Why disable sessions? They do introduce a performance overhead. Under normal circumstances, this overhead is minimal, especially compared to database access. However, when dealing with very basic tasks, the overhead can become noticeable. But be careful about disabling sessions: this will also disable such features as CSRF (Cross-Site Request Forgery) protection.
The next function is clientSessionDuration. This function gives the number of minutes that a session should be active. The default is 120 (2 hours).
This value ends up affecting the session cookie in two ways: firstly, it determines the expiration date for the cookie itself. More importantly, however, the session expiration timestamp is encoded inside the session signature. When Yesod decodes the signature, it checks if the date is in the past; if so, it ignores the session values.
Every time Yesod sends a response to the client, it sends an updated session cookie with a new expire date. This way, even if you do not change the session values themselves, a session will not time out if the user continues to browse your site.
And this leads very nicely to the last function: sessionIpAddress. By default, Yesod also encodes the client’s IP address inside the cookie to prevent session hijacking. In general, this is a good thing. However, some ISPs are known for putting their users behind proxies that rewrite their IP addresses, sometimes changing the source IP in the middle of the session. If this happens, and you have sessionIpAddress
enabled, the user’s session will be reset. Turning this setting to False
will allow a session to continue under such circumstances, at the cost of exposing a user to session hijacking.
Session Operations
Like most frameworks, a session in Yesod is a key-value store. The base session API boils down to four functions: lookupSession
gets a value for a key (if available), getSession
returns all of the key/value pairs, setSession
sets a value for a key, and deleteSession
clears a value for a key.
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
import Yesod
import Control.Applicative ((<$>), (<*>))
import qualified Web.ClientSession as CS
data SessionExample = SessionExample
mkYesod "SessionExample" [parseRoutes|
/ Root GET POST
|]
getRoot :: Handler RepHtml
getRoot = do
sess <- getSession
hamletToRepHtml [hamlet|
<form method=post>
<input type=text name=key>
<input type=text name=val>
<input type=submit>
<h1>#{show sess}
|]
postRoot :: Handler ()
postRoot = do
(key, mval) <- runInputPost $ (,) <$> ireq textField "key" <*> iopt textField "val"
case mval of
Nothing -> deleteSession key
Just val -> setSession key val
liftIO $ print (key, mval)
redirect Root
instance Yesod SessionExample where
-- Make the session timeout 1 minute so that it's easier to play with
makeSessionBackend _ = do
key <- CS.getKey CS.defaultKeyFile
return $ Just $ clientSessionBackend key 1
instance RenderMessage SessionExample FormMessage where
renderMessage _ _ = defaultFormMessage
main :: IO ()
main = warpDebug 3000 SessionExample
Messages
One usage of sessions previously alluded to is messages. They come to solve a common problem in web development: the user performs a POST
request, the web app makes a change, and then the web app wants to simultaneously redirect the user to a new page and send the user a success message. (This is known as Post/Redirect/Get.)
Yesod provides a pair of functions to make this very easy: setMessage
stores a value in the session, and getMessage
both reads the value most recently put into the session, and clears the old value so it does not accidently get displayed twice.
It is recommended to have a call to getMessage
in defaultLayout
so that any available message is shown to a user immediately, without having to remember to add getMessage
calls to every handler.
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
QuasiQuotes, MultiParamTypeClasses #-}
import Yesod
data Messages = Messages
mkYesod "Messages" [parseRoutes|
/ RootR GET
/set-message SetMessageR POST
|]
instance Yesod Messages where
defaultLayout widget = do
pc <- widgetToPageContent widget
mmsg <- getMessage
hamletToRepHtml [hamlet|
$doctype 5
<html>
<head>
<title>#{pageTitle pc}
^{pageHead pc}
<body>
$maybe msg <- mmsg
<p>Your message was: #{msg}
^{pageBody pc}
|]
instance RenderMessage Messages FormMessage where
renderMessage _ _ = defaultFormMessage
getRootR :: Handler RepHtml
getRootR = defaultLayout [whamlet|
<form method=post action=@{SetMessageR}>
My message is: #
<input type=text name=message>
<input type=submit>
|]
postSetMessageR :: Handler ()
postSetMessageR = do
msg <- runInputPost $ ireq textField "message"
setMessage $ toHtml msg
redirect RootR
main :: IO ()
main = warpDebug 3000 Messages
Initial page load, no message
New message entered in text box
After form submit, message appears at top of page
After refresh, the message is cleared
Ultimate Destination
Not to be confused with a horror film, this concept is used internally in yesod-auth. Suppose a user requests a page that requires authentication. If the user is not yet logged in, you need to send him/her to the login page. A well-designed web app will then send them back to the first page they requested. That’s what we call the ultimate destination.
redirectUltDest
sends the user to the ultimate destination set in his/her session, clearing that value from the session. It takes a default destination as well, in case there is no destination set. For setting the session, there are three options:
setUltDest
sets the destination to the given URLsetUltDestCurrent
sets the destination to the currently requested URL.setUltDestReferer
sets the destination based on theReferer
header (the page that led the user to the current page).
Let’s look at a small sample app. It will allow the user to set his/her name in the session, and then tell the user his/her name from another route. If the name hasn’t been set yet, the user will be redirected to the set name page, with an ultimate destination set to come back to the current page.
{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell,
QuasiQuotes, MultiParamTypeClasses #-}
import Yesod
data UltDest = UltDest
mkYesod "UltDest" [parseRoutes|
/ RootR GET
/setname SetNameR GET POST
/sayhello SayHelloR GET
|]
instance Yesod UltDest
instance RenderMessage UltDest FormMessage where
renderMessage _ _ = defaultFormMessage
getRootR = defaultLayout [whamlet|
<p>
<a href=@{SetNameR}>Set your name
<p>
<a href=@{SayHelloR}>Say hello
|]
-- Display the set name form
getSetNameR = defaultLayout [whamlet|
<form method=post>
My name is #
<input type=text name=name>
. #
<input type=submit value="Set name">
|]
-- Retreive the submitted name from the user
postSetNameR :: Handler ()
postSetNameR = do
-- Get the submitted name and set it in the session
name <- runInputPost $ ireq textField "name"
setSession "name" name
-- After we get a name, redirect to the ultimate destination.
-- If no destination is set, default to the homepage
redirectUltDest RootR
getSayHelloR = do
-- Lookup the name value set in the session
mname <- lookupSession "name"
case mname of
Nothing -> do
-- No name in the session, set the current page as
-- the ultimate destination and redirect to the
-- SetName page
setUltDestCurrent
setMessage "Please tell me your name"
redirect SetNameR
Just name -> defaultLayout [whamlet|
<p>Welcome #{name}
|]
main :: IO ()
main = warpDebug 3000 UltDest
Summary
Sessions are the number one way we bypass the statelessness imposed by HTTP. We shouldn’t consider this an escape hatch to perform whatever actions we want: statelessness in web applications is a virtue, and we should respect it whenever possible. However, there are specific cases where it is vital to retain some state.
The session API in Yesod is very simple. It provides a key-value store, and a few convenience functions built on top for common use cases. If used properly, with small payloads, sessions should be an unobtrusive part of your web development.