Skip to content

Commit a1dec9f

Browse files
authored
feat(fxhttpserver): Added possibility to register custom error handler (#314)
1 parent 0a9f325 commit a1dec9f

File tree

11 files changed

+231
-22
lines changed

11 files changed

+231
-22
lines changed

fxhttpserver/README.md

+72-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* [Middlewares](#middlewares)
2020
* [Handlers](#handlers)
2121
* [Handlers groups](#handlers-groups)
22+
* [Error Handler](#error-handler)
2223
* [WebSocket](#websocket)
2324
* [Templates](#templates)
2425
* [Override](#override)
@@ -126,7 +127,7 @@ modules:
126127
namespace: foo # http server metrics namespace (empty by default)
127128
subsystem: bar # http server metrics subsystem (empty by default)
128129
buckets: 0.1, 1, 10 # to override default request duration buckets
129-
normalize:
130+
normalize:
130131
request_path: true # to normalize http request path, disabled by default
131132
response_status: true # to normalize http response status code (2xx, 3xx, ...), disabled by default
132133
templates:
@@ -145,7 +146,7 @@ Notes:
145146

146147
### Registration
147148

148-
This module offers the possibility to easily register handlers, groups and middlewares.
149+
This module offers the possibility to easily register handlers, groups, middlewares and error handlers.
149150

150151
#### Middlewares
151152

@@ -204,7 +205,7 @@ func main() {
204205
fxmetrics.FxMetricsModule,
205206
fxgenerate.FxGenerateModule,
206207
fxhttpserver.FxHttpServerModule, // load the module
207-
fx.Provide(
208+
fx.Options(
208209
fxhttpserver.AsMiddleware(middleware.CORS(), fxhttpserver.GlobalUse), // register echo CORS middleware via echo.Use()
209210
fxhttpserver.AsMiddleware(NewSomeMiddleware, fxhttpserver.GlobalPre), // register and autowire the SomeMiddleware via echo.Pre()
210211
),
@@ -296,15 +297,15 @@ func main() {
296297
fxmetrics.FxMetricsModule,
297298
fxgenerate.FxGenerateModule,
298299
fxhttpserver.FxHttpServerModule, // load the module
299-
fx.Provide(
300+
fx.Options(
300301
// register and autowire the SomeHandler handler for [GET] /some-path, with the SomeMiddleware and echo CORS() middlewares
301302
fxhttpserver.AsHandler("GET", "/some-path", NewSomeHandler, NewSomeMiddleware, middleware.CORS()),
302303
),
303304
).Run()
304305
}
305306
```
306307

307-
Notes:
308+
Notes:
308309

309310
- you can specify several valid HTTP methods (comma separated) while registering a handler, for example `fxhttpserver.AsHandler("GET,POST", ...)`
310311
- you can use the shortcut `*` to register a handler for all valid HTTP methods, for example `fxhttpserver.AsHandler("*", ...)`
@@ -416,7 +417,7 @@ func main() {
416417
fxmetrics.FxMetricsModule,
417418
fxgenerate.FxGenerateModule,
418419
fxhttpserver.FxHttpServerModule, // load the module
419-
fx.Provide(
420+
fx.Options(
420421
// register and autowire the SomeHandler handler with NewSomeMiddleware middleware for [GET] /group/some-path
421422
// register and autowire the OtherHandler handler with echo CORS middleware for [POST] /group/other-path
422423
// register the echo CSRF middleware for all handlers of this group
@@ -439,6 +440,67 @@ Notes:
439440
- you can use the shortcut `*` to register a handler for all valid HTTP methods, for example `fxhttpserver.NewHandlerRegistration("*", ...)`
440441
- valid HTTP methods are `CONNECT`, `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, `PUT`, `TRACE`, `PROPFIND` and `REPORT`
441442

443+
#### Error Handler
444+
445+
You can use the `AsErrorHandler()` function to register a custom error handler on your http server.
446+
447+
You can provide any [ErrorHandler](registry.go) interface implementation (will be autowired from Fx container)
448+
449+
```go
450+
package main
451+
452+
import (
453+
"fmt"
454+
"net/http"
455+
456+
"github.com/ankorstore/yokai/config"
457+
"github.com/ankorstore/yokai/fxconfig"
458+
"github.com/ankorstore/yokai/fxgenerate"
459+
"github.com/ankorstore/yokai/fxhttpserver"
460+
"github.com/ankorstore/yokai/fxlog"
461+
"github.com/ankorstore/yokai/fxmetrics"
462+
"github.com/ankorstore/yokai/fxtrace"
463+
"github.com/ankorstore/yokai/httpserver"
464+
"github.com/labstack/echo/v4"
465+
"github.com/labstack/echo/v4/middleware"
466+
"go.uber.org/fx"
467+
)
468+
469+
type SomeErrorHandler struct {
470+
config *config.Config
471+
}
472+
473+
func NewSomeErrorHandler(config *config.Config) *SomeErrorHandler {
474+
return &SomeErrorHandler{
475+
config: config,
476+
}
477+
}
478+
479+
func (h *SomeErrorHandler) Handle() echo.HTTPErrorHandler {
480+
return func(err error, c echo.Context) {
481+
if c.Response().Committed {
482+
return
483+
}
484+
485+
c.String(http.StatusInternalServerError, fmt.Sprintf("error handled in %s: %s", h.config.AppName(), err))
486+
}
487+
}
488+
489+
func main() {
490+
fx.New(
491+
fxconfig.FxConfigModule, // load the module dependencies
492+
fxlog.FxLogModule,
493+
fxtrace.FxTraceModule,
494+
fxmetrics.FxMetricsModule,
495+
fxgenerate.FxGenerateModule,
496+
fxhttpserver.FxHttpServerModule, // load the module
497+
fx.Options(
498+
fxhttpserver.AsErrorHandler(NewSomeErrorHandler), // register SomeErrorHandler as error handler
499+
),
500+
).Run()
501+
}
502+
```
503+
442504
### WebSocket
443505

444506
This module supports the `WebSocket` protocol, see the [Echo documentation](https://echo.labstack.com/docs/cookbook/websocket) for more information.
@@ -468,9 +530,9 @@ And the template:
468530
```html
469531
<!-- templates/app.html -->
470532
<html>
471-
<body>
472-
<h1>App name is {{index . "name"}}</h1>
473-
</body>
533+
<body>
534+
<h1>App name is {{index . "name"}}</h1>
535+
</body>
474536
</html>
475537
```
476538

@@ -521,7 +583,7 @@ func main() {
521583
fxmetrics.FxMetricsModule,
522584
fxgenerate.FxGenerateModule,
523585
fxhttpserver.FxHttpServerModule, // load the module
524-
fx.Provide(
586+
fx.Options(
525587
fxhttpserver.AsHandler("GET", "/app", NewTemplateHandler),
526588
),
527589
).Run()

fxhttpserver/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/ankorstore/yokai/fxmetrics v1.2.0
1111
github.com/ankorstore/yokai/fxtrace v1.2.0
1212
github.com/ankorstore/yokai/generate v1.2.0
13-
github.com/ankorstore/yokai/httpserver v1.5.0
13+
github.com/ankorstore/yokai/httpserver v1.6.0
1414
github.com/ankorstore/yokai/log v1.2.0
1515
github.com/ankorstore/yokai/trace v1.3.0
1616
github.com/labstack/echo/v4 v4.12.0

fxhttpserver/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ github.com/ankorstore/yokai/fxtrace v1.2.0 h1:SXlWbjKSsb2wVH+hXSE9OD2VwyqkznwwW+
1212
github.com/ankorstore/yokai/fxtrace v1.2.0/go.mod h1:ch72eVTlIedETOApK7SXk2NEWpn3yYeM018dNRccocg=
1313
github.com/ankorstore/yokai/generate v1.2.0 h1:37siukjPGSS2kRnCnPhiuiF373+0tgwp0teXHnMsBhA=
1414
github.com/ankorstore/yokai/generate v1.2.0/go.mod h1:gqS/i20wnvCOhcXydYdiGcASzBaeuW7GK6YYg/kkuY4=
15-
github.com/ankorstore/yokai/httpserver v1.5.0 h1:42nfCFCGWuBKbwU8Jhlf1/ofrezDes8HlCa0mhiVoI8=
16-
github.com/ankorstore/yokai/httpserver v1.5.0/go.mod h1:AOCL4cK2bPKrtGFULvOvc8mKHAOw2bLW30CKJra2BB0=
15+
github.com/ankorstore/yokai/httpserver v1.6.0 h1:Xq3Jh1UM8tMQAnCM1wwGgi+Bm9NZwzJEJJcl56G4oNM=
16+
github.com/ankorstore/yokai/httpserver v1.6.0/go.mod h1:AOCL4cK2bPKrtGFULvOvc8mKHAOw2bLW30CKJra2BB0=
1717
github.com/ankorstore/yokai/log v1.2.0 h1:jiuDiC0dtqIGIOsFQslUHYoFJ1qjI+rOMa6dI1LBf2Y=
1818
github.com/ankorstore/yokai/log v1.2.0/go.mod h1:MVvUcms1AYGo0BT6l88B9KJdvtK6/qGKdgyKVXfbmyc=
1919
github.com/ankorstore/yokai/trace v1.3.0 h1:0ji32oymIcxTmH5h6GRWLo5ypwBbWrZkXRf9rWF9070=

fxhttpserver/module.go

+16-9
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,30 @@ func NewFxHttpServer(p FxHttpServerParam) (*echo.Echo, error) {
6363
)
6464

6565
// renderer
66-
var renderer echo.Renderer
66+
var echoRenderer echo.Renderer
6767
if p.Config.GetBool("modules.http.server.templates.enabled") {
68-
renderer = httpserver.NewHtmlTemplateRenderer(p.Config.GetString("modules.http.server.templates.path"))
68+
echoRenderer = httpserver.NewHtmlTemplateRenderer(p.Config.GetString("modules.http.server.templates.path"))
69+
}
70+
71+
// error handler
72+
var echoErrorHandler ErrorHandler
73+
resolvedErrorHandlers := p.Registry.ResolveErrorHandlers()
74+
if len(resolvedErrorHandlers) > 0 {
75+
echoErrorHandler = resolvedErrorHandlers[0]
76+
} else {
77+
echoErrorHandler = httpserver.NewJsonErrorHandler(
78+
p.Config.GetBool("modules.http.server.errors.obfuscate") || !appDebug,
79+
p.Config.GetBool("modules.http.server.errors.stack") || appDebug,
80+
)
6981
}
7082

7183
// server
7284
httpServer, err := p.Factory.Create(
7385
httpserver.WithDebug(appDebug),
7486
httpserver.WithBanner(false),
7587
httpserver.WithLogger(echoLogger),
76-
httpserver.WithRenderer(renderer),
77-
httpserver.WithHttpErrorHandler(
78-
httpserver.JsonErrorHandler(
79-
p.Config.GetBool("modules.http.server.errors.obfuscate") || !appDebug,
80-
p.Config.GetBool("modules.http.server.errors.stack") || appDebug,
81-
),
82-
),
88+
httpserver.WithRenderer(echoRenderer),
89+
httpserver.WithHttpErrorHandler(echoErrorHandler.Handle()),
8390
)
8491
if err != nil {
8592
return nil, fmt.Errorf("failed to create http server: %w", err)

fxhttpserver/module_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/ankorstore/yokai/fxconfig"
1111
"github.com/ankorstore/yokai/fxgenerate"
1212
"github.com/ankorstore/yokai/fxhttpserver"
13+
"github.com/ankorstore/yokai/fxhttpserver/testdata/errorhandler"
1314
"github.com/ankorstore/yokai/fxhttpserver/testdata/factory"
1415
"github.com/ankorstore/yokai/fxhttpserver/testdata/handler"
1516
"github.com/ankorstore/yokai/fxhttpserver/testdata/middleware"
@@ -1145,6 +1146,37 @@ func TestModuleWithTemplates(t *testing.T) {
11451146
)
11461147
}
11471148

1149+
func TestModuleWithCustomErrorHandler(t *testing.T) {
1150+
t.Setenv("APP_CONFIG_PATH", "testdata/config")
1151+
t.Setenv("APP_DEBUG", "true")
1152+
1153+
var httpServer *echo.Echo
1154+
1155+
fxtest.New(
1156+
t,
1157+
fx.NopLogger,
1158+
fxconfig.FxConfigModule,
1159+
fxlog.FxLogModule,
1160+
fxtrace.FxTraceModule,
1161+
fxmetrics.FxMetricsModule,
1162+
fxgenerate.FxGenerateModule,
1163+
fxhttpserver.FxHttpServerModule,
1164+
fx.Options(
1165+
fxhttpserver.AsHandler("GET", "/error", handler.NewTestErrorHandler),
1166+
fxhttpserver.AsErrorHandler(errorhandler.NewTestErrorHandler),
1167+
),
1168+
fx.Populate(&httpServer),
1169+
).RequireStart().RequireStop()
1170+
1171+
// [GET] /error
1172+
req := httptest.NewRequest(http.MethodGet, "/error", nil)
1173+
rec := httptest.NewRecorder()
1174+
httpServer.ServeHTTP(rec, req)
1175+
1176+
assert.Equal(t, http.StatusInternalServerError, rec.Code)
1177+
assert.Contains(t, rec.Body.String(), "error handled in test error handler of test: test error")
1178+
}
1179+
11481180
func TestModuleWithInvalidHandlerMethods(t *testing.T) {
11491181
t.Setenv("APP_CONFIG_PATH", "testdata/config")
11501182
t.Setenv("APP_DEBUG", "true")

fxhttpserver/register.go

+11
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,14 @@ func RegisterHandlersGroup(handlersGroupRegistration *HandlersGroupRegistration)
291291
),
292292
)
293293
}
294+
295+
// AsErrorHandler replaces the default error handler.
296+
func AsErrorHandler(errorHandler any) fx.Option {
297+
return fx.Provide(
298+
fx.Annotate(
299+
errorHandler,
300+
fx.As(new(ErrorHandler)),
301+
fx.ResultTags(`group:"httpserver-error-handlers"`),
302+
),
303+
)
304+
}

fxhttpserver/register_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package fxhttpserver_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/ankorstore/yokai/fxhttpserver"
8+
"github.com/ankorstore/yokai/fxhttpserver/testdata/errorhandler"
79
"github.com/stretchr/testify/assert"
810
)
911

@@ -122,3 +124,11 @@ func TestHandlersGroupRegistration(t *testing.T) {
122124
})
123125
}
124126
}
127+
128+
func TestErrorHandlerRegistration(t *testing.T) {
129+
t.Parallel()
130+
131+
result := fxhttpserver.AsErrorHandler(errorhandler.NewTestErrorHandler)
132+
133+
assert.Equal(t, "fx.provideOption", fmt.Sprintf("%T", result))
134+
}

fxhttpserver/registry.go

+14
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ type Handler interface {
1717
Handle() echo.HandlerFunc
1818
}
1919

20+
// ErrorHandler is the interface for error handlers.
21+
type ErrorHandler interface {
22+
Handle() echo.HTTPErrorHandler
23+
}
24+
2025
// HttpServerRegistry is the registry collecting middlewares, handlers, handlers groups and their definitions.
2126
type HttpServerRegistry struct {
2227
middlewares []Middleware
2328
middlewareDefinitions []MiddlewareDefinition
2429
handlers []Handler
2530
handlerDefinitions []HandlerDefinition
2631
handlersGroupDefinitions []HandlersGroupDefinition
32+
errorHandlers []ErrorHandler
2733
}
2834

2935
// FxHttpServerRegistryParam allows injection of the required dependencies in [NewFxHttpServerRegistry].
@@ -34,16 +40,19 @@ type FxHttpServerRegistryParam struct {
3440
Handlers []Handler `group:"httpserver-handlers"`
3541
HandlerDefinitions []HandlerDefinition `group:"httpserver-handler-definitions"`
3642
HandlersGroupDefinitions []HandlersGroupDefinition `group:"httpserver-handlers-group-definitions"`
43+
ErrorHandlers []ErrorHandler `group:"httpserver-error-handlers"`
3744
}
3845

3946
// NewFxHttpServerRegistry returns as new [HttpServerRegistry].
4047
func NewFxHttpServerRegistry(p FxHttpServerRegistryParam) *HttpServerRegistry {
4148
return &HttpServerRegistry{
49+
4250
middlewares: p.Middlewares,
4351
middlewareDefinitions: p.MiddlewareDefinitions,
4452
handlers: p.Handlers,
4553
handlerDefinitions: p.HandlerDefinitions,
4654
handlersGroupDefinitions: p.HandlersGroupDefinitions,
55+
errorHandlers: p.ErrorHandlers,
4756
}
4857
}
4958

@@ -143,6 +152,11 @@ func (r *HttpServerRegistry) ResolveHandlersGroups() ([]ResolvedHandlersGroup, e
143152
return resolvedHandlersGroups, nil
144153
}
145154

155+
// ResolveErrorHandlers resolves resolves a list of [ErrorHandler].
156+
func (r *HttpServerRegistry) ResolveErrorHandlers() []ErrorHandler {
157+
return r.errorHandlers
158+
}
159+
146160
func (r *HttpServerRegistry) resolveMiddlewareDefinition(middlewareDefinition MiddlewareDefinition) (ResolvedMiddleware, error) {
147161
if middlewareDefinition.Concrete() {
148162
if castMiddleware, ok := middlewareDefinition.Middleware().(func(echo.HandlerFunc) echo.HandlerFunc); ok {

fxhttpserver/registry_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fxhttpserver_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/ankorstore/yokai/fxhttpserver"
@@ -17,6 +18,8 @@ var testHandler = func(c echo.Context) error {
1718
return nil
1819
}
1920

21+
var testErrorHandler = func(err error, c echo.Context) {}
22+
2023
type testMiddlewareDefinitionMock struct {
2124
mock.Mock
2225
}
@@ -87,6 +90,10 @@ func (h testHandlerImplementation) Handle() echo.HandlerFunc {
8790
return testHandler
8891
}
8992

93+
type testErrorHandlerImplementation struct{}
94+
95+
func (h testErrorHandlerImplementation) Handle() echo.HTTPErrorHandler { return testErrorHandler }
96+
9097
func TestNewFxHttpServerRegistry(t *testing.T) {
9198
t.Parallel()
9299

@@ -468,3 +475,20 @@ func TestResolveHandlersGroupFailureOnInvalidHandlerMiddlewareImplementation(t *
468475
assert.Error(t, err)
469476
assert.Equal(t, "cannot cast middleware definition as MiddlewareFunc", err.Error())
470477
}
478+
479+
func TestResolveErrorHandlerSuccess(t *testing.T) {
480+
t.Parallel()
481+
482+
param := fxhttpserver.FxHttpServerRegistryParam{
483+
ErrorHandlers: []fxhttpserver.ErrorHandler{
484+
testErrorHandlerImplementation{},
485+
},
486+
}
487+
registry := fxhttpserver.NewFxHttpServerRegistry(param)
488+
489+
resolvedErrorHandlers := registry.ResolveErrorHandlers()
490+
491+
assert.Len(t, resolvedErrorHandlers, 1)
492+
493+
assert.Equal(t, "echo.HTTPErrorHandler", fmt.Sprintf("%T", resolvedErrorHandlers[0].Handle()))
494+
}

0 commit comments

Comments
 (0)