Handle Panic in HTTP Server by Using Middleware in Go

Handle Panic in HTTP Server by Using Middleware in Go

If you have code in Go, most likely you have ever encountered a panic. Panic can stop your application if it is not recovered. Fortunately, in the Go HTTP server, there is already a panic recovery, so the server will continue to run if panic is encountered. But the client will not get any response from the server if a panic happens. It is good that you have your own panic recovery that responds with an error message.

How to recover panic using middleware

To recover from panic, we use recover function. The recover function is called inside a defered function. If panic happen, it will return the value of the panic. If not, it will return nil.

1
2
3
4
5
6
7
8
9
func myFunc() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("got panic", err)
        }
    }()

    // do someting that may cause panic..
}
We can do something to monitor if panic happens in middleware, such as write to the log, send a notification to the engineer (using slack or email), trigger an alert, or other things that suitable for your needs.

Why should we use middleware to handle panic in the HTTP server? A middleware is a function that can run before or after your HTTP handler. So when a request to the server happens, the flow of control will go to middleware first, then the handler, then back to the middleware. It is very suitable to handle panic in middleware. When panic is encountered, the process will defer to the middleware, that we put our recovery function. The middleware is also reusable so we can use it for all handlers with little change. Below are a few samples of middleware to recover from panic.

There are many request router library in Go. So I will give some examples of the middleware for some router libraries.

Go net/http Router

Below is middleware for the default Go’s net/http handler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func panicRecovery(h func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                buf := make([]byte, 2048)
                n := runtime.Stack(buf, false)
                buf = buf[:n]

                fmt.Printf("recovering from err %v\n %s", err, buf)
                w.Write([]byte(`{"error":"our server got panic"}`))
            }
        }()

        h(w, r)
    }
}
The middleware capture panic, write the log, and respond with an error message. To use the middleware:
1
2
3
4
5
6
7
// to use the middleware:
http.HandleFunc("/panic_test", panicRecovery(myHandler))

// the handler to test:
func myHandler(w http.ResponseWriter, r *http.Request) {
    panic("panic dari httpHandler")
}

julienschmidt/httprouter

This is the middleware if you use for julienschmidt/httprouter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func panicRecovery(h func(w http.ResponseWriter, r *http.Request, p httprouter.Params)) func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
        defer func() {
            if err := recover(); err != nil {
                buf := make([]byte, 2048)
                n := runtime.Stack(buf, false)
                buf = buf[:n]

                fmt.Printf("recovering from err %v\n %s", err, buf)
                w.Write([]byte(`{"error":"our server got panic"}`))
            }
        }()

        h(w, r, p)
    }
}

gorilla/mux

If you use gorilla/mux, you can use method Use of the router. The middleware that is appended using Use method will be called before every handler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func panicRecovery(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                buf := make([]byte, 2048)
                n := runtime.Stack(buf, false)
                buf = buf[:n]

                fmt.Printf("recovering from err %v\n %s", err, buf)
                w.Write([]byte(`{"error":"our server got panic"}`))
            }
        }()

        h.ServeHTTP(w, r)
    })
}
To register the middleware:
1
2
3
4
5
6
    r := mux.NewRouter()

    r.HandleFunc("/panic_test", myHandler)
    r.Use(panicRecovery)

    http.ListenAndServe(":5005", r)

go-chi/chi

chi has its own middleware to recover from panic, see here. It recovers from panic and prints the stack trace. But if you want to use your own middleware, you can use the same middleware with gorilla/mux above because the function’s signature is the same.

Conclusion

Middleware is a very suitable place to handle panic in an HTTP server. Even though there is a default panic recovery in the HTTP server, it is better if we use our own middleware. The best thing about using your own middleware to recover from panic is that you can add monitoring and alert for immediate mitigation. You can give a response that matched your response type and structure. You can also add something more suitable for your use case, such as returning HTTP code 500 when panic happens.
Leave a comment because the comment section is always open.


See also

comments powered by Disqus