Panic recovery
In a simple Go application, when your code panics it will result in the application being terminated straight away.
But our web application is a bit more sophisticated. Go’s HTTP server assumes that the effect of any panic is isolated to the goroutine serving the active HTTP request (remember, every request is handled in it’s own goroutine).
Specifically, following a panic our server will log a stack trace to the server error log (which we will talk about later in the book), unwind the stack for the affected goroutine (calling any deferred functions along the way) and close the underlying HTTP connection. But it won’t terminate the application, so importantly, any panic in your handlers won’t bring down your server.
But if a panic does happen in one of our handlers, what will the user see?
Let’s take a look and introduce a deliberate panic into our home
handler.
package main ... func (app *application) home(w http.ResponseWriter, r *http.Request) { panic("oops! something went wrong") // Deliberate panic snippets, err := app.snippets.Latest() if err != nil { app.serverError(w, r, err) return } data := app.newTemplateData(r) data.Snippets = snippets app.render(w, r, http.StatusOK, "home.tmpl", data) } ...
Restart your application…
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
… and make a HTTP request for the home page from a second terminal window:
$ curl -i http://localhost:4000 curl: (52) Empty reply from server
Unfortunately, all we get is an empty response due to Go closing the underlying HTTP connection following the panic.
This isn’t a great experience for the user. It would be more appropriate and meaningful to send them a proper HTTP response with a 500 Internal Server Error
status instead.
A neat way of doing this is to create some middleware which recovers the panic and calls our app.serverError()
helper method. To do this, we can leverage the fact that deferred functions are always called when the stack is being unwound following a panic.
Open up your middleware.go
file and add the following code:
package main import ( "fmt" // New import "net/http" ) ... func (app *application) recoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create a deferred function (which will always be run in the event // of a panic as Go unwinds the stack). defer func() { // Use the builtin recover function to check if there has been a // panic or not. If there has... if err := recover(); err != nil { // Set a "Connection: close" header on the response. w.Header().Set("Connection", "close") // Call the app.serverError helper method to return a 500 // Internal Server response. app.serverError(w, r, fmt.Errorf("%s", err)) } }() next.ServeHTTP(w, r) }) }
There are two details about this which are worth explaining:
Setting the
Connection: Close
header on the response acts as a trigger to make Go’s HTTP server automatically close the current connection after a response has been sent. It also informs the user that the connection will be closed. Note: If the protocol being used is HTTP/2, Go will automatically strip theConnection: Close
header from the response (so it is not malformed) and send aGOAWAY
frame.The value returned by the builtin
recover()
function has the typeany
, and its underlying type could bestring
,error
, or something else — whatever the parameter passed topanic()
was. In our case, it’s the string"oops! something went wrong"
. In the code above, we normalize this into anerror
by using thefmt.Errorf()
function to create a newerror
object containing the default textual representation of theany
value, and then pass thiserror
to theapp.serverError()
helper method.
Let’s now put this to use in the routes.go
file, so that it is the first thing in our chain to be executed (so that it covers panics in all subsequent middleware and handlers).
package main import "net/http" func (app *application) routes() http.Handler { mux := http.NewServeMux() fileServer := http.FileServer(http.Dir("./ui/static/")) mux.Handle("GET /static/", http.StripPrefix("/static", fileServer)) mux.HandleFunc("GET /{$}", app.home) mux.HandleFunc("GET /snippet/view/{id}", app.snippetView) mux.HandleFunc("GET /snippet/create", app.snippetCreate) mux.HandleFunc("POST /snippet/create", app.snippetCreatePost) // Wrap the existing chain with the recoverPanic middleware. return app.recoverPanic(app.logRequest(commonHeaders(mux))) }
If you restart the application and make a request for the homepage now, you should see a nicely formed 500 Internal Server Error
response following the panic, including the Connection: close
header that we talked about.
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
$ curl -i http://localhost:4000 HTTP/1.1 500 Internal Server Error Connection: close Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com Content-Type: text/plain; charset=utf-8 Referrer-Policy: origin-when-cross-origin Server: Go X-Content-Type-Options: nosniff X-Frame-Options: deny X-Xss-Protection: 0 Date: Wed, 18 Mar 2024 11:29:23 GMT Content-Length: 22 Internal Server Error
Before we continue, head back to your home
handler and remove the deliberate panic from the code.
package main ... func (app *application) home(w http.ResponseWriter, r *http.Request) { snippets, err := app.snippets.Latest() if err != nil { app.serverError(w, r, err) return } data := app.newTemplateData(r) data.Snippets = snippets app.render(w, r, http.StatusOK, "home.tmpl", data) } ...
Additional information
Panic recovery in background goroutines
It’s important to realize that our middleware will only recover panics that happen in the same goroutine that executed the recoverPanic()
middleware.
If, for example, you have a handler which spins up another goroutine (e.g. to do some background processing), then any panics that happen in the second goroutine will not be recovered — not by the recoverPanic()
middleware… and not by the panic recovery built into Go HTTP server. They will cause your application to exit and bring down the server.
So, if you are spinning up additional goroutines from within your web application and there is any chance of a panic, you must make sure that you recover any panics from within those too. For example:
func (app *application) myHandler(w http.ResponseWriter, r *http.Request) { ... // Spin up a new goroutine to do some background processing. go func() { defer func() { if err := recover(); err != nil { app.logger.Error(fmt.Sprint(err)) } }() doSomeBackgroundProcessing() }() w.Write([]byte("OK")) }