monorepo/cloud/maplefile-backend/pkg/maplefile/client/errors.go

157 lines
4.2 KiB
Go

// Package client provides a Go SDK for interacting with the MapleFile API.
package client
import (
"encoding/json"
"fmt"
"strings"
)
// ProblemDetail represents an RFC 9457 problem detail response from the API.
type ProblemDetail struct {
Type string `json:"type"`
Status int `json:"status"`
Title string `json:"title"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
Timestamp string `json:"timestamp"`
TraceID string `json:"trace_id,omitempty"`
}
// APIError wraps ProblemDetail for the error interface.
type APIError struct {
ProblemDetail
}
// Error returns a formatted error message from the ProblemDetail.
func (e *APIError) Error() string {
var errMsg strings.Builder
if e.Detail != "" {
errMsg.WriteString(e.Detail)
} else {
errMsg.WriteString(e.Title)
}
if len(e.Errors) > 0 {
errMsg.WriteString("\n\nValidation errors:")
for field, message := range e.Errors {
errMsg.WriteString(fmt.Sprintf("\n - %s: %s", field, message))
}
}
return errMsg.String()
}
// StatusCode returns the HTTP status code from the error.
func (e *APIError) StatusCode() int {
return e.Status
}
// GetValidationErrors returns the validation errors map.
func (e *APIError) GetValidationErrors() map[string]string {
return e.Errors
}
// GetFieldError returns the error message for a specific field, or empty string if not found.
func (e *APIError) GetFieldError(field string) string {
if e.Errors == nil {
return ""
}
return e.Errors[field]
}
// HasFieldError checks if a specific field has a validation error.
func (e *APIError) HasFieldError(field string) bool {
if e.Errors == nil {
return false
}
_, exists := e.Errors[field]
return exists
}
// IsNotFound checks if the error is a 404 Not Found error.
func IsNotFound(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return apiErr.Status == 404
}
return false
}
// IsUnauthorized checks if the error is a 401 Unauthorized error.
func IsUnauthorized(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return apiErr.Status == 401
}
return false
}
// IsForbidden checks if the error is a 403 Forbidden error.
func IsForbidden(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return apiErr.Status == 403
}
return false
}
// IsValidationError checks if the error has validation errors.
func IsValidationError(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return len(apiErr.Errors) > 0
}
return false
}
// IsConflict checks if the error is a 409 Conflict error.
func IsConflict(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return apiErr.Status == 409
}
return false
}
// IsTooManyRequests checks if the error is a 429 Too Many Requests error.
func IsTooManyRequests(err error) bool {
if apiErr, ok := err.(*APIError); ok {
return apiErr.Status == 429
}
return false
}
// parseErrorResponse attempts to parse an error response body into an APIError.
// It tries RFC 9457 format first, then falls back to legacy format.
//
// Note: RFC 9457 specifies that error responses should use Content-Type: application/problem+json,
// but we parse based on the response structure rather than Content-Type for maximum compatibility.
func parseErrorResponse(body []byte, statusCode int) error {
// Try to parse as RFC 9457 ProblemDetail
// The presence of the "type" field distinguishes RFC 9457 from legacy responses
var problem ProblemDetail
if err := json.Unmarshal(body, &problem); err == nil && problem.Type != "" {
return &APIError{ProblemDetail: problem}
}
// Fallback for non-RFC 9457 errors
var errorResponse map[string]interface{}
if err := json.Unmarshal(body, &errorResponse); err == nil {
if errMsg, ok := errorResponse["message"].(string); ok {
return &APIError{
ProblemDetail: ProblemDetail{
Status: statusCode,
Title: errMsg,
Detail: errMsg,
},
}
}
}
// Last resort: return raw body as error
return &APIError{
ProblemDetail: ProblemDetail{
Status: statusCode,
Title: fmt.Sprintf("HTTP %d", statusCode),
Detail: string(body),
},
}
}