package handlers import ( "encoding/json" "log" "net/http" ) // 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("Accept") isJSON := accept == "application/json" isHTMX := r.Header.Get("HX-Request") != "" if isJSON { // JSON response w.Header().Set("Content-Type", "application/json") 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("Content-Type", "text/html") 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, ) }