🛡️ fix: prevent OOM on large/decompressed requests; skip heavy prompt meta when token count is disabled

Clamp request body size (including post-decompression) to avoid memory exhaustion caused by huge payloads/zip bombs, especially with large-context Claude requests. Add a configurable `MAX_REQUEST_BODY_MB` (default `32`) and document it.

- Enforce max request body size after gzip/br decompression via `http.MaxBytesReader`
- Add a secondary size guard in `common.GetRequestBody` and cache-safe handling
- Return **413 Request Entity Too Large** on oversized bodies in relay entry
- Avoid building large `TokenCountMeta.CombineText` when both token counting and sensitive check are disabled (use lightweight meta for pricing)
- Update READMEs (CN/EN/FR/JA) with `MAX_REQUEST_BODY_MB`
- Fix a handful of vet/formatting issues encountered during the change
- `go test ./...` passes
This commit is contained in:
t0ng7u
2025-12-16 17:00:19 +08:00
parent 87195a07b0
commit c2ed76ddfd
21 changed files with 149 additions and 35 deletions

View File

@@ -305,6 +305,7 @@ docker run --name new-api -d --restart always \
| `REDIS_CONN_STRING` | Redis connection string | - | | `REDIS_CONN_STRING` | Redis connection string | - |
| `STREAMING_TIMEOUT` | Streaming timeout (seconds) | `300` | | `STREAMING_TIMEOUT` | Streaming timeout (seconds) | `300` |
| `STREAM_SCANNER_MAX_BUFFER_MB` | Max per-line buffer (MB) for the stream scanner; increase when upstream sends huge image/base64 payloads | `64` | | `STREAM_SCANNER_MAX_BUFFER_MB` | Max per-line buffer (MB) for the stream scanner; increase when upstream sends huge image/base64 payloads | `64` |
| `MAX_REQUEST_BODY_MB` | Max request body size (MB, counted **after decompression**; prevents huge requests/zip bombs from exhausting memory). Exceeding it returns `413` | `32` |
| `AZURE_DEFAULT_API_VERSION` | Azure API version | `2025-04-01-preview` | | `AZURE_DEFAULT_API_VERSION` | Azure API version | `2025-04-01-preview` |
| `ERROR_LOG_ENABLED` | Error log switch | `false` | | `ERROR_LOG_ENABLED` | Error log switch | `false` |

View File

@@ -301,6 +301,7 @@ docker run --name new-api -d --restart always \
| `REDIS_CONN_STRING` | Chaine de connexion Redis | - | | `REDIS_CONN_STRING` | Chaine de connexion Redis | - |
| `STREAMING_TIMEOUT` | Délai d'expiration du streaming (secondes) | `300` | | `STREAMING_TIMEOUT` | Délai d'expiration du streaming (secondes) | `300` |
| `STREAM_SCANNER_MAX_BUFFER_MB` | Taille max du buffer par ligne (Mo) pour le scanner SSE ; à augmenter quand les sorties image/base64 sont très volumineuses (ex. images 4K) | `64` | | `STREAM_SCANNER_MAX_BUFFER_MB` | Taille max du buffer par ligne (Mo) pour le scanner SSE ; à augmenter quand les sorties image/base64 sont très volumineuses (ex. images 4K) | `64` |
| `MAX_REQUEST_BODY_MB` | Taille maximale du corps de requête (Mo, comptée **après décompression** ; évite les requêtes énormes/zip bombs qui saturent la mémoire). Dépassement ⇒ `413` | `32` |
| `AZURE_DEFAULT_API_VERSION` | Version de l'API Azure | `2025-04-01-preview` | | `AZURE_DEFAULT_API_VERSION` | Version de l'API Azure | `2025-04-01-preview` |
| `ERROR_LOG_ENABLED` | Interrupteur du journal d'erreurs | `false` | | `ERROR_LOG_ENABLED` | Interrupteur du journal d'erreurs | `false` |

View File

