34. HTML templates

Chapter 33 ended with a problem: printing a visitor's text into HTML lets the visitor write part of your page. Go's html/template package solves this. You write the page once, with gaps in it, and the package fills the gaps and escapes the values for you.

A first template

A template is text with actions between {{ and }}. Here is exercises/34/01-first-template.go:

// Chapter 34 example: a template in a string, filled from a struct.
// Run with: go run exercises/34/01-first-template.go

package main

import (
    "fmt"
    "html/template"
    "net/http"
)

type playerPage struct {
    Name  string
    Level int
}

const playerText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Name}}</title>
</head>
<body>
<h1>{{.Name}}</h1>
<p>Level {{.Level}}</p>
</body>
</html>
`

var playerTemplate = template.Must(template.New("player").Parse(playerText))

func handleHome(w http.ResponseWriter, r *http.Request) {
    data := playerPage{Name: "Keiko", Level: 7}

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    err := playerTemplate.Execute(w, data)
    if err != nil {
        fmt.Println("Template error:", err)
    }
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

Open http://localhost:8080, and the page shows the heading Keiko and the line Level 7. The body of the response contains:

<title>Keiko</title>
<h1>Keiko</h1>
<p>Level 7</p>

Three things are new.

{{.Name}} means "put the Name field here". The dot is the value you passed in.

template.Must(template.New("player").Parse(playerText)) reads from the inside out. template.New makes an empty template with a name, which appears in its error messages. Parse reads your text, and returns the template and an error. template.Must takes both, and crashes the program if the error is not nil. Crashing sounds rude, but this runs once, when the program starts, on text you wrote. A mistake there can never be caused by a visitor, and it is better found at once. So a variable outside main can hold the result. For example, a missing } on the <h1> line, as in {{.Name}, stops the program straight away:

panic: template: player:8: bad character U+007D '}'

playerTemplate.Execute(w, data) fills in the gaps and writes the result to w.

Fields must be exported

The template package is a different package from yours, so Chapter 25's rule applies: it can see only names that start with a capital letter. With a field called name and {{.name}} in the template, the page stops partway, and the terminal says:

Template error: template: player:5:9: executing "player" at <.name>: name is an unexported field of struct type main.playerPage

If you change the field to name and leave the template with {{.Name}}, the message is different, but the cause is the same:

Template error: template: player:5:9: executing "player" at <.Name>: can't evaluate field Name in type main.playerPage

Go cannot catch these while compiling, because a template is only a string until the program runs. Go's compiler checks your code, and not your templates.

When Execute fails

That error has a catch. Execute writes as it goes along, so by the time it fails, part of the page has gone out with status 200. It is too late to change the status. exercises/34/02-execute-error.go shows this, with a template that asks for a field that does not exist:

curl.exe -i http://localhost:8080/direct
HTTP/1.1 200 OK
<h1>Keiko</h1>
<p>

The visitor gets half a page, marked as a success. The fix is to build the page in memory first, and send it only if that worked:

func handleBuffered(w http.ResponseWriter, r *http.Request) {
    data := pageData{Name: "Keiko"}

    var page bytes.Buffer
    err := brokenTemplate.Execute(&page, data)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

bytes.Buffer from the bytes package is a place in memory to write to. Execute accepts it for the same reason that it accepts w. Its first parameter is an interface, io.Writer, and any type with the right Write method satisfies it, as in Chapter 29. Write has a pointer receiver on the buffer, so you pass &page. Forgetting the & stops the compiler:

cannot use page (variable of type bytes.Buffer) as io.Writer value in argument to brokenTemplate.Execute: bytes.Buffer does not implement io.Writer (method Write has pointer receiver)

page.WriteTo(w) sends what the buffer holds. Now the result is honest:

curl.exe -i http://localhost:8080/buffered
HTTP/1.1 500 Internal Server Error
The page could not be built.

The terminal gets the details, and the visitor gets a short message:

Template error: template: broken:2:5: executing "broken" at <.Missing>: can't evaluate field Missing in type main.pageData

Details of your program are for you, not for visitors. From here on, every handler builds its page in a buffer.

range and if

Templates have loops and conditions. Here is the template in exercises/34/03-range-and-if.go:

<ul>
{{range .Items}}
<li>{{.Name}}{{if .Rare}} (rare){{end}}</li>
{{else}}
<li>Nothing here yet.</li>
{{end}}
</ul>

And the data it gets:

type item struct {
    Name string
    Rare bool
}

type listPage struct {
    Title string
    Items []item
}
  • {{range .Items}} repeats everything up to its {{end}}, once per item. Inside it, the dot changes to the current item, so {{.Name}} is that item's name.
  • {{else}} in a range runs when the slice is empty. That is the place for a message like Nothing here yet.
  • {{if .Rare}} shows its content when the value is true. For strings, numbers and slices, the empty value counts as false, so {{if .Error}} asks whether there is an error message to show.

Both routes in this program use the same template. Blank lines are left out here:

curl.exe http://localhost:8080/
<h1>My bag</h1>
<ul>
<li>sword</li>
<li>moon stone (rare)</li>
<li>potion</li>
</ul>

curl.exe http://localhost:8080/empty
<h1>An empty bag</h1>
<ul>
<li>Nothing here yet.</li>
</ul>

In the real output, the template's line endings around {{range}} leave blank lines in the HTML. Browsers ignore them, so they do no harm.

Open exercises/34/03-range-and-if.go. Add a field Weight int to item, give each item a weight, and show it in the list. Remember to use a capital letter.

Automatic escaping

Here is the echo from Chapter 33, made safe. This is exercises/34/04-safe-echo.go:

// Chapter 34 example: the echo from Chapter 33, made safe with html/template.
// Run with: go run exercises/34/04-safe-echo.go

package main

import (
    "bytes"
    "fmt"
    "html/template"
    "net/http"
)

type echoPage struct {
    Text string
}

const echoText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Echo</title>
</head>
<body>
<p>You said: {{.Text}}</p>
<form action="/echo" method="get">
<p><input type="text" name="text" value="{{.Text}}"></p>
<p><button type="submit">Say it again</button></p>
</form>
</body>
</html>
`

