package handlers import ( "encoding/json" "fmt" "log" "net/http" c "github.com/juanatsap/cv-site/internal/constants" ) // ErrorResponse represents a structured error response type ErrorResponse struct { Error string `json:"error"` Message string `json:"message,omitempty"` Code int `json:"code"` } // AppError represents an application error with context type AppError struct { Err error Message string StatusCode int Internal bool // If true, don't expose details to client } // Error implements the error interface func (e *AppError) Error() string { if e.Err != nil { return e.Err.Error() } return e.Message } // NewAppError creates a new application error func NewAppError(err error, message string, statusCode int, internal bool) *AppError { return &AppError{ Err: err, Message: message, StatusCode: statusCode, Internal: internal, } } // HandleError handles errors consistently across the application func HandleError(w http.ResponseWriter, r *http.Request, err error) { var appErr *AppError // Check if it's an AppError switch e := err.(type) { case *AppError: appErr = e default: // Unknown error - treat as internal server error appErr = NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true) } // Log the error if appErr.Internal { log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, appErr.Err) } else { log.Printf("CLIENT ERROR [%s %s]: %s (status: %d)", r.Method, r.URL.Path, appErr.Message, appErr.StatusCode) } // Determine response based on Accept header accept := r.Header.Get(c.HeaderAccept) isJSON := accept == c.ContentTypeJSON isHTMX := r.Header.Get(c.HeaderHXRequest) != "" if isJSON { // JSON response w.Header().Set(c.HeaderContentType, c.ContentTypeJSON) w.WriteHeader(appErr.StatusCode) response := ErrorResponse{ Error: http.StatusText(appErr.StatusCode), Code: appErr.StatusCode, } // Only include message if not internal if !appErr.Internal { response.Message = appErr.Message } if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("ERROR encoding JSON response: %v", err) } return } if isHTMX { // HTMX response - return simple HTML fragment w.Header().Set(c.HeaderContentType, c.ContentTypeHTMLFragment) w.WriteHeader(appErr.StatusCode) message := appErr.Message if appErr.Internal { message = "An error occurred. Please try again later." } if _, err := w.Write([]byte("
" + message + "
")); err != nil { log.Printf("ERROR writing HTMX error response: %v", err) } return } // Standard HTTP error response message := appErr.Message if appErr.Internal { message = "Internal Server Error" } http.Error(w, message, appErr.StatusCode) } // Common error constructors func NotFoundError(message string) *AppError { return NewAppError(nil, message, http.StatusNotFound, false) } func BadRequestError(message string) *AppError { return NewAppError(nil, message, http.StatusBadRequest, false) } func InternalError(err error) *AppError { return NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true) } func TemplateError(err error, templateName string) *AppError { return NewAppError( err, "Error rendering template: "+templateName, http.StatusInternalServerError, true, ) } func DataLoadError(err error, dataType string) *AppError { return NewAppError( err, "Error loading "+dataType+" data", http.StatusInternalServerError, true, ) } // ============================================================================== // TYPED ERRORS // Domain-specific error types for better error handling // ============================================================================== // ErrorCode represents a specific error condition type ErrorCode string const ( ErrCodeInvalidLanguage ErrorCode = "INVALID_LANGUAGE" ErrCodeInvalidLength ErrorCode = "INVALID_LENGTH" ErrCodeInvalidIcons ErrorCode = "INVALID_ICONS" ErrCodeInvalidTheme ErrorCode = "INVALID_THEME" ErrCodeInvalidVersion ErrorCode = "INVALID_VERSION" ErrCodeTemplateNotFound ErrorCode = "TEMPLATE_NOT_FOUND" ErrCodeTemplateRender ErrorCode = "TEMPLATE_RENDER" ErrCodeDataLoad ErrorCode = "DATA_LOAD" ErrCodePDFGeneration ErrorCode = "PDF_GENERATION" ErrCodeMethodNotAllowed ErrorCode = "METHOD_NOT_ALLOWED" ErrCodeUnauthorized ErrorCode = "UNAUTHORIZED" ErrCodeForbidden ErrorCode = "FORBIDDEN" ErrCodeRateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED" ) // DomainError represents a domain-specific error type DomainError struct { Code ErrorCode Message string Err error StatusCode int Field string // Optional field that caused the error } // Error implements the error interface func (e *DomainError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Code, e.Err) } return fmt.Sprintf("%s: %s", e.Code, e.Message) } // Unwrap returns the underlying error func (e *DomainError) Unwrap() error { return e.Err } // NewDomainError creates a new domain error func NewDomainError(code ErrorCode, message string, statusCode int) *DomainError { return &DomainError{ Code: code, Message: message, StatusCode: statusCode, } } // WithError adds an underlying error func (e *DomainError) WithError(err error) *DomainError { e.Err = err return e } // WithField adds field information func (e *DomainError) WithField(field string) *DomainError { e.Field = field return e } // NOTE: Domain error constructors were removed as they were unused. // If needed in the future, they can be re-added following the DomainError pattern above. // See git history for the previous implementation.