Suave.IO

Suave is a simple web development F# library providing a lightweight web server and a set of combinators to manipulate route flow and task composition.

Download this project as a .zip file Download this project as a tar.gz file
View on GitHub

State and Sessions in Suave

While web applications are technically stateless, there are still times where we need to start from a known state. So, let’s jump in and look at aspects of session cookies first, then how we can manage state among WebParts.

Suave provides storage interfaces for cookies in the Suave.State.CookieStateStore module. The statefulForSession WebPart can be composed to make a path session-aware. From there, the HttpContext.state function extracts the state information, and the get and set functions on the resulting StateStore object can be used to manipulate the contents of the state tracked by the session.

open Suave
open Suave.State.CookieStateStore

let setSessionValue (key : string) (value : 'T) : WebPart =
  context (fun ctx ->
    match HttpContext.state ctx with
    | Some state ->
        state.set key value
    | _ ->
        never // fail
    )

let getSessionValue (ctx : HttpContext) (key : string) : 'T option =
  match HttpContext.state ctx with
  | Some state ->
      state.get key
  | _ ->
      None

/// This a convenience function that turns a None string result into an empty string
let getStringSessionValue (ctx : HttpContext) (key : string) : string = 
  defaultArg (getSessionValue ctx key) ""

let cookieYes : WebPart =
  context (fun ctx -> OK (getStringSessionValue ctx "test"))

let cookieNo : WebPart =
  context (fun ctx -> OK (getStringSessionValue ctx "nope"))

let app =
  statefulForSession
  >=> setSessionValue "test" "123"
  >=> choose [
        path "/yes" >=> cookieYes
        path "/no" >=> cookieNo
        RequestErrors.NOT_FOUND
        ]

Server Keys

The contents of the cookie are encrypted before the cookie is sent. Suave’s default configuration generates a new server key each time the server is restarted. While this is not wrong, users would likely get quite annoyed if they lost their state because the server was restarted. Additionally, specifying a server key lets load-balanced servers access the same information.

The key generated by Suave is secure; we just don’t need it changing. To get a key, you can use the following code, either in an .fsx file (be sure to reference Suave.dll), or by placing the following code in your application’s entry point, before the startWebServer. Run it once; it will write a file called key.txt that contains a base-64 encoded string representing a random key, which we can use to configure our session key encryption. (If you put it in your application, be sure to remove it after you’ve run it.)

 
open Suave.Utils
open System

let writeKey key = System.IO.File.WriteAllText ("key.txt", key)
Crypto.generateKey Crypto.KeyLength
|> Convert.ToBase64String
|> writeKey

Now, key in hand, we can continue our example from above. (Note that hard-coding the key in the source code is a poor way to manage these keys; a configuration file is better, but environment variables or container configuration per environment is best.)

let suaveCfg =
  { defaultConfig with
      serverKey = ServerKey.fromBase64 [encoded-key]
    }

[<EntryPoint>]
let main argv = 
  startWebServer suaveCfg app
  0 

Suave uses the .NET Framework type BinaryFormatter to serialize the Map<string, obj> containing the session state; this is the default. However, the BinaryFormatter was removed in the .NET Core API, and the DataContractJsonSerializer does not recognize the Map<string, obj> type. One option is to utilize JSON.NET to serialize this object. To use that, ensure you’ve added the Newtonsoft.Json NuGet package to your project, then put the following code somewhere before the suaveCfg definition in the example above.

open Newtonsoft.Json

let utf8 = System.Text.Encoding.UTF8

type JsonNetCookieSerialiser () =
  interface CookieSerialiser with
    member x.serialise m =
      utf8.GetBytes (JsonConvert.SerializeObject m)
    member x.deserialise m =
      JsonConvert.DeserializeObject<Map<string, obj>> (utf8.GetString m)

Then, modify the configuration to use that serializer.

let suaveCfg =
  { defaultConfig with
      serverKey = Convert.FromBase64String [encoded-key]
      cookieSerialiser = new JsonNetCookieSerialiser()
    }

State among WebParts

Within the Writers module, Suave provides the functions setUserData and unsetUserData for adding items to the context’s userState property. The example below could be used to accrue a list of messages to be displayed to the user.

/// Read an item from the user state, downcast to the expected type
let readUserState ctx key : 'value =
  ctx.userState |> Map.tryFind key |> Option.map (fun x -> x :?> 'value) |> Option.get

let addUserMessage (message : string) : WebPart =
  context (fun ctx ->
    let read = readUserState ctx
    let existing =
      match ctx.userState |> Map.tryFind "messages" with
      | Some _ ->
          read "messages"
      | _ ->
          []
    Writers.setUserData "messages" (message :: existing))

let app =
  choose [
    path "/"
      >=> addUserMessage "It's a state!"
      >=> addUserMessage "Another one"
      >=> context (fun ctx -> Successful.OK (View.page ctx.userState))
    ]

In this example, View.page is a function that generates the output, using the user state Map<string, obj> to display the messages in a nice way.

We’ve covered two different ways of managing state. Session state persists throughout the session, while userData has a per-request lifetime.