33. Request dan formulir — Solusi PR

File solusi .go ada di exercises/33/homework/solutions/.

Soal 1 — Sapaan dari query string

Soal. Baca nama dari query string, dan tolak yang kosong.

Cara berpikir. Baca, periksa, kerjakan. Get mengembalikan "" untuk nama yang tidak ada maupun yang kosong, jadi satu pemeriksaan mencakup keduanya. Pemeriksaan itu diakhiri return. Jawabannya memuat input pengunjung, jadi berupa teks biasa, dan browser tidak akan menjalankan apa pun di dalamnya.

Solusi yang dikerjakan.

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

Hasilnya:

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.

Kesalahan umum.

  • Melewatkan return sesudah http.Error. Jawabannya lalu memuat Good morning, . sesudah error.
  • Melewatkan Content-Type. Go menebak dari byte pertama body, dan itu bukan sesuatu yang bisa diandalkan untuk teks pengunjung.

Soal 2 — Kelipatan dua dari path

Soal. Baca angka dari path, dan periksa.

Cara berpikir. Ada dua hal yang bisa salah, jadi ada dua pemeriksaan, berurutan. Yang pertama bertanya apakah itu angka. Yang kedua bertanya apakah angkanya dalam rentang, yang juga berarti n*2 tidak bisa overflow. Pola /double/{n} butuh sesuatu sesudah garis miring, jadi /double/ memberikan 404 dari router.

Solusi yang dikerjakan.

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

Hasilnya:

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.

Kesalahan umum.

  • Memakai angka sesudah Atoi gagal. Nilainya 0, yang tampak seperti jawaban sungguhan.
  • Menulis r.PathValue("number") padahal polanya {n}. Namanya harus cocok. Kalau tidak cocok, PathValue mengembalikan "", jadi setiap request mendapat 400.

Soal 3 — Formulir penilaian

Soal. Baca nilai dari POST, periksa, dan redirect.

Cara berpikir. Langkah yang sama lagi, dan langkah terakhirnya redirect. Nilai yang tidak ada sampai ke Atoi sebagai "", yang gagal, jadi tidak perlu pemeriksaan tersendiri. Redirect itulah yang membuat muat ulang aman.

Solusi yang dikerjakan.

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

Hasilnya:

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

Kesalahan umum.

  • Memercayai formulir. Formulir bisa saja bertuliskan min="1", dan request dari curl.exe tetap bisa datang dengan 9. Hanya pemeriksaan di handler yang berarti.
  • Memakai http.StatusMovedPermanently untuk redirect. Browser mengingat redirect jenis itu, dan mungkin tidak pernah bertanya lagi ke servermu. Pakai 303.
  • Mencoba membaca r.FormValue di dalam handleThanks. Redirect adalah request baru, dan nilai dari POST bukan bagian darinya.

Tantangan — Kalkulator

Soal. Formulir, empat operasi, dan error yang berguna.

Cara berpikir. Pecah menjadi potongan yang bisa dibaca sendiri, seperti di Bab 05. readNumber mengubah satu nilai dan memeriksa rentangnya, dan dipanggil dua kali. calculate mengetahui keempat operasi, dan mengembalikan error untuk operasi yang tidak dikenal atau pembagian dengan nol. Handler hanya menghubungkannya, dan mengubah setiap error menjadi status 400.

Formulir ini memakai GET karena menghitung tidak mengubah apa-apa. Jawabannya bisa dimuat ulang dan disimpan sebagai bookmark, dan tidak ada yang rusak kalau diminta dua kali. Alamatnya lalu menampilkan seluruh pertanyaan.

Solusi yang dikerjakan.

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

Hasilnya:

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 butuh string, dan err.Error() adalah metode yang memberikan teks sebuah error. Ini aman di sini karena setiap pesan error ditulis oleh program ini. Jawabannya mencetak op hanya sesudah calculate menerimanya, jadi nilainya salah satu dari empat kata yang dikenal.

Kesalahan umum.

  • Lupa pemeriksaan b == 0. Pembagian bilangan bulat dengan nol membuat handler crash. Server pulih dan berjalan terus, tetapi pengunjung tidak mendapat response.
  • Memercayai <select>. Request bisa mengirim op apa saja, jadi switch butuh jalan keluar untuk yang tidak dikenal.