github.com/seeker-insurance/kit@v0.0.13/web/error_handler.go (about) 1 package web 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "net/http" 8 "strconv" 9 10 "database/sql" 11 12 "github.com/asaskevich/govalidator" 13 "github.com/google/jsonapi" 14 "github.com/labstack/echo" 15 "github.com/lib/pq" 16 "github.com/seeker-insurance/kit/brake" 17 "github.com/seeker-insurance/kit/log" 18 "github.com/seeker-insurance/kit/str" 19 ) 20 21 //testCode is for internal testing 22 type testCode byte 23 24 const ( 25 alreadyCommited testCode = iota 26 methodIsHead 27 noContent 28 problemRendering 29 nilErr 30 normal 31 ) 32 33 var pq500s = map[string]bool{ 34 "undefined_function": true, 35 } 36 37 var criticalKeywords = []string{ 38 "reflect", 39 "dereference", 40 "runtime", 41 } 42 43 //errorHandler is the internal implementation of ErrorHandler. It returns a testCode, 44 //which does not fufill the interface expected by echo. Thus, it is wrapped by the function ErrorHandler. 45 func errorHandler(err error, c echo.Context) testCode { 46 if c.Response().Committed { 47 return alreadyCommited 48 } 49 if err == nil { 50 trackErr(errors.New("nil error sent into ErrorHandler"), c, 0) 51 return nilErr 52 } 53 54 status, apiError := toApiError(err) 55 56 if c.Request().Method == "HEAD" { 57 if err := c.NoContent(status); err != nil { 58 trackErr(err, c, status) 59 return noContent 60 } 61 return methodIsHead 62 } 63 64 if errRendering := renderApiErrors(c, apiError); errRendering != nil { 65 trackErr(errRendering, c, 0) 66 trackErr(err, c, status) 67 return problemRendering 68 } 69 70 trackErr(err, c, status) 71 return normal 72 } 73 74 //ErrorHandler handles errors 75 func ErrorHandler(err error, c echo.Context) { 76 errorHandler(err, c) 77 } 78 79 func toApiError(err error) (status int, apiErr *jsonapi.ErrorObject) { 80 var detail, code string 81 status = http.StatusInternalServerError 82 switch err := err.(type) { 83 case nil: 84 return 200, nil 85 case *jsonapi.ErrorObject: 86 if status, convErr := strconv.Atoi(err.Status); convErr == nil { 87 return status, err 88 } 89 err.Detail += fmt.Sprintf(" bad status: %s", err.Status) 90 91 return status, err 92 93 case *echo.HTTPError: 94 status = err.Code 95 if err.Message != nil { 96 detail = fmt.Sprint(err.Message) 97 } 98 return status, errorObj(status, http.StatusText(status), detail, code) 99 100 case *pq.Error: 101 detail = err.Message 102 code = err.Code.Name() 103 if _, ok := pq500s[code]; !ok { 104 status = http.StatusBadRequest 105 } 106 return status, errorObj(status, http.StatusText(status), detail, code) 107 108 case govalidator.Errors: 109 status, detail = http.StatusBadRequest, err.Error() 110 return status, errorObj(status, http.StatusText(status), detail, code) 111 112 case error: 113 switch err { 114 case sql.ErrNoRows: 115 status, detail = http.StatusNotFound, err.Error() 116 return http.StatusNotFound, errorObj(status, http.StatusText(status), "", "") 117 } 118 } 119 120 detail = err.Error() 121 return status, errorObj(status, http.StatusText(status), detail, code) 122 } 123 124 func renderApiErrors(c echo.Context, errors ...*jsonapi.ErrorObject) (err error) { 125 var b bytes.Buffer 126 if emptyOrAllNil(errors) { 127 return fmt.Errorf("no errors to render") 128 } 129 130 if err = jsonapi.MarshalErrors(&b, errors); err != nil { 131 return err 132 } 133 code, err := strconv.Atoi(errors[0].Status) 134 if err != nil { 135 return err 136 } 137 return c.Blob(code, jsonapi.MediaType, b.Bytes()) 138 } 139 140 func emptyOrAllNil(errs []*jsonapi.ErrorObject) bool { 141 for _, err := range errs { 142 if err != nil { 143 return false 144 } 145 } 146 return true 147 } 148 149 func errorObj(status int, title, detail, code string) *jsonapi.ErrorObject { 150 return &jsonapi.ErrorObject{ 151 Status: fmt.Sprintf("%d", status), 152 Title: title, 153 Detail: detail, 154 Code: code, 155 } 156 } 157 158 func trackErr(err error, c echo.Context, status int) { 159 if status > 0 && status < 500 { 160 return 161 } 162 notifyErr(err, c, status) 163 logErr(err) 164 } 165 166 func logErr(err error) { 167 log.ErrorWrap(err, "Uncaught Error") 168 } 169 170 func notifyErr(err error, c echo.Context, status int) { 171 if status == http.StatusUnauthorized { 172 return 173 } 174 175 sev := brake.SeverityError 176 if isCritical(err) { 177 sev = brake.SeverityCritical 178 } else if status > 0 && status < 500 { 179 sev = brake.SeverityWarn 180 } 181 182 brake.Notify(err, c.Request(), sev) 183 } 184 185 func isCritical(err error) bool { 186 return str.ContainsAny(err.Error(), criticalKeywords...) 187 }