@@ -310,6 +310,7 @@ docker run --name new-api -d --restart always \
| `REDIS_CONN_STRING` | Redis接続文字列 | - | | `REDIS_CONN_STRING` | Redis接続文字列 | - |
| `STREAMING_TIMEOUT` | ストリーミング応答のタイムアウト時間(秒) | `300` | | `STREAMING_TIMEOUT` | ストリーミング応答のタイムアウト時間(秒) | `300` |
| `STREAM_SCANNER_MAX_BUFFER_MB` | ストリームスキャナの1行あたりバッファ上限MB。4K画像など巨大なbase64 `data:` ペイロードを扱う場合は値を増加させてください | `64` | | `STREAM_SCANNER_MAX_BUFFER_MB` | ストリームスキャナの1行あたりバッファ上限MB。4K画像など巨大なbase64 `data:` ペイロードを扱う場合は値を増加させてください | `64` |
| `MAX_REQUEST_BODY_MB` | リクエストボディ最大サイズMB、**解凍後**に計測。巨大リクエスト/zip bomb によるメモリ枯渇を防止)。超過時は `413` | `32` |
| `AZURE_DEFAULT_API_VERSION` | Azure APIバージョン | `2025-04-01-preview` | | `AZURE_DEFAULT_API_VERSION` | Azure APIバージョン | `2025-04-01-preview` |
| `ERROR_LOG_ENABLED` | エラーログスイッチ | `false` | | `ERROR_LOG_ENABLED` | エラーログスイッチ | `false` |

View File

