How does net/http handle a panic in a handler? What are the pitfalls?
http.Server recovers panics in each connection's goroutine. It logs the panic with a stack trace to ErrorLog and closes the connection, so the client gets a broken response and the rest of the server keeps running. Panicking with http.ErrAbortHandler aborts the response without logging a stack trace.
Pitfalls: (1) goroutines spawned by the handler are not protected, and a panic there kills the process. (2) The client gets no proper 500. (3) State the handler modified, such as locks held without defer Unlock or half-written caches, may be left inconsistent. Many teams add their own middleware so they control the response and the logging:
func Recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec == nil {
return
}
if rec == http.ErrAbortHandler {
panic(rec) // preserve abort semantics
}
slog.Error("panic", "err", rec, "path", r.URL.Path,
"stack", string(debug.Stack()))
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}()
next.ServeHTTP(w, r)
})
}
If the handler already wrote headers, http.Error can't change the status code. gRPC does not recover handler panics by default, so use a recovery interceptor.
More on Error Handling & panics
- Q321Why is
errors.Is(err, fs.ErrNotExist)preferred overos.IsNotExist(err)? - Q322What is "asserting errors for behavior"? How would you implement retry logic based on error behavior?
- Q324How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?
- Q325What is the
io.Readererror contract, and why isif err != nil { return }wrong there? - Q326Implement a wrapper error type that adds context and works with
errors.Is/As. What happens if you forgetUnwrap? - Q327What does this print? (Which side's
Ismethod gets called)