veylant/internal/apierror/errors.go
2026-02-23 13:35:04 +01:00

124 lines
3.5 KiB
Go

// Package apierror defines OpenAI-compatible typed errors for the Veylant proxy.
// All error responses follow the OpenAI JSON format so that existing OpenAI SDK
// clients can handle them without modification.
package apierror
import (
"encoding/json"
"net/http"
"strconv"
)
// APIError represents an OpenAI-compatible error response body.
// Wire format: {"error":{"type":"...","message":"...","code":"..."}}
type APIError struct {
Type string `json:"type"`
Message string `json:"message"`
Code string `json:"code"`
HTTPStatus int `json:"-"`
RetryAfterSec int `json:"-"` // when > 0, sets the Retry-After response header (RFC 6585)
}
// envelope wraps APIError in the OpenAI {"error": ...} envelope.
type envelope struct {
Error *APIError `json:"error"`
}
// Error implements the error interface.
func (e *APIError) Error() string {
return e.Message
}
// WriteError serialises e as JSON and writes it to w with the correct HTTP status.
// When e.RetryAfterSec > 0 it also sets the Retry-After header (RFC 6585).
func WriteError(w http.ResponseWriter, e *APIError) {
if e.RetryAfterSec > 0 {
w.Header().Set("Retry-After", strconv.Itoa(e.RetryAfterSec))
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(e.HTTPStatus)
_ = json.NewEncoder(w).Encode(envelope{Error: e})
}
// WriteErrorWithRequestID is like WriteError but also echoes requestID in the
// X-Request-Id response header. Use this in middleware that has access to the
// request ID but where the header may not yet have been set by the RequestID
// middleware (e.g. when the request is short-circuited before reaching it).
func WriteErrorWithRequestID(w http.ResponseWriter, e *APIError, requestID string) {
if requestID != "" {
w.Header().Set("X-Request-Id", requestID)
}
WriteError(w, e)
}
// NewAuthError returns a 401 authentication_error.
func NewAuthError(msg string) *APIError {
return &APIError{
Type: "authentication_error",
Message: msg,
Code: "invalid_api_key",
HTTPStatus: http.StatusUnauthorized,
}
}
// NewForbiddenError returns a 403 permission_error.
func NewForbiddenError(msg string) *APIError {
return &APIError{
Type: "permission_error",
Message: msg,
Code: "insufficient_permissions",
HTTPStatus: http.StatusForbidden,
}
}
// NewBadRequestError returns a 400 invalid_request_error.
func NewBadRequestError(msg string) *APIError {
return &APIError{
Type: "invalid_request_error",
Message: msg,
Code: "invalid_request",
HTTPStatus: http.StatusBadRequest,
}
}
// NewUpstreamError returns a 502 upstream_error.
func NewUpstreamError(msg string) *APIError {
return &APIError{
Type: "api_error",
Message: msg,
Code: "upstream_error",
HTTPStatus: http.StatusBadGateway,
}
}
// NewRateLimitError returns a 429 rate_limit_error with Retry-After: 1 (RFC 6585).
func NewRateLimitError(msg string) *APIError {
return &APIError{
Type: "rate_limit_error",
Message: msg,
Code: "rate_limit_exceeded",
HTTPStatus: http.StatusTooManyRequests,
RetryAfterSec: 1,
}
}
// NewTimeoutError returns a 504 timeout_error.
func NewTimeoutError(msg string) *APIError {
return &APIError{
Type: "api_error",
Message: msg,
Code: "upstream_timeout",
HTTPStatus: http.StatusGatewayTimeout,
}
}
// NewInternalError returns a 500 internal_error.
func NewInternalError(msg string) *APIError {
return &APIError{
Type: "api_error",
Message: msg,
Code: "internal_error",
HTTPStatus: http.StatusInternalServerError,
}
}