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.
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 http.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 http.SetCookie()
function to write the cookie to the response.
In the GetFlash()
helper we use the r.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:
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 then the gorilla/sessions
package is a good option. This stores session
data in a signed (and optionally encrypted) cookie, which stops the data from being tampered with. Here's the previous
example implemented with this package instead:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
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)
return
}
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)
return
}
fm := session.Flashes("message")
if fm == nil {
fmt.Fprint(w, "No flash messages")
return
}
session.Save(r, w)
fmt.Fprintf(w, "%v", fm[0])
}