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/nothinggets status 200. - Writing
"GET /contact/", with a slash at the end. That matches everything below it, so/contact/extragets the page. - Writing
handleAbout()in thehttp.HandleFuncline. 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%, whichFprintftreats as the start of a verb.fmt.Fprinthas 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
403in place ofhttp.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.WriteHeaderfirst and then setting theContent-Typein the helper. The header is ignored. - Writing
w.WriteHeader(http.StatusNotFound)afterfmt.Fprintf. The page goes out with status 200. - Registering the 404 page as
"/". ThenPOST /aboutgets your 404 page, not 405.