mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
fix(auth): avoid penalizing credentials for client faults
This commit is contained in:
79
internal/clienterror/client_error.go
Normal file
79
internal/clienterror/client_error.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Package clienterror classifies upstream failures caused by the client request.
|
||||
package clienterror
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var requestFaultCodes = map[string]struct{}{
|
||||
"cyber_policy": {},
|
||||
"context_length_exceeded": {},
|
||||
"message_too_big": {},
|
||||
"string_above_max_length": {},
|
||||
"invalid_prompt": {},
|
||||
"invalid_value": {},
|
||||
"unsupported_value": {},
|
||||
"invalid_request_error": {},
|
||||
"previous_response_not_found": {},
|
||||
}
|
||||
|
||||
var requestFaultTypes = map[string]struct{}{
|
||||
"invalid_request": {},
|
||||
"invalid_request_error": {},
|
||||
"bad_request_error": {},
|
||||
"invalid_prompt": {},
|
||||
}
|
||||
|
||||
// IsRequestFault reports whether an upstream failure is caused by the request
|
||||
// and therefore must not rotate or penalize credentials.
|
||||
func IsRequestFault(status int, err error) bool {
|
||||
if status <= 0 && err != nil {
|
||||
type statusCoder interface {
|
||||
StatusCode() int
|
||||
}
|
||||
var statusErr statusCoder
|
||||
if errors.As(err, &statusErr) && statusErr != nil {
|
||||
status = statusErr.StatusCode()
|
||||
}
|
||||
}
|
||||
if hasRequestFaultBody(err) {
|
||||
return true
|
||||
}
|
||||
switch status {
|
||||
case http.StatusBadRequest,
|
||||
http.StatusConflict,
|
||||
http.StatusRequestEntityTooLarge,
|
||||
http.StatusUnprocessableEntity:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasRequestFaultBody(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
body := strings.TrimSpace(err.Error())
|
||||
if body == "" || !json.Valid([]byte(body)) {
|
||||
return false
|
||||
}
|
||||
for _, path := range []string{"error.code", "code", "response.error.code", "body.error.code"} {
|
||||
code := strings.ToLower(strings.TrimSpace(gjson.Get(body, path).String()))
|
||||
if _, ok := requestFaultCodes[code]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"error.type", "type", "response.error.type", "body.error.type"} {
|
||||
errType := strings.ToLower(strings.TrimSpace(gjson.Get(body, path).String()))
|
||||
if _, ok := requestFaultTypes[errType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
100
internal/clienterror/client_error_test.go
Normal file
100
internal/clienterror/client_error_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package clienterror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type statusError struct {
|
||||
status int
|
||||
body string
|
||||
}
|
||||
|
||||
func (e statusError) Error() string { return e.body }
|
||||
func (e statusError) StatusCode() int { return e.status }
|
||||
|
||||
func TestIsRequestFaultStructuredIdentifiers(t *testing.T) {
|
||||
for _, code := range []string{
|
||||
"cyber_policy",
|
||||
"context_length_exceeded",
|
||||
"message_too_big",
|
||||
"string_above_max_length",
|
||||
"invalid_prompt",
|
||||
"invalid_value",
|
||||
"unsupported_value",
|
||||
"invalid_request_error",
|
||||
"previous_response_not_found",
|
||||
} {
|
||||
t.Run("code/"+code, func(t *testing.T) {
|
||||
err := errors.New(`{"error":{"code":"` + code + `"}}`)
|
||||
if !IsRequestFault(http.StatusBadGateway, err) {
|
||||
t.Fatalf("code %q was not classified as a request fault", code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, errType := range []string{
|
||||
"invalid_request",
|
||||
"invalid_request_error",
|
||||
"bad_request_error",
|
||||
"invalid_prompt",
|
||||
} {
|
||||
t.Run("type/"+errType, func(t *testing.T) {
|
||||
err := errors.New(`{"error":{"type":"` + errType + `"}}`)
|
||||
if !IsRequestFault(http.StatusBadGateway, err) {
|
||||
t.Fatalf("type %q was not classified as a request fault", errType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRequestFault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status int
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "bad request status", status: http.StatusBadRequest, err: errors.New("bad request"), want: true},
|
||||
{name: "conflict status", status: http.StatusConflict, err: errors.New("conflict"), want: true},
|
||||
{name: "entity too large status", status: http.StatusRequestEntityTooLarge, err: errors.New("too large"), want: true},
|
||||
{name: "unprocessable status", status: http.StatusUnprocessableEntity, err: errors.New("unprocessable"), want: true},
|
||||
{
|
||||
name: "cyber policy behind bad gateway",
|
||||
status: http.StatusBadGateway,
|
||||
err: errors.New(`{"error":{"type":"invalid_request","code":"cyber_policy","message":"blocked"}}`),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "context length behind internal error",
|
||||
status: http.StatusInternalServerError,
|
||||
err: errors.New(`{"response":{"error":{"type":"server_error","code":"context_length_exceeded"}}}`),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid request type behind bad gateway",
|
||||
status: http.StatusBadGateway,
|
||||
err: errors.New(`{"body":{"error":{"type":"invalid_request","message":"invalid"}}}`),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "status from error",
|
||||
err: statusError{status: http.StatusConflict, body: "conflict"},
|
||||
want: true,
|
||||
},
|
||||
{name: "unauthorized", status: http.StatusUnauthorized, err: errors.New("invalid token")},
|
||||
{name: "quota", status: http.StatusTooManyRequests, err: errors.New("quota")},
|
||||
{name: "transport", status: http.StatusBadGateway, err: errors.New("unexpected EOF")},
|
||||
{name: "invalid JSON body", status: http.StatusBadGateway, err: errors.New(`{"error":`)},
|
||||
{name: "nil", status: 0},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := IsRequestFault(tc.status, tc.err); got != tc.want {
|
||||
t.Fatalf("IsRequestFault(%d, %v) = %t, want %t", tc.status, tc.err, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user