auth2/internal/errors/http.go
Justin Hammond 802c1e137b Improve test coverage to 81% and fix validation error handling
- Add comprehensive tests for pkg/log achieving 100% coverage
- Add tests for basic auth provider factory and utils (98.5% coverage)
- Fix missing HTTP status mapping for validation errors in internal/errors
- Overall test coverage improved from 49.1% to 81.0%

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-27 22:00:28 +08:00

196 lines
No EOL
5.5 KiB
Go

package errors
import (
"encoding/json"
"net/http"
stderrors "errors"
)
// HTTPError represents an error returned via HTTP
type HTTPError struct {
// Code is the error code
Code string `json:"code"`
// Message is the error message
Message string `json:"message"`
// Details contains additional error details
Details map[string]interface{} `json:"details,omitempty"`
// Status is the HTTP status code
Status int `json:"-"`
}
// HTTPErrorResponse is the response body for HTTP errors
type HTTPErrorResponse struct {
// Error is the error information
Error HTTPError `json:"error"`
}
// NewHTTPError creates a new HTTPError
func NewHTTPError(code ErrorCode, message string, status int) *HTTPError {
return &HTTPError{
Code: string(code),
Message: message,
Details: make(map[string]interface{}),
Status: status,
}
}
// WithDetails adds details to the HTTPError
func (e *HTTPError) WithDetails(details map[string]interface{}) *HTTPError {
// Create a new error
newError := &HTTPError{
Code: e.Code,
Message: e.Message,
Status: e.Status,
Details: make(map[string]interface{}),
}
// Copy existing details, if any
for k, v := range e.Details {
newError.Details[k] = v
}
// Add new details
for k, v := range details {
newError.Details[k] = v
}
return newError
}
// WriteResponse writes the HTTPError to the HTTP response
func (e *HTTPError) WriteResponse(w http.ResponseWriter) {
response := HTTPErrorResponse{
Error: *e,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.Status)
// If there's an error encoding the response, log it but don't try to write it
if err := json.NewEncoder(w).Encode(response); err != nil {
// In a real implementation, this would use a logger
// log.Printf("Error encoding error response: %v", err)
}
}
// ErrorToHTTP converts an Error to an HTTPError
func ErrorToHTTP(err error) *HTTPError {
if err == nil {
return nil
}
// Check if we have a direct mapping for this error to a status code
status := errorToHTTPStatus(err)
// Default to internal server error for unknown errors
httpErr := &HTTPError{
Code: "internal",
Message: err.Error(),
Details: make(map[string]interface{}),
Status: status, // Use the status from errorToHTTPStatus
}
// Check if the error is an Error
var e *Error
if stderrors.As(err, &e) {
message := e.Message
if message == "" {
message = e.Error()
}
httpErr = &HTTPError{
Code: string(e.ErrorCode),
Message: message,
Details: e.Details,
Status: status,
}
}
return httpErr
}
// Common HTTP errors
var (
HTTPErrBadRequest = NewHTTPError(CodeInvalidArgument, "Bad request", http.StatusBadRequest)
HTTPErrUnauthorized = NewHTTPError(CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
HTTPErrForbidden = NewHTTPError(CodeForbidden, "Forbidden", http.StatusForbidden)
HTTPErrNotFound = NewHTTPError(CodeNotFound, "Not found", http.StatusNotFound)
HTTPErrMethodNotAllowed = NewHTTPError(CodeUnsupported, "Method not allowed", http.StatusMethodNotAllowed)
HTTPErrConflict = NewHTTPError(CodeAlreadyExists, "Conflict", http.StatusConflict)
HTTPErrTooManyRequests = NewHTTPError(CodeRateLimited, "Too many requests", http.StatusTooManyRequests)
HTTPErrInternalServerError = NewHTTPError(CodeInternal, "Internal server error", http.StatusInternalServerError)
HTTPErrServiceUnavailable = NewHTTPError(CodeUnavailable, "Service unavailable", http.StatusServiceUnavailable)
)
// errorToHTTPStatus maps errors to HTTP status codes
func errorToHTTPStatus(err error) int {
switch {
case Is(err, ErrNotFound):
return http.StatusNotFound
case Is(err, ErrAlreadyExists):
return http.StatusConflict
case Is(err, ErrInvalidArgument):
return http.StatusBadRequest
case Is(err, ErrInvalidOperation):
return http.StatusBadRequest
case Is(err, ErrUnauthenticated):
return http.StatusUnauthorized
case Is(err, ErrUnauthorized):
return http.StatusForbidden
case Is(err, ErrForbidden):
return http.StatusForbidden
case Is(err, ErrRateLimited):
return http.StatusTooManyRequests
case Is(err, ErrTimeout):
return http.StatusGatewayTimeout
case Is(err, ErrCanceled):
return http.StatusRequestTimeout
case Is(err, ErrServiceUnavailable):
return http.StatusServiceUnavailable
default:
// Check if the error has a specific code
code := GetErrorCode(err)
switch code {
case CodeNotFound:
return http.StatusNotFound
case CodeAlreadyExists:
return http.StatusConflict
case CodeInvalidArgument:
return http.StatusBadRequest
case CodeInvalidOperation:
return http.StatusBadRequest
case CodeUnauthenticated:
return http.StatusUnauthorized
case CodeUnauthorized:
return http.StatusForbidden
case CodeForbidden:
return http.StatusForbidden
case CodeRateLimited:
return http.StatusTooManyRequests
case CodeTimeout:
return http.StatusGatewayTimeout
case CodeCanceled:
return http.StatusRequestTimeout
case CodeUnavailable:
return http.StatusServiceUnavailable
case CodeValidation:
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
}
// WriteErrorResponse writes an error to the HTTP response
func WriteErrorResponse(w http.ResponseWriter, err error) {
httpErr := ErrorToHTTP(err)
httpErr.WriteResponse(w)
}
// WriteJSONError writes a JSON error to the HTTP response
func WriteJSONError(w http.ResponseWriter, code ErrorCode, message string, status int) {
httpErr := NewHTTPError(code, message, status)
httpErr.WriteResponse(w)
}