// 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" CodeNotImplemented = "NOT_IMPLEMENTED" ) // 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(), "") } // Packet types for binary WebSocket protocol const ( PacketTypeSuccess = 0x00 PacketTypeError = 0x01 PacketTypeData = 0x02 ) // SendErrorPacket sends an error packet over WebSocket using binary protocol func SendErrorPacket(conn *websocket.Conn, code, message, details string) error { // Build binary error packet: [type:1][code_len:1][code:var][msg_len:2][msg:var][details_len:2][details:var] buf := make([]byte, 0, 1+1+len(code)+2+len(message)+2+len(details)) buf = append(buf, PacketTypeError) buf = append(buf, byte(len(code))) buf = append(buf, []byte(code)...) buf = append(buf, byte(len(message)>>8), byte(len(message))) buf = append(buf, []byte(message)...) buf = append(buf, byte(len(details)>>8), byte(len(details))) buf = append(buf, []byte(details)...) return conn.WriteMessage(websocket.BinaryMessage, buf) } // SendSuccessPacket sends a success packet over WebSocket using binary protocol // The data is serialized as JSON within the binary packet func SendSuccessPacket(conn *websocket.Conn, data map[string]any) error { // Build binary success packet: [type:1][json_data:var] jsonData, err := json.Marshal(data) if err != nil { return err } buf := make([]byte, 0, 1+len(jsonData)) buf = append(buf, PacketTypeSuccess) buf = append(buf, jsonData...) return conn.WriteMessage(websocket.BinaryMessage, buf) } // 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", "") ErrNotImplemented = NewErrorResponse(CodeNotImplemented, "feature not implemented", "") ) // NewNotImplemented creates a not implemented error with feature name func NewNotImplemented(feature string) ErrorResponse { return NewErrorResponse( CodeNotImplemented, fmt.Sprintf("%s is not yet implemented", feature), "This feature is planned but not yet available. See documentation for alternatives.", ) } // NewNotImplementedWithIssue creates a not implemented error with GitHub issue reference func NewNotImplementedWithIssue(feature string, issueURL string) ErrorResponse { return NewErrorResponse( CodeNotImplemented, fmt.Sprintf("%s is not yet implemented", feature), fmt.Sprintf("Track progress at: %s", issueURL), ) } // IsNotImplemented checks if error is a not implemented error func IsNotImplemented(err error) bool { if errResp, ok := err.(ErrorResponse); ok { return errResp.Code == CodeNotImplemented } return false } // 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 case CodeNotImplemented: return http.StatusNotImplemented default: return http.StatusInternalServerError } }