fetch_ml/internal/api/errors/errors.go
Jeremie Fraeys c18a8619fe
feat(api): add structured error package and refactor handlers
New error handling:
- Add internal/api/errors/errors.go with structured API error types
- Standardize error codes across all API endpoints
- Add user-facing error messages vs internal error details separation

Handler improvements:
- jupyter/handlers.go: better workspace lifecycle and error handling
- plugins/handlers.go: plugin management with validation
- groups/handlers.go: group CRUD with capability metadata
- jobs/handlers.go: job submission and monitoring improvements
- datasets/handlers.go: dataset upload/download with progress
- validate/handlers.go: manifest validation with detailed errors
- audit/handlers.go: audit log querying with filters

Server configuration:
- server_config.go: refined config loading with validation
- server_gen.go: improved code generation for OpenAPI specs
2026-03-12 12:04:46 -04:00

129 lines
4.4 KiB
Go

// Package errors provides centralized error handling for the API
package errors
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
// Error codes - centralized to ensure consistency across all API handlers
const (
CodeUnknownError = "UNKNOWN_ERROR"
CodeInvalidRequest = "INVALID_REQUEST"
CodeAuthenticationFailed = "AUTHENTICATION_FAILED"
CodePermissionDenied = "PERMISSION_DENIED"
CodeResourceNotFound = "RESOURCE_NOT_FOUND"
CodeResourceAlreadyExists = "RESOURCE_ALREADY_EXISTS"
CodeServerOverloaded = "SERVER_OVERLOADED"
CodeDatabaseError = "DATABASE_ERROR"
CodeNetworkError = "NETWORK_ERROR"
CodeStorageError = "STORAGE_ERROR"
CodeTimeout = "TIMEOUT"
CodeJobNotFound = "JOB_NOT_FOUND"
CodeJobAlreadyRunning = "JOB_ALREADY_RUNNING"
CodeJobFailedToStart = "JOB_FAILED_TO_START"
CodeJobExecutionFailed = "JOB_EXECUTION_FAILED"
CodeJobCancelled = "JOB_CANCELLED"
CodeOutOfMemory = "OUT_OF_MEMORY"
CodeDiskFull = "DISK_FULL"
CodeInvalidConfiguration = "INVALID_CONFIGURATION"
CodeServiceUnavailable = "SERVICE_UNAVAILABLE"
CodeBadRequest = "BAD_REQUEST"
CodeForbidden = "FORBIDDEN"
CodeNotFound = "NOT_FOUND"
)
// ErrorResponse represents a standardized error response
type ErrorResponse struct {
ErrorMsg string `json:"error"`
Code string `json:"code"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
}
// NewErrorResponse creates a new error response
func NewErrorResponse(code, message, details string) ErrorResponse {
return ErrorResponse{
ErrorMsg: "true",
Code: code,
Message: message,
Details: details,
}
}
// Error implements the error interface
func (e ErrorResponse) Error() string {
if e.Details != "" {
return fmt.Sprintf("[%s] %s: %s", e.Code, e.Message, e.Details)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// WriteHTTPError writes an error response to an HTTP ResponseWriter
func WriteHTTPError(w http.ResponseWriter, statusCode int, errCode, message, details string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
resp := NewErrorResponse(errCode, message, details)
_ = json.NewEncoder(w).Encode(resp)
}
// WriteHTTPErrorFromError writes an error response from an error
func WriteHTTPErrorFromError(w http.ResponseWriter, statusCode int, err error) {
if errResp, ok := err.(ErrorResponse); ok {
WriteHTTPError(w, statusCode, errResp.Code, errResp.Message, errResp.Details)
return
}
WriteHTTPError(w, statusCode, CodeUnknownError, err.Error(), "")
}
// SendErrorPacket sends an error packet over WebSocket
func SendErrorPacket(conn *websocket.Conn, code, message, details string) error {
resp := NewErrorResponse(code, message, details)
return conn.WriteJSON(resp)
}
// SendSuccessPacket sends a success packet over WebSocket
// Automatically adds "success": true to the response
func SendSuccessPacket(conn *websocket.Conn, data map[string]any) error {
response := make(map[string]any, len(data)+1)
response["success"] = true
for k, v := range data {
response[k] = v
}
return conn.WriteJSON(response)
}
// Common error responses as typed errors for use with errors.Is/errors.As
var (
ErrNotFound = NewErrorResponse(CodeNotFound, "resource not found", "")
ErrPermissionDenied = NewErrorResponse(CodePermissionDenied, "permission denied", "")
ErrInvalidRequest = NewErrorResponse(CodeInvalidRequest, "invalid request", "")
ErrServiceUnavailable = NewErrorResponse(CodeServiceUnavailable, "service unavailable", "")
ErrServerOverloaded = NewErrorResponse(CodeServerOverloaded, "server overloaded", "")
)
// HTTPStatusFromCode maps error codes to HTTP status codes
func HTTPStatusFromCode(code string) int {
switch code {
case CodeInvalidRequest, CodeBadRequest:
return http.StatusBadRequest
case CodeAuthenticationFailed:
return http.StatusUnauthorized
case CodePermissionDenied, CodeForbidden:
return http.StatusForbidden
case CodeResourceNotFound, CodeNotFound, CodeJobNotFound:
return http.StatusNotFound
case CodeResourceAlreadyExists:
return http.StatusConflict
case CodeServerOverloaded, CodeServiceUnavailable:
return http.StatusServiceUnavailable
case CodeTimeout:
return http.StatusRequestTimeout
default:
return http.StatusInternalServerError
}
}