170 lines
4.9 KiB
Go
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})
|
|
}
|
|
}
|
|
}
|
|
}
|