33. Requests and forms — Homework solutions
The .go solution files are in
exercises/33/homework/solutions/.
Problem 1 — Greet from the query string
Problem. Read a name from the query string, and refuse an empty one.
How to think about it. Read, check, work.
Get returns "" for both a missing name and an
empty one, so one check covers both. The check ends in
return. The reply contains visitor input, so it is plain
text, and a browser will not run anything in it.
Worked solution.
package main
import (
"fmt"
"net/http"
)
func handleGreet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Please give a name.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Good morning, %s.\n", name)
}
func main() {
http.HandleFunc("GET /greet", handleGreet)
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/greet?name=Keiko"
HTTP/1.1 200 OK
Good morning, Keiko.
curl.exe -i http://localhost:8080/greet
HTTP/1.1 400 Bad Request
Please give a name.
Common mistakes.
- Leaving out
returnafterhttp.Error. The reply then hasGood morning, .after the error. - Leaving out the
Content-Type. Go guesses from the first bytes of the body, which is not something to rely on for visitor text.
Problem 2 — Double from the path
Problem. Read a number from the path, and check it.
How to think about it. Two things can be wrong, so
there are two checks, in order. The first asks whether it is a number at
all. The second asks whether the number is in range, which also means
that n*2 cannot overflow. The pattern
/double/{n} needs something after the slash, so
/double/ gives 404 from the router.
Worked solution.
package main
import (
"fmt"
"net/http"
"strconv"
)
func handleDouble(w http.ResponseWriter, r *http.Request) {
n, err := strconv.Atoi(r.PathValue("n"))
if err != nil {
http.Error(w, "The path must end in a whole number.", http.StatusBadRequest)
return
}
if n < -1000000 || n > 1000000 {
http.Error(w, "The number must be from -1000000 to 1000000.", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d doubled is %d\n", n, n*2)
}
func main() {
http.HandleFunc("GET /double/{n}", handleDouble)
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/double/21
HTTP/1.1 200 OK
21 doubled is 42
curl.exe -i http://localhost:8080/double/abc
HTTP/1.1 400 Bad Request
The path must end in a whole number.
curl.exe -i http://localhost:8080/double/2000000
HTTP/1.1 400 Bad Request
The number must be from -1000000 to 1000000.
Common mistakes.
- Using the number after a failed Atoi. It is
0, which looks like a real answer. - Writing
r.PathValue("number")when the pattern says{n}. The names must match. With no match,PathValuereturns"", so every request gets a 400.
Problem 3 — A rating form
Problem. Read a rating from a POST, check it, and redirect.
How to think about it. The same steps again, and the
last step is the redirect. A missing rating reaches Atoi as
"", which fails, so it needs no separate check. The
redirect is what makes a reload safe.
Worked solution.
package main
import (
"fmt"
"net/http"
"strconv"
)
const formPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rate this book</title>
</head>
<body>
<h1>Rate this book</h1>
<form action="/rating" method="post">
<p><label>Rating from 1 to 5: <input type="text" name="rating"></label></p>
<p><button type="submit">Send</button></p>
</form>
</body>
</html>
`
func handleForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, formPage)
}
func handleRating(w http.ResponseWriter, r *http.Request) {
rating, err := strconv.Atoi(r.FormValue("rating"))
if err != nil {
http.Error(w, "The rating must be a whole number.", http.StatusBadRequest)
return
}
if rating < 1 || rating > 5 {
http.Error(w, "The rating must be from 1 to 5.", http.StatusBadRequest)
return
}
fmt.Println("Rating received:", rating)
http.Redirect(w, r, "/thanks", http.StatusSeeOther)
}
func handleThanks(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Thank you for your rating.")
}
func main() {
http.HandleFunc("GET /{$}", handleForm)
http.HandleFunc("POST /rating", handleRating)
http.HandleFunc("GET /thanks", handleThanks)
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 -d "rating=4" http://localhost:8080/rating
HTTP/1.1 303 See Other
Location: /thanks
curl.exe -i -d "rating=9" http://localhost:8080/rating
HTTP/1.1 400 Bad Request
The rating must be from 1 to 5.
curl.exe -i -d "rating=abc" http://localhost:8080/rating
HTTP/1.1 400 Bad Request
The rating must be a whole number.
curl.exe -i http://localhost:8080/rating
HTTP/1.1 405 Method Not Allowed
Allow: POST
Method Not Allowed
Common mistakes.
- Trusting the form. The form could say
min="1", and a request fromcurl.exewould still arrive with9. Only a check in the handler counts. - Using
http.StatusMovedPermanentlyfor the redirect. Browsers remember that kind of redirect, and may never ask your server again. Use 303. - Trying to read
r.FormValueinsidehandleThanks. The redirect is a new request, and the values from the POST are not part of it.
Challenge — A calculator
Problem. A form, four operations, and useful errors.
How to think about it. Split it into pieces that can
be read alone, as in Chapter 05. readNumber converts one
value and checks its range, and is called twice. calculate
knows the four operations, and returns an error for an unknown one or
for division by zero. The handler only connects them, and turns each
error into a status 400.
This form uses GET because working out a sum changes nothing. The answer can be reloaded and bookmarked, and nothing is harmed if it is asked for twice. The address then shows the whole question.
Worked solution.
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
)
const formPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Calculator</title>
</head>
<body>
<h1>Calculator</h1>
<form action="/calc" method="get">
<p><label>a: <input type="text" name="a"></label></p>
<p><label>op:
<select name="op">
<option>add</option>
<option>sub</option>
<option>mul</option>
<option>div</option>
</select>
</label></p>
<p><label>b: <input type="text" name="b"></label></p>
<p><button type="submit">Calculate</button></p>
</form>
</body>
</html>
`
const limit = 1000000
// readNumber converts one form value, and checks its range.
func readNumber(label string, text string) (int, error) {
n, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("%s must be a whole number", label)
}
if n < -limit || n > limit {
return 0, fmt.Errorf("%s must be from %d to %d", label, -limit, limit)
}
return n, nil
}
// calculate returns the answer, or an error that is safe to show the visitor.
func calculate(a int, op string, b int) (int, error) {
switch op {
case "add":
return a + b, nil
case "sub":
return a - b, nil
case "mul":
return a * b, nil
case "div":
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
return 0, errors.New("op must be add, sub, mul or div")
}
func handleForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, formPage)
}
func handleCalc(w http.ResponseWriter, r *http.Request) {
a, err := readNumber("a", r.FormValue("a"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
b, err := readNumber("b", r.FormValue("b"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
op := r.FormValue("op")
answer, err := calculate(a, op, b)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d %s %d = %d\n", a, op, b, answer)
}
func main() {
http.HandleFunc("GET /{$}", handleForm)
http.HandleFunc("GET /calc", handleCalc)
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/calc?a=6&op=div&b=3"
HTTP/1.1 200 OK
6 div 3 = 2
curl.exe -i "http://localhost:8080/calc?a=6&op=div&b=0"
HTTP/1.1 400 Bad Request
cannot divide by zero
curl.exe -i "http://localhost:8080/calc?a=1&op=pow&b=1"
HTTP/1.1 400 Bad Request
op must be add, sub, mul or div
curl.exe -i "http://localhost:8080/calc?a=2000000&op=mul&b=2"
HTTP/1.1 400 Bad Request
a must be from -1000000 to 1000000
http.Error needs a string, and err.Error()
is the method that gives an error's text. This is safe here because
every error message is written by this program. The reply prints
op only after calculate has accepted it, so it
is one of the four known words.
Common mistakes.
- Forgetting the check for
b == 0. Integer division by zero crashes the handler. The server recovers and carries on, but the visitor gets no response. - Trusting the
<select>. A request can send anyopat all, so theswitchneeds a way out for an unknown one.