Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [#521] User can custom recover when a request panic #103

Merged
merged 87 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
e06c940
[Feature] User can custom recover when a request panic
KlassnayaAfrodita Nov 15, 2024
09245fe
Update middleware_timeout.go
KlassnayaAfrodita Nov 15, 2024
5168d1e
Update route.go
KlassnayaAfrodita Nov 15, 2024
32ee690
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 15, 2024
29d36ee
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 15, 2024
88daede
Update context_request.go
KlassnayaAfrodita Nov 15, 2024
e82b340
Update view.go
KlassnayaAfrodita Nov 15, 2024
f3efb50
Update gin.go
KlassnayaAfrodita Nov 15, 2024
61b20aa
Update context_request_test.go
KlassnayaAfrodita Nov 15, 2024
a771bce
Update service_provider.go
KlassnayaAfrodita Nov 15, 2024
3b77bd0
Update view.go
KlassnayaAfrodita Nov 15, 2024
cc7a261
Update gin.go
KlassnayaAfrodita Nov 15, 2024
21125ef
Update context_request_test.go
KlassnayaAfrodita Nov 16, 2024
f3c666a
Update context_request.go
KlassnayaAfrodita Nov 18, 2024
adf6a8c
Update context_request_test.go
KlassnayaAfrodita Nov 18, 2024
0a3998c
Update gin.go
KlassnayaAfrodita Nov 18, 2024
d001e4d
Update middleware_timeout.go
KlassnayaAfrodita Nov 18, 2024
28af043
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 18, 2024
b0a62cd
Update route.go
KlassnayaAfrodita Nov 18, 2024
59ae234
Update service_provider.go
KlassnayaAfrodita Nov 18, 2024
5745c8f
Update view.go
KlassnayaAfrodita Nov 18, 2024
fae6fbe
Update view.go
KlassnayaAfrodita Nov 18, 2024
98f64a5
Update route.go
KlassnayaAfrodita Nov 18, 2024
076581a
Update route.go
KlassnayaAfrodita Nov 21, 2024
d5d8a93
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
998e3c4
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
9a3074c
Update route.go
KlassnayaAfrodita Nov 21, 2024
a019972
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
3f2edef
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
f7a0ac6
Update context_request.go
KlassnayaAfrodita Nov 21, 2024
36978b1
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
92c38d4
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 21, 2024
f66dadd
Update context_request_test.go
KlassnayaAfrodita Nov 21, 2024
3915909
Update middleware_timeout.go
KlassnayaAfrodita Nov 21, 2024
faf902e
Update gin.go
KlassnayaAfrodita Nov 22, 2024
cf0b756
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 22, 2024
0023e51
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 22, 2024
3c5f3bc
Update view.go
KlassnayaAfrodita Nov 26, 2024
fb1faf6
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 26, 2024
8f27f62
Update middleware_timeout_test.go
KlassnayaAfrodita Nov 26, 2024
49f6b21
Update route.go
KlassnayaAfrodita Dec 13, 2024
0e34013
Update middleware_timeout.go
KlassnayaAfrodita Dec 13, 2024
99e06c8
Merge branch 'master' into patch-3
KlassnayaAfrodita Dec 13, 2024
c6d79c8
Update middleware_timeout.go
KlassnayaAfrodita Dec 13, 2024
e0727a7
Update context_request.go
KlassnayaAfrodita Dec 13, 2024
8a0c44e
Update context_request.go
KlassnayaAfrodita Dec 17, 2024
173e471
Update middleware_timeout.go
KlassnayaAfrodita Dec 17, 2024
e4a1360
Update service_provider.go
KlassnayaAfrodita Dec 17, 2024
f30e2f4
Update middleware_timeout.go
KlassnayaAfrodita Dec 17, 2024
6217114
Update route.go
KlassnayaAfrodita Dec 17, 2024
35e0182
Update view.go
KlassnayaAfrodita Dec 17, 2024
ace488a
Update gin.go
KlassnayaAfrodita Dec 17, 2024
f7f209e
Update route.go
KlassnayaAfrodita Dec 17, 2024
440ba87
Update context_request.go
KlassnayaAfrodita Dec 17, 2024
ca0c57f
Update service_provider.go
KlassnayaAfrodita Dec 17, 2024
4e8ddfb
Update route.go
KlassnayaAfrodita Dec 17, 2024
dfa4bd0
Update route.go
KlassnayaAfrodita Dec 17, 2024
44b41f6
Update route.go
KlassnayaAfrodita Dec 17, 2024
4346508
Update route.go
KlassnayaAfrodita Dec 17, 2024
1682fe7
Update middleware_timeout.go
KlassnayaAfrodita Dec 17, 2024
60320c1
Merge branch 'master' into patch-3
KlassnayaAfrodita Dec 17, 2024
28dcae8
Update view.go
KlassnayaAfrodita Dec 18, 2024
4aceba8
Update middleware_timeout.go
KlassnayaAfrodita Dec 19, 2024
601dbd0
Update middleware_timeout.go
KlassnayaAfrodita Dec 19, 2024
a79cae7
Update middleware_timeout.go
KlassnayaAfrodita Dec 19, 2024
a581045
Update middleware_timeout.go
KlassnayaAfrodita Dec 19, 2024
5a0101e
Update middleware_timeout.go
KlassnayaAfrodita Dec 19, 2024
1c07943
Update middleware_timeout_test.go
KlassnayaAfrodita Dec 19, 2024
f020930
Update middleware_timeout_test.go
KlassnayaAfrodita Dec 19, 2024
a0b8834
Update view.go
KlassnayaAfrodita Dec 19, 2024
47c53e7
Update view.go
KlassnayaAfrodita Dec 19, 2024
343d493
Update gin.go
KlassnayaAfrodita Dec 23, 2024
41d57a1
Update route.go
KlassnayaAfrodita Dec 23, 2024
057f865
Update service_provider.go
KlassnayaAfrodita Dec 23, 2024
4d82e79
Update view.go
KlassnayaAfrodita Dec 23, 2024
e6fb55f
Update route.go
KlassnayaAfrodita Dec 23, 2024
5df7701
Update route_test.go
KlassnayaAfrodita Dec 24, 2024
aa56ca1
Update route_test.go
KlassnayaAfrodita Dec 24, 2024
66e139e
Update route.go
KlassnayaAfrodita Dec 24, 2024
46fc772
Update route_test.go
KlassnayaAfrodita Dec 26, 2024
48144d5
Update route_test.go
KlassnayaAfrodita Dec 26, 2024
72a5064
Update route.go
KlassnayaAfrodita Dec 26, 2024
e7eef7b
Update route.go
KlassnayaAfrodita Dec 26, 2024
ee526b3
Update route_test.go
KlassnayaAfrodita Dec 26, 2024
4d7e660
Update route_test.go
KlassnayaAfrodita Dec 26, 2024
5371bd7
Update middleware_timeout_test.go
KlassnayaAfrodita Dec 26, 2024
cdf7d2f
Merge branch 'master' into patch-3
hwbrzzl Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion context_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ func NewContextRequest(ctx *Context, log log.Log, validation contractsvalidate.V
request := contextRequestPool.Get().(*ContextRequest)
httpBody, err := getHttpBody(ctx)
if err != nil {
log.Error(fmt.Sprintf("%+v", err))
LogFacade.Error(fmt.Sprintf("%+v", errors.Unwrap(err)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to modify this, I optimized the code in a previous PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please modify this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do I need to return log?
patch-3 of my fork currently uses LogFacade

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log is fine, please update your branch.

}
request.ctx = ctx
request.instance = ctx.instance
request.httpBody = httpBody
request.log = log
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

request.validation = validation
return request
}
Expand Down
6 changes: 4 additions & 2 deletions context_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ func (s *ContextRequestSuite) SetupTest() {
ValidationFacade = validation.NewValidation()

var err error
s.route, err = NewRoute(s.mockConfig, nil)
s.Require().Nil(err)
route, err := NewRoute(s.mockConfig, nil)
s.Require().NotNil(route)
s.Require().NoError(err)
s.route = route
}

func (s *ContextRequestSuite) TearDownTest() {
Expand Down
5 changes: 3 additions & 2 deletions facades/gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package facades
import (
"log"

"github.com/goravel/framework/contracts/route"

hwbrzzl marked this conversation as resolved.
Show resolved Hide resolved
"github.com/goravel/gin"

"github.com/goravel/framework/contracts/route"
)

func Route(driver string) route.Route {
instance, err := gin.App.MakeWith(gin.RouteBinding, map[string]any{
"driver": driver,
})

if err != nil {
log.Fatalln(err)
return nil
Expand Down
19 changes: 4 additions & 15 deletions middleware_timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,15 @@ func Timeout(timeout time.Duration) contractshttp.Middleware {
done := make(chan struct{})

go func() {
defer func() {
if r := recover(); r != nil {
if LogFacade != nil {
LogFacade.Request(ctx.Request()).Error(r)
}

// TODO can be customized in https://github.com/goravel/goravel/issues/521
_ = ctx.Response().Status(http.StatusInternalServerError).String("Internal Server Error").Render()
}

close(done)
}()

defer HandleRecover(ctx, globalRecoverCallback)
ctx.Request().Next()
close(done)
}()

select {
case <-done:
case <-ctx.Request().Origin().Context().Done():
if errors.Is(ctx.Request().Origin().Context().Err(), context.DeadlineExceeded) {
case <-timeoutCtx.Done():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
ctx.Request().AbortWithStatus(http.StatusGatewayTimeout)
}
}
Expand Down
17 changes: 11 additions & 6 deletions middleware_timeout_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test case to test the custom recover as well.

Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"testing"
"time"

"github.com/gin-gonic/gin"
contractshttp "github.com/goravel/framework/contracts/http"
mocksconfig "github.com/goravel/framework/mocks/config"
mockslog "github.com/goravel/framework/mocks/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

Expand All @@ -24,16 +24,23 @@ func TestTimeoutMiddleware(t *testing.T) {

route.Middleware(Timeout(1*time.Second)).Get("/timeout", func(ctx contractshttp.Context) contractshttp.Response {
time.Sleep(2 * time.Second)

return ctx.Response().Success().String("timeout")
return nil
})

route.Middleware(Timeout(1*time.Second)).Get("/normal", func(ctx contractshttp.Context) contractshttp.Response {
return ctx.Response().Success().String("normal")
})

route.Middleware(Timeout(1*time.Second)).Get("/panic", func(ctx contractshttp.Context) contractshttp.Response {
panic(1)
})

globalRecover := func(ctx contractshttp.Context, err any) {
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, gin.H{"error": "Internal Panic"})
}

route.Recover(globalRecover)

w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/timeout", nil)
require.NoError(t, err)
Expand All @@ -54,11 +61,9 @@ func TestTimeoutMiddleware(t *testing.T) {
require.NoError(t, err)

mockLog := mockslog.NewLog(t)
mockLog.EXPECT().Request(mock.Anything).Return(mockLog).Once()
mockLog.EXPECT().Error(mock.Anything).Once()
LogFacade = mockLog

route.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "Internal Server Error", w.Body.String())
assert.Equal(t, "{\"error\":\"Internal Panic\"}", w.Body.String())
}
22 changes: 22 additions & 0 deletions route.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/savioxavier/termlink"
)

var globalRecoverCallback func(ctx httpcontract.Context, err any)

type Route struct {
route.Router
config config.Config
Expand Down Expand Up @@ -89,6 +91,26 @@ func (r *Route) GlobalMiddleware(middlewares ...httpcontract.Middleware) {
r.setMiddlewares(middlewares)
}

func HandleRecover(ctx httpcontract.Context, recoverCallback func(ctx httpcontract.Context, err any)) {
if err := recover(); err != nil {
if recoverCallback != nil {
recoverCallback(ctx, err)
} else {
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"})
}
}
}

func (r *Route) Recover(callback func(ctx httpcontract.Context, err any)) {
globalRecoverCallback = callback
r.setMiddlewares([]httpcontract.Middleware{
func(ctx httpcontract.Context) {
defer HandleRecover(ctx, globalRecoverCallback)
ctx.Request().Next()
},
})
}

func (r *Route) Listen(l net.Listener) error {
r.outputRoutes()
color.Green().Println(termlink.Link("[HTTP] Listening and serving HTTP on", "http://"+l.Addr().String()))
Expand Down
53 changes: 53 additions & 0 deletions route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,66 @@ import (
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
contractshttp "github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
configmocks "github.com/goravel/framework/mocks/config"
"github.com/stretchr/testify/assert"
)

func TestRecoverWithCustomCallback(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test case to test the default recover as well.

mockConfig := configmocks.NewConfig(t)
mockConfig.EXPECT().GetBool("app.debug").Return(true).Once()
mockConfig.EXPECT().GetInt("http.drivers.gin.body_limit", 4096).Return(4096).Once()

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/recover", nil)

route, err := NewRoute(mockConfig, nil)
assert.Nil(t, err)

globalRecover := func(ctx contractshttp.Context, err any) {
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, gin.H{"error": "Internal Panic"})
}

route.Recover(globalRecover)

route.Get("/recover", func(ctx contractshttp.Context) contractshttp.Response {
panic(1)
})

route.ServeHTTP(w, req)

assert.Equal(t, "{\"error\":\"Internal Panic\"}", w.Body.String())
assert.Equal(t, http.StatusInternalServerError, w.Code)

mockConfig.AssertExpectations(t)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case is pretty good.


func TestRecoverWithDefaultCallback(t *testing.T) {
mockConfig := configmocks.NewConfig(t)
mockConfig.EXPECT().GetBool("app.debug").Return(true).Once()
mockConfig.EXPECT().GetInt("http.drivers.gin.body_limit", 4096).Return(4096).Once()

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/recover", nil)

route, err := NewRoute(mockConfig, nil)
assert.Nil(t, err)

route.Get("/recover", func(ctx contractshttp.Context) contractshttp.Response {
panic(1)
})

route.ServeHTTP(w, req)

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

mockConfig.AssertExpectations(t)
}

func TestFallback(t *testing.T) {
mockConfig := &configmocks.Config{}
mockConfig.EXPECT().GetBool("app.debug").Return(true).Once()
Expand Down
4 changes: 4 additions & 0 deletions service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ func (receiver *ServiceProvider) Boot(app foundation.Application) {
if ConfigFacade = app.MakeConfig(); ConfigFacade == nil {
color.Errorln(errors.ConfigFacadeNotSet.SetModule(module))
}

if LogFacade = app.MakeLog(); LogFacade == nil {
color.Errorln(errors.LogFacadeNotSet.SetModule(module))
}

if ValidationFacade = app.MakeValidation(); ValidationFacade == nil {
color.Errorln(errors.New("validation facade is not initialized").SetModule(module))
}

if ViewFacade = app.MakeView(); ViewFacade == nil {
color.Errorln(errors.New("view facade is not initialized").SetModule(module))
}
Expand All @@ -50,3 +53,4 @@ func (receiver *ServiceProvider) Boot(app foundation.Application) {
"config/cors.go": app.ConfigPath("cors.go"),
})
}

5 changes: 3 additions & 2 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func NewView(instance *gin.Context) *View {

func (receive *View) Make(view string, data ...any) contractshttp.Response {
shared := ViewFacade.GetShared()

if len(data) == 0 {
return &HtmlResponse{shared, receive.instance, view}
} else {
Expand All @@ -28,11 +29,9 @@ func (receive *View) Make(view string, data ...any) contractshttp.Response {
for key, value := range dataMap {
shared[key] = value
}

return &HtmlResponse{shared, receive.instance, view}
case reflect.Map:
fillShared(data[0], shared)

return &HtmlResponse{data[0], receive.instance, view}
default:
panic(fmt.Sprintf("make %s view failed, data must be map or struct", view))
Expand Down Expand Up @@ -82,6 +81,7 @@ func structToMap(data any) map[string]any {
func fillShared(data any, shared map[string]any) {
dataValue := reflect.ValueOf(data)
keys := dataValue.MapKeys()

for key, value := range shared {
exist := false
for _, k := range keys {
Expand All @@ -90,6 +90,7 @@ func fillShared(data any, shared map[string]any) {
break
}
}

if !exist {
dataValue.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value))
}
Expand Down
Loading