Every shop has bugs nobody has found yet. In this shop the Gift card (product 8) has no category: it is NULL in the database, and a Go string can't hold NULL, so Scan returns an error and the route calls panic(err). When a route panics, Go's net/http logs it with a stack trace and closes the connection: the visitor gets no answer at all, not even a status code.
You rescue a panic with a deferred function. A deferred call runs when its function ends, also when it ends in a panic, and recover() inside it stops the panic and gives its value, or nil when nothing panicked. Here it is in a small program, away from any route:
func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println("stopped a panic:", err)
}
}()
panic("something broke")
}
It prints stopped a panic: something broke instead of crashing. A 500 answer should never carry the error's own text: it names your columns, tables and files, gives an attacker a map of your code, and means nothing to a customer. Send a short message instead, and write the details to the log with log.Printf, where only you read them. A function you defer from the route can take w and r, so it can answer and say which request failed.
The check sends three requests, closes the database for a fourth, and runs your test on the shop and on two broken copies. It must print:
The shop gets three requests:
GET /products/1 -> 200 {"category": "Kitchen", "name": "Mug"}
GET /products/99 -> 404 {"error": "Product not found"}
GET /products/8 -> 500 {"error": "Something went wrong on our side"}
log: GET /products/8 failed: sql: Scan error on column index 1, name "category": converting NULL to string is unsupported
The database closes:
GET /products/1 -> 500 {"error": "Something went wrong on our side"}
log: GET /products/1 failed: sql: database is closed
Your tests on the shop:
TestCrashAnswersJSON ... ok
1 test: OK
Your tests on a copy without serverError, where net/http closes the connection:
TestCrashAnswersJSON ... ERROR
panic: runtime error: invalid memory address or nil pointer dereference
1 test: 1 failed, so they caught this bug
Your tests on a copy that sends the error to the visitor:
TestCrashAnswersJSON ... FAIL
Not equal: expected map[error:Something went wrong on our side], actual map[error:sql: Scan error on column index 1, name "category": converting NULL to string is unsupported]
1 test: 1 failed, so they caught this bug
Your task: below newApp, write serverError(w http.ResponseWriter, r *http.Request). It calls recover(), and when that gives an error, it logs one line with log.Printf and the text "%s %s failed: %v", filled with the method, the path and the error, then answers {"error": "Something went wrong on our side"} with writeJSON and the status 500. Defer a call to it as the first line of the route's function. Then, in ErrorTests, write TestCrashAnswersJSON: get /products/8 and check both the status code and the JSON body.
Assignment
Follow the Coding Tutorial to add serverError with its log line, defer it in the route, then write a test that checks both the status and the JSON body of a crash.
Hintrecover() only stops a panic inside a deferred call, so serverError does nothing until the route says defer serverError(w, r). A test that checks only the status code catches the copy that gives no answer, but lets the leaky copy through, because that is a 500 too: compare readJSON(response) with the map as well.
Free plan: 30 tutor messages. PRO is unlimited.
