Skip to content

Commit

Permalink
Merge pull request #28 from retail-ai-inc/feature/error-handler-middl…
Browse files Browse the repository at this point in the history
…eware

SCT-156 Build: ErrorHandlerMiddleware pattern in Bean framework
  • Loading branch information
tanvir-retailai authored Jan 11, 2022
2 parents 893f0b6 + 4e9c5c8 commit c30574b
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 9 deletions.
24 changes: 21 additions & 3 deletions internal/project/framework/bean.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,32 @@ import (
/**#bean*/
validate "demo/framework/internals/validator"
/*#bean.replace(validate "{{ .PkgPath }}/framework/internals/validator")**/

/**#bean*/
berror "demo/framework/internals/error"
/*#bean.replace(berror "{{ .PkgPath }}/framework/internals/error")**/
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/spf13/viper"
)

type Bean struct {
Validate func(c echo.Context, vd *validator.Validate)
BeforeBootstrap func()
Validate func(c echo.Context, vd *validator.Validate)
BeforeBootstrap func()
errorHandlerMiddlewares []berror.ErrorHandlerMiddleware
}

func (b *Bean) Bootstrap() {
// Create a new echo instance
e := bootstrap.New()

b.UseErrorHandlerMiddleware(
berror.ValidationErrorHanderMiddleware,
berror.APIErrorHanderMiddleware,
berror.HTTPErrorHanderMiddleware,
berror.DefaultErrorHanderMiddleware,
)

e.HTTPErrorHandler = berror.ErrorHandlerChain(b.errorHandlerMiddlewares...)
// before bean bootstrap
if b.BeforeBootstrap != nil {
b.BeforeBootstrap()
Expand All @@ -44,3 +55,10 @@ func (b *Bean) Bootstrap() {
e.Logger.Fatal(err)
}
}

func (b *Bean) UseErrorHandlerMiddleware(errorHandlerMiddleware ...berror.ErrorHandlerMiddleware) {
if b.errorHandlerMiddlewares == nil {
b.errorHandlerMiddlewares = []berror.ErrorHandlerMiddleware{}
}
b.errorHandlerMiddlewares = append(b.errorHandlerMiddlewares, errorHandlerMiddleware...)
}
6 changes: 0 additions & 6 deletions internal/project/framework/bootstrap/kernel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ import (
"demo/framework/internals/binder"
/*#bean.replace("{{ .PkgPath }}/framework/internals/binder")**/
/**#bean*/
ierror "demo/framework/internals/error"
/*#bean.replace(ierror "{{ .PkgPath }}/framework/internals/error")**/
/**#bean*/
"demo/framework/internals/global"
/*#bean.replace("{{ .PkgPath }}/framework/internals/global")**/
/**#bean*/
Expand Down Expand Up @@ -55,9 +52,6 @@ func New() *echo.Echo {
// Initialize the global echo instance. This is useful to print log from `internals` packages.
global.EchoInstance = e

// This will handle invalid JSON and other errors.
e.HTTPErrorHandler = ierror.HTTPErrorHandler

// Hide default `Echo` banner during startup.
e.HideBanner = true

Expand Down
109 changes: 109 additions & 0 deletions internal/project/framework/internals/error/error_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**#bean*/ /*#bean.replace({{ .Copyright }})**/

package error

import (
/**#bean*/
"demo/framework/internals/sentry"
/*#bean.replace("{{ .PkgPath }}/framework/internals/sentry")**/
/**#bean*/
"demo/framework/internals/validator"
/*#bean.replace("{{ .PkgPath }}/framework/internals/validator")**/
"github.com/labstack/echo/v4"
"net/http"
"strings"
)

// return value: bool is true when HandlerMiddleware match the error,otherwise false
// return value: error will be sent to sentry if not nil

type ErrorHandlerMiddleware func(err error, c echo.Context) (bool, error)

func ErrorHandlerChain(middlewares ...ErrorHandlerMiddleware) echo.HTTPErrorHandler {
return func(err error, c echo.Context) {

if c.Response().Committed {
return
}

for _, middleware := range middlewares {
catched, e := middleware(err, c)
if e != nil {
sentry.PushData(c, e, nil, true)
}
if catched {
break
}
}
}
}

func ValidationErrorHanderMiddleware(e error, c echo.Context) (bool, error) {
he, ok := e.(*validator.ValidationError)
if !ok {
return false, nil
}
err := c.JSON(http.StatusUnprocessableEntity, errorResp{
ErrorCode: API_DATA_VALIDATION_FAILED,
Errors: he.ErrCollection(),
ErrorMsg: nil,
})

return ok, err
}

func APIErrorHanderMiddleware(e error, c echo.Context) (bool, error) {
he, ok := e.(*APIError)
if !ok {
return false, nil
}

if he.HTTPStatusCode >= 404 {
sentry.PushData(c, he, nil, true)
}

err := c.JSON(he.HTTPStatusCode, errorResp{
ErrorCode: he.GlobalErrCode,
Errors: nil,
ErrorMsg: he.Error(),
})

return ok, err
}

func HTTPErrorHanderMiddleware(e error, c echo.Context) (bool, error) {
he, ok := e.(*echo.HTTPError)
if !ok {
return false, nil
}

// Just in case to capture this unused type error.
err := c.JSON(he.Code, errorResp{
ErrorCode: UNKNOWN_ERROR_CODE,
Errors: nil,
ErrorMsg: he.Message,
})

return ok, err
}

func DefaultErrorHanderMiddleware(_ error, c echo.Context) (bool, error) {
// Get Content-Type parameter from request header to identify the request content type. If the request is for
// html then we should display the error in html.
contentType := c.Request().Header.Get("Content-Type")

if strings.ToLower(contentType) == "text/html" {
err := c.HTML(http.StatusInternalServerError, "<strong>Internal server error.</strong>")
return true, err
}

// All other panic errors.
// Sentry already captured the panic and send notification in sentry-recover middleware.
err := c.JSON(http.StatusInternalServerError, errorResp{
ErrorCode: INTERNAL_SERVER_ERROR,
Errors: nil,
ErrorMsg: nil, // TODO: Put some generic message.
})

return true, err
}
153 changes: 153 additions & 0 deletions internal/project/framework/internals/error/error_middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**#bean*/ /*#bean.replace({{ .Copyright }})**/

package error

import (
/**#bean*/
bvalidator "demo/framework/internals/validator"
/*#bean.replace(bvalidator "{{ .PkgPath }}/framework/internals/validator")**/
"encoding/json"
"errors"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)

func TestErrorHandlerChain(t *testing.T) {
e := echo.New()
e.HTTPErrorHandler = ErrorHandlerChain(
FakeErrorHandlerMiddleware,
ValidationErrorHanderMiddleware,
APIErrorHanderMiddleware,
HTTPErrorHanderMiddleware,
DefaultErrorHanderMiddleware)

//assert fake error
fakeErr := newFakeError("fake error message")
checkFakeErr := func(response *httptest.ResponseRecorder, err error) {
assert.Equal(t, fakeErr, err)
assert.Equal(t, http.StatusInternalServerError, response.Code)
var res errorResp
e := json.Unmarshal(response.Body.Bytes(), &res)
assert.Nil(t, e)
assert.Equal(t, ErrorCode("fakeErrorCode"), res.ErrorCode)
assert.Nil(t, res.Errors)
assert.Equal(t, fakeErr.Error(), res.ErrorMsg)
}
run(e, fakeErr, checkFakeErr)

//assert api error
apiErr := NewAPIError(http.StatusUnauthorized, UNAUTHORIZED_ACCESS, errors.New("UNAUTHORIZED"))
checkAPIErr := func(response *httptest.ResponseRecorder, err error) {
assert.Equal(t, apiErr, err)
assert.Equal(t, http.StatusUnauthorized, response.Code)
var res errorResp
e := json.Unmarshal(response.Body.Bytes(), &res)
assert.Nil(t, e)
assert.Equal(t, UNAUTHORIZED_ACCESS, res.ErrorCode)
assert.Nil(t, res.Errors)
assert.Equal(t, apiErr.Error(), res.ErrorMsg)
}
run(e, apiErr, checkAPIErr)

//assert validation error
validationErr := &bvalidator.ValidationError{
Err: validator.ValidationErrors([]validator.FieldError{}),
}
checkValidationErr := func(response *httptest.ResponseRecorder, err error) {
assert.Equal(t, validationErr, err)
assert.Equal(t, http.StatusUnprocessableEntity, response.Code)
var res errorResp
e := json.Unmarshal(response.Body.Bytes(), &res)
assert.Nil(t, e)
assert.Equal(t, API_DATA_VALIDATION_FAILED, res.ErrorCode)
assert.Nil(t, res.ErrorMsg)
}
run(e, validationErr, checkValidationErr)

//assert http error
httpErr := &echo.HTTPError{
Code: http.StatusNotFound,
Message: "404 Not Found",
}
checkHttpErr := func(response *httptest.ResponseRecorder, err error) {
assert.Equal(t, httpErr, err)
assert.Equal(t, http.StatusNotFound, response.Code)
var res errorResp
e := json.Unmarshal(response.Body.Bytes(), &res)
assert.Nil(t, e)
assert.Equal(t, UNKNOWN_ERROR_CODE, res.ErrorCode)
assert.Nil(t, res.Errors)
assert.Equal(t, httpErr.Message, res.ErrorMsg)
}
run(e, httpErr, checkHttpErr)

//assert default error
defaultErr := errors.New("default error")
checkDefaultErr := func(response *httptest.ResponseRecorder, err error) {
assert.Equal(t, defaultErr, err)
assert.Equal(t, http.StatusInternalServerError, response.Code)
var res errorResp
e := json.Unmarshal(response.Body.Bytes(), &res)
assert.Equal(t, http.StatusInternalServerError, response.Code)
assert.Nil(t, e)
assert.Equal(t, INTERNAL_SERVER_ERROR, res.ErrorCode)
assert.Nil(t, res.Errors)
assert.Nil(t, res.ErrorMsg)
}
run(e, defaultErr, checkDefaultErr)
}

func FakeErrorHandlerMiddleware(e error, c echo.Context) (bool, error) {
he, ok := e.(*fakeError)
if !ok {
return false, nil
}

err := c.JSON(http.StatusInternalServerError, errorResp{
ErrorCode: "fakeErrorCode",
Errors: nil,
ErrorMsg: he.Error(),
})

return ok, err
}

func run(e *echo.Echo,
err error,
assertFunc func(response *httptest.ResponseRecorder, err error),
) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
runWithRequest(e, req, err, assertFunc)
}

func runWithRequest(e *echo.Echo,
req *http.Request,
er error,
assertFunc func(response *httptest.ResponseRecorder, err error),
) {
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := func(c echo.Context) error {
return er
}(c)
e.HTTPErrorHandler(err, c)
assertFunc(rec, err)
}

type fakeError struct {
Message string
}

func (f *fakeError) Error() string {
return f.Message
}

func newFakeError(msg string) error {
return &fakeError{
Message: msg,
}
}
6 changes: 6 additions & 0 deletions internal/project/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,11 @@ func main() {
// Add your own validation function here.
}

// Below is an example of how you can set custom error handler middleware
// bean can call `UseErrorHandlerMiddleware` multiple times
bean.UseErrorHandlerMiddleware(func(e error, c echo.Context) (bool, error) {
return false, nil
})

bean.Bootstrap()
}

0 comments on commit c30574b

Please sign in to comment.