Simple Flash Messages in Go

Often in web applications you need to temporarily store data in-between requests, such as an error or success message during the Post-Redirect-Get process for a form submission. Frameworks such as Rails and Django have the concept of transient single-use flash messages to help with this.

In this post I'm going to look at a way to create your own cookie-based flash messages in Go.

We'll start by creating a directory for the project, along with a flash.go file for our code and a main.go file for an example application.

$ mkdir flash-example
$ cd flash-example
$ touch flash.go main.go

In order to keep our request handlers nice and clean, we'll create our primary SetFlash() and GetFlash() helper functions in the flash.go file.

File: flash.go
package main

import (
  "encoding/base64"
  "net/http"
  "time"
)

func SetFlash(w http.ResponseWriter, name string, value []byte) {
  c := &http.Cookie{Name: name, Value: encode(value)}
  http.SetCookie(w, c)
}

func GetFlash(w http.ResponseWriter, r *http.Request, name string) ([]byte, error) {
  c, err := r.Cookie(name)
  if err != nil {
    switch err {
    case http.ErrNoCookie:
      return nil, nil
    default:
      return nil, err
    }
  }
  value, err := decode(c.Value)
  if err != nil {
    return nil, err
  }
  dc := &http.Cookie{Name: name, MaxAge: -1, Expires: time.Unix(1, 0)}
  http.SetCookie(w, dc)
  return value, nil
}

// -------------------------

func encode(src []byte) string {
  return base64.URLEncoding.EncodeToString(src)
}

func decode(src string) ([]byte, error) {
  return base64.URLEncoding.DecodeString(src)
}

Our SetFlash() function is pretty succinct.

It creates a new Cookie, containing the name of the flash message and the content. You'll notice that we're encoding the content – this is because RFC 6265 is quite strict about the characters cookie values can contain, and encoding to base64 ensures our value satisfies the permitted character set. We then use the SetCookie function to write the cookie to the response.

In the GetFlash() helper we use the request.Cookie method to load up the cookie containing the flash message – returning nil if it doesn't exist – and then decode the value from base64 back into a byte array.

Because we want a flash message to only be available once, we need to instruct clients to not resend the cookie with future requests. We can do this by setting a new cookie with exactly the same name, with MaxAge set to a negative number and Expiry set to a historical time (to cater for old versions of IE). You should note that Go will only set an expiry time on a cookie if it is after the Unix epoch, so we've set ours for 1 second after that.

Let's use these helper functions in a short example:

File: main.go
package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/set", set)
  http.HandleFunc("/get", get)
  fmt.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func set(w http.ResponseWriter, r *http.Request) {
  fm := []byte("This is a flashed message!")
  SetFlash(w, "message", fm)
}

func get(w http.ResponseWriter, r *http.Request) {
  fm, err := GetFlash(w, r, "message")
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
  if fm == nil {
    fmt.Fprint(w, "No flash messages")
    return
  }
  fmt.Fprintf(w, "%s", fm)
}

Run the application:

$ go run main.go flash.go
Listening...

And make some requests against it using cURL:

$ curl -i --cookie-jar cj localhost:3000/set
HTTP/1.1 200 OK
Set-Cookie: message=VGhpcyBpcyBhIGZsYXNoZWQgbWVzc2FnZSE=
Content-Type: text/plain; charset=utf-8
Content-Length: 0

$ curl -i --cookie-jar cj --cookie cj localhost:3000/get
HTTP/1.1 200 OK
Set-Cookie: message=; Expires=Thu, 01 Jan 1970 00:00:01 UTC; Max-Age=0
Content-Type: text/plain; charset=utf-8
Content-Length: 26

This is a flashed message!

$ curl -i --cookie-jar cj --cookie cj localhost:3000/get
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 17

No flash messages

You can see our flash message being set, retrieved, and then not passed with subsequent requests as expected.

Additional Tools

If you don't want to roll your own helpers for flash messages, or need them to be 'signed' to prevent tampering, then the Gorilla Sessions package is a good option. Here's the previous example implemented with Gorilla instead:

package main

import (
  "fmt"
  "github.com/gorilla/sessions"
  "net/http"
)

func main() {
  http.HandleFunc("/set", set)
  http.HandleFunc("/get", get)
  fmt.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

var store = sessions.NewCookieStore([]byte("a-secret-string"))

func set(w http.ResponseWriter, r *http.Request) {
  session, err := store.Get(r, "flash-session")
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
  session.AddFlash("This is a flashed message!", "message")
  session.Save(r, w)
}

func get(w http.ResponseWriter, r *http.Request) {
  session, err := store.Get(r, "flash-session")
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
  fm := session.Flashes("message")
  if fm == nil {
    fmt.Fprint(w, "No flash messages")
    return
  }
  session.Save(r, w)
  fmt.Fprintf(w, "%v", fm[0])
}