@@ -306,6 +306,7 @@ docker run --name new-api -d --restart always \
| `REDIS_CONN_STRING` | Redis 连接字符串 | - | | `REDIS_CONN_STRING` | Redis 连接字符串 | - |
| `STREAMING_TIMEOUT` | 流式超时时间(秒) | `300` | | `STREAMING_TIMEOUT` | 流式超时时间(秒) | `300` |
| `STREAM_SCANNER_MAX_BUFFER_MB` | 流式扫描器单行最大缓冲MB图像生成等超大 `data:` 片段(如 4K 图片 base64需适当调大 | `64` | | `STREAM_SCANNER_MAX_BUFFER_MB` | 流式扫描器单行最大缓冲MB图像生成等超大 `data:` 片段(如 4K 图片 base64需适当调大 | `64` |
| `MAX_REQUEST_BODY_MB` | 请求体最大大小MB**解压后**计;防止超大请求/zip bomb 导致内存暴涨),超过将返回 `413` | `32` |
| `AZURE_DEFAULT_API_VERSION` | Azure API 版本 | `2025-04-01-preview` | | `AZURE_DEFAULT_API_VERSION` | Azure API 版本 | `2025-04-01-preview` |
| `ERROR_LOG_ENABLED` | 错误日志开关 | `false` | | `ERROR_LOG_ENABLED` | 错误日志开关 | `false` |

View File

@@ -18,18 +18,47 @@ import (
const KeyRequestBody = "key_request_body" const KeyRequestBody = "key_request_body"
func GetRequestBody(c *gin.Context) ([]byte, error) { var ErrRequestBodyTooLarge = errors.New("request body too large")
requestBody, _ := c.Get(KeyRequestBody)
if requestBody != nil { func IsRequestBodyTooLargeError(err error) bool {
return requestBody.([]byte), nil if err == nil {
return false
} }
requestBody, err := io.ReadAll(c.Request.Body) if errors.Is(err, ErrRequestBodyTooLarge) {
return true
}
var mbe *http.MaxBytesError
return errors.As(err, &mbe)
}
func GetRequestBody(c *gin.Context) ([]byte, error) {
cached, exists := c.Get(KeyRequestBody)
if exists && cached != nil {
if b, ok := cached.([]byte); ok {
return b, nil
}
}
maxMB := constant.MaxRequestBodyMB
if maxMB <= 0 {
maxMB = 64
}
maxBytes := int64(maxMB) << 20
limited := io.LimitReader(c.Request.Body, maxBytes+1)
body, err := io.ReadAll(limited)
if err != nil { if err != nil {
_ = c.Request.Body.Close()
if IsRequestBodyTooLargeError(err) {
return nil, ErrRequestBodyTooLarge
}
return nil, err return nil, err
} }
_ = c.Request.Body.Close() _ = c.Request.Body.Close()
c.Set(KeyRequestBody, requestBody) if int64(len(body)) > maxBytes {
return requestBody.([]byte), nil return nil, ErrRequestBodyTooLarge
}
c.Set(KeyRequestBody, body)
return body, nil
} }
func UnmarshalBodyReusable(c *gin.Context, v any) error { func UnmarshalBodyReusable(c *gin.Context, v any) error {

View File

@@ -117,6 +117,8 @@ func initConstantEnv() {
constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true) constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20) constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 64) constant.StreamScannerMaxBufferMB = GetEnvOrDefault("STREAM_SCANNER_MAX_BUFFER_MB", 64)
// MaxRequestBodyMB 请求体最大大小(解压后),用于防止超大请求/zip bomb导致内存暴涨
constant.MaxRequestBodyMB = GetEnvOrDefault("MAX_REQUEST_BODY_MB", 32)
// ForceStreamOption 覆盖请求参数强制返回usage信息 // ForceStreamOption 覆盖请求参数强制返回usage信息
constant.ForceStreamOption = GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true) constant.ForceStreamOption = GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
constant.CountToken = GetEnvOrDefaultBool("CountToken", true) constant.CountToken = GetEnvOrDefaultBool("CountToken", true)

View File

@@ -9,6 +9,7 @@ var CountToken bool
var GetMediaToken bool var GetMediaToken bool
var GetMediaTokenNotStream bool var GetMediaTokenNotStream bool
var UpdateTask bool var UpdateTask bool
var MaxRequestBodyMB int
var AzureDefaultAPIVersion string var AzureDefaultAPIVersion string
var GeminiVisionMaxImageNum int var GeminiVisionMaxImageNum int
var NotifyLimitCount int var NotifyLimitCount int

View File

@@ -114,7 +114,7 @@ func DiscordOAuth(c *gin.Context) {
DiscordBind(c) DiscordBind(c)
return return
} }
if !system_setting.GetDiscordSettings().Enabled { if !system_setting.GetDiscordSettings().Enabled {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "管理员未开启通过 Discord 登录以及注册", "message": "管理员未开启通过 Discord 登录以及注册",

View File

@@ -2,6 +2,7 @@ package controller
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -104,7 +105,12 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
request, err := helper.GetAndValidateRequest(c, relayFormat) request, err := helper.GetAndValidateRequest(c, relayFormat)
if err != nil { if err != nil {
newAPIError = types.NewError(err, types.ErrorCodeInvalidRequest) // Map "request body too large" to 413 so clients can handle it correctly
if common.IsRequestBodyTooLargeError(err) || errors.Is(err, common.ErrRequestBodyTooLarge) {
newAPIError = types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusRequestEntityTooLarge, types.ErrOptionWithSkipRetry())
} else {
newAPIError = types.NewError(err, types.ErrorCodeInvalidRequest)
}
return return
} }
@@ -114,9 +120,17 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
return return
} }
meta := request.GetTokenCountMeta() needSensitiveCheck := setting.ShouldCheckPromptSensitive()
needCountToken := constant.CountToken
// Avoid building huge CombineText (strings.Join) when token counting and sensitive check are both disabled.
var meta *types.TokenCountMeta
if needSensitiveCheck || needCountToken {
meta = request.GetTokenCountMeta()
} else {
meta = fastTokenCountMetaForPricing(request)
}
if setting.ShouldCheckPromptSensitive() { if needSensitiveCheck && meta != nil {
contains, words := service.CheckSensitiveText(meta.CombineText) contains, words := service.CheckSensitiveText(meta.CombineText)
if contains { if contains {
logger.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(words, ", "))) logger.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(words, ", ")))
@@ -218,6 +232,33 @@ func addUsedChannel(c *gin.Context, channelId int) {
c.Set("use_channel", useChannel) c.Set("use_channel", useChannel)
} }
func fastTokenCountMetaForPricing(request dto.Request) *types.TokenCountMeta {
if request == nil {
return &types.TokenCountMeta{}
}
meta := &types.TokenCountMeta{
TokenType: types.TokenTypeTokenizer,
}
switch r := request.(type) {
case *dto.GeneralOpenAIRequest:
if r.MaxCompletionTokens > r.MaxTokens {
meta.MaxTokens = int(r.MaxCompletionTokens)
} else {
meta.MaxTokens = int(r.MaxTokens)
}
case *dto.OpenAIResponsesRequest:
meta.MaxTokens = int(r.MaxOutputTokens)
case *dto.ClaudeRequest:
meta.MaxTokens = int(r.MaxTokens)
case *dto.ImageRequest:
// Pricing for image requests depends on ImagePriceRatio; safe to compute even when CountToken is disabled.
return r.GetTokenCountMeta()
default:
// Best-effort: leave CombineText empty to avoid large allocations.
}
return meta
}
func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service.RetryParam) (*model.Channel, *types.NewAPIError) { func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service.RetryParam) (*model.Channel, *types.NewAPIError) {
if info.ChannelMeta == nil { if info.ChannelMeta == nil {
autoBan := c.GetBool("auto_ban") autoBan := c.GetBool("auto_ban")

View File

@@ -88,7 +88,7 @@ func UpdateSunoTaskAll(ctx context.Context, taskChannelM map[int][]string, taskM
for channelId, taskIds := range taskChannelM { for channelId, taskIds := range taskChannelM {
err := updateSunoTaskAll(ctx, channelId, taskIds, taskM) err := updateSunoTaskAll(ctx, channelId, taskIds, taskM)
if err != nil { if err != nil {
logger.LogError(ctx, fmt.Sprintf("渠道 #%d 更新异步任务失败: %d", channelId, err.Error())) logger.LogError(ctx, fmt.Sprintf("渠道 #%d 更新异步任务失败: %s", channelId, err.Error()))
} }
} }
return nil return nil
@@ -141,7 +141,7 @@ func updateSunoTaskAll(ctx context.Context, channelId int, taskIds []string, tas
return err return err
} }
if !responseItems.IsSuccess() { if !responseItems.IsSuccess() {
common.SysLog(fmt.Sprintf("渠道 #%d 未完成的任务有: %d, 成功获取到任务数: %d", channelId, len(taskIds), string(responseBody))) common.SysLog(fmt.Sprintf("渠道 #%d 未完成的任务有: %d, 成功获取到任务数: %s", channelId, len(taskIds), string(responseBody)))
return err return err
} }

View File

@@ -7,12 +7,12 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log"
"net/http"
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting"
"io"
"log"
"net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"

View File

@@ -162,7 +162,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
} }
midjourneyModel, mjErr, success := service.GetMjRequestModel(relayMode, &midjourneyRequest) midjourneyModel, mjErr, success := service.GetMjRequestModel(relayMode, &midjourneyRequest)
if mjErr != nil { if mjErr != nil {
return nil, false, fmt.Errorf(mjErr.Description) return nil, false, fmt.Errorf("%s", mjErr.Description)
} }
if midjourneyModel == "" { if midjourneyModel == "" {
if !success { if !success {

View File

@@ -5,32 +5,69 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/QuantumNous/new-api/constant"
"github.com/andybalholm/brotli" "github.com/andybalholm/brotli"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type readCloser struct {
io.Reader
closeFn func() error
}
func (rc *readCloser) Close() error {
if rc.closeFn != nil {
return rc.closeFn()
}
return nil
}
func DecompressRequestMiddleware() gin.HandlerFunc { func DecompressRequestMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if c.Request.Body == nil || c.Request.Method == http.MethodGet { if c.Request.Body == nil || c.Request.Method == http.MethodGet {
c.Next() c.Next()
return return
} }
maxMB := constant.MaxRequestBodyMB
if maxMB <= 0 {
maxMB = 64
}
maxBytes := int64(maxMB) << 20
origBody := c.Request.Body
wrapMaxBytes := func(body io.ReadCloser) io.ReadCloser {
return http.MaxBytesReader(c.Writer, body, maxBytes)
}
switch c.GetHeader("Content-Encoding") { switch c.GetHeader("Content-Encoding") {
case "gzip": case "gzip":
gzipReader, err := gzip.NewReader(c.Request.Body) gzipReader, err := gzip.NewReader(origBody)
if err != nil { if err != nil {
_ = origBody.Close()
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)
return return
} }
defer gzipReader.Close() // Replace the request body with the decompressed data, and enforce a max size (post-decompression).
c.Request.Body = wrapMaxBytes(&readCloser{
// Replace the request body with the decompressed data Reader: gzipReader,
c.Request.Body = io.NopCloser(gzipReader) closeFn: func() error {
_ = gzipReader.Close()
return origBody.Close()
},
})
c.Request.Header.Del("Content-Encoding") c.Request.Header.Del("Content-Encoding")
case "br": case "br":
reader := brotli.NewReader(c.Request.Body) reader := brotli.NewReader(origBody)
c.Request.Body = io.NopCloser(reader) c.Request.Body = wrapMaxBytes(&readCloser{
Reader: reader,
closeFn: func() error {
return origBody.Close()
},
})
c.Request.Header.Del("Content-Encoding") c.Request.Header.Del("Content-Encoding")
default:
// Even for uncompressed bodies, enforce a max size to avoid huge request allocations.
c.Request.Body = wrapMaxBytes(origBody)
} }
// Continue processing the request // Continue processing the request

View File

@@ -18,7 +18,7 @@ var awsModelIDMap = map[string]string{
"claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0",
"claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0", "claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0",
"claude-haiku-4-5-20251001": "anthropic.claude-haiku-4-5-20251001-v1:0", "claude-haiku-4-5-20251001": "anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-opus-4-5-20251101": "anthropic.claude-opus-4-5-20251101-v1:0", "claude-opus-4-5-20251101": "anthropic.claude-opus-4-5-20251101-v1:0",
// Nova models // Nova models
"nova-micro-v1:0": "amazon.nova-micro-v1:0", "nova-micro-v1:0": "amazon.nova-micro-v1:0",
"nova-lite-v1:0": "amazon.nova-lite-v1:0", "nova-lite-v1:0": "amazon.nova-lite-v1:0",

View File

@@ -150,7 +150,7 @@ func baiduHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respon
return types.NewError(err, types.ErrorCodeBadResponseBody), nil return types.NewError(err, types.ErrorCodeBadResponseBody), nil
} }
if baiduResponse.ErrorMsg != "" { if baiduResponse.ErrorMsg != "" {
return types.NewError(fmt.Errorf(baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil return types.NewError(fmt.Errorf("%s", baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil
} }
fullTextResponse := responseBaidu2OpenAI(&baiduResponse) fullTextResponse := responseBaidu2OpenAI(&baiduResponse)
jsonResponse, err := json.Marshal(fullTextResponse) jsonResponse, err := json.Marshal(fullTextResponse)
@@ -175,7 +175,7 @@ func baiduEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *ht
return types.NewError(err, types.ErrorCodeBadResponseBody), nil return types.NewError(err, types.ErrorCodeBadResponseBody), nil
} }
if baiduResponse.ErrorMsg != "" { if baiduResponse.ErrorMsg != "" {
return types.NewError(fmt.Errorf(baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil return types.NewError(fmt.Errorf("%s", baiduResponse.ErrorMsg), types.ErrorCodeBadResponseBody), nil
} }
fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse) fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse)
jsonResponse, err := json.Marshal(fullTextResponse) jsonResponse, err := json.Marshal(fullTextResponse)

View File

@@ -208,7 +208,7 @@ func handleCozeEvent(c *gin.Context, event string, data string, responseText *st
return return
} }
common.SysLog(fmt.Sprintf("stream event error: ", errorData.Code, errorData.Message)) common.SysLog(fmt.Sprintf("stream event error: %v %v", errorData.Code, errorData.Message))
} }
} }

View File

@@ -196,7 +196,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
} }
if jResp.Code != 10000 { if jResp.Code != 10000 {
taskErr = service.TaskErrorWrapper(fmt.Errorf(jResp.Message), fmt.Sprintf("%d", jResp.Code), http.StatusInternalServerError) taskErr = service.TaskErrorWrapper(fmt.Errorf("%s", jResp.Message), fmt.Sprintf("%d", jResp.Code), http.StatusInternalServerError)
return return
} }

