211 lines · 6.3 KB · Go
RawDownload
// Package clienterror classifies upstream failures caused by the client request.package clienterror import (	"context"	"encoding/json"	"errors"	"net/http"	"strings" 	"github.com/tidwall/gjson") // StatusClientClosedRequest is the nginx-style status used when the client// aborts the request before the proxy finishes (context.Canceled).const StatusClientClosedRequest = 499 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":        {},} // HTTPStatusFromError extracts an HTTP status from err.// Explicit StatusCode() values win. Otherwise context.Canceled maps to 499// and context.DeadlineExceeded maps to 504. Returns 0 when unknown.func HTTPStatusFromError(err error) int {	if err == nil {		return 0	}	type statusCoder interface {		StatusCode() int	}	var sc statusCoder	if errors.As(err, &sc) && sc != nil {		if code := sc.StatusCode(); code > 0 {			return code		}	}	if errors.Is(err, context.Canceled) {		return StatusClientClosedRequest	}	if errors.Is(err, context.DeadlineExceeded) {		return http.StatusGatewayTimeout	}	return 0} // HTTPStatusFromErrorOr is like HTTPStatusFromError but returns fallback when// the error does not carry a known status.func HTTPStatusFromErrorOr(err error, fallback int) int {	if code := HTTPStatusFromError(err); code > 0 {		return code	}	return fallback} // 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()		}	}	// Payment and rate-limit statuses are authoritative even when an upstream	// pairs them with a generic invalid_request_error body. The credential must	// remain eligible for cooldown and rotation.	if status == http.StatusPaymentRequired || status == http.StatusTooManyRequests {		return false	}	// DeepSeek reports an invalid API key as 401 with the authentication_error	// type alongside the same generic code. Preserve that credential failure	// classification without weakening generic request-fault handling.	if status == http.StatusUnauthorized && hasAuthenticationErrorBody(err) {		return false	}	// Model not found indicates a credential-model capability mismatch rather than	// a caller request error. Preserve rotation and cooldown for the model.	if hasModelNotFoundErrorBody(err) {		return false	}	if hasRequestFaultBody(err) {		return true	}	if err != nil && IsItemNotPersisted(err.Error()) {		return true	}	switch status {	case http.StatusBadRequest,		http.StatusConflict,		http.StatusRequestEntityTooLarge,		http.StatusUnprocessableEntity:		return true	default:		return false	}} // IsItemNotPersisted matches the upstream 404 raised when a request references a// response item the upstream never stored because `store` was false. The upstream// sends this as a plain-text message rather than a JSON body, so it cannot be// recognized through the structured identifiers above.//// The request can only succeed once the client rebuilds it without the stale// reference, so it is a request fault: rotating credentials cannot help, and the// client must be told rather than left to retry the same broken input.func IsItemNotPersisted(message string) bool {	lower := strings.ToLower(message)	return strings.Contains(lower, "item with id") &&		strings.Contains(lower, "not found") &&		strings.Contains(lower, "items are not persisted when `store` is set to false")} func hasModelNotFoundErrorBody(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 code == "model_not_found" || code == "model_not_found_error" {			return true		}	}	return false} func hasAuthenticationErrorBody(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.type", "type", "response.error.type", "body.error.type"} {		if errType := strings.ToLower(strings.TrimSpace(gjson.Get(body, path).String())); errType == "authentication_error" {			return true		}	}	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} // IsClientCancellation reports whether an HTTP status code or error represents// a client-initiated cancellation (HTTP 499 StatusClientClosedRequest or context.Canceled).func IsClientCancellation(status int, err error) bool {	if status == StatusClientClosedRequest {		return true	}	if err != nil {		if errors.Is(err, context.Canceled) {			return true		}		type statusCoder interface {			StatusCode() int		}		var sc statusCoder		if errors.As(err, &sc) && sc != nil && sc.StatusCode() == StatusClientClosedRequest {			return true		}		lower := strings.ToLower(err.Error())		if strings.Contains(lower, "context canceled") || strings.Contains(lower, "client closed request") {			return true		}	}	return false}