Skip to content

Commit

Permalink
Add error middleware (#35)
Browse files Browse the repository at this point in the history
* Add error middleware

if there is a problem marshalling and writing json the error
middleware handles mapping the context errors to a result error
and returning json as a 500

* add service input

define a method for exposing a request body struct and auto marshaling 
into the given struct. The struct is then stored on the req object and 
returned as a param to the config handler
  • Loading branch information
rossnelson committed May 5, 2024
1 parent ca90a3a commit 69249ab
Show file tree
Hide file tree
Showing 17 changed files with 388 additions and 27 deletions.
1 change: 1 addition & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
_ "github.com/simiancreative/simiango/examples/services/param"
_ "github.com/simiancreative/simiango/examples/services/pg"
_ "github.com/simiancreative/simiango/examples/services/rabbit"
_ "github.com/simiancreative/simiango/examples/services/request-receiver"
_ "github.com/simiancreative/simiango/examples/services/sample"
_ "github.com/simiancreative/simiango/examples/services/stream"
_ "github.com/simiancreative/simiango/examples/services/unsafe"
Expand Down
24 changes: 23 additions & 1 deletion examples/services/error/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package error
import (
"fmt"

"github.com/jackc/pgtype"

"github.com/simiancreative/simiango/server"
"github.com/simiancreative/simiango/service"
)

// JUST AN ERROR

var Config = service.Config{
Kind: service.DIRECT,
Method: "GET",
Expand All @@ -18,8 +22,26 @@ func direct(req service.Req) (interface{}, error) {
return nil, fmt.Errorf("this is an error")
}

// BAD JSON

var badJsonConfig = service.Config{
Kind: service.DIRECT,
Method: "GET",
Path: "/error/bad-json",
Direct: badJson,
}

type Product struct {
ID string `json:"id" db:"id"`
Meta pgtype.JSONB `json:"meta" db:"meta"`
}

func badJson(_ service.Req) (interface{}, error) {
return Product{}, nil
}

// dont forget to import your package in your main.go for initialization
// _ "path/to/project/direct"
func init() {
server.AddService(Config)
server.AddServices([]service.Config{Config, badJsonConfig})
}
31 changes: 31 additions & 0 deletions examples/services/request-receiver/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package requestreceiver

import (
"github.com/simiancreative/simiango/server"
"github.com/simiancreative/simiango/service"
)

var Config = service.Config{
Kind: service.DIRECT,
Method: "POST",
Path: "/request-receiver",
Direct: direct,
Input: input,
}

type Product struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
}

func direct(req service.Req) (interface{}, error) {
return req.Input, nil
}

func input() interface{} {
return &Product{}
}

func init() {
server.AddService(Config)
}
14 changes: 6 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/jackc/pgtype v1.14.3
github.com/jackc/pgx v3.6.2+incompatible
github.com/jedib0t/go-pretty/v6 v6.3.7
github.com/jmoiron/sqlx v1.3.5
Expand All @@ -35,7 +36,7 @@ require (
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.3
golang.org/x/crypto v0.17.0
golang.org/x/crypto v0.20.0
gopkg.in/validator.v2 v2.0.1
)

Expand All @@ -59,6 +60,7 @@ require (
github.com/golang/gddo v0.0.0-20200310004957-95ce5a452273 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.16.0 // indirect
Expand Down Expand Up @@ -89,20 +91,16 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/cockroachdb/apd v1.1.0 // indirect
github.com/getsentry/sentry-go v0.27.0
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 // indirect
github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/net v0.21.0 // indirect
)
133 changes: 125 additions & 8 deletions go.sum

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions meta/rescue.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func recoverGinPanic(c *gin.Context, buildResp func(*gin.Context, map[string]int
}

id, _ := c.Get("request_id")
stack := stack()
stack := Stack()
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
Expand Down Expand Up @@ -98,7 +98,7 @@ func recoverGinPanic(c *gin.Context, buildResp func(*gin.Context, map[string]int
}
}

func stack() []string {
func Stack() []string {
skip := 3
result := []string{}
// As we loop, we open files and read them. These variables record the currently
Expand Down Expand Up @@ -131,7 +131,7 @@ func stack() []string {

func stackAsBuf() []byte {
buf := new(bytes.Buffer) // the returned data
lines := stack()
lines := Stack()

for _, line := range lines {
fmt.Fprint(buf, line)
Expand Down
2 changes: 1 addition & 1 deletion meta/rescue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestRecoverGinPanic_BrokenPipe(t *testing.T) {
}

func TestStack(t *testing.T) {
stack := stack()
stack := Stack()
if len(stack) == 0 {
t.Fatalf("Expected stack trace, got empty")
}
Expand Down
3 changes: 1 addition & 2 deletions monitoring/sentry/sentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"

"github.com/simiancreative/simiango/logger"
"github.com/simiancreative/simiango/service"
)

func Enable() {
Expand Down Expand Up @@ -47,7 +46,7 @@ func RecoverAndThrow() {
}

// CaptureError is a helper function to capture an error and return it so the caller can handle it
func GinCaptureError(c *gin.Context, err *service.ResultError) *service.ResultError {
func GinCaptureError(c *gin.Context, err error) error {
hub := sentrygin.GetHubFromContext(c)

if hub == nil {
Expand Down
46 changes: 46 additions & 0 deletions monitoring/sentry/sentry_scope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package sentry_test

import (
"errors"
"net/http/httptest"
"testing"

sentrygin "github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"

"github.com/simiancreative/simiango/monitoring/sentry"
)

func TestGinCaptureError(t *testing.T) {
t.Run("sentryScopeFunc is not ok", func(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())

hub := sentrygin.CurrentHub()
c.Set("sentry", hub)

err := errors.New("test error")
returnedErr := sentry.GinCaptureError(c, err)

assert.Equal(t, err, returnedErr)
})
}

func TestScopeFunctionError(t *testing.T) {
t.Run("scopeFunction is not ok", func(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())

hub := sentrygin.CurrentHub()
c.Set("sentry", hub)

// Set a non-function value for "sentryScopeFunc"
c.Set("sentryScopeFunc", "not a function")

err := errors.New("test error")
returnedErr := sentry.GinCaptureError(c, err)

assert.Equal(t, err, returnedErr)
})
}
30 changes: 30 additions & 0 deletions server/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package server

import (
"net/http"

"github.com/gin-gonic/gin"

"github.com/simiancreative/simiango/service"
)

func errorMiddleware(c *gin.Context) {
c.Next()

if len(c.Errors) == 0 {
return
}

resp := service.ResultError{
Status: http.StatusInternalServerError,
Message: "Internal Server Error",
}

for _, err := range c.Errors {
resp.Reasons = append(resp.Reasons, map[string]interface{}{
"error": err.Error(),
})
}

c.JSON(http.StatusInternalServerError, resp)
}
43 changes: 43 additions & 0 deletions server/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package server

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"

"github.com/simiancreative/simiango/service"
)

func TestErrorMiddleware(t *testing.T) {
// Happy path
t.Run("no errors", func(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

errorMiddleware(c)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "", w.Body.String())
})

// Error path
t.Run("with errors", func(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

c.Error(gin.Error{
Err: &service.ResultError{},
Type: gin.ErrorTypePublic,
})

errorMiddleware(c)

assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
}
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func Init() {
servertiming.Middleware(),
JSONLogMiddleware,
meta.GinRecovery(handleRecovery),
errorMiddleware,
)
}

Expand Down
4 changes: 2 additions & 2 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ func handleService(config service.Config) gin.HandlerFunc {
var result interface{}
var err *service.ResultError

value, ok := kinds[config.Kind]
handler, ok := kinds[config.Kind]
if !ok {
result, err = handleDefault(config, req)
}

if ok {
result, err = value(config, req)
result, err = handler(config, req)
}

timer.Stop()
Expand Down
5 changes: 4 additions & 1 deletion server/service_handle_direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func handleDirect(config service.Config, req service.Req) (interface{}, *service
}
}

if err := parseBody(config, &req); err != nil {
return nil, handleError(err)
}

result, err := config.Direct(req)

if err == nil && result == nil {
Expand All @@ -41,7 +45,6 @@ func handleDirectAuth(
}

err = config.Auth(req)

if err != nil {
err = fmt.Errorf("Authentication Failed: %v", err.Error())
resultErr := service.ToResultError(err, "service auth failed", 401)
Expand Down
19 changes: 18 additions & 1 deletion server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ func rawBody(source io.ReadCloser) []byte {
return []byte(reqBody)
}

func parseBody(config service.Config, req *service.Req) error {
if config.Input == nil {
return nil
}

input := config.Input()

err := service.ParseBody(req.Body, input)
if err != nil {
return err
}

req.Input = input

return nil
}

func parseParams(params gin.Params, url *url.URL) service.RawParams {
parsedParams := service.RawParams{}

Expand Down Expand Up @@ -61,7 +78,7 @@ func handleErrorResp(err *service.ResultError, c *gin.Context) *service.ResultEr
return err
}

return sentry.GinCaptureError(c, err)
return sentry.GinCaptureError(c, err).(*service.ResultError)
}

func handleError(err error) *service.ResultError {
Expand Down
Loading

0 comments on commit 69249ab

Please sign in to comment.