View File

@@ -186,7 +186,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
return return
} }
if kResp.Code != 0 { if kResp.Code != 0 {
taskErr = service.TaskErrorWrapperLocal(fmt.Errorf(kResp.Message), "task_failed", http.StatusBadRequest) taskErr = service.TaskErrorWrapperLocal(fmt.Errorf("%s", kResp.Message), "task_failed", http.StatusBadRequest)
return return
} }
ov := dto.NewOpenAIVideo() ov := dto.NewOpenAIVideo()

View File

@@ -105,7 +105,7 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
return return
} }
if !sunoResponse.IsSuccess() { if !sunoResponse.IsSuccess() {
taskErr = service.TaskErrorWrapper(fmt.Errorf(sunoResponse.Message), sunoResponse.Code, http.StatusInternalServerError) taskErr = service.TaskErrorWrapper(fmt.Errorf("%s", sunoResponse.Message), sunoResponse.Code, http.StatusInternalServerError)
return return
} }

View File

@@ -196,7 +196,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.
// handle response // handle response
if resp != nil && resp.StatusCode != http.StatusOK { if resp != nil && resp.StatusCode != http.StatusOK {
responseBody, _ := io.ReadAll(resp.Body) responseBody, _ := io.ReadAll(resp.Body)
taskErr = service.TaskErrorWrapper(fmt.Errorf(string(responseBody)), "fail_to_fetch_task", resp.StatusCode) taskErr = service.TaskErrorWrapper(fmt.Errorf("%s", string(responseBody)), "fail_to_fetch_task", resp.StatusCode)
return return
} }

View File

@@ -3,9 +3,9 @@ package system_setting
import "github.com/QuantumNous/new-api/setting/config" import "github.com/QuantumNous/new-api/setting/config"
type DiscordSettings struct { type DiscordSettings struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ClientId string `json:"client_id"` ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
} }
// 默认配置 // 默认配置