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  }