network/pkg/errors/http.go
2026-01-20 10:03:55 +02:00

282 lines
7.8 KiB
Go

package errors
import (
"encoding/json"
"errors"
"net/http"
)
// HTTPError represents an HTTP error response.
type HTTPError struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
// Error implements the error interface.
func (e *HTTPError) Error() string {
return e.Message
}
// StatusCode returns the HTTP status code for an error.
// It maps error codes to appropriate HTTP status codes.
func StatusCode(err error) int {
if err == nil {
return http.StatusOK
}
// Check if it's our custom error type
var customErr Error
if errors.As(err, &customErr) {
return codeToHTTPStatus(customErr.Code())
}
// Check for specific error types
var (
validationErr *ValidationError
notFoundErr *NotFoundError
unauthorizedErr *UnauthorizedError
forbiddenErr *ForbiddenError
conflictErr *ConflictError
timeoutErr *TimeoutError
rateLimitErr *RateLimitError
serviceErr *ServiceError
)
switch {
case errors.As(err, &validationErr):
return http.StatusBadRequest
case errors.As(err, &notFoundErr):
return http.StatusNotFound
case errors.As(err, &unauthorizedErr):
return http.StatusUnauthorized
case errors.As(err, &forbiddenErr):
return http.StatusForbidden
case errors.As(err, &conflictErr):
return http.StatusConflict
case errors.As(err, &timeoutErr):
return http.StatusRequestTimeout
case errors.As(err, &rateLimitErr):
return http.StatusTooManyRequests
case errors.As(err, &serviceErr):
return http.StatusServiceUnavailable
}
// Check sentinel errors
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrUnauthorized):
return http.StatusUnauthorized
case errors.Is(err, ErrForbidden):
return http.StatusForbidden
case errors.Is(err, ErrConflict):
return http.StatusConflict
case errors.Is(err, ErrInvalidInput):
return http.StatusBadRequest
case errors.Is(err, ErrTimeout):
return http.StatusRequestTimeout
case errors.Is(err, ErrServiceUnavailable):
return http.StatusServiceUnavailable
case errors.Is(err, ErrTooManyRequests):
return http.StatusTooManyRequests
case errors.Is(err, ErrInternal):
return http.StatusInternalServerError
}
// Default to internal server error
return http.StatusInternalServerError
}
// codeToHTTPStatus maps error codes to HTTP status codes.
func codeToHTTPStatus(code string) int {
switch code {
case CodeOK:
return http.StatusOK
case CodeCancelled:
return 499 // Client Closed Request
case CodeUnknown, CodeInternal:
return http.StatusInternalServerError
case CodeInvalidArgument, CodeValidation, CodeFailedPrecondition:
return http.StatusBadRequest
case CodeDeadlineExceeded, CodeTimeout:
return http.StatusRequestTimeout
case CodeNotFound:
return http.StatusNotFound
case CodeAlreadyExists, CodeConflict:
return http.StatusConflict
case CodePermissionDenied, CodeForbidden:
return http.StatusForbidden
case CodeResourceExhausted, CodeRateLimit:
return http.StatusTooManyRequests
case CodeAborted:
return http.StatusConflict
case CodeOutOfRange:
return http.StatusBadRequest
case CodeUnimplemented:
return http.StatusNotImplemented
case CodeUnavailable, CodeServiceUnavailable:
return http.StatusServiceUnavailable
case CodeDataLoss, CodeDatabaseError, CodeStorageError:
return http.StatusInternalServerError
case CodeUnauthenticated, CodeUnauthorized, CodeAuthError:
return http.StatusUnauthorized
case CodeCacheError, CodeNetworkError, CodeExecutionError,
CodeCompilationError, CodeConfigError, CodeCryptoError,
CodeSerializationError:
return http.StatusInternalServerError
default:
return http.StatusInternalServerError
}
}
// ToHTTPError converts an error to an HTTPError.
func ToHTTPError(err error, traceID string) *HTTPError {
if err == nil {
return &HTTPError{
Status: http.StatusOK,
Code: CodeOK,
Message: "success",
TraceID: traceID,
}
}
httpErr := &HTTPError{
Status: StatusCode(err),
TraceID: traceID,
Details: make(map[string]string),
}
// Extract details from custom error types
var customErr Error
if errors.As(err, &customErr) {
httpErr.Code = customErr.Code()
httpErr.Message = customErr.Message()
} else {
httpErr.Code = CodeInternal
httpErr.Message = err.Error()
}
// Add type-specific details
var (
validationErr *ValidationError
notFoundErr *NotFoundError
unauthorizedErr *UnauthorizedError
forbiddenErr *ForbiddenError
conflictErr *ConflictError
timeoutErr *TimeoutError
rateLimitErr *RateLimitError
serviceErr *ServiceError
internalErr *InternalError
)
switch {
case errors.As(err, &validationErr):
if validationErr.Field != "" {
httpErr.Details["field"] = validationErr.Field
}
case errors.As(err, &notFoundErr):
if notFoundErr.Resource != "" {
httpErr.Details["resource"] = notFoundErr.Resource
}
if notFoundErr.ID != "" {
httpErr.Details["id"] = notFoundErr.ID
}
case errors.As(err, &unauthorizedErr):
if unauthorizedErr.Realm != "" {
httpErr.Details["realm"] = unauthorizedErr.Realm
}
case errors.As(err, &forbiddenErr):
if forbiddenErr.Resource != "" {
httpErr.Details["resource"] = forbiddenErr.Resource
}
if forbiddenErr.Action != "" {
httpErr.Details["action"] = forbiddenErr.Action
}
case errors.As(err, &conflictErr):
if conflictErr.Resource != "" {
httpErr.Details["resource"] = conflictErr.Resource
}
if conflictErr.Field != "" {
httpErr.Details["field"] = conflictErr.Field
}
case errors.As(err, &timeoutErr):
if timeoutErr.Operation != "" {
httpErr.Details["operation"] = timeoutErr.Operation
}
if timeoutErr.Duration != "" {
httpErr.Details["duration"] = timeoutErr.Duration
}
case errors.As(err, &rateLimitErr):
if rateLimitErr.RetryAfter > 0 {
httpErr.Details["retry_after"] = string(rune(rateLimitErr.RetryAfter))
}
case errors.As(err, &serviceErr):
if serviceErr.Service != "" {
httpErr.Details["service"] = serviceErr.Service
}
case errors.As(err, &internalErr):
if internalErr.Operation != "" {
httpErr.Details["operation"] = internalErr.Operation
}
}
return httpErr
}
// WriteHTTPError writes an error response to an http.ResponseWriter.
func WriteHTTPError(w http.ResponseWriter, err error, traceID string) {
httpErr := ToHTTPError(err, traceID)
w.Header().Set("Content-Type", "application/json")
// Add retry-after header for rate limit errors
var rateLimitErr *RateLimitError
if errors.As(err, &rateLimitErr) && rateLimitErr.RetryAfter > 0 {
w.Header().Set("Retry-After", string(rune(rateLimitErr.RetryAfter)))
}
// Add WWW-Authenticate header for unauthorized errors
var unauthorizedErr *UnauthorizedError
if errors.As(err, &unauthorizedErr) && unauthorizedErr.Realm != "" {
w.Header().Set("WWW-Authenticate", `Bearer realm="`+unauthorizedErr.Realm+`"`)
}
w.WriteHeader(httpErr.Status)
json.NewEncoder(w).Encode(httpErr)
}
// HTTPStatusToCode converts an HTTP status code to an error code.
func HTTPStatusToCode(status int) string {
switch status {
case http.StatusOK:
return CodeOK
case http.StatusBadRequest:
return CodeInvalidArgument
case http.StatusUnauthorized:
return CodeUnauthenticated
case http.StatusForbidden:
return CodePermissionDenied
case http.StatusNotFound:
return CodeNotFound
case http.StatusConflict:
return CodeAlreadyExists
case http.StatusRequestTimeout:
return CodeDeadlineExceeded
case http.StatusTooManyRequests:
return CodeResourceExhausted
case http.StatusNotImplemented:
return CodeUnimplemented
case http.StatusServiceUnavailable:
return CodeUnavailable
case http.StatusInternalServerError:
return CodeInternal
default:
if status >= 400 && status < 500 {
return CodeInvalidArgument
}
return CodeInternal
}
}