mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
* fix(relay): mask only the upstream address in error messages MaskSensitiveInfo masked every URL, domain and IPv4 in upstream error messages, which garbled documentation links, email domains and dotted identifiers that clients need to read. It now takes the upstream addresses of the request: the channel base URL, the origin the gateway actually dialed (recorded in doRequest, DoWssRequest, the AWS SDK client and the Xunfei and Volcengine TTS dials, which covers absolute Advanced Custom routes), the channel proxy and the environment proxy. URLs and bare hostnames on those hosts and all IPv4 addresses are masked; other URLs and domains stay readable. With no upstream address no address is masked. Credentials are masked on every URL regardless of host: api_key values, query values and userinfo. The URL matcher also keeps quotes, colons and semicolons that follow a URL, keeps parentheses and apostrophes inside it, masks an IPv4 URL host whole and keeps a country-code TLD when the host has a port. * fix(relay): keep preset official endpoints readable in masked errors A channel on the base URL new-api presets for its type reaches a public official endpoint, so its address is not masked. Only addresses the admin configured are: a custom base URL, the origin of an Advanced Custom route, and the channel and environment proxies. Preset hosts are compared with their port so a configured localhost upstream stays apart from the Ollama preset. The origins recorded for the AWS SDK, Xunfei and Volcengine TTS dials and the plugin allowedHosts masking are dropped: they are all official endpoints. * fix(relay): mask IPv4 addresses and plugin allowed hosts in every error IPv4 addresses in network errors include the gateway's own address, so they are masked even when the channel uses a preset base URL. Task plugin build failures mask the plugin's allowedHosts again: an installed plugin may point them at private hosts. * fix(relay): mask bracketed IPv6 addresses in errors Go network errors write IPv6 addresses in brackets, as in "dial tcp [2001:db8::1]:443". Like IPv4, they are the resolved upstream or the gateway's own address, so they are masked on every error once the bracketed content parses as an IP.
233 lines
6.9 KiB
Go
233 lines
6.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
taskdto "github.com/QuantumNous/new-api/dto"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
)
|
|
|
|
func MidjourneyErrorWrapper(code int, desc string) *taskdto.MidjourneyResponse {
|
|
return &taskdto.MidjourneyResponse{
|
|
Code: code,
|
|
Description: desc,
|
|
}
|
|
}
|
|
|
|
func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int) *taskdto.MidjourneyResponseWithStatusCode {
|
|
return &taskdto.MidjourneyResponseWithStatusCode{
|
|
StatusCode: statusCode,
|
|
Response: *MidjourneyErrorWrapper(code, desc),
|
|
}
|
|
}
|
|
|
|
//// OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
|
|
//func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
|
|
// text := err.Error()
|
|
// lowerText := strings.ToLower(text)
|
|
// if !strings.HasPrefix(lowerText, "get file base64 from url") && !strings.HasPrefix(lowerText, "mime type is not supported") {
|
|
// if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
|
|
// common.SysLog(fmt.Sprintf("error: %s", text))
|
|
// text = "请求上游地址失败"
|
|
// }
|
|
// }
|
|
// openAIError := dto.OpenAIError{
|
|
// Message: text,
|
|
// Type: "new_api_error",
|
|
// Code: code,
|
|
// }
|
|
// return &dto.OpenAIErrorWithStatusCode{
|
|
// Error: openAIError,
|
|
// StatusCode: statusCode,
|
|
// }
|
|
//}
|
|
//
|
|
//func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
|
|
// openaiErr := OpenAIErrorWrapper(err, code, statusCode)
|
|
// openaiErr.LocalError = true
|
|
// return openaiErr
|
|
//}
|
|
|
|
func ClaudeErrorWrapper(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode {
|
|
text := err.Error()
|
|
lowerText := strings.ToLower(text)
|
|
if !strings.HasPrefix(lowerText, "get file base64 from url") {
|
|
if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
|
|
common.SysLog(common.LogText("error: %s", text))
|
|
text = "request to the upstream address failed"
|
|
}
|
|
}
|
|
claudeError := types.ClaudeError{
|
|
Message: text,
|
|
Type: "new_api_error",
|
|
}
|
|
return &dto.ClaudeErrorWithStatusCode{
|
|
Error: claudeError,
|
|
StatusCode: statusCode,
|
|
}
|
|
}
|
|
|
|
func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode {
|
|
claudeErr := ClaudeErrorWrapper(err, code, statusCode)
|
|
claudeErr.LocalError = true
|
|
return claudeErr
|
|
}
|
|
|
|
func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
|
|
newApiErr = types.InitOpenAIError(types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
|
|
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
CloseResponseBodyGracefully(resp)
|
|
var errResponse dto.GeneralErrorResponse
|
|
responseBodyText := string(responseBody)
|
|
responseBodyPreview := common.LocalLogPreview(responseBodyText)
|
|
buildErrWithBody := func(message string) error {
|
|
if message == "" {
|
|
return fmt.Errorf("bad response status code %d, body: %s", resp.StatusCode, responseBodyText)
|
|
}
|
|
return fmt.Errorf("bad response status code %d, message: %s, body: %s", resp.StatusCode, message, responseBodyText)
|
|
}
|
|
|
|
err = common.Unmarshal(responseBody, &errResponse)
|
|
if err != nil {
|
|
if showBodyWhenFail {
|
|
newApiErr.Err = buildErrWithBody("")
|
|
} else {
|
|
logger.LogError(ctx, common.LogText("bad response status code %d, body: %s", resp.StatusCode, responseBodyPreview))
|
|
newApiErr.Err = fmt.Errorf("bad response status code %d", resp.StatusCode)
|
|
}
|
|
return
|
|
}
|
|
|
|
if common.GetJsonType(errResponse.Error) == "object" {
|
|
// General format error (OpenAI, Anthropic, Gemini, etc.)
|
|
oaiError := errResponse.TryToOpenAIError()
|
|
if oaiError != nil {
|
|
newApiErr = types.WithOpenAIError(*oaiError, resp.StatusCode)
|
|
if showBodyWhenFail {
|
|
newApiErr.Err = buildErrWithBody(newApiErr.Error())
|
|
}
|
|
return
|
|
}
|
|
}
|
|
message := errResponse.ToMessage()
|
|
if message == "" {
|
|
// The body parsed as JSON but carried no usable error message; log the
|
|
// raw body so the upstream failure remains diagnosable.
|
|
logger.LogError(ctx, common.LogText("bad response status code %d with empty error message, body: %s", resp.StatusCode, responseBodyPreview))
|
|
}
|
|
newApiErr = types.NewOpenAIError(errors.New(message), types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
|
|
if showBodyWhenFail {
|
|
newApiErr.Err = buildErrWithBody(newApiErr.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
func ResetStatusCode(newApiErr *types.NewAPIError, statusCodeMappingStr string) {
|
|
if newApiErr == nil {
|
|
return
|
|
}
|
|
if statusCodeMappingStr == "" || statusCodeMappingStr == "{}" {
|
|
return
|
|
}
|
|
statusCodeMapping := make(map[string]any)
|
|
err := common.Unmarshal([]byte(statusCodeMappingStr), &statusCodeMapping)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if newApiErr.StatusCode == http.StatusOK {
|
|
return
|
|
}
|
|
codeStr := strconv.Itoa(newApiErr.StatusCode)
|
|
if value, ok := statusCodeMapping[codeStr]; ok {
|
|
intCode, ok := parseStatusCodeMappingValue(value)
|
|
if !ok {
|
|
return
|
|
}
|
|
newApiErr.StatusCode = intCode
|
|
}
|
|
}
|
|
|
|
func parseStatusCodeMappingValue(value any) (int, bool) {
|
|
switch v := value.(type) {
|
|
case string:
|
|
if v == "" {
|
|
return 0, false
|
|
}
|
|
statusCode, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return statusCode, true
|
|
case float64:
|
|
if v != math.Trunc(v) {
|
|
return 0, false
|
|
}
|
|
return int(v), true
|
|
case int:
|
|
return v, true
|
|
case json.Number:
|
|
statusCode, err := strconv.Atoi(v.String())
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return statusCode, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func TaskErrorWrapperLocal(err error, code string, statusCode int) *taskdto.TaskError {
|
|
openaiErr := TaskErrorWrapper(err, code, statusCode)
|
|
openaiErr.LocalError = true
|
|
return openaiErr
|
|
}
|
|
|
|
// TaskErrorWrapper masks the upstream addresses of the task channel in network
|
|
// errors; see common.MaskSensitiveInfo.
|
|
func TaskErrorWrapper(err error, code string, statusCode int, upstreamURLs ...string) *taskdto.TaskError {
|
|
text := err.Error()
|
|
lowerText := strings.ToLower(text)
|
|
if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
|
|
common.SysLog(common.LogText("error: %s", text))
|
|
//text = "请求上游地址失败"
|
|
text = common.MaskSensitiveInfo(text, upstreamURLs...)
|
|
}
|
|
//避免暴露内部错误
|
|
taskError := &taskdto.TaskError{
|
|
Code: code,
|
|
Message: text,
|
|
StatusCode: statusCode,
|
|
Error: err,
|
|
}
|
|
|
|
return taskError
|
|
}
|
|
|
|
// TaskErrorFromAPIError 将 PreConsumeBilling 返回的 NewAPIError 转换为 TaskError。
|
|
func TaskErrorFromAPIError(apiErr *types.NewAPIError) *taskdto.TaskError {
|
|
if apiErr == nil {
|
|
return nil
|
|
}
|
|
return &taskdto.TaskError{
|
|
Code: string(apiErr.GetErrorCode()),
|
|
Message: apiErr.Err.Error(),
|
|
StatusCode: apiErr.StatusCode,
|
|
Error: apiErr.Err,
|
|
}
|
|
}
|