var echoTemplate = template.Must(template.New("echo").Parse(echoText))

func handleEcho(w http.ResponseWriter, r *http.Request) {
    data := echoPage{Text: r.URL.Query().Get("text")}

    var page bytes.Buffer
    err := echoTemplate.Execute(&page, data)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    http.HandleFunc("GET /echo", handleEcho)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err := http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

Send the same attack as before, with the same kind of command:

curl.exe -G --data-urlencode "text=<script>alert('hello')</script>" http://localhost:8080/echo

The two lines of the body that hold the text are:

<p>You said: &lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;</p>
<p><input type="text" name="text" value="&lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;"></p>

Your handler does nothing special. html/template replaced each special character with a code that a browser shows and does not run. The visitor sees the text <script>alert('hello')</script> on the page, as ordinary text, and no box pops up. Open http://localhost:8080/echo?text=<script>alert('hello')</script> to compare it with Chapter 33.

The package also knows where in the page each gap is. Look at the second line above: the value is inside an attribute, between " and ", where a " would end it. An attack that tries to break out of the attribute gets its quotes escaped too:

<p><input type="text" name="text" value="&#34; onmouseover=&#34;alert(1)"></p>

Why not text/template

Go has a second package with nearly the same functions, text/template. It does no escaping, because it is for text that is not HTML. exercises/34/06-text-template-danger.go puts the same input through both. It has no server, and it uses the import aliases from Chapter 25 because both packages are called template:

// Chapter 34 example: the same input through text/template and html/template.
// This program has no server. It prints both results and stops.
// Run with: go run exercises/34/06-text-template-danger.go

package main

import (
    "fmt"
    htmltemplate "html/template"
    "os"
    texttemplate "text/template"
)

const pageText = "<p>{{.}}</p>\n"

const input = `<script>alert("hello")</script>`

func main() {
    wrong := texttemplate.Must(texttemplate.New("wrong").Parse(pageText))
    right := htmltemplate.Must(htmltemplate.New("right").Parse(pageText))

    fmt.Println("text/template:")
    err := wrong.Execute(os.Stdout, input)
    if err != nil {
        fmt.Println("Error:", err)
    }

    fmt.Println("html/template:")
    err = right.Execute(os.Stdout, input)
    if err != nil {
        fmt.Println("Error:", err)
    }
}

Output:

text/template:
<p><script>alert("hello")</script></p>
html/template:
<p>&lt;script&gt;alert(&#34;hello&#34;)&lt;/script&gt;</p>

The code is otherwise identical, and the wrong import compiles and looks fine. So check the import line: for a web page, it must be html/template. An editor's automatic import can pick the wrong one.

A template in a file

A template of more than a few lines is easier to edit in a file of its own. The file needs to be found when the program runs, so this example is a module with its own folder, exercises/34/05-template-file/:

05-template-file/
    go.mod
    main.go
    templates/
        scores.html

templates/scores.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Scores of {{.Player}}</title>
</head>
<body>
<h1>Scores of {{.Player}}</h1>
<ul>
{{range .Scores}}
<li>{{.}}</li>
{{end}}
</ul>
</body>
</html>

Inside {{range .Scores}}, each item is a plain int, so {{.}} means the value itself.

main.go:

// Chapter 34 example: a template in its own file.
//
// Run from this folder, because the template path is relative to it:
//   cd exercises/34/05-template-file
//   go run .

package main

import (
    "bytes"
    "fmt"
    "html/template"
    "net/http"
)

type scorePage struct {
    Player string
    Scores []int
}

var scoreTemplate *template.Template

func handleHome(w http.ResponseWriter, r *http.Request) {
    data := scorePage{Player: "Keiko", Scores: []int{40, 25, 70}}

    var page bytes.Buffer
    err := scoreTemplate.Execute(&page, data)
    if err != nil {
        fmt.Println("Template error:", err)
        http.Error(w, "The page could not be built.", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    page.WriteTo(w)
}

func main() {
    parsed, err := template.ParseFiles("templates/scores.html")
    if err != nil {
        fmt.Println("Could not read the template:", err)
        fmt.Println("Run this program from its own folder, and check the template.")
        return
    }
    scoreTemplate = parsed

    http.HandleFunc("GET /{$}", handleHome)

    fmt.Println("Server running at http://localhost:8080")
    fmt.Println("Press Ctrl+C to stop it.")

    err = http.ListenAndServe("localhost:8080", nil)
    if err != nil {
        fmt.Println("Server stopped:", err)
    }
}

template.ParseFiles reads and parses the file, and returns the template and an error. This time the error can happen for reasons outside the code, so main checks it, prints a clear message, and stops. It does not use template.Must, because a crash is a poor way of telling a person that they are in the wrong folder.

The path is relative to the folder that the program runs from. So run it from its own folder:

cd exercises/34/05-template-file
go run .

To go back up to go-work afterwards:

cd ../../..

Try running it from go-work by naming the file. It compiles, because this program has only one .go file, and then it cannot find the template:

go run exercises/34/05-template-file/main.go
Could not read the template: open templates/scores.html: no such file or directory
Run this program from its own folder, and check the template.

On Windows, the end of the first line is worded differently: it says that the system cannot find the path specified.

The page in this file, like every page in this book, uses nothing from other servers. There are no outside stylesheets, scripts or fonts. Each one would mean a request to another computer every time someone opens your page.

Go into exercises/34/05-template-file. Add a heading to templates/scores.html, and restart the server. The file is read once, at the start, so the change does not appear until you restart.

Homework

Homework starters are in exercises/34/homework/. Each one runs as it is and shows a placeholder. The challenge is a module, so cd into its folder and use go run ..

Problem 1 — A profile page

Open exercises/34/homework/01-profile.go. Define a struct with the fields Name, City and Level. In handleHome, fill one with your own values, and pass it to the template. Finish the template so that it shows all three.

Problem 2 — A task list

Open exercises/34/homework/02-task-list.go. The tasks are in a package-level slice, and nothing changes them. Finish the template: show one <li> per task, add (done) after the name of a finished task, and show Nothing to do. when the slice is empty. The slice itself is passed to Execute, so {{range .}} loops over it.

Problem 3 — A safe greeting

Open exercises/34/homework/03-safe-greeting.go. GET /greet?name=Keiko shows Hello, Keiko! on an HTML page. Use a template, so that a name such as <b>Keiko</b> shows up as text and not in bold. Check it with curl.exe, and look for &lt; in the reply. A missing or empty name replies with status 400.

Challenge — Players from a template file

Folder: exercises/34/homework/04-players/.

Finish templates/players.html so that it lists the players, and says so when there are none. Then finish handleHome in main.go. GET / shows every player, and GET /?min=50 shows only players with a score of 50 or more. A min that is not a whole number replies with status 400. The form on the page shows the min that was asked for.

Stuck or finished? Open the homework solutions page.