vibe-proxy/backend/sdk/api/handlers/handlers_errors.go
2026-08-24 00:10:41 +02:00

170 lines
4.9 KiB
Go

package handlers
import (
"bytes"
"errors"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"golang.org/x/net/context"
)
func statusFromError(err error) int {
return clienterror.HTTPStatusFromError(err)
}
func isAuthSelectionUnavailable(err error) bool {
var authErr *coreauth.Error
if !errors.As(err, &authErr) || authErr == nil {
return false
}
code := strings.TrimSpace(authErr.Code)
return code == "auth_not_found" || code == "auth_unavailable"
}
func enrichAuthSelectionError(err error, providers []string, model string) error {
if err == nil {
return nil
}
var authErr *coreauth.Error
if !errors.As(err, &authErr) || authErr == nil {
return err
}
code := strings.TrimSpace(authErr.Code)
if code != "auth_not_found" && code != "auth_unavailable" {
return err
}
providerText := strings.Join(providers, ",")
if providerText == "" {
providerText = "unknown"
}
modelText := strings.TrimSpace(model)
if modelText == "" {
modelText = "unknown"
}
baseMessage := strings.TrimSpace(authErr.Message)
if baseMessage == "" {
baseMessage = "no auth available"
}
detail := fmt.Sprintf("%s (providers=%s, model=%s)", baseMessage, providerText, modelText)
// Clarify the most common alias confusion between Anthropic route names and internal provider keys.
if strings.Contains(","+providerText+",", ",claude,") {
detail += "; check Claude auth/key session and cooldown state via /v0/management/auth-files"
}
status := authErr.HTTPStatus
if status <= 0 {
status = http.StatusServiceUnavailable
}
return &coreauth.Error{
Code: authErr.Code,
Message: detail,
Retryable: authErr.Retryable,
HTTPStatus: status,
}
}
// WriteErrorResponse writes an error message to the response writer using the HTTP status embedded in the message.
func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.ErrorMessage) {
status := http.StatusInternalServerError
if msg != nil && msg.StatusCode > 0 {
status = msg.StatusCode
}
if msg != nil && msg.DirectResponse {
writeDirectErrorResponse(c, status, msg)
return
}
if msg != nil && msg.Error != nil {
for _, value := range coreauth.SafeResponseHeaders(msg.Error).Values("Retry-After") {
c.Writer.Header().Add("Retry-After", value)
}
}
if msg != nil && msg.Addon != nil && PassthroughHeadersEnabled(h.Cfg) {
for key, values := range msg.Addon {
if len(values) == 0 || IsCPAReservedResponseHeader(key) {
continue
}
c.Writer.Header().Del(key)
for _, value := range values {
c.Writer.Header().Add(key, value)
}
}
}
errText := http.StatusText(status)
if msg != nil && msg.Error != nil {
if v := strings.TrimSpace(msg.Error.Error()); v != "" {
errText = v
}
}
body := BuildErrorResponseBody(status, errText)
// Append first to preserve upstream response logs, then drop duplicate payloads if already recorded.
var previous []byte
if existing, exists := c.Get("API_RESPONSE"); exists {
if existingBytes, ok := existing.([]byte); ok && len(existingBytes) > 0 {
previous = existingBytes
}
}
appendAPIResponse(c, body)
trimmedErrText := strings.TrimSpace(errText)
trimmedBody := bytes.TrimSpace(body)
if len(previous) > 0 {
if (trimmedErrText != "" && bytes.Contains(previous, []byte(trimmedErrText))) ||
(len(trimmedBody) > 0 && bytes.Contains(previous, trimmedBody)) {
c.Set("API_RESPONSE", previous)
}
}
if !c.Writer.Written() {
c.Writer.Header().Set("Content-Type", "application/json")
}
c.Status(status)
_, _ = c.Writer.Write(body)
}
func writeDirectErrorResponse(c *gin.Context, status int, msg *interfaces.ErrorMessage) {
for key, values := range FilterUpstreamHeaders(msg.Headers) {
if len(values) == 0 || IsCPAReservedResponseHeader(key) {
continue
}
c.Writer.Header().Del(key)
for _, value := range values {
c.Writer.Header().Add(key, value)
}
}
body := bytes.Clone(msg.Body)
appendAPIResponse(c, body)
if !c.Writer.Written() && c.Writer.Header().Get("Content-Type") == "" {
c.Writer.Header().Set("Content-Type", "application/json")
}
c.Status(status)
_, _ = c.Writer.Write(body)
}
func (h *BaseAPIHandler) LoggingAPIResponseError(ctx context.Context, err *interfaces.ErrorMessage) {
if h.Cfg.RequestLog {
if ginContext, ok := ctx.Value("gin").(*gin.Context); ok {
if apiResponseErrors, isExist := ginContext.Get("API_RESPONSE_ERROR"); isExist {
if slicesAPIResponseError, isOk := apiResponseErrors.([]*interfaces.ErrorMessage); isOk {
slicesAPIResponseError = append(slicesAPIResponseError, err)
ginContext.Set("API_RESPONSE_ERROR", slicesAPIResponseError)
}
} else {
// Create new response data entry
ginContext.Set("API_RESPONSE_ERROR", []*interfaces.ErrorMessage{err})
}
}
}
}