Let's Go Further Structured Logging and Error Handling › Panic Recovery
Previous · Contents · Next
Chapter 10.2.

Panic Recovery

At the moment any panics in our API handlers will be recovered automatically by Go’s http.Server. This will unwind the stack for the affected goroutine (calling any deferred functions along the way), close the underlying HTTP connection, and log an error message and stack trace.

This behavior is OK, but it would be better for the client if we could also send a 500 Internal Server Error response to explain that something has gone wrong — rather than just closing the HTTP connection with no context.

In Let’s Go we talked through the details of how to do this by creating some middleware to recover the panic, and it makes sense to do the same thing again here.

If you’re following along, go ahead and create a cmd/api/middleware.go file:

$ touch cmd/api/middleware.go

And inside that file add a new recoverPanic() middleware:

File: cmd/api/middleware.go
package main

import (
    "fmt"
    "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 err := recover(); err != nil {
                // If there was a panic, set a "Connection: close" header on the 
                // response. This acts as a trigger to make Go's HTTP server 
                // automatically close the current connection after a response has been 
                // sent.
                w.Header().Set("Connection", "close")
                // The value returned by recover() has the type any, so we use
                // fmt.Errorf() to normalize it into an error and call our 
                // serverErrorResponse() helper. In turn, this will log the error using
                // our custom Logger type at the ERROR level and send the client a 500
                // Internal Server Error response.
                app.serverErrorResponse(w, r, fmt.Errorf("%s", err))
            }
        }()

        next.ServeHTTP(w, r)
    })
}

Once that’s done, we need to update our cmd/api/routes.go file so that the recoverPanic() middleware wraps our router. This will ensure that the middleware runs for every one of our API endpoints.

File: cmd/api/routes.go
package main

...

// Update the routes() method to return a http.Handler instead of a *httprouter.Router.
func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)

    router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)

    // Wrap the router with the panic recovery middleware.
    return app.recoverPanic(router)
}

Now that’s in place, if there is a panic in one of our API handlers the recoverPanic() middleware will recover it and call our regular app.serverErrorResponse() helper. In turn, that will log the error using our custom Logger and send the client a nice 500 Internal Server Error response with a JSON body.


Additional Information

Panic recovery in other goroutines

It’s really 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 background goroutine will not be recovered — not by the recoverPanic() middleware… and not by the panic recovery built into http.Server. These panics will cause your application to exit and bring down the server.

So, if you are spinning up additional goroutines from within your handlers and there is any chance of a panic, you must make sure that you recover any panics from within those goroutines too.

We’ll look at this topic in more detail later in the book, and demonstrate how to deal with it when we use a background goroutine to send welcome emails to our API users.