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
129 lines
4.4 KiB
Go
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
|
|
}
|
|
}
|