├── .gitignore ├── .travis.yml ├── LICENSE ├── README.MD ├── catchers ├── log.go └── slack.go ├── go.mod ├── go.sum ├── panicMiddleware.go └── panicMiddleware_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | - master 5 | os: 6 | - linux 7 | - osx 8 | dist: trusty 9 | sudo: false 10 | install: true 11 | script: 12 | - env GO111MODULE=on go build 13 | - env GO111MODULE=on go test ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matt Boyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # go-panic-catch 2 | [![GoDoc](https://godoc.org/github.com/MatthewJamesBoyle/go-panic-catch?status.svg)](https://godoc.org/github.com/MatthewJamesBoyle/go-panic-catch) [![Build Status](https://travis-ci.org/MatthewJamesBoyle/go-panic-catch.svg?branch=master)](https://travis-ci.org/MatthewJamesBoyle/go-panic-catch) 3 | 4 | go-panic-catch is a simple middleware for golang web servers that recovers from panics and enables you to handle panics gracefully. 5 | 6 | To get it: 7 | 8 | ```go get github.com/matthewjamesboyle/go-panic-catch``` 9 | 10 | And then to use it: 11 | ``` 12 | package main 13 | 14 | import ( 15 | "github.com/matthewjamesboyle/go-panic-catch" 16 | "github.com/matthewjamesboyle/go-panic-catch/catchers" 17 | "net/http" 18 | ) 19 | 20 | func main() { 21 | fn := func(writer http.ResponseWriter, req *http.Request) { 22 | panic("ut oh") 23 | } 24 | 25 | //something that satisfies panicHandler interface 26 | slack := catchers.NewSlack("some-webhook-url") 27 | 28 | server := &http.Server{ 29 | Handler: goCatch.PanicMiddleware(*slack, "recovering from panic will write this to slack", http.HandlerFunc(fn)), 30 | } 31 | 32 | server.ListenAndServe() 33 | } 34 | 35 | ``` 36 | 37 | It should also work with all the popular web servers such as gorillia mux and gin. 38 | 39 | go-panic-catch currently comes with two "catchers" that handle panics. 40 | * slack catcher - will write a message to slack everytime your web server panics 41 | * log - Logs "message" every time there is a panic. 42 | 43 | Want to implement your own handler? Simply implement the following interface: 44 | ``` 45 | type PanicHandler interface { 46 | HandlePanic(message string) error 47 | } 48 | ``` 49 | 50 | Some ideas for other handlers: 51 | * email everytime there is a panic. 52 | * increment a prometheus counter. 53 | 54 | (contributions appreciated if you want to work on these) 55 | -------------------------------------------------------------------------------- /catchers/log.go: -------------------------------------------------------------------------------- 1 | package catchers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | //Log is a an empty struct that satisfies the PanicHandler interface. 9 | type Log struct{} 10 | 11 | //HandlePanic takes the message and logs it to fmt.println if an unhandled panic occurs within your server. 12 | func (l Log) HandlePanic(message string) error { 13 | fmt.Fprintln(os.Stderr, message) 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /catchers/slack.go: -------------------------------------------------------------------------------- 1 | package catchers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/pkg/errors" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | var ( 12 | ErrSlackCallFailed = errors.New("Slack Call failed") 13 | ) 14 | 15 | type slackMessage struct { 16 | Message string `json:"text"` 17 | } 18 | 19 | //Slack is a catcher for your panic handler middleware. 20 | // You shouldn't instantiate one directly, you should use NewSlack() 21 | type Slack struct { 22 | webhookUrl string 23 | httpClient *http.Client 24 | } 25 | 26 | //Newslack takes a webhook url that you want to write to when a panic happens. 27 | //Returns a *Slack. 28 | func NewSlack(webhookurl string) *Slack { 29 | return &Slack{ 30 | webhookUrl: webhookurl, 31 | httpClient: &http.Client{ 32 | Timeout: time.Second * 10, 33 | }, 34 | } 35 | } 36 | 37 | //HandlePanic is the function that will be called in the panic handle middleware if your program panics. 38 | // It takes a message that will be written to the webhook if your server does panic. 39 | // HandlePanic can return an error if it cannot marshall the message into JSON, building a request fails, or the call to Slack fails. 40 | func (s Slack) HandlePanic(message string) error { 41 | b, err := json.Marshal(slackMessage{Message: message}) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | req, err := http.NewRequest("POST", s.webhookUrl, bytes.NewBuffer(b)) 47 | if err != nil { 48 | return err 49 | } 50 | res, err := s.httpClient.Do(req) 51 | 52 | if err != nil || res.StatusCode >= 400 { 53 | return ErrSlackCallFailed 54 | } 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/matthewjamesboyle/go-panic-catch 2 | 3 | require github.com/pkg/errors v0.8.1 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /panicMiddleware.go: -------------------------------------------------------------------------------- 1 | package goCatch 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | //PanicHandler is an interface. It is taken by the PanicMiddleware. All PanicHandlers are responsible for dealing with unexpected 10 | //panics in your server. 11 | type PanicHandler interface { 12 | HandlePanic(message string) error 13 | } 14 | 15 | //PanicMiddleware should be wrapped around all other handlers in your web server. It returns a http.handler and should be flexible enough 16 | // to work with all popular go Web servers. If your PanicHandler fails, it will log to the console. 17 | func PanicMiddleware(ph PanicHandler, message string, next http.Handler) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | defer func() { 20 | if r := recover(); r != nil { 21 | err := ph.HandlePanic(message) 22 | if err != nil { 23 | fmt.Fprintln(os.Stderr, "panic handler failed to recover from the panic.") 24 | } 25 | 26 | w.WriteHeader(http.StatusInternalServerError) 27 | return 28 | } 29 | }() 30 | next.ServeHTTP(w, r) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /panicMiddleware_test.go: -------------------------------------------------------------------------------- 1 | package goCatch 2 | 3 | import ( 4 | "fmt" 5 | "github.com/matthewjamesboyle/go-panic-catch/catchers" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestCatchPanicMiddleware(t *testing.T) { 12 | t.Run("Calls next successfully with no panic", func(t *testing.T) { 13 | 14 | fn := func(writer http.ResponseWriter, req *http.Request) { 15 | writer.WriteHeader(http.StatusTeapot) 16 | writer.Write([]byte("some-string")) 17 | } 18 | req := httptest.NewRequest("GET", "/aPath", nil) 19 | w := httptest.NewRecorder() 20 | log := catchers.Log{} 21 | 22 | PanicMiddleware(log, "", http.HandlerFunc(fn)).ServeHTTP(w, req) 23 | 24 | if w.Code != http.StatusTeapot { 25 | t.Fatal(fmt.Sprintf("Expected %d, but got %d", http.StatusTeapot, w.Code)) 26 | } 27 | if w.Body.String() != "some-string" { 28 | t.Fatal(fmt.Sprintf("Expected %s, but got %s", "some-string", w.Body.String())) 29 | } 30 | }) 31 | 32 | t.Run("Catches panic if next panics", func(t *testing.T) { 33 | fn := func(writer http.ResponseWriter, req *http.Request) { 34 | panic("ut oh") 35 | } 36 | 37 | req := httptest.NewRequest("GET", "/aPath", nil) 38 | w := httptest.NewRecorder() 39 | log := catchers.Log{} 40 | 41 | PanicMiddleware(log, "", http.HandlerFunc(fn)).ServeHTTP(w, req) 42 | 43 | }) 44 | 45 | t.Run("server returns a 500 if next panics", func(t *testing.T) { 46 | fn := func(writer http.ResponseWriter, req *http.Request) { 47 | panic("ut oh") 48 | } 49 | 50 | req := httptest.NewRequest("GET", "/aPath", nil) 51 | w := httptest.NewRecorder() 52 | log := catchers.Log{} 53 | 54 | PanicMiddleware(log, "", http.HandlerFunc(fn)).ServeHTTP(w, req) 55 | if w.Code != http.StatusInternalServerError { 56 | t.Fatalf("Expected to get a 500, but got %d", w.Code) 57 | } 58 | 59 | }) 60 | 61 | // Todo: add a webhook url below and run this test to watch the panic appear in slack :) 62 | //t.Run("test slack handler", func(t *testing.T) { 63 | // fn := func(writer http.ResponseWriter, req *http.Request) { 64 | // panic("ut oh") 65 | // } 66 | // 67 | // req := httptest.NewRequest("GET", "/aPath", nil) 68 | // w := httptest.NewRecorder() 69 | // slack := catchers.NewSlack("") 70 | // PanicMiddleware(*slack, "you just panicked!", http.HandlerFunc(fn)).ServeHTTP(w, req) 71 | // 72 | //}) 73 | } 74 | --------------------------------------------------------------------------------