Let's Go Stateful HTTP › Working with session data
Previous · Contents · Next
Chapter 8.3.

Working with session data

In this chapter let’s put the session functionality to work and use it to persist the confirmation flash message between HTTP requests that we discussed earlier.

We’ll begin in our cmd/web/handlers.go file and update our snippetCreatePost method so that a flash message is added to the user’s session data if — and only if — the snippet was created successfully. Like so:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    var form snippetCreateForm

    err := app.decodePostForm(r, &form)
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
    form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
    form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
    form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

    if !form.Valid() {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

    id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // Use the Put() method to add a string value ("Snippet successfully 
    // created!") and the corresponding key ("flash") to the session data.
    app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!")

    http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

That’s nice and simple, but there are a couple of things to point out:

Next up we want our snippetView handler to retrieve the flash message (if one exists in the session for the current user) and pass it to the HTML template for subsequent display.

Because we want to display the flash message once only, we actually want to retrieve and remove the message from the session data. We can do both these operations at the same time by using the PopString() method.

I’ll demonstrate:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    // Use the PopString() method to retrieve the value for the "flash" key.
    // PopString() also deletes the key and value from the session data, so it
    // acts like a one-time fetch. If there is no matching key in the session
    // data this will return the empty string.
    flash := app.sessionManager.PopString(r.Context(), "flash")

    data := app.newTemplateData(r)
    data.Snippet = snippet

    // Pass the flash message to the template.
    data.Flash = flash 

    app.render(w, r, http.StatusOK, "view.tmpl", data)
}

...

If you try to run the application now, the compiler will (rightly) grumble that the Flash field isn’t defined in our templateData struct. Go ahead and add it in like so:

File: cmd/web/templates.go
package main

import (
    "html/template"
    "path/filepath"
    "time"

    "snippetbox.alexedwards.net/internal/models"
)

type templateData struct {
    CurrentYear int
    Snippet     models.Snippet
    Snippets    []models.Snippet
    Form        any
    Flash       string // Add a Flash field to the templateData struct.
}

...

And now, we can update our base.tmpl file to display the flash message, if one exists.

File: ui/html/base.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
        <link rel='stylesheet' href='/static/css/main.css'>
        <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
        <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        {{template "nav" .}}
        <main>
            <!-- Display the flash message if one exists -->
            {{with .Flash}}
                <div class='flash'>{{.}}</div>
            {{end}}
            {{template "main" .}}
        </main>
        <footer>
            Powered by <a href='https://golang.org/'>Go</a> in {{.CurrentYear}}
        </footer>
        <script src='/static/js/main.js' type='text/javascript'></script>
    </body>
</html>
{{end}}

Remember, the {{with .Flash}} block will only be executed if the value of .Flash is not the empty string. So, if there’s no "flash" key in the current user’s session, the result is that the chunk of new markup simply won’t be displayed.

Once that’s done, save all your files and restart the application. Try adding a new snippet like so…

08.03-01.png

And after redirection you should see the flash message now being displayed:

08.03-02.png

If you try refreshing the page, you can confirm that the flash message is no longer shown — it was a one-off message for the current user immediately after they created the snippet.

08.03-03.png

Auto-displaying flash messages

A little improvement we can make (which will save us some work later in the project) is to automate the display of flash messages, so that any message is automatically included the next time any page is rendered.

We can do this by adding any flash message to the template data via the newTemplateData() helper method that we made earlier, like so:

File: cmd/web/helpers.go
package main

...

func (app *application) newTemplateData(r *http.Request) templateData {
    return templateData{
        CurrentYear: time.Now().Year(),
        // Add the flash message to the template data, if one exists.
        Flash:       app.sessionManager.PopString(r.Context(), "flash"),
    }
}

...

Making that change means that we no longer need to check for the flash message within the snippetView handler, and the code can be reverted to look like this:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    data := app.newTemplateData(r)
    data.Snippet = snippet

    app.render(w, r, http.StatusOK, "view.tmpl", data)
}

...

Feel free to try running the application again and creating another snippet. You should find that the flash message functionality still works as expected.


Additional information

Behind the scenes of session management

I’d like to take a moment to unpack some of the ‘magic’ behind session management and explain how it works behind the scenes.

If you like, open up the developer tools in your web browser and take a look at the cookie data for one of the pages. You should see a cookie named session in the request data, similar to this:

08.03-04.png

This is the session cookie, and it will be sent back to the Snippetbox application with every request that your browser makes.

The session cookie contains the session token — also sometimes known as the session ID. The session token is a high-entropy random string, which in my case is the value y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4 (yours will be different).

It’s important to emphasize that the session token is just a random string. In itself, it doesn’t carry or convey any session data (like the flash message that we set in this chapter).

Next, you might like to open up a terminal to MySQL and run a SELECT query against the sessions table to lookup the session token that you see in your browser. Like so:

mysql> SELECT * FROM sessions WHERE token = 'y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4';
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
| token                                       | data                                                                                                                                                                                                                                             | expiry                     |
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
| y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4 | 0x26FF81030102FF820001020108446561646C696E6501FF8400010656616C75657301FF8600000010FF830501010454696D6501FF8400000027FF85040101176D61705B737472696E675D696E74657266616365207B7D01FF8600010C0110000016FF82010F010000000ED9F4496109B650EBFFFF010000 | 2024-03-18 11:29:23.179505 |
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
1 row in set (0.00 sec)

This should return one record. The data value here is the thing that actually contains the user’s session data. Specifically, what we’re looking at is a MySQL BLOB (binary large object) containing a gob-encoded representation of the session data.

Each and every time we make a change to our session data, this data value will be updated to reflect the changes.

Lastly, the final column in the database is the expiry time, after which the session will no longer be considered valid.

So, what happens in our application is that the LoadAndSave() middleware checks each incoming request for a session cookie. If a session cookie is present, it reads the session token from the cookie and retrieves the corresponding session data from the database (while also checking that the session hasn’t expired). It then adds the session data to the request context so it can be used in your handlers.

Any changes that you make to the session data in your handlers are updated in the request context, and then the LoadAndSave() middleware updates the database with any changes to the session data before it returns.