backend-service/internal/middleware/error.go
ats-tech25 ddfa2de49e feat(app): Implement comprehensive application lifecycle management
- Add new `app` package to manage application initialization and lifecycle
- Refactor `main.go` to use new application management approach
- Implement graceful shutdown with context timeout and signal handling
- Add dependency injection container initialization
- Enhance logging with configurable log levels and structured logging
- Update configuration loading and server initialization process
- Modify Jitsi configuration in `.env` for custom deployment
- Improve error handling and logging throughout application startup
- Centralize application startup and shutdown logic in single package
Introduces a more robust and flexible application management system with improved initialization, logging, and shutdown capabilities.
2025-11-07 19:22:26 +00:00

164 lines
4.6 KiB
Go

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())
}
}