diff --git a/pkg/api/router.go b/pkg/api/router.go index d9e56fd..dc1270a 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -89,11 +89,19 @@ func NewRouter(options ...RouterOption) *Router { // RegisterService registers a service with the router. // The service config must have a Name field set. // The service will be registered at the route "/{cfg.Name}". +// If a service with the same Name is already registered, the call is +// ignored with a warning - without this guard, chi.Mount would panic +// and crash the process. func (r *Router) RegisterService( cfg *config.ServiceConfig, handler Handler, opts ...HandlerOption, ) { + if r.isServiceRegistered(cfg.Name) { + slog.Warn("Service already registered, skipping", "name", cfg.Name) + return + } + options := &handlerOptions{} for _, opt := range opts { opt(options) @@ -162,11 +170,19 @@ func WithMiddleware(mw []func(*middleware.Params) func(http.Handler) http.Handle // RegisterHTTPHandler registers a Handler as a service. // The handlerFactory receives the service DB and returns the handler. // The service will be registered at the route "/{cfg.Name}". +// If a service with the same Name is already registered, the call is +// ignored with a warning - without this guard, chi.Mount would panic +// and crash the process. func (r *Router) RegisterHTTPHandler( cfg *config.ServiceConfig, handlerFactory func(db.DB) Handler, opts ...HandlerOption, ) { + if r.isServiceRegistered(cfg.Name) { + slog.Warn("Service already registered, skipping", "name", cfg.Name) + return + } + options := &handlerOptions{} for _, opt := range opts { opt(options) @@ -239,6 +255,13 @@ func (r *Router) GetServices() map[string]*ServiceItem { return res } +func (r *Router) isServiceRegistered(name string) bool { + r.mu.RLock() + defer r.mu.RUnlock() + _, ok := r.services[name] + return ok +} + // GetDB returns the database for a specific service. // Returns nil if the service is not registered. func (r *Router) GetDB(serviceName string) db.DB { diff --git a/pkg/api/router_test.go b/pkg/api/router_test.go index 98af80d..71e3bbb 100644 --- a/pkg/api/router_test.go +++ b/pkg/api/router_test.go @@ -215,6 +215,60 @@ func TestRouter_RegisterService(t *testing.T) { }) } +func TestRouter_RegisterService_DuplicateNameIsIgnored(t *testing.T) { + router := newTestRouter(t) + + cfg := config.NewServiceConfig() + cfg.Name = "petstore" + first := &mockService{ + name: "petstore", + config: cfg, + routes: func(r chi.Router) { + r.Get("/ping", func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("first")) + }) + }, + } + second := &mockService{ + name: "petstore", + config: cfg, + routes: func(r chi.Router) { + r.Get("/ping", func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("second")) + }) + }, + } + + router.RegisterService(cfg, first) + assert.NotPanics(t, func() { router.RegisterService(cfg, second) }) + + req := httptest.NewRequest(http.MethodGet, "/petstore/ping", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + assert.Equal(t, "first", w.Body.String()) +} + +func TestRouter_RegisterHTTPHandler_DuplicateNameIsIgnored(t *testing.T) { + router := newTestRouter(t) + + cfg := config.NewServiceConfig() + cfg.Name = "petstore" + factory := func(_ db.DB) Handler { + return &mockService{ + name: "petstore", + config: cfg, + routes: func(r chi.Router) { + r.Get("/ping", func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("ok")) + }) + }, + } + } + + router.RegisterHTTPHandler(cfg, factory) + assert.NotPanics(t, func() { router.RegisterHTTPHandler(cfg, factory) }) +} + func TestRouter_MiddlewareOrder(t *testing.T) { t.Run("Middleware executes in correct order", func(t *testing.T) { router := newTestRouter(t)