diff --git a/pkg/dbmanager/providers/postgres_listener_example_test.go b/pkg/dbmanager/providers/postgres_listener_example_test.go index 03f0452..bd979ee 100644 --- a/pkg/dbmanager/providers/postgres_listener_example_test.go +++ b/pkg/dbmanager/providers/postgres_listener_example_test.go @@ -3,6 +3,7 @@ package providers_test import ( "context" "fmt" + "log" "time" "github.com/bitechdev/ResolveSpec/pkg/dbmanager" @@ -29,14 +30,14 @@ func ExamplePostgresListener_basic() { ctx := context.Background() if err := provider.Connect(ctx, cfg); err != nil { - panic(fmt.Sprintf("Failed to connect: %v", err)) + log.Fatalf("Failed to connect: %v", err) } defer provider.Close() // Get listener listener, err := provider.GetListener(ctx) if err != nil { - panic(fmt.Sprintf("Failed to get listener: %v", err)) + log.Fatalf("Failed to get listener: %v", err) } // Subscribe to a channel with a handler @@ -44,13 +45,13 @@ func ExamplePostgresListener_basic() { fmt.Printf("Received notification on %s: %s\n", channel, payload) }) if err != nil { - panic(fmt.Sprintf("Failed to listen: %v", err)) + log.Fatalf("Failed to listen: %v", err) } // Send a notification err = listener.Notify(ctx, "user_events", `{"event":"user_created","user_id":123}`) if err != nil { - panic(fmt.Sprintf("Failed to notify: %v", err)) + log.Fatalf("Failed to notify: %v", err) } // Wait for notification to be processed @@ -58,7 +59,7 @@ func ExamplePostgresListener_basic() { // Unsubscribe from the channel if err := listener.Unlisten("user_events"); err != nil { - panic(fmt.Sprintf("Failed to unlisten: %v", err)) + log.Fatalf("Failed to unlisten: %v", err) } } @@ -80,13 +81,13 @@ func ExamplePostgresListener_multipleChannels() { ctx := context.Background() if err := provider.Connect(ctx, cfg); err != nil { - panic(fmt.Sprintf("Failed to connect: %v", err)) + log.Fatalf("Failed to connect: %v", err) } defer provider.Close() listener, err := provider.GetListener(ctx) if err != nil { - panic(fmt.Sprintf("Failed to get listener: %v", err)) + log.Fatalf("Failed to get listener: %v", err) } // Listen to multiple channels @@ -97,7 +98,7 @@ func ExamplePostgresListener_multipleChannels() { fmt.Printf("[%s] %s\n", ch, payload) }) if err != nil { - panic(fmt.Sprintf("Failed to listen on %s: %v", channel, err)) + log.Fatalf("Failed to listen on %s: %v", channel, err) } } @@ -140,14 +141,14 @@ func ExamplePostgresListener_withDBManager() { provider := providers.NewPostgresProvider() if err := provider.Connect(ctx, cfg); err != nil { - panic(err) + log.Fatal(err) } defer provider.Close() // Get listener listener, err := provider.GetListener(ctx) if err != nil { - panic(err) + log.Fatal(err) } // Subscribe to application events @@ -186,13 +187,13 @@ func ExamplePostgresListener_errorHandling() { ctx := context.Background() if err := provider.Connect(ctx, cfg); err != nil { - panic(fmt.Sprintf("Failed to connect: %v", err)) + log.Fatalf("Failed to connect: %v", err) } defer provider.Close() listener, err := provider.GetListener(ctx) if err != nil { - panic(fmt.Sprintf("Failed to get listener: %v", err)) + log.Fatalf("Failed to get listener: %v", err) } // The listener automatically reconnects if the connection is lost diff --git a/pkg/resolvemcp/handler.go b/pkg/resolvemcp/handler.go index 579e215..4ed7c9c 100644 --- a/pkg/resolvemcp/handler.go +++ b/pkg/resolvemcp/handler.go @@ -197,8 +197,19 @@ func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interfac return defaultSchema, entity } +// recoverPanic catches a panic from the current goroutine and returns it as an error. +// Usage: defer recoverPanic(&returnedErr) +func recoverPanic(err *error) { + if r := recover(); r != nil { + msg := fmt.Sprintf("%v", r) + logger.Error("[resolvemcp] panic recovered: %s", msg) + *err = fmt.Errorf("internal error: %s", msg) + } +} + // executeRead reads records from the database and returns raw data + metadata. -func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, options common.RequestOptions) (interface{}, *common.Metadata, error) { +func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, options common.RequestOptions) (_ interface{}, _ *common.Metadata, retErr error) { + defer recoverPanic(&retErr) model, err := h.registry.GetModelByEntity(schema, entity) if err != nil { return nil, nil, fmt.Errorf("model not found: %w", err) @@ -254,15 +265,6 @@ func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, op query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", cu.Expression, cu.Name)) } - // Preloads - if len(options.Preload) > 0 { - var err error - query, err = h.applyPreloads(model, query, options.Preload) - if err != nil { - return nil, nil, fmt.Errorf("failed to apply preloads: %w", err) - } - } - // Filters query = h.applyFilters(query, options.Filters) @@ -304,7 +306,7 @@ func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, op } } - // Count + // Count — must happen before preloads are applied; Bun panics when counting with relations. total, err := query.Count(ctx) if err != nil { return nil, nil, fmt.Errorf("error counting records: %w", err) @@ -318,6 +320,15 @@ func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, op query = query.Offset(*options.Offset) } + // Preloads — applied after count to avoid Bun panic when counting with relations. + if len(options.Preload) > 0 { + var preloadErr error + query, preloadErr = h.applyPreloads(model, query, options.Preload) + if preloadErr != nil { + return nil, nil, fmt.Errorf("failed to apply preloads: %w", preloadErr) + } + } + // BeforeRead hook hookCtx.Query = query if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil { @@ -378,7 +389,8 @@ func (h *Handler) executeRead(ctx context.Context, schema, entity, id string, op } // executeCreate inserts one or more records. -func (h *Handler) executeCreate(ctx context.Context, schema, entity string, data interface{}) (interface{}, error) { +func (h *Handler) executeCreate(ctx context.Context, schema, entity string, data interface{}) (_ interface{}, retErr error) { + defer recoverPanic(&retErr) model, err := h.registry.GetModelByEntity(schema, entity) if err != nil { return nil, fmt.Errorf("model not found: %w", err) @@ -462,7 +474,8 @@ func (h *Handler) executeCreate(ctx context.Context, schema, entity string, data } // executeUpdate updates a record by ID. -func (h *Handler) executeUpdate(ctx context.Context, schema, entity, id string, data interface{}) (interface{}, error) { +func (h *Handler) executeUpdate(ctx context.Context, schema, entity, id string, data interface{}) (_ interface{}, retErr error) { + defer recoverPanic(&retErr) model, err := h.registry.GetModelByEntity(schema, entity) if err != nil { return nil, fmt.Errorf("model not found: %w", err) @@ -572,7 +585,8 @@ func (h *Handler) executeUpdate(ctx context.Context, schema, entity, id string, } // executeDelete deletes a record by ID. -func (h *Handler) executeDelete(ctx context.Context, schema, entity, id string) (interface{}, error) { +func (h *Handler) executeDelete(ctx context.Context, schema, entity, id string) (_ interface{}, retErr error) { + defer recoverPanic(&retErr) if id == "" { return nil, fmt.Errorf("delete requires an ID") } diff --git a/pkg/server/example_test.go b/pkg/server/example_test.go index 032b6f0..ec71502 100644 --- a/pkg/server/example_test.go +++ b/pkg/server/example_test.go @@ -3,6 +3,7 @@ package server_test import ( "context" "fmt" + "log" "net/http" "time" @@ -29,18 +30,18 @@ func ExampleManager_basic() { GZIP: true, // Enable GZIP compression }) if err != nil { - panic(err) + log.Fatal(err) } // Start all servers if err := mgr.StartAll(); err != nil { - panic(err) + log.Fatal(err) } // Server is now running... // When done, stop gracefully if err := mgr.StopAll(); err != nil { - panic(err) + log.Fatal(err) } } @@ -61,7 +62,7 @@ func ExampleManager_https() { SSLKey: "/path/to/key.pem", }) if err != nil { - panic(err) + log.Fatal(err) } // Option 2: Self-signed certificate (for development) @@ -73,27 +74,27 @@ func ExampleManager_https() { SelfSignedSSL: true, }) if err != nil { - panic(err) + log.Fatal(err) } // Option 3: Let's Encrypt / AutoTLS (for production) _, err = mgr.Add(server.Config{ - Name: "https-server-letsencrypt", - Host: "0.0.0.0", - Port: 443, - Handler: handler, - AutoTLS: true, - AutoTLSDomains: []string{"example.com", "www.example.com"}, - AutoTLSEmail: "admin@example.com", + Name: "https-server-letsencrypt", + Host: "0.0.0.0", + Port: 443, + Handler: handler, + AutoTLS: true, + AutoTLSDomains: []string{"example.com", "www.example.com"}, + AutoTLSEmail: "admin@example.com", AutoTLSCacheDir: "./certs-cache", }) if err != nil { - panic(err) + log.Fatal(err) } // Start all servers if err := mgr.StartAll(); err != nil { - panic(err) + log.Fatal(err) } // Cleanup @@ -136,7 +137,7 @@ func ExampleManager_gracefulShutdown() { IdleTimeout: 120 * time.Second, }) if err != nil { - panic(err) + log.Fatal(err) } // Start servers and block until shutdown signal (SIGINT/SIGTERM) @@ -164,7 +165,7 @@ func ExampleManager_healthChecks() { Handler: mux, }) if err != nil { - panic(err) + log.Fatal(err) } // Add health and readiness endpoints @@ -173,7 +174,7 @@ func ExampleManager_healthChecks() { // Start the server if err := mgr.StartAll(); err != nil { - panic(err) + log.Fatal(err) } // Health check returns: @@ -204,7 +205,7 @@ func ExampleManager_multipleServers() { GZIP: true, }) if err != nil { - panic(err) + log.Fatal(err) } // Admin API server (different port) @@ -218,7 +219,7 @@ func ExampleManager_multipleServers() { Handler: adminHandler, }) if err != nil { - panic(err) + log.Fatal(err) } // Metrics server (internal only) @@ -232,18 +233,18 @@ func ExampleManager_multipleServers() { Handler: metricsHandler, }) if err != nil { - panic(err) + log.Fatal(err) } // Start all servers at once if err := mgr.StartAll(); err != nil { - panic(err) + log.Fatal(err) } // Get specific server instance publicInstance, err := mgr.Get("public-api") if err != nil { - panic(err) + log.Fatal(err) } fmt.Printf("Public API running on: %s\n", publicInstance.Addr()) @@ -253,7 +254,7 @@ func ExampleManager_multipleServers() { // Stop all servers gracefully (in parallel) if err := mgr.StopAll(); err != nil { - panic(err) + log.Fatal(err) } } @@ -273,11 +274,11 @@ func ExampleManager_monitoring() { Handler: handler, }) if err != nil { - panic(err) + log.Fatal(err) } if err := mgr.StartAll(); err != nil { - panic(err) + log.Fatal(err) } // Check server status