227 lines
5.5 KiB
Go
227 lines
5.5 KiB
Go
// Package errors provides structured error types for the Prefect API client.
|
|
package errors
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// APIError represents an error returned by the Prefect API.
|
|
type APIError struct {
|
|
// StatusCode is the HTTP status code
|
|
StatusCode int
|
|
|
|
// Message is the error message
|
|
Message string
|
|
|
|
// Details contains additional error details from the API
|
|
Details map[string]interface{}
|
|
|
|
// RequestID is the request ID if available
|
|
RequestID string
|
|
|
|
// Response is the raw HTTP response
|
|
Response *http.Response
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *APIError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("prefect api error (status %d): %s", e.StatusCode, e.Message)
|
|
}
|
|
return fmt.Sprintf("prefect api error (status %d)", e.StatusCode)
|
|
}
|
|
|
|
// IsNotFound returns true if the error is a 404 Not Found error.
|
|
func (e *APIError) IsNotFound() bool {
|
|
return e.StatusCode == http.StatusNotFound
|
|
}
|
|
|
|
// IsUnauthorized returns true if the error is a 401 Unauthorized error.
|
|
func (e *APIError) IsUnauthorized() bool {
|
|
return e.StatusCode == http.StatusUnauthorized
|
|
}
|
|
|
|
// IsForbidden returns true if the error is a 403 Forbidden error.
|
|
func (e *APIError) IsForbidden() bool {
|
|
return e.StatusCode == http.StatusForbidden
|
|
}
|
|
|
|
// IsRateLimited returns true if the error is a 429 Too Many Requests error.
|
|
func (e *APIError) IsRateLimited() bool {
|
|
return e.StatusCode == http.StatusTooManyRequests
|
|
}
|
|
|
|
// IsServerError returns true if the error is a 5xx server error.
|
|
func (e *APIError) IsServerError() bool {
|
|
return e.StatusCode >= 500 && e.StatusCode < 600
|
|
}
|
|
|
|
// ValidationError represents a validation error from the API.
|
|
type ValidationError struct {
|
|
*APIError
|
|
ValidationErrors []ValidationDetail
|
|
}
|
|
|
|
// ValidationDetail represents a single validation error detail.
|
|
type ValidationDetail struct {
|
|
Loc []string `json:"loc"`
|
|
Msg string `json:"msg"`
|
|
Type string `json:"type"`
|
|
Ctx interface{} `json:"ctx,omitempty"`
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *ValidationError) Error() string {
|
|
if len(e.ValidationErrors) == 0 {
|
|
return e.APIError.Error()
|
|
}
|
|
return fmt.Sprintf("validation error: %s", e.ValidationErrors[0].Msg)
|
|
}
|
|
|
|
// NewAPIError creates a new APIError from an HTTP response.
|
|
func NewAPIError(resp *http.Response) error {
|
|
if resp == nil {
|
|
return &APIError{
|
|
StatusCode: 0,
|
|
Message: "nil response",
|
|
}
|
|
}
|
|
|
|
apiErr := &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Response: resp,
|
|
RequestID: resp.Header.Get("X-Request-ID"),
|
|
}
|
|
|
|
// Try to read and parse the response body
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err == nil && len(body) > 0 {
|
|
// Try to parse as JSON
|
|
var errorResponse struct {
|
|
Detail interface{} `json:"detail"`
|
|
}
|
|
if json.Unmarshal(body, &errorResponse) == nil {
|
|
// Check if detail is a validation error (422)
|
|
if resp.StatusCode == http.StatusUnprocessableEntity {
|
|
return parseValidationError(apiErr, errorResponse.Detail)
|
|
}
|
|
|
|
// Handle string detail
|
|
if msg, ok := errorResponse.Detail.(string); ok {
|
|
apiErr.Message = msg
|
|
} else if details, ok := errorResponse.Detail.(map[string]interface{}); ok {
|
|
apiErr.Details = details
|
|
if msg, ok := details["message"].(string); ok {
|
|
apiErr.Message = msg
|
|
}
|
|
}
|
|
} else {
|
|
// Not JSON, use raw body as message
|
|
apiErr.Message = string(body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to status text if no message
|
|
if apiErr.Message == "" {
|
|
apiErr.Message = http.StatusText(resp.StatusCode)
|
|
}
|
|
|
|
return apiErr
|
|
}
|
|
|
|
// parseValidationError parses validation errors from the API response.
|
|
func parseValidationError(apiErr *APIError, detail interface{}) error {
|
|
valErr := &ValidationError{
|
|
APIError: apiErr,
|
|
}
|
|
|
|
// Detail can be a list of validation errors
|
|
if details, ok := detail.([]interface{}); ok {
|
|
for _, d := range details {
|
|
if detailMap, ok := d.(map[string]interface{}); ok {
|
|
var vd ValidationDetail
|
|
|
|
// Parse loc
|
|
if loc, ok := detailMap["loc"].([]interface{}); ok {
|
|
for _, l := range loc {
|
|
if s, ok := l.(string); ok {
|
|
vd.Loc = append(vd.Loc, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse msg
|
|
if msg, ok := detailMap["msg"].(string); ok {
|
|
vd.Msg = msg
|
|
}
|
|
|
|
// Parse type
|
|
if typ, ok := detailMap["type"].(string); ok {
|
|
vd.Type = typ
|
|
}
|
|
|
|
// Parse ctx
|
|
if ctx, ok := detailMap["ctx"]; ok {
|
|
vd.Ctx = ctx
|
|
}
|
|
|
|
valErr.ValidationErrors = append(valErr.ValidationErrors, vd)
|
|
}
|
|
}
|
|
}
|
|
|
|
return valErr
|
|
}
|
|
|
|
// IsNotFound checks if an error is a 404 Not Found error.
|
|
func IsNotFound(err error) bool {
|
|
if apiErr, ok := err.(*APIError); ok {
|
|
return apiErr.IsNotFound()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsUnauthorized checks if an error is a 401 Unauthorized error.
|
|
func IsUnauthorized(err error) bool {
|
|
if apiErr, ok := err.(*APIError); ok {
|
|
return apiErr.IsUnauthorized()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsForbidden checks if an error is a 403 Forbidden error.
|
|
func IsForbidden(err error) bool {
|
|
if apiErr, ok := err.(*APIError); ok {
|
|
return apiErr.IsForbidden()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsRateLimited checks if an error is a 429 Too Many Requests error.
|
|
func IsRateLimited(err error) bool {
|
|
if apiErr, ok := err.(*APIError); ok {
|
|
return apiErr.IsRateLimited()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsServerError checks if an error is a 5xx server error.
|
|
func IsServerError(err error) bool {
|
|
if apiErr, ok := err.(*APIError); ok {
|
|
return apiErr.IsServerError()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsValidationError checks if an error is a validation error.
|
|
func IsValidationError(err error) bool {
|
|
_, ok := err.(*ValidationError)
|
|
return ok
|
|
}
|