From 02f72638b8e4c39dfb43f0a554d22fb423cc8747 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Fri, 2 Dec 2022 16:36:15 +0200 Subject: [PATCH] added error event hooks --- CHANGELOG.md | 2 ++ apis/base.go | 26 +++++++++++++++++--------- core/app.go | 10 ++++++++++ core/base.go | 12 ++++++++++++ core/events.go | 5 +++++ tests/api.go | 14 ++++++++++++++ tests/app.go | 10 ++++++++++ 7 files changed, 70 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12769ea..327687f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ```go app.OnBeforeBootstrap() app.OnAfterBootstrap() + app.OnBeforeApiError() + app.OnAfterApiError() app.OnRealtimeDisconnectRequest() app.OnRealtimeBeforeMessageSend() app.OnRealtimeAfterMessageSend() diff --git a/apis/base.go b/apis/base.go index 934594ff..33071cf3 100644 --- a/apis/base.go +++ b/apis/base.go @@ -64,19 +64,27 @@ func InitApi(app core.App) (*echo.Echo, error) { apiErr = NewBadRequestError("", err) } - // Send response - var cErr error - if c.Request().Method == http.MethodHead { - // @see https://github.com/labstack/echo/issues/608 - cErr = c.NoContent(apiErr.Code) - } else { - cErr = c.JSON(apiErr.Code, apiErr) + event := &core.ApiErrorEvent{ + HttpContext: c, + Error: apiErr, } + hookErr := app.OnBeforeApiError().Trigger(event, func(e *core.ApiErrorEvent) error { + // Send response + if e.HttpContext.Request().Method == http.MethodHead { + // @see https://github.com/labstack/echo/issues/608 + return e.HttpContext.NoContent(apiErr.Code) + } + + return e.HttpContext.JSON(apiErr.Code, apiErr) + }) + // truly rare case; eg. client already disconnected - if cErr != nil && app.IsDebug() { - log.Println(cErr) + if hookErr != nil && app.IsDebug() { + log.Println(hookErr) } + + app.OnAfterApiError().Trigger(event) } // admin ui routes diff --git a/core/app.go b/core/app.go index c962bf84..4efb4a20 100644 --- a/core/app.go +++ b/core/app.go @@ -92,6 +92,16 @@ type App interface { // allowing you to adjust its options and attach new routes. OnBeforeServe() *hook.Hook[*ServeEvent] + // OnBeforeApiError hook is triggered right before sending an error API + // response to the client, allowing you to further modify the error data + // or to return a completely different API response (using [hook.StopPropagation]). + OnBeforeApiError() *hook.Hook[*ApiErrorEvent] + + // OnAfterApiError hook is triggered right after sending an error API + // response to the client. + // It could be used to log the final API error in external services. + OnAfterApiError() *hook.Hook[*ApiErrorEvent] + // --------------------------------------------------------------- // Dao event hooks // --------------------------------------------------------------- diff --git a/core/base.go b/core/base.go index 7d5801fa..ff4f4e4e 100644 --- a/core/base.go +++ b/core/base.go @@ -43,6 +43,8 @@ type BaseApp struct { onBeforeBootstrap *hook.Hook[*BootstrapEvent] onAfterBootstrap *hook.Hook[*BootstrapEvent] onBeforeServe *hook.Hook[*ServeEvent] + onBeforeApiError *hook.Hook[*ApiErrorEvent] + onAfterApiError *hook.Hook[*ApiErrorEvent] // dao event hooks onModelBeforeCreate *hook.Hook[*ModelEvent] @@ -135,6 +137,8 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp { onBeforeBootstrap: &hook.Hook[*BootstrapEvent]{}, onAfterBootstrap: &hook.Hook[*BootstrapEvent]{}, onBeforeServe: &hook.Hook[*ServeEvent]{}, + onBeforeApiError: &hook.Hook[*ApiErrorEvent]{}, + onAfterApiError: &hook.Hook[*ApiErrorEvent]{}, // dao event hooks onModelBeforeCreate: &hook.Hook[*ModelEvent]{}, @@ -405,6 +409,14 @@ func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] { return app.onBeforeServe } +func (app *BaseApp) OnBeforeApiError() *hook.Hook[*ApiErrorEvent] { + return app.onBeforeApiError +} + +func (app *BaseApp) OnAfterApiError() *hook.Hook[*ApiErrorEvent] { + return app.onAfterApiError +} + // ------------------------------------------------------------------- // Dao event hooks // ------------------------------------------------------------------- diff --git a/core/events.go b/core/events.go index 0a811f81..25f8a7c5 100644 --- a/core/events.go +++ b/core/events.go @@ -25,6 +25,11 @@ type ServeEvent struct { Router *echo.Echo } +type ApiErrorEvent struct { + HttpContext echo.Context + Error error +} + // ------------------------------------------------------------------- // Model DAO events data // ------------------------------------------------------------------- diff --git a/tests/api.go b/tests/api.go index 5c74cd60..554290a6 100644 --- a/tests/api.go +++ b/tests/api.go @@ -134,6 +134,20 @@ func (scenario *ApiScenario) Test(t *testing.T) { } } + // to minimize the breaking changes we always expect the error + // events to be called on API error + if res.StatusCode >= 400 { + if scenario.ExpectedEvents == nil { + scenario.ExpectedEvents = map[string]int{} + } + if _, ok := scenario.ExpectedEvents["OnBeforeApiError"]; !ok { + scenario.ExpectedEvents["OnBeforeApiError"] = 1 + } + if _, ok := scenario.ExpectedEvents["OnAfterApiError"]; !ok { + scenario.ExpectedEvents["OnAfterApiError"] = 1 + } + } + if len(testApp.EventCalls) > len(scenario.ExpectedEvents) { t.Errorf("[%s] Expected events %v, got %v", prefix, scenario.ExpectedEvents, testApp.EventCalls) } diff --git a/tests/app.go b/tests/app.go index 253080a7..4005a1cc 100644 --- a/tests/app.go +++ b/tests/app.go @@ -87,6 +87,16 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) { TestMailer: &TestMailer{}, } + t.OnBeforeApiError().Add(func(e *core.ApiErrorEvent) error { + t.EventCalls["OnBeforeApiError"]++ + return nil + }) + + t.OnAfterApiError().Add(func(e *core.ApiErrorEvent) error { + t.EventCalls["OnAfterApiError"]++ + return nil + }) + t.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error { t.EventCalls["OnModelBeforeCreate"]++ return nil