32. Your first web server — Homework solutions

The .go solution files are in exercises/32/homework/solutions/.

Problem 1 — Three pages

Problem. Three routes for GET, and 404 for everything else.

How to think about it. One handler for each page, and one line in main for each handler. The home page needs "GET /{$}", because "GET /" would match every path, and /contact/extra would get the home page. "GET /contact" has no slash at the end, so it matches only that exact path.

Worked solution.

package main

import (
    "fmt"
    "net/http"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This is the home page.")
}

func handleAbout(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This server has three pages.")
}

func handleContact(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Write to Keiko at the front desk.")
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)
    http.HandleFunc("GET /about", handleAbout)
    http.HandleFunc("GET /contact", handleContact)

    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)
    }
}

The results:

curl.exe -i http://localhost:8080/contact/extra
HTTP/1.1 404 Not Found
404 page not found

curl.exe -i -X POST http://localhost:8080/about
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD

Method Not Allowed

Common mistakes.

  • Writing "GET /" for the home page. Then /nothing gets status 200.
  • Writing "GET /contact/", with a slash at the end. That matches everything below it, so /contact/extra gets the page.
  • Writing handleAbout() in the http.HandleFunc line. That calls the function and passes its result. Pass the name, with no ().

Problem 2 — An HTML page

Problem. A page with a heading, a paragraph and a list.

How to think about it. The page does not change, so a constant holds it. It has several lines and some " characters, so a raw string fits. Set the header before writing the body.

Worked solution.

package main

import (
    "fmt"
    "net/http"
)

const homePage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Things I like</title>
</head>
<body>
<h1>Things I like</h1>
<p>Here are three of them.</p>
<ul>
<li>Fried rice</li>
<li>Long walks</li>
<li>Small programs</li>
</ul>
</body>
</html>
`

func handleHome(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    fmt.Fprint(w, homePage)
}

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)
    }
}

Common mistakes.

  • Using fmt.Fprintf(w, homePage). It works until the page contains a %, which Fprintf treats as the start of a verb. fmt.Fprint has no such problem.
  • Setting the header after fmt.Fprint. It is ignored, with no message.
  • Leaving out charset=utf-8, so that text such as é may show as strange characters.

Problem 3 — Status codes

Problem. Three routes, each with its own status.

How to think about it. The first does nothing special, because 200 is what you get if you set no status. The other two are the two helper functions, and each is one line.

Worked solution.

package main

import (
    "fmt"
    "net/http"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Try /ok, /gone and /locked.")
}

func handleOK(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "All good.")
}

func handleGone(w http.ResponseWriter, r *http.Request) {
    http.NotFound(w, r)
}

func handleLocked(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "Locked.", http.StatusForbidden)
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)
    http.HandleFunc("GET /ok", handleOK)
    http.HandleFunc("GET /gone", handleGone)
    http.HandleFunc("GET /locked", handleLocked)

    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)
    }
}

The results:

curl.exe -i http://localhost:8080/ok
HTTP/1.1 200 OK
All good.

curl.exe -i http://localhost:8080/gone
HTTP/1.1 404 Not Found
404 page not found

curl.exe -i http://localhost:8080/locked
HTTP/1.1 403 Forbidden
Locked.

Common mistakes.

  • Writing 403 in place of http.StatusForbidden. It works, but the name tells the next reader what it means.
  • Mixing up the order of http.Error. It is the writer, then the text, then the status.

Challenge — A small site

Problem. A helper that writes a whole page, used by several handlers, and your own 404 page.

How to think about it. The page is the same every time except for two pieces, so it can be a format string with %s in it, filled in with fmt.Fprintf. The helper does the three steps in the order that a response goes out: headers, status, body. That is why status is one of its parameters: the 404 handler cannot set the status first and then call a helper that sets a header.

"GET /" catches every GET path that nothing more specific matches, so it is the place for a 404 page. It is better than "/" here: other methods on /about still get 405, because the router sees that the path exists for GET.

Worked solution.

package main

import (
    "fmt"
    "net/http"
)

const pageFormat = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>%s</title>
</head>
<body>
<h1>%s</h1>
<p>%s</p>
<p><a href="/">Home</a> | <a href="/about">About</a> | <a href="/games">Games</a></p>
</body>
</html>
`

// writePage sends a whole page. The title and the body must be text from this
// program, never text from a visitor, because they go into the HTML as they are.
func writePage(w http.ResponseWriter, status int, title string, body string) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.WriteHeader(status)
    fmt.Fprintf(w, pageFormat, title, title, body)
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "Home", "Welcome to a small site.")
}

func handleAbout(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "About", "This site is one Go file.")
}

func handleGames(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusOK, "Games", "Number Guess and Monster Battle.")
}

func handleNotFound(w http.ResponseWriter, r *http.Request) {
    writePage(w, http.StatusNotFound, "Not found", "There is no page at this address.")
}

func main() {
    http.HandleFunc("GET /{$}", handleHome)
    http.HandleFunc("GET /about", handleAbout)
    http.HandleFunc("GET /games", handleGames)
    http.HandleFunc("GET /", handleNotFound)

    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)
    }
}

The comment on writePage matters. This helper puts its arguments into HTML as they are. That is fine for text in your own program, and unsafe for text from a visitor. The next chapter shows why, and Chapter 34 shows the proper tool.

Common mistakes.

  • Calling w.WriteHeader first and then setting the Content-Type in the helper. The header is ignored.
  • Writing w.WriteHeader(http.StatusNotFound) after fmt.Fprintf. The page goes out with status 200.
  • Registering the 404 page as "/". Then POST /about gets your 404 page, not 405.