package middleware import ( "fmt" "net/http" "runtime/debug" "attune-heart-therapy/internal/errors" "attune-heart-therapy/internal/logger" "github.com/gin-gonic/gin" ) // ErrorHandlerMiddleware handles errors and panics in a structured way func ErrorHandlerMiddleware() gin.HandlerFunc { return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { log := logger.New("error_handler") if recovered != nil { // Handle panic err := fmt.Errorf("panic recovered: %v", recovered) log.Error("Panic recovered", err, map[string]interface{}{ "method": c.Request.Method, "path": c.Request.URL.Path, "client_ip": c.ClientIP(), "user_agent": c.Request.UserAgent(), "stack": string(debug.Stack()), }) // Return internal server error for panics appErr := errors.ErrInternalServer.WithDetails("An unexpected error occurred") c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) c.Abort() return } // Handle regular errors if len(c.Errors) > 0 { err := c.Errors.Last() handleError(c, err.Err, log) } }) } // handleError processes different types of errors and returns appropriate responses func handleError(c *gin.Context, err error, log *logger.Logger) { // Check if it's already an AppError if appErr := errors.GetAppError(err); appErr != nil { logError(log, c, appErr, appErr.Cause) c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) return } // Handle validation errors if validationErrs, ok := err.(errors.ValidationErrors); ok { appErr := validationErrs.ToAppError() logError(log, c, appErr, err) c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) return } // Handle other known error types appErr := classifyError(err) logError(log, c, appErr, err) c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) } // classifyError converts generic errors to AppErrors based on error content func classifyError(err error) *errors.AppError { errMsg := err.Error() // Database errors if containsAny(errMsg, []string{"database", "sql", "connection", "timeout"}) { return errors.ErrDatabaseError.WithCause(err) } // Network/external API errors if containsAny(errMsg, []string{"network", "connection refused", "timeout", "dns"}) { return errors.ErrExternalAPI.WithCause(err) } // Validation errors if containsAny(errMsg, []string{"invalid", "validation", "required", "format"}) { return errors.ErrValidationFailed.WithCause(err) } // Not found errors if containsAny(errMsg, []string{"not found", "does not exist"}) { return errors.ErrNotFound.WithCause(err) } // Default to internal server error return errors.ErrInternalServer.WithCause(err) } // logError logs error information with context func logError(log *logger.Logger, c *gin.Context, appErr *errors.AppError, cause error) { fields := map[string]interface{}{ "error_code": appErr.Code, "method": c.Request.Method, "path": c.Request.URL.Path, "client_ip": c.ClientIP(), "user_agent": c.Request.UserAgent(), "status": appErr.HTTPStatus, } // Add user ID if available if userID, exists := c.Get("user_id"); exists { fields["user_id"] = userID } // Add trace ID if available if traceID, exists := c.Get("trace_id"); exists { fields["trace_id"] = traceID } // Add error fields if available if appErr.Fields != nil { for k, v := range appErr.Fields { fields["error_"+k] = v } } // Log based on severity if appErr.HTTPStatus >= 500 { log.Error("Server error occurred", cause, fields) } else if appErr.HTTPStatus >= 400 { log.Warn("Client error occurred", fields) } else { log.Info("Request completed with error", fields) } } // containsAny checks if a string contains any of the given substrings func containsAny(s string, substrings []string) bool { for _, substr := range substrings { if len(s) >= len(substr) { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } } } return false } // NotFoundHandler handles 404 errors func NotFoundHandler() gin.HandlerFunc { return func(c *gin.Context) { appErr := errors.ErrNotFound.WithDetails(fmt.Sprintf("Route %s %s not found", c.Request.Method, c.Request.URL.Path)) c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) } } // MethodNotAllowedHandler handles 405 errors func MethodNotAllowedHandler() gin.HandlerFunc { return func(c *gin.Context) { appErr := errors.New(errors.ErrCodeValidationFailed, "Method not allowed", http.StatusMethodNotAllowed). WithDetails(fmt.Sprintf("Method %s not allowed for route %s", c.Request.Method, c.Request.URL.Path)) c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse()) } }